diff --git a/packages/app-mobile/components/side-menu-content.tsx b/packages/app-mobile/components/side-menu-content.tsx index a0beb4a41..7c1a5159b 100644 --- a/packages/app-mobile/components/side-menu-content.tsx +++ b/packages/app-mobile/components/side-menu-content.tsx @@ -25,6 +25,7 @@ import { TextStyle, ViewStyle } from 'react-native'; import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer'; import useOnLongPressProps from '../utils/hooks/useOnLongPressProps'; import { TouchableRipple } from 'react-native-paper'; +import shim from '@joplin/lib/shim'; const { substrWithEllipsis } = require('@joplin/lib/string-utils'); interface Props { @@ -154,6 +155,7 @@ const useStyles = (themeId: number) => { type Styles = ReturnType; +type FolderEventHandler = (folder: FolderEntity)=> void; interface FolderItemProps { themeId: number; hasChildren: boolean; @@ -164,47 +166,56 @@ interface FolderItemProps { styles: Styles; alwaysShowFolderIcons: boolean; - onPress(folder: FolderEntity): void; - onTogglePress(folder: FolderEntity): void; - onLongPress(folder: FolderEntity): void; + onPress: FolderEventHandler; + onTogglePress: FolderEventHandler; + onLongPress: FolderEventHandler; } const FolderItem: React.FC = props => { - const theme = themeStyle(props.themeId); - const styles_ = props.styles; + const styles = useMemo(() => { + const theme = themeStyle(props.themeId); - const folderButtonStyle: ViewStyle = { - flex: 1, - flexDirection: 'row', - flexBasis: 'auto', - height: 36, - alignItems: 'center', - paddingRight: theme.marginRight, - paddingLeft: 10, - }; - const selected = props.selected; - if (selected) folderButtonStyle.backgroundColor = theme.selectedColor; - folderButtonStyle.paddingLeft = props.depth * 10 + theme.marginLeft; + return StyleSheet.create({ + buttonWrapper: { flex: 1, flexDirection: 'row' }, + folderButton: { + flex: 1, + flexDirection: 'row', + flexBasis: 'auto', + height: 36, + alignItems: 'center', + paddingRight: theme.marginRight, - const iconWrapperStyle: ViewStyle = { paddingLeft: 10, paddingRight: 10 }; - if (selected) iconWrapperStyle.backgroundColor = theme.selectedColor; - - let iconWrapper = null; + backgroundColor: props.selected ? theme.selectedColor : undefined, + paddingLeft: props.depth * 10 + theme.marginLeft, + }, + iconWrapper: { + paddingLeft: 10, + paddingRight: 10, + backgroundColor: props.selected ? theme.selectedColor : undefined, + }, + }); + }, [props.selected, props.depth, props.themeId]); + const baseStyles = props.styles; const collapsed = props.collapsed; const iconName = collapsed ? 'chevron-down' : 'chevron-up'; - const iconComp = ; + const iconComp = ; - iconWrapper = !props.hasChildren ? null : ( + const onTogglePress = useCallback(() => { + props.onTogglePress(props.folder); + }, [props.folder, props.onTogglePress]); + + const iconWrapper = !props.hasChildren ? null : ( { - props.onTogglePress(props.folder); - }} + style={styles.iconWrapper} + onPress={onTogglePress} + accessibilityLabel={_('Expand %s', props.folder.title)} - accessibilityLabel={collapsed ? _('Expand') : _('Collapse')} - accessibilityState={{ expanded: !collapsed }} - accessibilityRole="togglebutton" + aria-pressed={!collapsed} + accessibilityState={{ checked: !collapsed }} + // The togglebutton role is only supported on Android and iOS. + // On web, the button role with aria-pressed creates a togglebutton. + accessibilityRole={shim.mobilePlatform() === 'web' ? 'button' : 'togglebutton'} > {iconComp} @@ -217,18 +228,18 @@ const FolderItem: React.FC = props => { if (folderId === getTrashFolderId()) { folderIcon = getTrashFolderIcon(FolderIconType.FontAwesome); } else if (props.alwaysShowFolderIcons) { - return ; + return ; } else { return null; } } if (folderIcon.type === FolderIconType.Emoji) { - return {folderIcon.emoji}; + return {folderIcon.emoji}; } else if (folderIcon.type === FolderIconType.DataUrl) { - return ; + return ; } else if (folderIcon.type === FolderIconType.FontAwesome) { - return ; + return ; } else { throw new Error(`Unsupported folder icon type: ${folderIcon.type}`); } @@ -244,11 +255,17 @@ const FolderItem: React.FC = props => { const longPressProps = useOnLongPressProps({ onLongPress, - actionDescription: _('Notebook options'), + actionDescription: _('Show notebook options'), }); + const folderTitle = Folder.displayTitle(props.folder); + // React Native doesn't seem to include an equivalent to web's aria-level. + // To allow screen reader users to determine whether a notebook is a subnotebook or not, + // depth is specified with an accessibilityLabel: + const folderDepthDescription = props.depth > 0 ? _('(level %d)', props.depth) : ''; + const accessibilityLabel = `${folderTitle} ${folderDepthDescription}`.trim(); return ( - + = props => { accessibilityHint={_('Opens notebook')} role='button' > - + {renderFolderIcon(props.folder.id, folderIcon)} - - {Folder.displayTitle(props.folder)} + + {folderTitle} @@ -546,7 +567,17 @@ const SideMenuContentComponent = (props: Props) => { />; }; - const renderSidebarButton = (key: string, title: string, iconName: string, onPressHandler: ()=> void = null, selected = false) => { + type SidebarButtonOptions = { + onPress?: ()=> void; + selected?: boolean; + isHeader?: boolean; + }; + const renderSidebarButton = ( + key: string, + title: string, + iconName: string, + { onPress = null, selected = false, isHeader = false }: SidebarButtonOptions = {}, + ) => { let icon = ; if (key === 'synchronize_button') { @@ -554,16 +585,20 @@ const SideMenuContentComponent = (props: Props) => { } const content = ( - + {icon} {title} ); - if (!onPressHandler) return content; + if (!onPress) return content; return ( - + {content} ); @@ -571,7 +606,7 @@ const SideMenuContentComponent = (props: Props) => { const makeDivider = (key: string) => { const theme = themeStyle(props.themeId); - return ; + return ; }; const renderBottomPanel = () => { @@ -581,15 +616,15 @@ const SideMenuContentComponent = (props: Props) => { items.push(makeDivider('divider_1')); - items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'folder-open', newFolderButton_press)); + items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'folder-open', { onPress: newFolderButton_press })); - items.push(renderSidebarButton('tag_button', _('Tags'), 'pricetag', tagButton_press)); + items.push(renderSidebarButton('tag_button', _('Tags'), 'pricetag', { onPress: tagButton_press })); if (props.profileConfig && props.profileConfig.profiles.length > 1) { - items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'people-circle-outline', switchProfileButton_press)); + items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'people-circle-outline', { onPress: switchProfileButton_press })); } - items.push(renderSidebarButton('config_button', _('Configuration'), 'settings', configButton_press)); + items.push(renderSidebarButton('config_button', _('Configuration'), 'settings', { onPress: configButton_press })); items.push(makeDivider('divider_2')); @@ -611,7 +646,7 @@ const SideMenuContentComponent = (props: Props) => { if (resourceFetcherText) fullReport.push(resourceFetcherText); if (decryptionReportText) fullReport.push(decryptionReportText); - items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'sync', synchronize_press)); + items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'sync', { onPress: synchronize_press })); if (fullReport.length) { items.push( @@ -640,11 +675,16 @@ const SideMenuContentComponent = (props: Props) => { // using padding. So instead creating blank elements for padding bottom and top. items.push(); - items.push(renderSidebarButton('all_notes', _('All notes'), 'document', allNotesButton_press, props.notesParentType === 'SmartFilter')); + items.push(renderSidebarButton('all_notes', _('All notes'), 'document', { + onPress: allNotesButton_press, + selected: props.notesParentType === 'SmartFilter', + })); items.push(makeDivider('divider_all')); - items.push(renderSidebarButton('folder_header', _('Notebooks'), 'folder')); + items.push(renderSidebarButton('folder_header', _('Notebooks'), 'folder', { + isHeader: true, + })); const folderTree = useMemo(() => { return buildFolderTree(props.folders);