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:
parent
86b918b261
commit
cf9a0f435a
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user