1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-27 10:32:58 +02:00

Mobile: Add long-press tooltips (#6758)

This commit is contained in:
Henry Heino 2022-08-21 14:03:41 -07:00 committed by GitHub
parent 8ef9804cab
commit a5e6491cda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 276 additions and 55 deletions

View File

@ -842,6 +842,9 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
packages/app-mobile/components/CameraView.d.ts
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView.js.map
packages/app-mobile/components/CustomButton.d.ts
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map

3
.gitignore vendored
View File

@ -831,6 +831,9 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
packages/app-mobile/components/CameraView.d.ts
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView.js.map
packages/app-mobile/components/CustomButton.d.ts
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map

View File

@ -0,0 +1,190 @@
//
// A button with a long-press action. Long-pressing the button displays a tooltip
//
const React = require('react');
import { ReactNode } from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useState, useMemo, useCallback, useRef } from 'react';
import { View, Text, Pressable, ViewStyle, PressableStateCallbackType, StyleProp, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole } from 'react-native';
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
type ButtonClickListener = ()=> void;
interface ButtonProps {
onPress: ButtonClickListener;
// Accessibility label and text shown in a tooltip
description?: string;
children: ReactNode;
themeId: number;
style?: ViewStyle;
pressedStyle?: ViewStyle;
contentStyle?: ViewStyle;
// Additional accessibility information. See View.accessibilityHint
accessibilityHint?: string;
// Role of the button. Defaults to 'button'.
accessibilityRole?: AccessibilityRole;
accessibilityState?: AccessibilityState;
disabled?: boolean;
}
const CustomButton = (props: ButtonProps) => {
const [tooltipVisible, setTooltipVisible] = useState(false);
const [buttonLayout, setButtonLayout] = useState<LayoutRectangle|null>(null);
const tooltipStyles = useTooltipStyles(props.themeId);
// See https://blog.logrocket.com/react-native-touchable-vs-pressable-components/
// for more about animating Pressable buttons.
const fadeAnim = useRef(new Animated.Value(1)).current;
const animationDuration = 100; // ms
const onPressIn = useCallback(() => {
// Fade out.
Animated.timing(fadeAnim, {
toValue: 0.5,
duration: animationDuration,
useNativeDriver: true,
}).start();
}, [fadeAnim]);
const onPressOut = useCallback(() => {
// Fade in.
Animated.timing(fadeAnim, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
}).start();
setTooltipVisible(false);
}, [fadeAnim]);
const onLongPress = useCallback(() => {
setTooltipVisible(true);
}, []);
// Select different user-specified styles if selected/unselected.
const onStyleChange = useCallback((state: PressableStateCallbackType): StyleProp<ViewStyle> => {
let result = { ...props.style };
if (state.pressed) {
result = {
...result,
...props.pressedStyle,
};
}
return result;
}, [props.pressedStyle, props.style]);
const onButtonLayout = useCallback((event: LayoutChangeEvent) => {
const layoutEvt = event.nativeEvent.layout;
// Copy the layout event
setButtonLayout({ ...layoutEvt });
}, []);
const button = (
<Pressable
onPress={props.onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={ onStyleChange }
disabled={ props.disabled ?? false }
onLayout={ onButtonLayout }
accessibilityLabel={props.description}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole ?? 'button'}
accessibilityState={props.accessibilityState}
>
<Animated.View style={{
opacity: fadeAnim,
...props.contentStyle,
}}>
{ props.children }
</Animated.View>
</Pressable>
);
const tooltip = (
<View
// Any information given by the tooltip should also be provided via
// [accessibilityLabel]/[accessibilityHint]. As such, we can hide the tooltip
// from the screen reader.
// On Android:
importantForAccessibility='no-hide-descendants'
// On iOS:
accessibilityElementsHidden={true}
// Position the menu beneath the button so the tooltip appears in the
// correct location.
style={{
left: buttonLayout?.x,
top: buttonLayout?.y,
position: 'absolute',
zIndex: -1,
}}
>
<Menu
opened={tooltipVisible}
renderer={renderers.Popover}
rendererProps={{
preferredPlacement: 'bottom',
anchorStyle: tooltipStyles.anchor,
}}>
<MenuTrigger
// Don't show/hide when pressed (let the Pressable handle opening/closing)
disabled={true}
style={{
// Ensure that the trigger region has the same size as the button.
width: buttonLayout?.width ?? 0,
height: buttonLayout?.height ?? 0,
}}
/>
<MenuOptions
customStyles={{ optionsContainer: tooltipStyles.optionsContainer }}
>
<Text style={tooltipStyles.text}>
{props.description}
</Text>
</MenuOptions>
</Menu>
</View>
);
return (
<>
{props.description ? tooltip : null}
{button}
</>
);
};
const useTooltipStyles = (themeId: number) => {
return useMemo(() => {
const themeData: Theme = themeStyle(themeId);
return StyleSheet.create({
text: {
color: themeData.raisedColor,
padding: 4,
},
anchor: {
backgroundColor: themeData.raisedBackgroundColor,
},
optionsContainer: {
backgroundColor: themeData.raisedBackgroundColor,
},
});
}, [themeId]);
};
export default CustomButton;

View File

@ -115,6 +115,7 @@ function NoteEditor(props: Props, ref: any) {
` : '';
const editorSettings: EditorSettings = {
themeId: props.themeId,
themeData: editorTheme(props.themeId),
katexEnabled: Setting.value('markdown.plugin.katex') as boolean,
};

View File

@ -1,15 +1,14 @@
// Displays a find/replace dialog
const React = require('react');
const { StyleSheet } = require('react-native');
const { TextInput, View, Text, TouchableOpacity } = require('react-native');
const { useMemo, useState, useEffect } = require('react');
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
import { SearchControl, SearchState, EditorSettings } from './types';
import { _ } from '@joplin/lib/locale';
import { BackHandler } from 'react-native';
import { BackHandler, TextInput, View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Theme } from '@joplin/lib/themes/type';
import CustomButton from '../CustomButton';
const buttonSize = 48;
@ -33,6 +32,7 @@ export interface SearchPanelProps {
interface ActionButtonProps {
styles: any;
themeId: number;
iconName: string;
title: string;
onPress: Callback;
@ -42,30 +42,32 @@ const ActionButton = (
props: ActionButtonProps
) => {
return (
<TouchableOpacity
<CustomButton
themeId={props.themeId}
style={props.styles.button}
onPress={props.onPress}
accessibilityLabel={props.title}
accessibilityRole='button'
description={props.title}
>
<MaterialCommunityIcon name={props.iconName} style={props.styles.buttonText}/>
</TouchableOpacity>
</CustomButton>
);
};
interface ToggleButtonProps {
styles: any;
themeId: number;
iconName: string;
title: string;
active: boolean;
onToggle: Callback;
}
const ToggleButton = (props: ToggleButtonProps) => {
const active = props.active;
return (
<TouchableOpacity
<CustomButton
themeId={props.themeId}
style={{
...props.styles.toggleButton,
...(active ? props.styles.toggleButtonActive : {}),
@ -75,20 +77,20 @@ const ToggleButton = (props: ToggleButtonProps) => {
accessibilityState={{
checked: props.active,
}}
accessibilityLabel={props.title}
description={props.title}
accessibilityRole='switch'
>
<MaterialCommunityIcon name={props.iconName} style={
active ? props.styles.activeButtonText : props.styles.buttonText
}/>
</TouchableOpacity>
</CustomButton>
);
};
const useStyles = (theme: Theme) => {
return useMemo(() => {
const buttonStyle = {
const buttonStyle: ViewStyle = {
width: buttonSize,
height: buttonSize,
backgroundColor: theme.backgroundColor4,
@ -136,8 +138,9 @@ const useStyles = (theme: Theme) => {
};
export const SearchPanel = (props: SearchPanelProps) => {
const placeholderColor = props.editorSettings.themeData.color3;
const styles = useStyles(props.editorSettings.themeData);
const theme = props.editorSettings.themeData;
const placeholderColor = theme.color3;
const styles = useStyles(theme);
const [showingAdvanced, setShowAdvanced] = useState(false);
@ -185,9 +188,10 @@ export const SearchPanel = (props: SearchPanelProps) => {
}, [state.dialogVisible]);
const themeId = props.editorSettings.themeId;
const closeButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="close"
onPress={control.hideSearch}
@ -197,6 +201,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const showDetailsButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-down"
onPress={() => setShowAdvanced(true)}
@ -206,6 +211,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const hideDetailsButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-up"
onPress={() => setShowAdvanced(false)}
@ -255,6 +261,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const toNextButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-right"
onPress={control.findNext}
@ -264,6 +271,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const toPrevButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-left"
onPress={control.findPrevious}
@ -273,6 +281,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const replaceButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="swap-horizontal"
onPress={control.replaceCurrent}
@ -282,6 +291,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const replaceAllButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="reply-all"
onPress={control.replaceAll}
@ -291,6 +301,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const regexpButton = (
<ToggleButton
themeId={themeId}
styles={styles}
iconName="regex"
onToggle={() => {
@ -305,6 +316,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const caseSensitiveButton = (
<ToggleButton
themeId={themeId}
styles={styles}
iconName="format-letter-case"
onToggle={() => {

View File

@ -1,5 +1,6 @@
// Types related to the NoteEditor
import { Theme } from '@joplin/lib/themes/type';
import { CodeMirrorControl } from './CodeMirror/types';
// Controls for the entire editor (including dialogs)
@ -10,7 +11,12 @@ export interface EditorControl extends CodeMirrorControl {
}
export interface EditorSettings {
themeData: any;
// EditorSettings objects are deserialized within WebViews, where
// [themeStyle(themeId: number)] doesn't work. As such, we need both
// the [themeId] and [themeData].
themeId: number;
themeData: Theme;
katexEnabled: boolean;
}

View File

@ -16,6 +16,7 @@ const { dialogs } = require('../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { localSyncInfoFromState } = require('@joplin/lib/services/synchronizer/syncInfoUtils');
const { showMissingMasterKeyMessage } = require('@joplin/lib/services/e2ee/utils');
import CustomButton from './CustomButton';
Icon.loadFont();
@ -223,6 +224,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
render() {
const themeId = Setting.value('theme');
function sideMenuButton(styles, onPress) {
return (
<TouchableOpacity
@ -283,19 +285,23 @@ class ScreenHeaderComponent extends React.PureComponent {
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
return (
<TouchableOpacity
<CustomButton
onPress={options.onPress}
style={{ padding: 0 }}
themeId={themeId}
disabled={!!options.disabled}
accessibilityRole="button">
<View style={viewStyle}>{icon}</View>
</TouchableOpacity>
description={options.description}
contentStyle={viewStyle}
>
{icon}
</CustomButton>
);
};
const renderUndoButton = () => {
return renderTopButton({
iconName: 'arrow-undo-circle-sharp',
description: _('Undo'),
onPress: this.props.onUndoButtonPress,
visible: this.props.showUndoButton,
disabled: this.props.undoButtonDisabled,
@ -305,6 +311,7 @@ class ScreenHeaderComponent extends React.PureComponent {
const renderRedoButton = () => {
return renderTopButton({
iconName: 'arrow-redo-circle-sharp',
description: _('Redo'),
onPress: this.props.onRedoButtonPress,
visible: this.props.showRedoButton,
});
@ -312,65 +319,65 @@ class ScreenHeaderComponent extends React.PureComponent {
function selectAllButton(styles, onPress) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
accessibilityLabel={_('Select all')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</View>
</TouchableOpacity>
themeId={themeId}
description={_('Select all')}
contentStyle={styles.iconButton}
>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</CustomButton>
);
}
function searchButton(styles, onPress) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
accessibilityLabel={_('Search')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-search" style={styles.topIcon} />
</View>
</TouchableOpacity>
description={_('Search')}
themeId={themeId}
contentStyle={styles.iconButton}
>
<Icon name="md-search" style={styles.topIcon} />
</CustomButton>
);
}
function deleteButton(styles, onPress, disabled) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Delete')}
themeId={themeId}
description={_('Delete')}
accessibilityHint={
disabled ? null : _('Delete selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-trash" style={styles.topIcon} />
</View>
</TouchableOpacity>
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-trash" style={styles.topIcon} />
</CustomButton>
);
}
function duplicateButton(styles, onPress, disabled) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Duplicate')}
themeId={themeId}
description={_('Duplicate')}
accessibilityHint={
disabled ? null : _('Duplicate selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-copy" style={styles.topIcon} />
</View>
</TouchableOpacity>
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-copy" style={styles.topIcon} />
</CustomButton>
);
}
@ -424,7 +431,6 @@ class ScreenHeaderComponent extends React.PureComponent {
}
const createTitleComponent = (disabled) => {
const themeId = Setting.value('theme');
const theme = themeStyle(themeId);
const folderPickerOptions = this.props.folderPickerOptions;

View File

@ -50,7 +50,7 @@
"react-native-image-picker": "^2.3.4",
"react-native-image-resizer": "^1.3.0",
"react-native-modal-datetime-picker": "^9.0.0",
"react-native-popup-menu": "^0.10.0",
"react-native-popup-menu": "^0.15.13",
"react-native-quick-actions": "^0.3.13",
"react-native-rsa-native": "^2.0.4",
"react-native-securerandom": "^1.0.0-rc.0",

View File

@ -4077,7 +4077,7 @@ __metadata:
react-native-image-picker: ^2.3.4
react-native-image-resizer: ^1.3.0
react-native-modal-datetime-picker: ^9.0.0
react-native-popup-menu: ^0.10.0
react-native-popup-menu: ^0.15.13
react-native-quick-actions: ^0.3.13
react-native-rsa-native: ^2.0.4
react-native-securerandom: ^1.0.0-rc.0
@ -28077,10 +28077,10 @@ __metadata:
languageName: node
linkType: hard
"react-native-popup-menu@npm:^0.10.0":
version: 0.10.0
resolution: "react-native-popup-menu@npm:0.10.0"
checksum: c1aeed63f012d3afa71efa9ce40846d4e67165a38160aa7db7ddbae06b426d76f9631a81798515563a946887747d88deae19430585cd1a8ab36e9374cdbccaba
"react-native-popup-menu@npm:^0.15.13":
version: 0.15.13
resolution: "react-native-popup-menu@npm:0.15.13"
checksum: a251cf0336e607ad23eff6e71c08a3fafca9d0a91a910f958b902373d2a1aa49a437ecb9554c9bac1f605511d5dd53669a8903584a6c1cf742f913e2f4b9bd0b
languageName: node
linkType: hard