From 0ecbde6bac0c93ef232fea9c9f7fcad037d739f1 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sun, 30 Jan 2022 20:28:35 +0530 Subject: [PATCH] feat: improve change editor menu keyboard navigation (#831) --- .../javascripts/components/NotesList.tsx | 7 +- .../NotesOptions/ChangeEditorOption.tsx | 2 +- .../changeEditor/EditorAccordionMenu.tsx | 151 ++++++++++-------- app/assets/javascripts/views/constants.ts | 5 + app/assets/stylesheets/_sn.scss | 5 + 5 files changed, 98 insertions(+), 72 deletions(-) diff --git a/app/assets/javascripts/components/NotesList.tsx b/app/assets/javascripts/components/NotesList.tsx index 56a0c6a33..abf742e29 100644 --- a/app/assets/javascripts/components/NotesList.tsx +++ b/app/assets/javascripts/components/NotesList.tsx @@ -6,6 +6,10 @@ import { SNNote } from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { NotesListItem } from './NotesListItem'; +import { + FOCUSABLE_BUT_NOT_TABBABLE, + NOTES_LIST_SCROLL_THRESHOLD, +} from '@/views/constants'; type Props = { application: WebApplication; @@ -16,9 +20,6 @@ type Props = { paginate: () => void; }; -const FOCUSABLE_BUT_NOT_TABBABLE = -1; -const NOTES_LIST_SCROLL_THRESHOLD = 200; - export const NotesList: FunctionComponent = observer( ({ application, diff --git a/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx index 0aacc230a..35422a18d 100644 --- a/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx +++ b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx @@ -270,7 +270,7 @@ export const ChangeEditorOption: FunctionComponent = ({ ...changeEditorMenuPosition, position: 'fixed', }} - className="sn-dropdown flex flex-col py-1 max-h-120 min-w-68 fixed overflow-y-auto" + className="sn-dropdown flex flex-col py-0.5 max-h-120 min-w-68 fixed overflow-y-auto" > const getGroupBtnId = (groupId: string) => groupId + '-button'; +const isElementHidden = (element: Element) => !element.clientHeight; + export const EditorAccordionMenu: FunctionComponent< EditorAccordionMenuProps > = ({ @@ -34,7 +37,6 @@ export const EditorAccordionMenu: FunctionComponent< }) => { const [activeGroupId, setActiveGroupId] = useState(''); const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]); - const [focusedItemIndex, setFocusedItemIndex] = useState(); const premiumModal = usePremiumModal(); const isSelectedEditor = useCallback( @@ -64,78 +66,85 @@ export const EditorAccordionMenu: FunctionComponent< useEffect(() => { if ( - typeof focusedItemIndex === 'undefined' && - activeGroupId.length && - menuItemRefs.current.length + isOpen && + !menuItemRefs.current.some((btn) => btn === document.activeElement) ) { - const activeGroupIndex = menuItemRefs.current.findIndex( - (item) => item?.id === getGroupBtnId(activeGroupId) - ); - setFocusedItemIndex(activeGroupIndex); - } - }, [activeGroupId, focusedItemIndex]); + const selectedEditor = groups + .map((group) => group.items) + .flat() + .find((item) => isSelectedEditor(item)); - useEffect(() => { - if ( - typeof focusedItemIndex === 'number' && - focusedItemIndex > -1 && - isOpen - ) { - const focusedItem = menuItemRefs.current[focusedItemIndex]; - const containingGroupId = focusedItem?.closest( - '[data-accordion-group]' - )?.id; - if ( - !focusedItem?.id && - containingGroupId && - containingGroupId !== activeGroupId - ) { - setActiveGroupId(containingGroupId); + if (selectedEditor) { + const editorButton = menuItemRefs.current.find( + (btn) => btn?.dataset.itemName === selectedEditor.name + ); + editorButton?.focus(); } - focusedItem?.focus(); } - }, [activeGroupId, focusedItemIndex, isOpen]); + }, [groups, isOpen, isSelectedEditor]); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - switch (e.key) { - case KeyboardKey.Up: { - if ( - typeof focusedItemIndex === 'number' && - menuItemRefs.current.length - ) { - let previousItemIndex = focusedItemIndex - 1; - if (previousItemIndex < 0) { - previousItemIndex = menuItemRefs.current.length - 1; - } - setFocusedItemIndex(previousItemIndex); - } - e.preventDefault(); - break; - } - case KeyboardKey.Down: { - if ( - typeof focusedItemIndex === 'number' && - menuItemRefs.current.length - ) { - let nextItemIndex = focusedItemIndex + 1; - if (nextItemIndex > menuItemRefs.current.length - 1) { - nextItemIndex = 0; - } - setFocusedItemIndex(nextItemIndex); - } - e.preventDefault(); - break; - } + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === KeyboardKey.Down || e.key === KeyboardKey.Up) { + e.preventDefault(); + } else { + return; + } + + let items = menuItemRefs.current; + + if (!activeGroupId) { + items = items.filter((btn) => btn?.id); + } + + const currentItemIndex = + items.findIndex((btn) => btn === document.activeElement) ?? 0; + + if (e.key === KeyboardKey.Up) { + let previousItemIndex = currentItemIndex - 1; + if (previousItemIndex < 0) { + previousItemIndex = items.length - 1; } - }; + const previousItem = items[previousItemIndex]; + if (previousItem) { + if (isElementHidden(previousItem)) { + const previousItemGroupId = previousItem.closest( + '[data-accordion-group]' + )?.id; + if (previousItemGroupId) { + setActiveGroupId(previousItemGroupId); + } + setTimeout(() => { + previousItem.focus(); + }, 10); + } - document.addEventListener('keydown', handleKeyDown); + previousItem.focus(); + } + } - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [focusedItemIndex, groups]); + if (e.key === KeyboardKey.Down) { + let nextItemIndex = currentItemIndex + 1; + if (nextItemIndex > items.length - 1) { + nextItemIndex = 0; + } + const nextItem = items[nextItemIndex]; + if (nextItem) { + if (isElementHidden(nextItem)) { + const nextItemGroupId = nextItem.closest( + '[data-accordion-group]' + )?.id; + if (nextItemGroupId) { + setActiveGroupId(nextItemGroupId); + } + setTimeout(() => { + nextItem.focus(); + }, 10); + } + + nextItem?.focus(); + } + } + }; const selectEditor = (item: EditorMenuItem) => { if (item.component) { @@ -160,12 +169,17 @@ export const EditorAccordionMenu: FunctionComponent< return ( -
+

-
+
); })} diff --git a/app/assets/javascripts/views/constants.ts b/app/assets/javascripts/views/constants.ts index fc5422b31..dd83ea727 100644 --- a/app/assets/javascripts/views/constants.ts +++ b/app/assets/javascripts/views/constants.ts @@ -1,6 +1,11 @@ export const PANEL_NAME_NOTES = 'notes'; export const PANEL_NAME_NAVIGATION = 'navigation'; + export const EMAIL_REGEX = /^([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/; + export const MENU_MARGIN_FROM_APP_BORDER = 5; export const MAX_MENU_SIZE_MULTIPLIER = 30; + +export const FOCUSABLE_BUT_NOT_TABBABLE = -1; +export const NOTES_LIST_SCROLL_THRESHOLD = 200; diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 21b268ba4..38b7d7c6c 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -504,6 +504,11 @@ padding-right: 3rem; } +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + .sn-component .py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem;