Skip to content

Commit

Permalink
feat: improve change editor menu keyboard navigation (#831)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara authored Jan 30, 2022
1 parent b932e2a commit 0ecbde6
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 70 deletions.
7 changes: 4 additions & 3 deletions app/assets/javascripts/components/NotesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,9 +20,6 @@ type Props = {
paginate: () => void;
};

const FOCUSABLE_BUT_NOT_TABBABLE = -1;
const NOTES_LIST_SCROLL_THRESHOLD = 200;

export const NotesList: FunctionComponent<Props> = observer(
({
application,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
...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"
>
<PremiumModalProvider state={appState.features}>
<EditorAccordionMenu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Icon } from '@/components/Icon';
import { usePremiumModal } from '@/components/Premium';
import { KeyboardKey } from '@/services/ioService';
import { WebApplication } from '@/ui_models/application';
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/views/constants';
import { SNComponent } from '@standardnotes/snjs';
import { Fragment, FunctionComponent } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
Expand All @@ -22,6 +23,8 @@ const getGroupId = (group: EditorMenuGroup) =>

const getGroupBtnId = (groupId: string) => groupId + '-button';

const isElementHidden = (element: Element) => !element.clientHeight;

export const EditorAccordionMenu: FunctionComponent<
EditorAccordionMenuProps
> = ({
Expand All @@ -34,7 +37,6 @@ export const EditorAccordionMenu: FunctionComponent<
}) => {
const [activeGroupId, setActiveGroupId] = useState('');
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
const [focusedItemIndex, setFocusedItemIndex] = useState<number>();
const premiumModal = usePremiumModal();

const isSelectedEditor = useCallback(
Expand Down Expand Up @@ -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);
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);
}
e.preventDefault();
break;
setTimeout(() => {
previousItem.focus();
}, 10);
}

previousItem.focus();
}
};
}

document.addEventListener('keydown', handleKeyDown);
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);
}

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [focusedItemIndex, groups]);
nextItem?.focus();
}
}
};

const selectEditor = (item: EditorMenuItem) => {
if (item.component) {
Expand All @@ -160,12 +169,17 @@ export const EditorAccordionMenu: FunctionComponent<

return (
<Fragment key={groupId}>
<div id={groupId} data-accordion-group>
<div
id={groupId}
data-accordion-group
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
onKeyDown={handleKeyDown}
>
<h3 className="m-0">
<button
aria-controls={contentId}
aria-expanded={activeGroupId === groupId}
className="sn-dropdown-item focus:bg-info-backdrop justify-between py-2.5"
className="sn-dropdown-item focus:bg-info-backdrop justify-between py-3"
id={buttonId}
type="button"
onClick={() => {
Expand Down Expand Up @@ -211,6 +225,7 @@ export const EditorAccordionMenu: FunctionComponent<
return (
<button
role="radio"
data-item-name={item.name}
onClick={() => {
selectEditor(item);
}}
Expand Down Expand Up @@ -247,7 +262,7 @@ export const EditorAccordionMenu: FunctionComponent<
</div>
</div>
</div>
<div className="min-h-1px my-1 bg-border hide-if-last-child"></div>
<div className="min-h-1px bg-border hide-if-last-child"></div>
</Fragment>
);
})}
Expand Down
5 changes: 5 additions & 0 deletions app/assets/javascripts/views/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions app/assets/stylesheets/_sn.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 0ecbde6

Please sign in to comment.