Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile: Accessibility: Add checked/unchecked accessibility information to the "sort notes by" dialog #11411

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager/PromptButton.js
packages/app-mobile/components/DialogManager/PromptDialog.js
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
packages/app-mobile/components/DialogManager/index.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager/PromptButton.js
packages/app-mobile/components/DialogManager/PromptDialog.js
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
packages/app-mobile/components/DialogManager/index.js
Expand Down
83 changes: 83 additions & 0 deletions packages/app-mobile/components/DialogManager/PromptButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from 'react';
import { useMemo } from 'react';
import { StyleSheet, TextStyle, View } from 'react-native';
import { TouchableRipple, Text } from 'react-native-paper';
import { PromptButtonSpec } from './types';
import { ThemeStyle, themeStyle } from '../global-style';
import Icon from '../Icon';

interface Props {
themeId: number;
buttonSpec: PromptButtonSpec;
}

const useStyles = (theme: ThemeStyle) => {
return useMemo(() => {
const buttonText: TextStyle = {
color: theme.color4,
textAlign: 'center',
};

return StyleSheet.create({
buttonContainer: {
// This applies the borderRadius to the TouchableRipple's parent, which
// seems necessary on Android.
borderRadius: theme.borderRadius,
overflow: 'hidden',
},
button: {
borderRadius: theme.borderRadius,
padding: 10,
},
buttonContent: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'baseline',
},
buttonText,
icon: {
...buttonText,
marginRight: 8,
},
});
}, [theme]);
};

const PromptButton: React.FC<Props> = props => {
const theme = themeStyle(props.themeId);
const styles = useStyles(theme);

const { checked, text, iconChecked, onPress } = props.buttonSpec;

const isCheckbox = (checked ?? null) !== null;
const icon = checked ? (
<>
<Icon
accessibilityLabel={null}
style={styles.icon}
name={iconChecked ?? 'fas fa-check'}
/>
</>
) : null;

return (
<View style={styles.buttonContainer}>
<TouchableRipple
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request switches from a React Native Paper Button to a TouchableRipple. Unlike <Button>, TouchableRipple allows providing a custom accessibilityState. This allows communicating whether the button represents a checked/unchecked menu item.

On web, aria-checked seems necessary — react-native-web seems to ignore the accessibilityState's checked option.

onPress={onPress}
style={styles.button}
rippleColor={theme.backgroundColorHover4}
accessibilityRole={isCheckbox ? 'checkbox' : 'button'}
accessibilityState={isCheckbox ? { checked } : null}
aria-checked={isCheckbox ? checked : undefined}
>
<View style={styles.buttonContent}>
{icon}
<Text style={styles.buttonText}>{text}</Text>
</View>
</TouchableRipple>
</View>
);
};

export default PromptButton;
24 changes: 9 additions & 15 deletions packages/app-mobile/components/DialogManager/PromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { Button, Dialog, Divider, Surface, Text } from 'react-native-paper';
import { Dialog, Divider, Surface, Text } from 'react-native-paper';
import { DialogType, PromptDialogData } from './types';
import { StyleSheet } from 'react-native';
import { useMemo } from 'react';
import { themeStyle } from '../global-style';
import PromptButton from './PromptButton';

interface Props {
dialog: PromptDialogData;
Expand All @@ -17,18 +18,12 @@ const useStyles = (themeId: number, isMenu: boolean) => {
return StyleSheet.create({
dialogContainer: {
backgroundColor: theme.backgroundColor,
borderRadius: 24,
paddingTop: 24,
borderRadius: theme.borderRadius,
paddingTop: theme.borderRadius,
marginLeft: 4,
marginRight: 4,
},

buttonScrollerContent: {
flexDirection: 'row',
justifyContent: 'flex-end',
flexWrap: 'wrap',
},

dialogContent: {
paddingBottom: 14,
},
Expand All @@ -53,12 +48,11 @@ const PromptDialog: React.FC<Props> = ({ dialog, themeId }) => {
const styles = useStyles(themeId, isMenu);

const buttons = dialog.buttons.map((button, index) => {
return (
<Button
key={`${index}-${button.text}`}
onPress={button.onPress}
>{button.text}</Button>
);
return <PromptButton
key={`${index}-${button.text}`}
buttonSpec={button}
themeId={themeId}
/>;
});
const titleComponent = <Text
variant='titleMedium'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Alert, Platform } from 'react-native';
import { DialogControl, DialogType, MenuChoice, PromptButton, PromptDialogData, PromptOptions } from '../types';
import { DialogControl, DialogType, MenuChoice, PromptButtonSpec, PromptDialogData, PromptOptions } from '../types';
import { _ } from '@joplin/lib/locale';
import { useMemo, useRef } from 'react';

Expand Down Expand Up @@ -32,7 +32,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
}]);
});
},
prompt: (title: string, message: string, buttons: PromptButton[] = defaultButtons, options?: PromptOptions) => {
prompt: (title: string, message: string, buttons: PromptButtonSpec[] = defaultButtons, options?: PromptOptions) => {
// Alert.alert doesn't work on web.
if (Platform.OS !== 'web') {
// Note: Alert.alert provides a more native style on iOS.
Expand Down Expand Up @@ -71,11 +71,11 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
key: `menu-dialog-${nextDialogIdRef.current++}`,
title: '',
message: title,
buttons: choices.map(choice => ({
text: choice.text,
buttons: choices.map(({ id, ...buttonProps }) => ({
...buttonProps,
onPress: () => {
dismiss();
resolve(choice.id);
resolve(id);
},
})),
onDismiss: dismiss,
Expand Down
17 changes: 11 additions & 6 deletions packages/app-mobile/components/DialogManager/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@

export interface PromptButton {
interface BaseButtonSpec {
text: string;
onPress?: ()=> void;
style?: 'cancel'|'default'|'destructive';

checked?: boolean|null;
iconChecked?: string;
}

export interface PromptButtonSpec extends BaseButtonSpec {
onPress?: ()=> void;
}

export interface PromptOptions {
cancelable?: boolean;
}

export interface MenuChoice<IdType> {
text: string;
export interface MenuChoice<IdType> extends BaseButtonSpec {
id: IdType;
}

export interface DialogControl {
info(message: string): Promise<void>;
error(message: string): Promise<void>;
prompt(title: string, message: string, buttons?: PromptButton[], options?: PromptOptions): void;
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
}

Expand All @@ -31,7 +36,7 @@ export interface PromptDialogData {
key: string;
title: string;
message: string;
buttons: PromptButton[];
buttons: PromptButtonSpec[];
onDismiss: (()=> void)|null;
}

8 changes: 8 additions & 0 deletions packages/app-mobile/components/global-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Platform, TextStyle, ViewStyle } from 'react-native';
import { themeById } from '@joplin/lib/theme';
import { Theme as BaseTheme } from '@joplin/lib/themes/type';

const Color = require('color');

const baseStyle = {
appearance: 'light',
fontSize: 16,
Expand All @@ -16,12 +18,15 @@ const baseStyle = {
};

export type ThemeStyle = BaseTheme & typeof baseStyle & {
backgroundColorHover4: string;

fontSize: number;
fontSizeSmaller: number;
marginRight: number;
marginLeft: number;
marginTop: number;
marginBottom: number;
borderRadius: number;
icon: TextStyle;
lineInput: ViewStyle;
buttonRow: ViewStyle;
Expand Down Expand Up @@ -112,6 +117,9 @@ function extraStyles(theme: BaseTheme) {
keyboardAppearance: theme.appearance,
color5: theme.color5 ?? theme.backgroundColor4,
backgroundColor5: theme.backgroundColor5 ?? theme.color4,

backgroundColorHover4: Color(theme.color4).alpha(0.12).rgb().string(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundColorHover4 determines the ripple/hover color for the <TouchableRipple> menu item component. This use of Color matches how additional theme colors are added for the desktop app. For example,

const borderColor4: string = Color(theme.color).alpha(0.3);

Additionally, .alpha(0.12) should match React Native's <Button> hover color.

borderRadius: 24,
};
}

Expand Down
22 changes: 12 additions & 10 deletions packages/app-mobile/components/screens/Notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import AccessibleView from '../accessibility/AccessibleView';
import { Dispatch } from 'redux';
import { DialogContext, DialogControl } from '../DialogManager';
import { useContext } from 'react';
import { MenuChoice } from '../DialogManager/types';

interface Props {
dispatch: Dispatch;
Expand Down Expand Up @@ -68,34 +69,35 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
};

private sortButton_press = async () => {
const buttons = [];
type IdType = { name: string; value: string|boolean };
const buttons: MenuChoice<IdType>[] = [];
const sortNoteOptions = Setting.enumOptions('notes.sortOrder.field');

const makeCheckboxText = function(selected: boolean, sign: string, label: string) {
const s = sign === 'tick' ? '✓' : '⬤';
return (selected ? `${s} ` : '') + label;
};

for (const field in sortNoteOptions) {
if (!sortNoteOptions.hasOwnProperty(field)) continue;
buttons.push({
text: makeCheckboxText(Setting.value('notes.sortOrder.field') === field, 'bullet', sortNoteOptions[field]),
text: sortNoteOptions[field],
iconChecked: 'fas fa-circle',
checked: Setting.value('notes.sortOrder.field') === field,
id: { name: 'notes.sortOrder.field', value: field },
});
}

buttons.push({
text: makeCheckboxText(Setting.value('notes.sortOrder.reverse'), 'tick', `[ ${Setting.settingMetadata('notes.sortOrder.reverse').label()} ]`),
text: `[ ${Setting.settingMetadata('notes.sortOrder.reverse').label()} ]`,
checked: Setting.value('notes.sortOrder.reverse'),
id: { name: 'notes.sortOrder.reverse', value: !Setting.value('notes.sortOrder.reverse') },
});

buttons.push({
text: makeCheckboxText(Setting.value('uncompletedTodosOnTop'), 'tick', `[ ${Setting.settingMetadata('uncompletedTodosOnTop').label()} ]`),
text: `[ ${Setting.settingMetadata('uncompletedTodosOnTop').label()} ]`,
checked: Setting.value('uncompletedTodosOnTop'),
id: { name: 'uncompletedTodosOnTop', value: !Setting.value('uncompletedTodosOnTop') },
});

buttons.push({
text: makeCheckboxText(Setting.value('showCompletedTodos'), 'tick', `[ ${Setting.settingMetadata('showCompletedTodos').label()} ]`),
text: `[ ${Setting.settingMetadata('showCompletedTodos').label()} ]`,
checked: Setting.value('showCompletedTodos'),
id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') },
});

Expand Down
1 change: 1 addition & 0 deletions packages/app-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@react-native-community/slider": "4.5.2",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
"constants-browserify": "1.0.0",
"crypto-browserify": "3.12.0",
"deprecated-react-native-prop-types": "5.0.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/app-mobile/utils/makeShowMessageBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { Alert } from 'react-native';
import { DialogControl } from '../components/DialogManager';
import { RefObject } from 'react';
import { MessageBoxType, ShowMessageBoxOptions } from '@joplin/lib/shim';
import { PromptButton } from '../components/DialogManager/types';
import { PromptButtonSpec } from '../components/DialogManager/types';


const makeShowMessageBox = (dialogControl: null|RefObject<DialogControl>) => (message: string, options: ShowMessageBoxOptions = null) => {
return new Promise<number>(resolve => {
const okButton: PromptButton = {
const okButton: PromptButtonSpec = {
text: _('OK'),
onPress: () => resolve(0),
};
const cancelButton: PromptButton = {
const cancelButton: PromptButtonSpec = {
text: _('Cancel'),
onPress: () => resolve(1),
style: 'cancel',
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8389,6 +8389,7 @@ __metadata:
babel-plugin-module-resolver: 4.1.0
babel-plugin-react-native-web: 0.19.12
buffer: 6.0.3
color: 3.2.1
constants-browserify: 1.0.0
crypto-browserify: 3.12.0
deprecated-react-native-prop-types: 5.0.0
Expand Down
Loading