From c434a17513b986d9032244e2bf75db075273332b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 7 Oct 2024 14:07:55 -0700 Subject: [PATCH] Improving sidebar ARIA information See https://github.com/laurent22/joplin/issues/10795 --- packages/app-desktop/gui/EmojiBox.tsx | 2 +- packages/app-desktop/gui/FolderIconBox.tsx | 2 +- .../gui/Sidebar/FolderAndTagList.tsx | 7 ++++--- .../gui/Sidebar/hooks/useFocusHandler.ts | 16 +++++++++++----- .../gui/Sidebar/hooks/useOnRenderItem.tsx | 18 ++++++++++++++++-- .../hooks/useOnSidebarKeyDownHandler.ts | 2 +- .../listItemComponents/AllNotesItem.tsx | 4 +++- .../Sidebar/listItemComponents/ExpandIcon.tsx | 2 +- .../Sidebar/listItemComponents/ExpandLink.tsx | 3 ++- .../Sidebar/listItemComponents/FolderItem.tsx | 10 ++++++---- .../Sidebar/listItemComponents/HeaderItem.tsx | 4 +++- .../listItemComponents/ListItemWrapper.tsx | 17 ++++++++++++----- .../gui/Sidebar/listItemComponents/TagItem.tsx | 4 +++- packages/tools/cspell/dictionary4.txt | 1 + 14 files changed, 65 insertions(+), 27 deletions(-) diff --git a/packages/app-desktop/gui/EmojiBox.tsx b/packages/app-desktop/gui/EmojiBox.tsx index 57e2b5865a3..ea4cc1d81b0 100644 --- a/packages/app-desktop/gui/EmojiBox.tsx +++ b/packages/app-desktop/gui/EmojiBox.tsx @@ -44,5 +44,5 @@ export default (props: Props) => { return spanFontSize; }, [props.width, props.height, props.emoji, containerReady, containerRef]); - return
{ containerRef.current = el; setContainerReady(true); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: props.width, height: props.height, fontSize }}>{props.emoji}
; + return
{ containerRef.current = el; setContainerReady(true); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: props.width, height: props.height, fontSize }}>{props.emoji}
; }; diff --git a/packages/app-desktop/gui/FolderIconBox.tsx b/packages/app-desktop/gui/FolderIconBox.tsx index ca67fbfeeb2..0f2e8ffd513 100644 --- a/packages/app-desktop/gui/FolderIconBox.tsx +++ b/packages/app-desktop/gui/FolderIconBox.tsx @@ -17,7 +17,7 @@ export default function(props: Props) { } else if (folderIcon.type === FolderIconType.DataUrl) { return ; } else if (folderIcon.type === FolderIconType.FontAwesome) { - return ; + return ; } else { throw new Error(`Unsupported folder icon type: ${folderIcon.type}`); } diff --git a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx index c149671dc4c..2f01d2f78be 100644 --- a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx +++ b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx @@ -39,12 +39,12 @@ const FolderAndTagList: React.FC = props => { listItems: listItems, }); - const [selectedListElement, setSelectedListElement] = useState(null); + const listContainerRef = useRef(null); const onRenderItem = useOnRenderItem({ ...props, selectedIndex, - onSelectedElementShown: setSelectedListElement, listItems, + containerRef: listContainerRef, }); const onKeyEventHandler = useOnSidebarKeyDownHandler({ @@ -56,11 +56,12 @@ const FolderAndTagList: React.FC = props => { }); const itemListRef = useRef>(); - const { focusSidebar } = useFocusHandler({ itemListRef, selectedListElement, selectedIndex, listItems }); + const { focusSidebar } = useFocusHandler({ itemListRef, selectedIndex, listItems }); useSidebarCommandHandler({ focusSidebar }); const [itemListContainer, setItemListContainer] = useState(null); + listContainerRef.current = itemListContainer; const listHeight = useElementHeight(itemListContainer); const listStyle = useMemo(() => ({ height: listHeight }), [listHeight]); diff --git a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts index da335eba7a6..ffe6ac2e266 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts @@ -5,7 +5,6 @@ import { focus } from '@joplin/lib/utils/focusHandler'; interface Props { itemListRef: RefObject>; - selectedListElement: HTMLElement|null; selectedIndex: number; listItems: ListItem[]; } @@ -46,16 +45,23 @@ const useScrollToSelectionHandler = ( }; const useFocusHandler = (props: Props) => { - const { itemListRef, selectedListElement, selectedIndex, listItems } = props; + const { itemListRef, selectedIndex, listItems } = props; useScrollToSelectionHandler(itemListRef, listItems, selectedIndex); const focusSidebar = useCallback(() => { - if (!selectedListElement || !itemListRef.current.isIndexVisible(selectedIndex)) { + if (!itemListRef.current.isIndexVisible(selectedIndex)) { itemListRef.current.makeItemIndexVisible(selectedIndex); } - focus('FolderAndTagList/useFocusHandler/focusSidebar', itemListRef.current.container); - }, [selectedListElement, selectedIndex, itemListRef]); + + // Select the focusable item, if it's visible + const selectableItem = itemListRef.current.container.querySelector('[role="treeitem"][tabindex="0"]'); + if (selectableItem) { + focus('FolderAndTagList/focusSidebarItem', selectableItem); + } else { + focus('FolderAndTagList/focusSidebarContainer', itemListRef.current.container); + } + }, [selectedIndex, itemListRef]); return { focusSidebar }; }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx index 07d4f78d6ed..a1185fe7aca 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -30,6 +30,7 @@ import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop'; import HeaderItem from '../listItemComponents/HeaderItem'; import AllNotesItem from '../listItemComponents/AllNotesItem'; import ListItemWrapper from '../listItemComponents/ListItemWrapper'; +import { focus } from '@joplin/lib/utils/focusHandler'; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; @@ -42,9 +43,9 @@ interface Props { plugins: PluginStates; folders: FolderEntity[]; collapsedFolderIds: string[]; + containerRef: React.RefObject; selectedIndex: number; - onSelectedElementShown: (element: HTMLElement)=> void; listItems: ListItem[]; } @@ -52,6 +53,12 @@ type ItemContextMenuListener = MouseEventHandler; const menuUtils = new MenuUtils(CommandService.instance()); +const focusListItem = (item: HTMLElement) => { + focus('useOnRenderItem', item); +}; + +const noFocusListItem = () => {}; + const useOnRenderItem = (props: Props) => { const pluginsRef = useRef(null); @@ -331,11 +338,14 @@ const useOnRenderItem = (props: Props) => { const itemCount = props.listItems.length; return useCallback((item: ListItem, index: number) => { const selected = props.selectedIndex === index; + const focusInList = document.hasFocus() && props.containerRef.current?.contains(document.activeElement); + const anchorRef = (focusInList && selected) ? focusListItem : noFocusListItem; if (item.kind === ListItemType.Tag) { const tag = item.tag; return { } return { } else if (item.kind === ListItemType.Header) { return { } else if (item.kind === ListItemType.AllNotes) { return { return ( { showFolderIcons, tagItem_click, props.selectedIndex, - props.onSelectedElementShown, + props.containerRef, itemCount, ]); }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts index 485b77b52cb..baac13aaa78 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts @@ -44,7 +44,7 @@ const useOnSidebarKeyDownHandler = (props: Props) => { indexChange = -1; } else if (event.code === 'ArrowDown') { indexChange = 1; - } else if (event.code === 'ArrowRight') { + } else if (event.code === 'Tab') { event.preventDefault(); if (event.shiftKey) { diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx index bdd94fce38d..deb97ca3934 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx @@ -10,7 +10,7 @@ import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSort import { _ } from '@joplin/lib/locale'; import { connect } from 'react-redux'; import EmptyExpandLink from './EmptyExpandLink'; -import ListItemWrapper from './ListItemWrapper'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids'); const Menu = bridge().Menu; @@ -18,6 +18,7 @@ const MenuItem = bridge().MenuItem; interface Props { dispatch: Dispatch; + anchorRef: ListItemRef; selected: boolean; index: number; itemCount: number; @@ -49,6 +50,7 @@ const AllNotesItem: React.FC = props => { return ( = props => { } return _('Expand %s', props.targetTitle); }; - return ; + return ; }; export default ExpandIcon; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx index e8841e449cb..8ce1a60fb9d 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx @@ -13,7 +13,8 @@ interface ExpandLinkProps { const ExpandLink: React.FC = props => { return props.hasChildren ? ( - + // The expand/collapse information is conveyed through ARIA. + ) : ( diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx index 457604c7e45..93885fb3a0e 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx @@ -10,7 +10,7 @@ import Folder from '@joplin/lib/models/Folder'; import { ModelType } from '@joplin/lib/BaseModel'; import { _ } from '@joplin/lib/locale'; import NoteCount from './NoteCount'; -import ListItemWrapper from './ListItemWrapper'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; const renderFolderIcon = (folderIcon: FolderIcon) => { if (!folderIcon) { @@ -27,6 +27,7 @@ const renderFolderIcon = (folderIcon: FolderIcon) => { }; interface FolderItemProps { + anchorRef: ListItemRef; hasChildren: boolean; showFolderIcon: boolean; isExpanded: boolean; @@ -67,11 +68,12 @@ function FolderItem(props: FolderItemProps) { return ( - + { folderItem_click(folderId); }} diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx index 9a70f43b17e..d1294afd00c 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx @@ -7,7 +7,7 @@ import { _ } from '@joplin/lib/locale'; import bridge from '../../../services/bridge'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import CommandService from '@joplin/lib/services/CommandService'; -import ListItemWrapper from './ListItemWrapper'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; @@ -15,6 +15,7 @@ const menuUtils = new MenuUtils(CommandService.instance()); interface Props { + anchorRef: ListItemRef; item: HeaderListItem; isSelected: boolean; onDrop: React.DragEventHandler|null; @@ -54,6 +55,7 @@ const HeaderItem: React.FC = props => { return ( ; + interface Props { + containerRef: ListItemRef; selected: boolean; itemIndex: number; itemCount: number; + expanded?: boolean|undefined; depth?: number; className?: string; children: (React.ReactNode[])|React.ReactNode; @@ -15,6 +18,7 @@ interface Props { onDragOver?: React.DragEventHandler; onDrop?: React.DragEventHandler; draggable?: boolean; + 'data-folder-id'?: string; } const ListItemWrapper: React.FC = props => { @@ -23,15 +27,17 @@ const ListItemWrapper: React.FC = props => { '--depth': props.depth, } as React.CSSProperties; }, [props.depth]); - + return (
= props => { role='treeitem' className={`list-item-wrapper ${props.selected ? '-selected' : ''} ${props.className ?? ''}`} style={style} + data-folder-id={props['data-folder-id']} > {props.children}
diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx index 3c0b9e0b6fd..8ddbbee2622 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx @@ -7,11 +7,12 @@ import BaseModel from '@joplin/lib/BaseModel'; import NoteCount from './NoteCount'; import Tag from '@joplin/lib/models/Tag'; import EmptyExpandLink from './EmptyExpandLink'; -import ListItemWrapper from './ListItemWrapper'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; export type TagLinkClickEvent = { tag: TagsWithNoteCountEntity|undefined }; interface Props { + anchorRef: ListItemRef; selected: boolean; tag: TagsWithNoteCountEntity; onTagDrop: React.DragEventHandler; @@ -37,6 +38,7 @@ const TagItem = (props: Props) => { return (