feat: close submenu if another submenu is opened (#911)

This commit is contained in:
Aman Harwara
2022-03-05 20:20:11 +05:30
committed by GitHub
parent 263640d476
commit 08fb913b0e
7 changed files with 265 additions and 273 deletions

View File

@@ -0,0 +1,129 @@
import { AppState } from '@/ui_models/app_state';
import {
calculateSubmenuStyle,
SubmenuStyle,
} from '@/utils/calculateSubmenuStyle';
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Icon } from '../Icon';
import { useCloseOnBlur } from '../utils';
type Props = {
appState: AppState;
};
export const AddTagOption: FunctionComponent<Props> = observer(
({ appState }) => {
const menuContainerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
});
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
const toggleTagsMenu = () => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
if (menuPosition) {
setMenuStyle(menuPosition);
console.log(menuPosition);
}
}
setIsMenuOpen(!isMenuOpen);
};
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(
menuButtonRef.current,
menuRef.current
);
if (newMenuPosition) {
setMenuStyle(newMenuPosition);
console.log(newMenuPosition);
}
}, []);
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle();
});
}
}, [isMenuOpen, recalculateMenuStyle]);
return (
<div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsMenuOpen(false);
}
}}
onBlur={closeOnBlur}
ref={menuButtonRef}
className="sn-dropdown-item justify-between"
>
<div className="flex items-center">
<Icon type="hashtag" className="mr-2 color-neutral" />
Add tag
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsMenuOpen(false);
menuButtonRef.current?.focus();
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
>
{appState.tags.tags.map((tag) => (
<button
key={tag.title}
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
onBlur={closeOnBlur}
onClick={() => {
appState.notes.isTagInSelectedNotes(tag)
? appState.notes.removeTagFromSelectedNotes(tag)
: appState.notes.addTagToSelectedNotes(tag);
}}
>
<span
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
${
appState.notes.isTagInSelectedNotes(tag)
? 'font-bold'
: ''
}`}
>
{appState.noteTags.getLongTitle(tag)}
</span>
</button>
))}
</DisclosurePanel>
</Disclosure>
</div>
);
}
);

View File

@@ -15,12 +15,12 @@ import {
calculateSubmenuStyle, calculateSubmenuStyle,
SubmenuStyle, SubmenuStyle,
} from '@/utils/calculateSubmenuStyle'; } from '@/utils/calculateSubmenuStyle';
import { useCloseOnBlur } from '../utils';
type ChangeEditorOptionProps = { type ChangeEditorOptionProps = {
appState: AppState; appState: AppState;
application: WebApplication; application: WebApplication;
note: SNNote; note: SNNote;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
}; };
type AccordionMenuGroup<T> = { type AccordionMenuGroup<T> = {
@@ -40,7 +40,6 @@ export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>;
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
application, application,
closeOnBlur,
note, note,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -50,9 +49,15 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
bottom: 0, bottom: 0,
maxHeight: 'auto', maxHeight: 'auto',
}); });
const menuContainerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => {
setIsOpen(open);
setIsVisible(open);
});
const toggleChangeEditorMenu = () => { const toggleChangeEditorMenu = () => {
if (!isOpen) { if (!isOpen) {
const menuStyle = calculateSubmenuStyle(buttonRef.current); const menuStyle = calculateSubmenuStyle(buttonRef.current);
@@ -81,49 +86,51 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
}, [isOpen]); }, [isOpen]);
return ( return (
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}> <div ref={menuContainerRef}>
<DisclosureButton <Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
onKeyDown={(event) => { <DisclosureButton
if (event.key === KeyboardKey.Escape) { onKeyDown={(event) => {
setIsOpen(false); if (event.key === KeyboardKey.Escape) {
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="sn-dropdown-item justify-between"
>
<div className="flex items-center">
<Icon type="dashboard" className="color-neutral mr-2" />
Change editor
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false);
buttonRef.current?.focus();
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
>
{isOpen && (
<ChangeEditorMenu
application={application}
closeOnBlur={closeOnBlur}
note={note}
isVisible={isVisible}
closeMenu={() => {
setIsOpen(false); setIsOpen(false);
}} }
/> }}
)} onBlur={closeOnBlur}
</DisclosurePanel> ref={buttonRef}
</Disclosure> className="sn-dropdown-item justify-between"
>
<div className="flex items-center">
<Icon type="dashboard" className="color-neutral mr-2" />
Change editor
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false);
buttonRef.current?.focus();
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
>
{isOpen && (
<ChangeEditorMenu
application={application}
closeOnBlur={closeOnBlur}
note={note}
isVisible={isVisible}
closeMenu={() => {
setIsOpen(false);
}}
/>
)}
</DisclosurePanel>
</Disclosure>
</div>
); );
}; };

View File

@@ -12,11 +12,11 @@ import { Action, ListedAccount, SNNote } from '@standardnotes/snjs';
import { Fragment, FunctionComponent } from 'preact'; import { Fragment, FunctionComponent } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Icon } from '../Icon'; import { Icon } from '../Icon';
import { useCloseOnBlur } from '../utils';
type Props = { type Props = {
application: WebApplication; application: WebApplication;
note: SNNote; note: SNNote;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
}; };
type ListedMenuGroup = { type ListedMenuGroup = {
@@ -230,8 +230,8 @@ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({
export const ListedActionsOption: FunctionComponent<Props> = ({ export const ListedActionsOption: FunctionComponent<Props> = ({
application, application,
note, note,
closeOnBlur,
}) => { }) => {
const menuContainerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null); const menuButtonRef = useRef<HTMLButtonElement>(null);
@@ -242,6 +242,8 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
maxHeight: 'auto', maxHeight: 'auto',
}); });
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
const toggleListedMenu = () => { const toggleListedMenu = () => {
if (!isMenuOpen) { if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current); const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
@@ -273,34 +275,36 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
}, [isMenuOpen, recalculateMenuStyle]); }, [isMenuOpen, recalculateMenuStyle]);
return ( return (
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}> <div ref={menuContainerRef}>
<DisclosureButton <Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
ref={menuButtonRef} <DisclosureButton
onBlur={closeOnBlur} ref={menuButtonRef}
className="sn-dropdown-item justify-between" onBlur={closeOnBlur}
> className="sn-dropdown-item justify-between"
<div className="flex items-center"> >
<Icon type="listed" className="color-neutral mr-2" /> <div className="flex items-center">
Listed actions <Icon type="listed" className="color-neutral mr-2" />
</div> Listed actions
<Icon type="chevron-right" className="color-neutral" /> </div>
</DisclosureButton> <Icon type="chevron-right" className="color-neutral" />
<DisclosurePanel </DisclosureButton>
ref={menuRef} <DisclosurePanel
style={{ ref={menuRef}
...menuStyle, style={{
position: 'fixed', ...menuStyle,
}} position: 'fixed',
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto" }}
> className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
{isMenuOpen && ( >
<ListedActionsMenu {isMenuOpen && (
application={application} <ListedActionsMenu
note={note} application={application}
recalculateMenuStyle={recalculateMenuStyle} note={note}
/> recalculateMenuStyle={recalculateMenuStyle}
)} />
</DisclosurePanel> )}
</Disclosure> </DisclosurePanel>
</Disclosure>
</div>
); );
}; };

View File

@@ -2,29 +2,20 @@ import { AppState } from '@/ui_models/app_state';
import { Icon } from '../Icon'; import { Icon } from '../Icon';
import { Switch } from '../Switch'; import { Switch } from '../Switch';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useRef, useState, useEffect, useMemo } from 'preact/hooks'; import { useState, useEffect, useMemo } from 'preact/hooks';
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure';
import { SNApplication, SNNote } from '@standardnotes/snjs'; import { SNApplication, SNNote } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { KeyboardModifier } from '@/services/ioService'; import { KeyboardModifier } from '@/services/ioService';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { ChangeEditorOption } from './ChangeEditorOption'; import { ChangeEditorOption } from './ChangeEditorOption';
import { import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
MENU_MARGIN_FROM_APP_BORDER,
MAX_MENU_SIZE_MULTIPLIER,
BYTES_IN_ONE_MEGABYTE,
} from '@/constants';
import { ListedActionsOption } from './ListedActionsOption'; import { ListedActionsOption } from './ListedActionsOption';
import { AddTagOption } from './AddTagOption';
export type NotesOptionsProps = { export type NotesOptionsProps = {
application: WebApplication; application: WebApplication;
appState: AppState; appState: AppState;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
onSubmenuChange?: (submenuOpen: boolean) => void;
}; };
type DeletePermanentlyButtonProps = { type DeletePermanentlyButtonProps = {
@@ -206,24 +197,7 @@ const NoteSizeWarning: FunctionComponent<{
) : null; ) : null;
export const NotesOptions = observer( export const NotesOptions = observer(
({ ({ application, appState, closeOnBlur }: NotesOptionsProps) => {
application,
appState,
closeOnBlur,
onSubmenuChange,
}: NotesOptionsProps) => {
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
top: number;
right?: number;
left?: number;
}>({
top: 0,
right: 0,
});
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>(
'auto'
);
const [altKeyDown, setAltKeyDown] = useState(false); const [altKeyDown, setAltKeyDown] = useState(false);
const toggleOn = (condition: (note: SNNote) => boolean) => { const toggleOn = (condition: (note: SNNote) => boolean) => {
@@ -246,14 +220,6 @@ export const NotesOptions = observer(
const unpinned = notes.some((note) => !note.pinned); const unpinned = notes.some((note) => !note.pinned);
const errored = notes.some((note) => note.errorDecrypting); const errored = notes.some((note) => note.errorDecrypting);
const tagsButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (onSubmenuChange) {
onSubmenuChange(tagsMenuOpen);
}
}, [tagsMenuOpen, onSubmenuChange]);
useEffect(() => { useEffect(() => {
const removeAltKeyObserver = application.io.addKeyObserver({ const removeAltKeyObserver = application.io.addKeyObserver({
modifiers: [KeyboardModifier.Alt], modifiers: [KeyboardModifier.Alt],
@@ -270,48 +236,6 @@ export const NotesOptions = observer(
}; };
}, [application]); }, [application]);
const openTagsMenu = () => {
const defaultFontSize = window.getComputedStyle(
document.documentElement
).fontSize;
const maxTagsMenuSize =
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
const { clientWidth, clientHeight } = document.documentElement;
const buttonRect = tagsButtonRef.current?.getBoundingClientRect();
const footerElementRect = document
.getElementById('footer-bar')
?.getBoundingClientRect();
const footerHeightInPx = footerElementRect?.height;
if (buttonRect && footerHeightInPx) {
if (
buttonRect.top + maxTagsMenuSize >
clientHeight - footerHeightInPx
) {
setTagsMenuMaxHeight(
clientHeight -
buttonRect.top -
footerHeightInPx -
MENU_MARGIN_FROM_APP_BORDER
);
}
if (buttonRect.right + maxTagsMenuSize > clientWidth) {
setTagsMenuPosition({
top: buttonRect.top,
right: clientWidth - buttonRect.left,
});
} else {
setTagsMenuPosition({
top: buttonRect.top,
left: buttonRect.right,
});
}
}
setTagsMenuOpen(!tagsMenuOpen);
};
const downloadSelectedItems = () => { const downloadSelectedItems = () => {
notes.forEach((note) => { notes.forEach((note) => {
const editor = application.componentManager.editorForNote(note); const editor = application.componentManager.editorForNote(note);
@@ -416,70 +340,12 @@ export const NotesOptions = observer(
<ChangeEditorOption <ChangeEditorOption
appState={appState} appState={appState}
application={application} application={application}
closeOnBlur={closeOnBlur}
note={notes[0]} note={notes[0]}
/> />
</> </>
)} )}
<div className="min-h-1px my-2 bg-border"></div> <div className="min-h-1px my-2 bg-border"></div>
{appState.tags.tagsCount > 0 && ( {appState.tags.tagsCount > 0 && <AddTagOption appState={appState} />}
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
}
}}
onBlur={closeOnBlur}
ref={tagsButtonRef}
className="sn-dropdown-item justify-between"
>
<div className="flex items-center">
<Icon type="hashtag" className={iconClass} />
{'Add tag'}
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
tagsButtonRef.current?.focus();
}
}}
style={{
...tagsMenuPosition,
maxHeight: tagsMenuMaxHeight,
position: 'fixed',
}}
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
>
{appState.tags.tags.map((tag) => (
<button
key={tag.title}
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
onBlur={closeOnBlur}
onClick={() => {
appState.notes.isTagInSelectedNotes(tag)
? appState.notes.removeTagFromSelectedNotes(tag)
: appState.notes.addTagToSelectedNotes(tag);
}}
>
<span
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
${
appState.notes.isTagInSelectedNotes(tag)
? 'font-bold'
: ''
}`}
>
{appState.noteTags.getLongTitle(tag)}
</span>
</button>
))}
</DisclosurePanel>
</Disclosure>
)}
{unpinned && ( {unpinned && (
<button <button
onBlur={closeOnBlur} onBlur={closeOnBlur}
@@ -604,11 +470,7 @@ export const NotesOptions = observer(
{notes.length === 1 ? ( {notes.length === 1 ? (
<> <>
<div className="min-h-1px my-2 bg-border"></div> <div className="min-h-1px my-2 bg-border"></div>
<ListedActionsOption <ListedActionsOption application={application} note={notes[0]} />
application={application}
closeOnBlur={closeOnBlur}
note={notes[0]}
/>
<div className="min-h-1px my-2 bg-border"></div> <div className="min-h-1px my-2 bg-border"></div>
<SpellcheckOptions appState={appState} note={notes[0]} /> <SpellcheckOptions appState={appState} note={notes[0]} />
<div className="min-h-1px my-2 bg-border"></div> <div className="min-h-1px my-2 bg-border"></div>

View File

@@ -30,11 +30,6 @@ export const NotesOptionsPanel = observer(
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null); const panelRef = useRef<HTMLDivElement>(null);
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen); const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
const [submenuOpen, setSubmenuOpen] = useState(false);
const onSubmenuChange = (open: boolean) => {
setSubmenuOpen(open);
};
return ( return (
<Disclosure <Disclosure
@@ -64,7 +59,7 @@ export const NotesOptionsPanel = observer(
> >
<DisclosureButton <DisclosureButton
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) { if (event.key === 'Escape') {
setOpen(false); setOpen(false);
} }
}} }}
@@ -77,7 +72,7 @@ export const NotesOptionsPanel = observer(
</DisclosureButton> </DisclosureButton>
<DisclosurePanel <DisclosurePanel
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) { if (event.key === 'Escape') {
setOpen(false); setOpen(false);
buttonRef.current?.focus(); buttonRef.current?.focus();
} }
@@ -96,7 +91,6 @@ export const NotesOptionsPanel = observer(
application={application} application={application}
appState={appState} appState={appState}
closeOnBlur={closeOnBlur} closeOnBlur={closeOnBlur}
onSubmenuChange={onSubmenuChange}
/> />
)} )}
</DisclosurePanel> </DisclosurePanel>

View File

@@ -6,10 +6,11 @@ import {
RefCallback, RefCallback,
ComponentChild, ComponentChild,
} from 'preact'; } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx'; import { JSXInternal } from 'preact/src/jsx';
import { MenuItem, MenuItemListElement } from './MenuItem'; import { MenuItem, MenuItemListElement } from './MenuItem';
import { KeyboardKey } from '@/services/ioService'; import { KeyboardKey } from '@/services/ioService';
import { useListKeyboardNavigation } from '../utils';
type MenuProps = { type MenuProps = {
className?: string; className?: string;
@@ -28,7 +29,6 @@ export const Menu: FunctionComponent<MenuProps> = ({
closeMenu, closeMenu,
isOpen, isOpen,
}: MenuProps) => { }: MenuProps) => {
const [currentIndex, setCurrentIndex] = useState(0);
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]); const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
const menuElementRef = useRef<HTMLMenuElement>(null); const menuElementRef = useRef<HTMLMenuElement>(null);
@@ -40,44 +40,21 @@ export const Menu: FunctionComponent<MenuProps> = ({
return; return;
} }
switch (event.key) { if (event.key === KeyboardKey.Escape) {
case KeyboardKey.Home: closeMenu?.();
setCurrentIndex(0); return;
break;
case KeyboardKey.End:
setCurrentIndex(
menuItemRefs.current.length ? menuItemRefs.current.length - 1 : 0
);
break;
case KeyboardKey.Down:
setCurrentIndex((index) => {
if (index + 1 < menuItemRefs.current.length) {
return index + 1;
} else {
return 0;
}
});
break;
case KeyboardKey.Up:
setCurrentIndex((index) => {
if (index - 1 > -1) {
return index - 1;
} else {
return menuItemRefs.current.length - 1;
}
});
break;
case KeyboardKey.Escape:
closeMenu?.();
break;
} }
}; };
useListKeyboardNavigation(menuElementRef);
useEffect(() => { useEffect(() => {
if (isOpen && menuItemRefs.current[currentIndex]) { if (isOpen && menuItemRefs.current.length > 0) {
menuItemRefs.current[currentIndex]?.focus(); setTimeout(() => {
menuElementRef.current?.focus();
});
} }
}, [currentIndex, isOpen]); }, [isOpen]);
const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => { const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => {
if (instance && instance.children) { if (instance && instance.children) {
@@ -128,7 +105,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
return ( return (
<menu <menu
className={`m-0 p-0 list-style-none ${className}`} className={`m-0 p-0 list-style-none focus:shadow-none ${className}`}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
ref={menuElementRef} ref={menuElementRef}
style={style} style={style}

View File

@@ -89,13 +89,17 @@ export const calculateDifferenceBetweenDatesInDays = (
export const useListKeyboardNavigation = ( export const useListKeyboardNavigation = (
container: Ref<HTMLElement | null> container: Ref<HTMLElement | null>
) => { ) => {
const [listItems, setListItems] = useState<NodeListOf<HTMLButtonElement>>(); const [listItems, setListItems] = useState<HTMLButtonElement[]>();
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0); const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
const focusItemWithIndex = useCallback( const focusItemWithIndex = useCallback(
(index: number) => { (index: number, items?: HTMLButtonElement[]) => {
setFocusedItemIndex(index); setFocusedItemIndex(index);
listItems?.[index]?.focus(); if (items && items.length > 0) {
items[index]?.focus();
} else {
listItems?.[index]?.focus();
}
}, },
[listItems] [listItems]
); );
@@ -103,7 +107,7 @@ export const useListKeyboardNavigation = (
useEffect(() => { useEffect(() => {
if (container.current) { if (container.current) {
container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE; container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE;
setListItems(container.current.querySelectorAll('button')); setListItems(Array.from(container.current.querySelectorAll('button')));
} }
}, [container]); }, [container]);
@@ -116,7 +120,13 @@ export const useListKeyboardNavigation = (
} }
if (!listItems?.length) { if (!listItems?.length) {
setListItems(container.current?.querySelectorAll('button')); setListItems(
Array.from(
container.current?.querySelectorAll(
'button'
) as NodeListOf<HTMLButtonElement>
)
);
} }
if (listItems) { if (listItems) {
@@ -143,16 +153,25 @@ export const useListKeyboardNavigation = (
const FIRST_ITEM_FOCUS_TIMEOUT = 20; const FIRST_ITEM_FOCUS_TIMEOUT = 20;
const containerFocusHandler = useCallback(() => { const containerFocusHandler = useCallback(() => {
if (listItems) { let temporaryItems = listItems && listItems?.length > 0 ? listItems : [];
const selectedItemIndex = Array.from(listItems).findIndex( if (!temporaryItems.length) {
temporaryItems = Array.from(
container.current?.querySelectorAll(
'button'
) as NodeListOf<HTMLButtonElement>
);
setListItems(temporaryItems);
}
if (temporaryItems.length > 0) {
const selectedItemIndex = Array.from(temporaryItems).findIndex(
(item) => item.dataset.selected (item) => item.dataset.selected
); );
const indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : 0; const indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : 0;
setTimeout(() => { setTimeout(() => {
focusItemWithIndex(indexToFocus); focusItemWithIndex(indexToFocus, temporaryItems);
}, FIRST_ITEM_FOCUS_TIMEOUT); }, FIRST_ITEM_FOCUS_TIMEOUT);
} }
}, [focusItemWithIndex, listItems]); }, [container, focusItemWithIndex, listItems]);
useEffect(() => { useEffect(() => {
const containerElement = container.current; const containerElement = container.current;