From 38be0e81a9669dad1dc71e54a88426f997d6fdcd Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:59:51 -0700 Subject: [PATCH] Desktop: Accessibility: Add ARIA information to the sidebar's notebook and tag list (#11196) --- .eslintignore | 3 + .gitignore | 3 + packages/app-desktop/gui/FolderIconBox.tsx | 2 +- packages/app-desktop/gui/ItemList.tsx | 83 +++++++++++++------ .../gui/Sidebar/FolderAndTagList.tsx | 19 ++++- .../gui/Sidebar/hooks/useFocusHandler.ts | 46 ++++------ .../gui/Sidebar/hooks/useOnRenderItem.tsx | 59 +++++++++---- .../Sidebar/hooks/useOnRenderListWrapper.tsx | 46 ++++++++++ .../hooks/useOnSidebarKeyDownHandler.ts | 41 +++++---- .../gui/Sidebar/hooks/useSidebarListData.ts | 21 ++--- .../gui/Sidebar/hooks/utils/toggleHeader.ts | 10 +++ .../listItemComponents/AllNotesItem.tsx | 24 ++++-- .../listItemComponents/EmptyExpandLink.tsx | 5 +- .../Sidebar/listItemComponents/ExpandIcon.tsx | 7 +- .../Sidebar/listItemComponents/ExpandLink.tsx | 5 +- .../Sidebar/listItemComponents/FolderItem.tsx | 51 +++++++++--- .../Sidebar/listItemComponents/HeaderItem.tsx | 39 ++++----- .../listItemComponents/ListItemWrapper.tsx | 66 +++++++++++++++ .../Sidebar/listItemComponents/TagItem.tsx | 19 +++-- packages/app-desktop/gui/Sidebar/style.scss | 4 +- .../app-desktop/gui/Sidebar/styles/index.ts | 16 ---- .../gui/Sidebar/styles/list-item-wrapper.scss | 25 ++++++ .../gui/Sidebar/styles/new-folder-button.scss | 25 ++++++ .../Sidebar/styles/sidebar-expand-link.scss | 1 + packages/app-desktop/gui/Sidebar/types.ts | 2 +- .../integration-tests/models/Sidebar.ts | 2 +- .../integration-tests/sidebar.spec.ts | 14 ++-- packages/lib/utils/focusHandler.ts | 20 +++-- packages/tools/cspell/dictionary4.txt | 1 + 29 files changed, 468 insertions(+), 191 deletions(-) create mode 100644 packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.tsx create mode 100644 packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts create mode 100644 packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx create mode 100644 packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss create mode 100644 packages/app-desktop/gui/Sidebar/styles/new-folder-button.scss diff --git a/.eslintignore b/.eslintignore index 659a3627c..3bb40cf20 100644 --- a/.eslintignore +++ b/.eslintignore @@ -419,16 +419,19 @@ packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js packages/app-desktop/gui/Sidebar/commands/index.js packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js +packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js +packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.js packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js +packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js packages/app-desktop/gui/Sidebar/styles/index.js diff --git a/.gitignore b/.gitignore index 51bb0ad32..f1c19367a 100644 --- a/.gitignore +++ b/.gitignore @@ -396,16 +396,19 @@ packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js packages/app-desktop/gui/Sidebar/commands/index.js packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js +packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js +packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.js packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js +packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js packages/app-desktop/gui/Sidebar/styles/index.js diff --git a/packages/app-desktop/gui/FolderIconBox.tsx b/packages/app-desktop/gui/FolderIconBox.tsx index ca67fbfee..0f2e8ffd5 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/ItemList.tsx b/packages/app-desktop/gui/ItemList.tsx index 0ffd582a1..145e177f0 100644 --- a/packages/app-desktop/gui/ItemList.tsx +++ b/packages/app-desktop/gui/ItemList.tsx @@ -5,12 +5,18 @@ interface Props { style: React.CSSProperties & { height: number }; itemHeight: number; items: ItemType[]; + disabled?: boolean; - onKeyDown?: KeyboardEventHandler; - itemRenderer: (item: ItemType, index: number)=> React.JSX.Element; className?: string; + + itemRenderer: (item: ItemType, index: number)=> React.JSX.Element; + renderContentWrapper?: (listItems: React.ReactNode[])=> React.ReactNode; + onKeyDown?: KeyboardEventHandler; onItemDrop?: DragEventHandler; + selectedIndex?: number; + alwaysRenderSelection?: boolean; + id?: string; role?: string; 'aria-label'?: string; @@ -23,13 +29,13 @@ interface State { class ItemList extends React.Component, State> { - private scrollTop_: number; + private lastScrollTop_: number; private listRef: React.MutableRefObject; public constructor(props: Props) { super(props); - this.scrollTop_ = 0; + this.lastScrollTop_ = 0; this.listRef = React.createRef(); @@ -46,10 +52,10 @@ class ItemList extends React.Component, State> { public updateStateItemIndexes(props: Props = undefined) { if (typeof props === 'undefined') props = this.props; - const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight); + const topItemIndex = Math.floor(this.offsetScroll() / props.itemHeight); const visibleItemCount = this.visibleItemCount(props); - let bottomItemIndex = topItemIndex + (visibleItemCount - 1); + let bottomItemIndex = topItemIndex + visibleItemCount; if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1; this.setState({ @@ -63,7 +69,7 @@ class ItemList extends React.Component, State> { } public offsetScroll() { - return this.scrollTop_; + return this.container?.scrollTop ?? this.lastScrollTop_; } public get container() { @@ -79,7 +85,7 @@ class ItemList extends React.Component, State> { } public onScroll: UIEventHandler = event => { - this.scrollTop_ = (event.target as HTMLElement).scrollTop; + this.lastScrollTop_ = (event.target as HTMLElement).scrollTop; this.updateStateItemIndexes(); }; @@ -104,23 +110,28 @@ class ItemList extends React.Component, State> { } public makeItemIndexVisible(itemIndex: number) { - if (this.isIndexVisible(itemIndex)) return; + // The first and last visible indices are often partially out of view and can thus be made more visible + if (this.isIndexVisible(itemIndex) && itemIndex !== this.lastVisibleIndex && itemIndex !== this.firstVisibleIndex) { + return; + } - const top = this.firstVisibleIndex; - - let scrollTop = 0; - if (itemIndex < top) { + const currentScroll = this.offsetScroll(); + let scrollTop = currentScroll; + if (itemIndex <= this.firstVisibleIndex) { scrollTop = this.props.itemHeight * itemIndex; - } else { - scrollTop = this.props.itemHeight * itemIndex - (this.visibleItemCount() - 1) * this.props.itemHeight; + } else if (itemIndex >= this.lastVisibleIndex - 1) { + const scrollBottom = this.props.itemHeight * (itemIndex + 1); + scrollTop = scrollBottom - this.props.style.height; } if (scrollTop < 0) scrollTop = 0; - this.scrollTop_ = scrollTop; - this.listRef.current.scrollTop = scrollTop; + if (currentScroll !== scrollTop) { + this.lastScrollTop_ = scrollTop; + this.listRef.current.scrollTop = scrollTop; - this.updateStateItemIndexes(); + this.updateStateItemIndexes(); + } } // shouldComponentUpdate(nextProps, nextState) { @@ -155,18 +166,42 @@ class ItemList extends React.Component, State> { return
; }; - const itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)]; + type RenderRange = { from: number; to: number }; + const renderableBlocks: RenderRange[] = []; - for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) { - const itemComp = this.props.itemRenderer(items[i], i); - itemComps.push(itemComp); + if (this.props.alwaysRenderSelection && isFinite(this.props.selectedIndex)) { + const selectionVisible = this.props.selectedIndex >= this.state.topItemIndex && this.props.selectedIndex <= this.state.bottomItemIndex; + const isValidSelection = this.props.selectedIndex >= 0 && this.props.selectedIndex < items.length; + if (!selectionVisible && isValidSelection) { + renderableBlocks.push({ from: this.props.selectedIndex, to: this.props.selectedIndex }); + } } - itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight)); + renderableBlocks.push({ from: this.state.topItemIndex, to: this.state.bottomItemIndex }); + + // Ascending order + renderableBlocks.sort(({ from: fromA }, { from: fromB }) => fromA - fromB); + + const itemComps: React.ReactNode[] = []; + for (let i = 0; i < renderableBlocks.length; i++) { + const currentBlock = renderableBlocks[i]; + if (i === 0) { + itemComps.push(blankItem('top', currentBlock.from * this.props.itemHeight)); + } + + for (let j = currentBlock.from; j <= currentBlock.to; j++) { + const itemComp = this.props.itemRenderer(items[j], j); + itemComps.push(itemComp); + } + + const nextBlockFrom = i + 1 < renderableBlocks.length ? renderableBlocks[i + 1].from : items.length; + itemComps.push(blankItem(`after-${i}`, (nextBlockFrom - currentBlock.to - 1) * this.props.itemHeight)); + } const classes = ['item-list']; if (this.props.className) classes.push(this.props.className); + const wrapContent = this.props.renderContentWrapper ?? ((children) => <>{children}); return (
extends React.Component, State> { onKeyDown={this.onKeyDown} onDrop={this.onDrop} > - {itemComps} + {wrapContent(itemComps)}
); } diff --git a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx index 85bb46069..a882f6224 100644 --- a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx +++ b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx @@ -14,6 +14,7 @@ import useFocusHandler from './hooks/useFocusHandler'; import useOnRenderItem from './hooks/useOnRenderItem'; import { ListItem } from './types'; import useSidebarCommandHandler from './hooks/useSidebarCommandHandler'; +import useOnRenderListWrapper from './hooks/useOnRenderListWrapper'; interface Props { dispatch: Dispatch; @@ -39,11 +40,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({ @@ -55,14 +57,17 @@ 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]); + const onRenderContentWrapper = useOnRenderListWrapper({ selectedIndex, onKeyDown: onKeyEventHandler }); + return (
= props => { className='items' ref={itemListRef} style={listStyle} + items={listItems} itemRenderer={onRenderItem} - onKeyDown={onKeyEventHandler} + renderContentWrapper={onRenderContentWrapper} + + // The selected item is the only item with tabindex=0. Always render it + // to allow the item list to be focused. + alwaysRenderSelection={true} + selectedIndex={selectedIndex} itemHeight={30} /> diff --git a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts index 9fad738dc..4f14ed027 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts @@ -1,29 +1,16 @@ -import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from 'react'; +import { RefObject, useCallback, useEffect, useMemo, useRef } from 'react'; import { ListItem } from '../types'; import ItemList from '../../ItemList'; import { focus } from '@joplin/lib/utils/focusHandler'; interface Props { itemListRef: RefObject>; - selectedListElement: HTMLElement|null; selectedIndex: number; listItems: ListItem[]; } -const useFocusAfterNextRenderHandler = ( - shouldFocusAfterNextRender: MutableRefObject, - selectedListElement: HTMLElement|null, -) => { - useEffect(() => { - if (!shouldFocusAfterNextRender.current || !selectedListElement) return; - focus('FolderAndTagList/useFocusHandler/afterRender', selectedListElement); - shouldFocusAfterNextRender.current = false; - }, [selectedListElement, shouldFocusAfterNextRender]); -}; - -const useRefocusOnSelectionChangeHandler = ( +const useScrollToSelectionHandler = ( itemListRef: RefObject>, - shouldFocusAfterNextRender: MutableRefObject, listItems: ListItem[], selectedIndex: number, ) => { @@ -49,32 +36,33 @@ const useRefocusOnSelectionChangeHandler = ( useEffect(() => { if (!itemListRef.current || !selectedItemKey) return; - const hasFocus = !!itemListRef.current.container.querySelector(':scope :focus'); - shouldFocusAfterNextRender.current = hasFocus; + const hasFocus = !!itemListRef.current.container.contains(document.activeElement); if (hasFocus) { itemListRef.current.makeItemIndexVisible(selectedIndexRef.current); } - }, [selectedItemKey, itemListRef, shouldFocusAfterNextRender]); + }, [selectedItemKey, itemListRef]); }; const useFocusHandler = (props: Props) => { - const { itemListRef, selectedListElement, selectedIndex, listItems } = props; + const { itemListRef, selectedIndex, listItems } = props; - // When set to true, when selectedListElement next changes, select it. - const shouldFocusAfterNextRender = useRef(false); - - useRefocusOnSelectionChangeHandler(itemListRef, shouldFocusAfterNextRender, listItems, selectedIndex); - useFocusAfterNextRenderHandler(shouldFocusAfterNextRender, selectedListElement); + useScrollToSelectionHandler(itemListRef, listItems, selectedIndex); const focusSidebar = useCallback(() => { - if (!selectedListElement || !itemListRef.current.isIndexVisible(selectedIndex)) { + if (!itemListRef.current.isIndexVisible(selectedIndex)) { itemListRef.current.makeItemIndexVisible(selectedIndex); - shouldFocusAfterNextRender.current = true; - } else { - focus('FolderAndTagList/useFocusHandler/focusSidebar', selectedListElement); } - }, [selectedListElement, selectedIndex, itemListRef]); + + const focusableItem = itemListRef.current.container.querySelector('[role="treeitem"][tabindex="0"]'); + const focusableContainer = itemListRef.current.container.querySelector('[role="tree"][tabindex="0"]'); + if (focusableItem) { + focus('FolderAndTagList/focusSidebarItem', focusableItem); + } else if (focusableContainer) { + // Handles the case where no items in the tree can be focused. + focus('FolderAndTagList/focusSidebarTree', focusableContainer); + } + }, [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 aad5bf709..9dc34e967 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -29,6 +29,8 @@ import Logger from '@joplin/utils/Logger'; 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; @@ -41,15 +43,27 @@ interface Props { plugins: PluginStates; folders: FolderEntity[]; collapsedFolderIds: string[]; + containerRef: React.RefObject; selectedIndex: number; - onSelectedElementShown: (element: HTMLElement)=> void; + listItems: ListItem[]; } type ItemContextMenuListener = MouseEventHandler; const menuUtils = new MenuUtils(CommandService.instance()); +const focusListItem = (item: HTMLElement|null) => { + if (item) { + // Avoid scrolling to the selected item when refocusing the note list. Such a refocus + // can happen if the note list rerenders and the selection is scrolled out of view and + // can cause scroll to change unexpectedly. + focus('useOnRenderItem', item, { preventScroll: true }); + } +}; + +const noFocusListItem = () => {}; + const useOnRenderItem = (props: Props) => { const pluginsRef = useRef(null); @@ -326,26 +340,24 @@ const useOnRenderItem = (props: Props) => { const selectedIndexRef = useRef(props.selectedIndex); selectedIndexRef.current = props.selectedIndex; + const itemCount = props.listItems.length; return useCallback((item: ListItem, index: number) => { const selected = props.selectedIndex === index; - const anchorRefCallback = selected ? ( - (element: HTMLElement) => { - if (selectedIndexRef.current === index) { - props.onSelectedElementShown(element); - } - } - ) : null; + 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 ; } else if (item.kind === ListItemType.Folder) { const folder = item.folder; @@ -368,7 +380,7 @@ const useOnRenderItem = (props: Props) => { } return { shareId={folder.share_id} parentId={folder.parent_id} showFolderIcon={showFolderIcons} + index={index} + itemCount={itemCount} />; } else if (item.kind === ListItemType.Header) { return ; } else if (item.kind === ListItemType.AllNotes) { return ; } else if (item.kind === ListItemType.Spacer) { return ( - + +
+
); } else { const exhaustivenessCheck: never = item; @@ -421,7 +451,8 @@ const useOnRenderItem = (props: Props) => { showFolderIcons, tagItem_click, props.selectedIndex, - props.onSelectedElementShown, + props.containerRef, + itemCount, ]); }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.tsx new file mode 100644 index 000000000..af0140d1d --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { useCallback } from 'react'; +import { _ } from '@joplin/lib/locale'; +import CommandService from '@joplin/lib/services/CommandService'; + +interface Props { + selectedIndex: number; + onKeyDown: React.KeyboardEventHandler; +} + +const onAddFolderButtonClick = () => { + void CommandService.instance().execute('newFolder'); +}; + +const NewFolderButton = () => { + // To allow it to be accessed by accessibility tools, the new folder button + // is not included in the portion of the list with role='tree'. + return ; +}; + +const useOnRenderListWrapper = ({ selectedIndex, onKeyDown }: Props) => { + return useCallback((listItems: React.ReactNode[]) => { + const listHasValidSelection = selectedIndex >= 0; + const allowContainerFocus = !listHasValidSelection; + return <> + +
+ {...listItems} +
+ ; + }, [selectedIndex, onKeyDown]); +}; + +export default useOnRenderListWrapper; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts index acd335c3e..b0c85897f 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts @@ -1,7 +1,8 @@ import { Dispatch } from 'redux'; -import { FolderListItem, ListItem, ListItemType, SetSelectedIndexCallback } from '../types'; +import { ListItem, ListItemType, SetSelectedIndexCallback } from '../types'; import { KeyboardEventHandler, useCallback } from 'react'; import CommandService from '@joplin/lib/services/CommandService'; +import toggleHeader from './utils/toggleHeader'; interface Props { dispatch: Dispatch; @@ -12,15 +13,20 @@ interface Props { } -const isToggleShortcut = (keyCode: string, selectedItem: FolderListItem, collapsedFolderIds: string[]) => { +const isToggleShortcut = (keyCode: string, selectedItem: ListItem, collapsedFolderIds: string[]) => { + if (selectedItem.kind !== ListItemType.Header && selectedItem.kind !== ListItemType.Folder) { + return false; + } + if (!['Space', 'ArrowLeft', 'ArrowRight'].includes(keyCode)) { return false; } + if (keyCode === 'Space') { return true; } - const isCollapsed = collapsedFolderIds.includes(selectedItem.folder.id); + const isCollapsed = 'expanded' in selectedItem ? !selectedItem.expanded : collapsedFolderIds.includes(selectedItem.folder.id); return (keyCode === 'ArrowRight') === isCollapsed; }; @@ -29,21 +35,22 @@ const useOnSidebarKeyDownHandler = (props: Props) => { return useCallback>((event) => { const selectedItem = listItems[selectedIndex]; - if (selectedItem?.kind === ListItemType.Folder && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) { - event.preventDefault(); - - dispatch({ - type: 'FOLDER_TOGGLE', - id: selectedItem.folder.id, - }); - } - - if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a - event.preventDefault(); - } - let indexChange = 0; - if (event.code === 'ArrowUp') { + + if (selectedItem && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) { + event.preventDefault(); + + if (selectedItem.kind === ListItemType.Folder) { + dispatch({ + type: 'FOLDER_TOGGLE', + id: selectedItem.folder.id, + }); + } else if (selectedItem.kind === ListItemType.Header) { + toggleHeader(selectedItem.id); + } + } else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a + event.preventDefault(); + } else if (event.code === 'ArrowUp') { indexChange = -1; } else if (event.code === 'ArrowDown') { indexChange = 1; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.ts b/packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.ts index f110722dd..e5f05f311 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.ts @@ -3,8 +3,7 @@ import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagLi import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types'; import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared'; import { _ } from '@joplin/lib/locale'; -import CommandService from '@joplin/lib/services/CommandService'; -import Setting from '@joplin/lib/models/Setting'; +import toggleHeader from './utils/toggleHeader'; interface Props { tags: TagsWithNoteCountEntity[]; @@ -14,16 +13,6 @@ interface Props { tagHeaderIsExpanded: boolean; } -const onAddFolderButtonClick = () => { - void CommandService.instance().execute('newFolder'); -}; - -const onHeaderClick = (headerId: HeaderId) => { - const settingKey = headerId === HeaderId.TagHeader ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded'; - const current = Setting.value(settingKey); - Setting.setValue(settingKey, !current); -}; - const useSidebarListData = (props: Props): ListItem[] => { const tagItems = useMemo(() => { return renderTags(props.tags, (tag): TagListItem => { @@ -60,10 +49,10 @@ const useSidebarListData = (props: Props): ListItem[] => { kind: ListItemType.Header, label: _('Notebooks'), iconName: 'icon-notebooks', + expanded: props.folderHeaderIsExpanded, id: HeaderId.FolderHeader, key: HeaderId.FolderHeader, - onClick: onHeaderClick, - onPlusButtonClick: onAddFolderButtonClick, + onClick: toggleHeader, extraProps: { ['data-folder-id']: '', }, @@ -79,10 +68,10 @@ const useSidebarListData = (props: Props): ListItem[] => { kind: ListItemType.Header, label: _('Tags'), iconName: 'icon-tags', + expanded: props.tagHeaderIsExpanded, id: HeaderId.TagHeader, key: HeaderId.TagHeader, - onClick: onHeaderClick, - onPlusButtonClick: null, + onClick: toggleHeader, extraProps: { }, supportsFolderDrop: false, }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts b/packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts new file mode 100644 index 000000000..719d2704d --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts @@ -0,0 +1,10 @@ +import Setting from '@joplin/lib/models/Setting'; +import { HeaderId } from '../../types'; + +const toggleHeader = (headerId: HeaderId) => { + const settingKey = headerId === HeaderId.TagHeader ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded'; + const current = Setting.value(settingKey); + Setting.setValue(settingKey, !current); +}; + +export default toggleHeader; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx index 70a5e5130..2bfaf2c68 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyledAllNotesIcon, StyledListItem, StyledListItemAnchor } from '../styles'; +import { StyledAllNotesIcon, StyledListItemAnchor } from '../styles'; import { useCallback } from 'react'; import { Dispatch } from 'redux'; import bridge from '../../../services/bridge'; @@ -10,6 +10,7 @@ import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSort import { _ } from '@joplin/lib/locale'; import { connect } from 'react-redux'; import EmptyExpandLink from './EmptyExpandLink'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids'); const Menu = bridge().Menu; @@ -17,8 +18,10 @@ const MenuItem = bridge().MenuItem; interface Props { dispatch: Dispatch; + anchorRef: ListItemRef; selected: boolean; - anchorRef: React.Ref; + index: number; + itemCount: number; } const menuUtils = new MenuUtils(CommandService.instance()); @@ -46,21 +49,28 @@ const AllNotesItem: React.FC = props => { }, []); return ( - + - + {_('All notes')} - + ); }; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.tsx index ea47ca9d8..79f4d9523 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import ExpandIcon from './ExpandIcon'; interface Props { + className?: string; } -const EmptyExpandLink: React.FC = _props => { - return ; +const EmptyExpandLink: React.FC = props => { + return ; }; export default EmptyExpandLink; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.tsx index d9ba70f1c..72a5235a9 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.tsx @@ -23,11 +23,12 @@ const ExpandIcon: React.FC = props => { return undefined; } if (props.isExpanded) { - return _('Collapse %s', props.targetTitle); + return _('Expanded, press space to collapse.'); } - return _('Expand %s', props.targetTitle); + return _('Collapsed, press space to expand.'); }; - return ; + const label = getLabel(); + 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 74d1d02ea..2828fd470 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx @@ -8,16 +8,17 @@ interface ExpandLinkProps { folderTitle: string; hasChildren: boolean; isExpanded: boolean; + className: string; onClick: MouseEventHandler; } const ExpandLink: React.FC = props => { return props.hasChildren ? ( - + ) : ( - + ); }; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx index dd7af6140..f2ffc77ba 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types'; import ExpandLink from './ExpandLink'; -import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles'; +import { StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles'; import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types'; import FolderIconBox from '../../FolderIconBox'; import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash'; @@ -10,6 +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, { ListItemRef } from './ListItemWrapper'; const renderFolderIcon = (folderIcon: FolderIcon) => { if (!folderIcon) { @@ -26,6 +27,7 @@ const renderFolderIcon = (folderIcon: FolderIcon) => { }; interface FolderItemProps { + anchorRef: ListItemRef; hasChildren: boolean; showFolderIcon: boolean; isExpanded: boolean; @@ -43,7 +45,9 @@ interface FolderItemProps { onFolderToggleClick_: ItemClickListener; shareId: string; selected: boolean; - anchorRef: React.Ref; + + index: number; + itemCount: number; } function FolderItem(props: FolderItemProps) { @@ -63,29 +67,50 @@ function FolderItem(props: FolderItemProps) { }; return ( - - + { folderItem_click(folderId); }} - onDoubleClick={onFolderToggleClick_} > {doRenderFolderIcon()}{folderTitle} {shareIcon} - + + ); } diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx index 357a17a7e..08080961c 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import { useCallback } from 'react'; -import { ButtonLevel } from '../../Button/Button'; -import { StyledAddButton, StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles'; +import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles'; import { HeaderId, HeaderListItem } from '../types'; -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, { ListItemRef } from './ListItemWrapper'; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; @@ -14,9 +13,12 @@ const menuUtils = new MenuUtils(CommandService.instance()); interface Props { + anchorRef: ListItemRef; item: HeaderListItem; + isSelected: boolean; onDrop: React.DragEventHandler|null; - anchorRef: React.Ref; + index: number; + itemCount: number; } const HeaderItem: React.FC = props => { @@ -42,30 +44,25 @@ const HeaderItem: React.FC = props => { } }, [itemId]); - const addButton = ; - return ( -
- - + + {item.label} - { item.onPlusButtonClick && addButton } -
+ ); }; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx new file mode 100644 index 000000000..89e246afe --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx @@ -0,0 +1,66 @@ +import { ModelType } from '@joplin/lib/BaseModel'; +import * as React from 'react'; +import { useMemo } from 'react'; + +export type ListItemRef = React.Ref; + +interface Props { + containerRef: ListItemRef; + selected: boolean; + itemIndex: number; + itemCount: number; + expanded?: boolean|undefined; + depth: number; + className?: string; + highlightOnHover: boolean; + children: (React.ReactNode[])|React.ReactNode; + + onContextMenu?: React.MouseEventHandler; + onDrag?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; + onDragOver?: React.DragEventHandler; + onDrop?: React.DragEventHandler; + draggable?: boolean; + 'data-folder-id'?: string; + 'data-id'?: string; + 'data-type'?: ModelType; +} + +const ListItemWrapper: React.FC = props => { + const style = useMemo(() => { + return { + '--depth': props.depth, + } as React.CSSProperties; + }, [props.depth]); + + return ( +
+ {props.children} +
+ ); +}; + +export default ListItemWrapper; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx index 6730173f0..15bfe8b03 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx @@ -1,22 +1,26 @@ import Setting from '@joplin/lib/models/Setting'; import * as React from 'react'; import { useCallback } from 'react'; -import { StyledListItem, StyledListItemAnchor, StyledSpanFix } from '../styles'; +import { StyledListItemAnchor, StyledSpanFix } from '../styles'; import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types'; import BaseModel from '@joplin/lib/BaseModel'; import NoteCount from './NoteCount'; import Tag from '@joplin/lib/models/Tag'; import EmptyExpandLink from './EmptyExpandLink'; +import ListItemWrapper, { ListItemRef } from './ListItemWrapper'; export type TagLinkClickEvent = { tag: TagsWithNoteCountEntity|undefined }; interface Props { + anchorRef: ListItemRef; selected: boolean; - anchorRef: React.Ref; tag: TagsWithNoteCountEntity; onTagDrop: React.DragEventHandler; onContextMenu: React.MouseEventHandler; onClick: (event: TagLinkClickEvent)=> void; + + itemCount: number; + index: number; } const TagItem = (props: Props) => { @@ -33,18 +37,21 @@ const TagItem = (props: Props) => { }, [props.onClick, tag]); return ( - { {Tag.displayTitle(tag)} {noteCount} - + ); }; diff --git a/packages/app-desktop/gui/Sidebar/style.scss b/packages/app-desktop/gui/Sidebar/style.scss index 59247115d..f1598f188 100644 --- a/packages/app-desktop/gui/Sidebar/style.scss +++ b/packages/app-desktop/gui/Sidebar/style.scss @@ -1,6 +1,8 @@ @use 'styles/folder-and-tag-list.scss'; +@use 'styles/list-item-wrapper.scss'; @use 'styles/note-count-label.scss'; @use 'styles/sidebar-expand-icon.scss'; @use 'styles/sidebar-expand-link.scss'; @use 'styles/sidebar-header-container.scss'; -@use 'styles/sidebar-spacer-item.scss'; \ No newline at end of file +@use 'styles/sidebar-spacer-item.scss'; +@use 'styles/new-folder-button.scss'; \ No newline at end of file diff --git a/packages/app-desktop/gui/Sidebar/styles/index.ts b/packages/app-desktop/gui/Sidebar/styles/index.ts index 37ba9fa9e..ff59ce8a4 100644 --- a/packages/app-desktop/gui/Sidebar/styles/index.ts +++ b/packages/app-desktop/gui/Sidebar/styles/index.ts @@ -49,22 +49,6 @@ export const StyledHeaderLabel = styled.span` font-weight: bold; `; -export const StyledListItem = styled.div` - box-sizing: border-box; - height: 30px; - display: flex; - flex-direction: row; - align-items: center; - padding-left: ${(props: StyleProps) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px; - background: ${(props: StyleProps) => props.selected ? props.theme.selectedColor2 : 'none'}; - /*text-transform: ${(props: StyleProps) => props.isSpecialItem ? 'uppercase' : 'none'};*/ - transition: 0.1s; - - &:hover { - background-color: ${(props: StyleProps) => props.theme.backgroundColorHover2}; - } -`; - function listItemTextColor(props: StyleProps) { if (props.isConflictFolder) return props.theme.colorError2; if (props.isSpecialItem) return props.theme.colorFaded2; diff --git a/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss b/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss new file mode 100644 index 000000000..a742fc935 --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss @@ -0,0 +1,25 @@ + +.list-item-wrapper { + box-sizing: border-box; + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + padding-left: calc(var(--joplin-main-padding) + (var(--depth) * 16px) - 16px); + background: none; + transition: 0.1s; + + // Show the toggle button first, even if it's markup is included later for a better screen reader + // experience. + > .toggle { + order: -1; + } + + &.-selected { + background: var(--joplin-selected-color2); + } + + &.-highlight-on-hover:hover { + background-color: var(--joplin-background-color-hover2); + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/Sidebar/styles/new-folder-button.scss b/packages/app-desktop/gui/Sidebar/styles/new-folder-button.scss new file mode 100644 index 000000000..2bfcb379d --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/styles/new-folder-button.scss @@ -0,0 +1,25 @@ + +.new-folder-button { + position: absolute; + top: 0; + inset-inline-end: 0; + + padding-inline-end: 15px; + padding-top: 4px; + height: 30px; + border: none; + + background-color: transparent; + font-size: var(--joplin-toolbar-icon-size); + color: var(--joplin-color2); + + &:hover { + color: var(--joplin-color-hover2); + background: none; + } + + &:active { + color: var(--joplin-color-active2); + background: none; + } +} diff --git a/packages/app-desktop/gui/Sidebar/styles/sidebar-expand-link.scss b/packages/app-desktop/gui/Sidebar/styles/sidebar-expand-link.scss index fa40ebaee..45f0ec18f 100644 --- a/packages/app-desktop/gui/Sidebar/styles/sidebar-expand-link.scss +++ b/packages/app-desktop/gui/Sidebar/styles/sidebar-expand-link.scss @@ -5,6 +5,7 @@ opacity: 0.8; text-decoration: none; padding-right: 8px; + text-align: center; display: flex; align-items: center; width: 16px; diff --git a/packages/app-desktop/gui/Sidebar/types.ts b/packages/app-desktop/gui/Sidebar/types.ts index 59fc7de04..4eb9b7202 100644 --- a/packages/app-desktop/gui/Sidebar/types.ts +++ b/packages/app-desktop/gui/Sidebar/types.ts @@ -21,10 +21,10 @@ interface BaseListItem { export interface HeaderListItem extends BaseListItem { kind: ListItemType.Header; label: string; + expanded: boolean; iconName: string; id: HeaderId; onClick: ((headerId: HeaderId, event: ReactMouseEvent)=> void)|null; - onPlusButtonClick: MouseEventHandler|null; extraProps: Record; supportsFolderDrop: boolean; } diff --git a/packages/app-desktop/integration-tests/models/Sidebar.ts b/packages/app-desktop/integration-tests/models/Sidebar.ts index d2c13b111..1a35d2a4b 100644 --- a/packages/app-desktop/integration-tests/models/Sidebar.ts +++ b/packages/app-desktop/integration-tests/models/Sidebar.ts @@ -19,7 +19,7 @@ export default class Sidebar { const submitButton = this.mainScreen.dialog.getByRole('button', { name: 'OK' }); await submitButton.click(); - return this.container.getByText(title); + return this.container.getByRole('treeitem', { name: title }); } private async sortBy(electronApp: ElectronApplication, option: string) { diff --git a/packages/app-desktop/integration-tests/sidebar.spec.ts b/packages/app-desktop/integration-tests/sidebar.spec.ts index 94d293783..5835abb69 100644 --- a/packages/app-desktop/integration-tests/sidebar.spec.ts +++ b/packages/app-desktop/integration-tests/sidebar.spec.ts @@ -56,24 +56,24 @@ test.describe('sidebar', () => { await sidebar.forceUpdateSorting(electronApp); + await expect(childFolderHeader).toBeVisible(); await childFolderHeader.dragTo(parentFolderHeader); // Verify that it's now a child folder -- expand and collapse the parent - const collapseButton = sidebar.container.getByRole('link', { name: 'Collapse Parent folder' }); - await expect(collapseButton).toBeVisible(); - await collapseButton.click(); + await expect(parentFolderHeader).toHaveJSProperty('ariaExpanded', 'true'); + const toggleButton = parentFolderHeader.getByRole('button', { name: /^(Expand|Collapse)/ }); + await toggleButton.click(); // Should be collapsed await expect(childFolderHeader).not.toBeAttached(); + await expect(parentFolderHeader).toHaveJSProperty('ariaExpanded', 'false'); - const expandButton = sidebar.container.getByRole('link', { name: 'Expand Parent folder' }); - await expandButton.click(); + await toggleButton.click(); // Should be possible to move back to the root const rootFolderHeader = sidebar.container.getByText('Notebooks'); await childFolderHeader.dragTo(rootFolderHeader); - await expect(collapseButton).not.toBeVisible(); - await expect(expandButton).not.toBeVisible(); + await expect(toggleButton).not.toBeVisible(); }); test('all notes section should list all notes', async ({ electronApp, mainWindow }) => { diff --git a/packages/lib/utils/focusHandler.ts b/packages/lib/utils/focusHandler.ts index be7ab5599..af38c0b2e 100644 --- a/packages/lib/utils/focusHandler.ts +++ b/packages/lib/utils/focusHandler.ts @@ -12,12 +12,16 @@ enum ToggleFocusAction { Blur = 'blur', } +interface FocusOptions { + preventScroll: boolean; +} + interface FocusableElement { - focus: ()=> void; + focus: (options?: FocusOptions)=> void; blur: ()=> void; } -const toggleFocus = (source: string, element: FocusableElement, action: ToggleFocusAction) => { +const toggleFocus = (source: string, element: FocusableElement, action: ToggleFocusAction, options: FocusOptions|null) => { if (!element) { logger.warn(`Tried action "${action}" on an undefined element: ${source}`); return; @@ -29,15 +33,19 @@ const toggleFocus = (source: string, element: FocusableElement, action: ToggleFo } logger.debug(`Action "${action}" from "${source}"`); - element[action](); + if (options) { + element[action](options); + } else { + element[action](); + } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -export const focus = (source: string, element: any) => { - toggleFocus(source, element, ToggleFocusAction.Focus); +export const focus = (source: string, element: any, options: FocusOptions|null = null) => { + toggleFocus(source, element, ToggleFocusAction.Focus, options); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export const blur = (source: string, element: any) => { - toggleFocus(source, element, ToggleFocusAction.Blur); + toggleFocus(source, element, ToggleFocusAction.Blur, null); }; diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index e67f10bb0..ae72b46fd 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -132,3 +132,4 @@ Famegear rcompare tabindex Backblaze +treeitem