Skip to content

Commit

Permalink
Improving sidebar ARIA information
Browse files Browse the repository at this point in the history
  • Loading branch information
personalizedrefrigerator committed Oct 7, 2024
1 parent 51f8e52 commit c434a17
Show file tree
Hide file tree
Showing 14 changed files with 65 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/app-desktop/gui/EmojiBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ export default (props: Props) => {
return spanFontSize;
}, [props.width, props.height, props.emoji, containerReady, containerRef]);

return <div className="emoji-box" ref={el => { containerRef.current = el; setContainerReady(true); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: props.width, height: props.height, fontSize }}>{props.emoji}</div>;
return <div className="emoji-box" role='img' ref={el => { containerRef.current = el; setContainerReady(true); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: props.width, height: props.height, fontSize }}>{props.emoji}</div>;
};
2 changes: 1 addition & 1 deletion packages/app-desktop/gui/FolderIconBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function(props: Props) {
} else if (folderIcon.type === FolderIconType.DataUrl) {
return <img style={{ width, height, opacity }} src={folderIcon.dataUrl} />;
} else if (folderIcon.type === FolderIconType.FontAwesome) {
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name}></i>;
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name} role='img'></i>;
} else {
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ const FolderAndTagList: React.FC<Props> = props => {
listItems: listItems,
});

const [selectedListElement, setSelectedListElement] = useState<HTMLElement|null>(null);
const listContainerRef = useRef<HTMLDivElement|null>(null);
const onRenderItem = useOnRenderItem({
...props,
selectedIndex,
onSelectedElementShown: setSelectedListElement,
listItems,
containerRef: listContainerRef,
});

const onKeyEventHandler = useOnSidebarKeyDownHandler({
Expand All @@ -56,11 +56,12 @@ const FolderAndTagList: React.FC<Props> = props => {
});

const itemListRef = useRef<ItemList<ListItem>>();
const { focusSidebar } = useFocusHandler({ itemListRef, selectedListElement, selectedIndex, listItems });
const { focusSidebar } = useFocusHandler({ itemListRef, selectedIndex, listItems });

useSidebarCommandHandler({ focusSidebar });

const [itemListContainer, setItemListContainer] = useState<HTMLDivElement|null>(null);
listContainerRef.current = itemListContainer;
const listHeight = useElementHeight(itemListContainer);
const listStyle = useMemo(() => ({ height: listHeight }), [listHeight]);

Expand Down
16 changes: 11 additions & 5 deletions packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { focus } from '@joplin/lib/utils/focusHandler';

interface Props {
itemListRef: RefObject<ItemList<ListItem>>;
selectedListElement: HTMLElement|null;
selectedIndex: number;
listItems: ListItem[];
}
Expand Down Expand Up @@ -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 };
};
Expand Down
18 changes: 16 additions & 2 deletions packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,16 +43,22 @@ interface Props {
plugins: PluginStates;
folders: FolderEntity[];
collapsedFolderIds: string[];
containerRef: React.RefObject<HTMLDivElement>;

selectedIndex: number;
onSelectedElementShown: (element: HTMLElement)=> void;
listItems: ListItem[];
}

type ItemContextMenuListener = MouseEventHandler<HTMLElement>;

const menuUtils = new MenuUtils(CommandService.instance());

const focusListItem = (item: HTMLElement) => {
focus('useOnRenderItem', item);
};

const noFocusListItem = () => {};

const useOnRenderItem = (props: Props) => {

const pluginsRef = useRef<PluginStates>(null);
Expand Down Expand Up @@ -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 <TagItem
key={item.key}
anchorRef={anchorRef}
selected={selected}
onClick={tagItem_click}
onTagDrop={onTagDrop_}
Expand Down Expand Up @@ -365,6 +375,7 @@ const useOnRenderItem = (props: Props) => {
}
return <FolderItem
key={item.key}
anchorRef={anchorRef}
selected={selected}
folderId={folder.id}
folderTitle={Folder.displayTitle(folder)}
Expand All @@ -388,6 +399,7 @@ const useOnRenderItem = (props: Props) => {
} else if (item.kind === ListItemType.Header) {
return <HeaderItem
key={item.id}
anchorRef={anchorRef}
item={item}
isSelected={selected}
onDrop={item.supportsFolderDrop ? onFolderDrop_ : null}
Expand All @@ -397,6 +409,7 @@ const useOnRenderItem = (props: Props) => {
} else if (item.kind === ListItemType.AllNotes) {
return <AllNotesItem
key={item.key}
anchorRef={anchorRef}
selected={selected}
index={index}
itemCount={itemCount}
Expand All @@ -405,6 +418,7 @@ const useOnRenderItem = (props: Props) => {
return (
<ListItemWrapper
key={item.key}
containerRef={anchorRef}
selected={selected}
itemIndex={index}
itemCount={itemCount}
Expand All @@ -430,7 +444,7 @@ const useOnRenderItem = (props: Props) => {
showFolderIcons,
tagItem_click,
props.selectedIndex,
props.onSelectedElementShown,
props.containerRef,
itemCount,
]);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ 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;
const MenuItem = bridge().MenuItem;

interface Props {
dispatch: Dispatch;
anchorRef: ListItemRef;
selected: boolean;
index: number;
itemCount: number;
Expand Down Expand Up @@ -49,6 +50,7 @@ const AllNotesItem: React.FC<Props> = props => {

return (
<ListItemWrapper
containerRef={props.anchorRef}
key="allNotesHeader"
selected={props.selected}
className={'list-item-container list-item-depth-0 all-notes'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const ExpandIcon: React.FC<ExpandIconProps> = props => {
}
return _('Expand %s', props.targetTitle);
};
return <i className={classNames.join(' ')} aria-label={getLabel()}></i>;
return <i className={classNames.join(' ')} title={getLabel()} role='img'></i>;
};

export default ExpandIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ interface ExpandLinkProps {

const ExpandLink: React.FC<ExpandLinkProps> = props => {
return props.hasChildren ? (
<a className='sidebar-expand-link' data-folder-id={props.folderId} onClick={props.onClick}>
// The expand/collapse information is conveyed through ARIA.
<a className='sidebar-expand-link' data-folder-id={props.folderId} onClick={props.onClick} aria-label=''>
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} targetTitle={props.folderTitle}/>
</a>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -27,6 +27,7 @@ const renderFolderIcon = (folderIcon: FolderIcon) => {
};

interface FolderItemProps {
anchorRef: ListItemRef;
hasChildren: boolean;
showFolderIcon: boolean;
isExpanded: boolean;
Expand Down Expand Up @@ -67,19 +68,20 @@ function FolderItem(props: FolderItemProps) {

return (
<ListItemWrapper
containerRef={props.anchorRef}
depth={depth}
selected={selected}
itemIndex={props.index}
itemCount={props.itemCount}
aria-expanded={hasChildren ? props.isExpanded : undefined}
expanded={hasChildren ? props.isExpanded : undefined}
className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`}
onDragStart={onFolderDragStart_}
onDragOver={onFolderDragOver_}
onDrop={onFolderDrop_}
draggable={draggable}
data-folder-id={folderId}
>
<ExpandLink hasChildren={hasChildren} folderTitle={folderTitle} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
<ExpandLink aria-label='' hasChildren={hasChildren} folderTitle={folderTitle} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
<StyledListItemAnchor
className="list-item"
isConflictFolder={folderId === Folder.conflictFolderId()}
Expand All @@ -90,7 +92,7 @@ function FolderItem(props: FolderItemProps) {
onContextMenu={itemContextMenu}
data-folder-id={folderId}
onDoubleClick={onFolderToggleClick_}

onClick={() => {
folderItem_click(folderId);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ 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;
const menuUtils = new MenuUtils(CommandService.instance());


interface Props {
anchorRef: ListItemRef;
item: HeaderListItem;
isSelected: boolean;
onDrop: React.DragEventHandler|null;
Expand Down Expand Up @@ -54,6 +55,7 @@ const HeaderItem: React.FC<Props> = props => {

return (
<ListItemWrapper
containerRef={props.anchorRef}
selected={props.isSelected}
itemIndex={props.index}
itemCount={props.itemCount}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { useMemo } from 'react';

export type ListItemRef = React.Ref<HTMLDivElement>;

interface Props {
containerRef: ListItemRef;
selected: boolean;
itemIndex: number;
itemCount: number;
expanded?: boolean|undefined;
depth?: number;
className?: string;
children: (React.ReactNode[])|React.ReactNode;
Expand All @@ -15,6 +18,7 @@ interface Props {
onDragOver?: React.DragEventHandler;
onDrop?: React.DragEventHandler;
draggable?: boolean;
'data-folder-id'?: string;
}

const ListItemWrapper: React.FC<Props> = props => {
Expand All @@ -23,15 +27,17 @@ const ListItemWrapper: React.FC<Props> = props => {
'--depth': props.depth,
} as React.CSSProperties;
}, [props.depth]);

return (
<div
ref={props.containerRef}
aria-posinset={props.itemIndex + 1}
aria-setsize={props.itemCount}
aria-selected={props.selected}
aria-level={props.depth}
// Focus is handled directly by the item list
tabIndex={-1}
aria-expanded={props.expanded}
// aria-level is 1-based, where depth is zero-based
aria-level={props.depth + 1}
tabIndex={props.selected ? 0 : -1}
onDrag={props.onDrag}
onDragStart={props.onDragStart}
onDragOver={props.onDragOver}
Expand All @@ -40,6 +46,7 @@ const ListItemWrapper: React.FC<Props> = props => {
role='treeitem'
className={`list-item-wrapper ${props.selected ? '-selected' : ''} ${props.className ?? ''}`}
style={style}
data-folder-id={props['data-folder-id']}
>
{props.children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>;
Expand All @@ -37,6 +38,7 @@ const TagItem = (props: Props) => {

return (
<ListItemWrapper
containerRef={props.anchorRef}
selected={selected}
className={`list-item-container ${selected ? 'selected' : ''}`}
onDrop={props.onTagDrop}
Expand Down
1 change: 1 addition & 0 deletions packages/tools/cspell/dictionary4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ Famegear
rcompare
tabindex
Backblaze
treeitem

0 comments on commit c434a17

Please sign in to comment.