1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Refactoring, add additional accessibility information

This commit is contained in:
Henry Heino 2024-12-23 10:22:48 -08:00
parent 86b918b261
commit cf9a0f435a

View File

@ -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<typeof useStyles>;
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<FolderItemProps> = 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 = <IonIcon name={iconName} style={styles_.folderToggleIcon} />;
const iconComp = <IonIcon name={iconName} style={baseStyles.folderToggleIcon} />;
iconWrapper = !props.hasChildren ? null : (
const onTogglePress = useCallback(() => {
props.onTogglePress(props.folder);
}, [props.folder, props.onTogglePress]);
const iconWrapper = !props.hasChildren ? null : (
<TouchableOpacity
style={iconWrapperStyle}
onPress={() => {
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}
</TouchableOpacity>
@ -217,18 +228,18 @@ const FolderItem: React.FC<FolderItemProps> = props => {
if (folderId === getTrashFolderId()) {
folderIcon = getTrashFolderIcon(FolderIconType.FontAwesome);
} else if (props.alwaysShowFolderIcons) {
return <IonIcon name="folder-outline" style={styles_.folderBaseIcon} />;
return <IonIcon name="folder-outline" style={baseStyles.folderBaseIcon} />;
} else {
return null;
}
}
if (folderIcon.type === FolderIconType.Emoji) {
return <Text style={styles_.folderEmojiIcon}>{folderIcon.emoji}</Text>;
return <Text style={baseStyles.folderEmojiIcon}>{folderIcon.emoji}</Text>;
} else if (folderIcon.type === FolderIconType.DataUrl) {
return <Image style={styles_.folderImageIcon} source={{ uri: folderIcon.dataUrl }}/>;
return <Image style={baseStyles.folderImageIcon} source={{ uri: folderIcon.dataUrl }}/>;
} else if (folderIcon.type === FolderIconType.FontAwesome) {
return <Icon style={styles_.folderBaseIcon} name={folderIcon.name} accessibilityLabel={null}/>;
return <Icon style={baseStyles.folderBaseIcon} name={folderIcon.name} accessibilityLabel={null}/>;
} else {
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
}
@ -244,11 +255,17 @@ const FolderItem: React.FC<FolderItemProps> = 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 (
<View key={props.folder.id} style={{ flex: 1, flexDirection: 'row' }}>
<View key={props.folder.id} style={styles.buttonWrapper}>
<TouchableRipple
style={{ flex: 1, flexBasis: 'auto' }}
onPress={onPress}
@ -256,10 +273,14 @@ const FolderItem: React.FC<FolderItemProps> = props => {
accessibilityHint={_('Opens notebook')}
role='button'
>
<View style={folderButtonStyle}>
<View style={styles.folderButton}>
{renderFolderIcon(props.folder.id, folderIcon)}
<Text numberOfLines={1} style={styles_.folderButtonText}>
{Folder.displayTitle(props.folder)}
<Text
numberOfLines={1}
style={baseStyles.folderButtonText}
accessibilityLabel={accessibilityLabel}
>
{folderTitle}
</Text>
</View>
</TouchableRipple>
@ -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 = <Icon name={`ionicon ${iconName}`} style={styles_.sidebarIcon} accessibilityLabel={null} />;
if (key === 'synchronize_button') {
@ -554,16 +585,20 @@ const SideMenuContentComponent = (props: Props) => {
}
const content = (
<View key={key} style={selected ? styles_.sideButtonSelected : styles_.sideButton}>
<View
key={key}
style={selected ? styles_.sideButtonSelected : styles_.sideButton}
accessibilityRole={isHeader ? 'header' : undefined}
>
{icon}
<Text style={styles_.sideButtonText}>{title}</Text>
</View>
);
if (!onPressHandler) return content;
if (!onPress) return content;
return (
<TouchableOpacity key={key} onPress={onPressHandler} accessibilityRole='button'>
<TouchableOpacity key={key} onPress={onPress} accessibilityRole='button'>
{content}
</TouchableOpacity>
);
@ -571,7 +606,7 @@ const SideMenuContentComponent = (props: Props) => {
const makeDivider = (key: string) => {
const theme = themeStyle(props.themeId);
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: theme.dividerColor }} key={key}></View>;
return <View role='separator' style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: theme.dividerColor }} key={key}></View>;
};
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(<View style={{ height: theme.marginTop }} key="bottom_top_hack" />);
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);