From 2f4cd67af0d5c9fbb5925535ab4aae3bce3eee0b Mon Sep 17 00:00:00 2001 From: Cyril Date: Fri, 8 Aug 2025 14:14:44 +0200 Subject: [PATCH] =?UTF-8?q?fixup!=20=E2=9C=A8(frontend)=20add=20keyboard?= =?UTF-8?q?=20navigation=20for=20subdocs=20with=20focus=20activation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc-tree/hooks/useDropdownKeyboardNav.ts | 62 ++++++++++--------- .../doc-tree/hooks/useKeyboardActivation.tsx | 32 ++++++++++ .../hooks/useTreeItemKeyboardActivate.tsx | 22 +------ 3 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownKeyboardNav.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownKeyboardNav.ts index 0fd41e6a6..36bb01d81 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownKeyboardNav.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownKeyboardNav.ts @@ -1,6 +1,9 @@ +// src/features/docs/doc-tree/hooks/useDropdownKeyboardNav.ts import { RefObject, useEffect } from 'react'; -import { DropdownMenuOption } from '../DropdownMenu'; +import { DropdownMenuOption } from '@/components/DropdownMenu'; + +import { useKeyboardActivation } from './useKeyboardActivation'; type UseDropdownKeyboardNavProps = { isOpen: boolean; @@ -19,6 +22,22 @@ export const useDropdownKeyboardNav = ({ setFocusedIndex, onOpenChange, }: UseDropdownKeyboardNavProps) => { + useKeyboardActivation(['Enter', ' '], isOpen, () => { + if (focusedIndex === -1) { + return; + } + + const enabledIndices = options + .map((opt, i) => (opt.show !== false && !opt.disabled ? i : -1)) + .filter((i) => i !== -1); + + const selectedOpt = options[enabledIndices[focusedIndex]]; + if (selectedOpt?.callback) { + onOpenChange(false); + void selectedOpt.callback(); + } + }); + useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!isOpen) { @@ -26,57 +45,42 @@ export const useDropdownKeyboardNav = ({ } const enabledIndices = options - .map((option, index) => - option.show !== false && !option.disabled ? index : -1, - ) - .filter((index) => index !== -1); + .map((opt, i) => (opt.show !== false && !opt.disabled ? i : -1)) + .filter((i) => i !== -1); switch (event.key) { - case 'ArrowDown': + case 'ArrowDown': { event.preventDefault(); const nextIndex = focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0; - const nextEnabledIndex = enabledIndices[nextIndex]; + const nextEnabled = enabledIndices[nextIndex]; setFocusedIndex(nextIndex); - menuItemRefs.current[nextEnabledIndex]?.focus(); + menuItemRefs.current[nextEnabled]?.focus(); break; + } - case 'ArrowUp': + case 'ArrowUp': { event.preventDefault(); const prevIndex = focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1; - const prevEnabledIndex = enabledIndices[prevIndex]; + const prevEnabled = enabledIndices[prevIndex]; setFocusedIndex(prevIndex); - menuItemRefs.current[prevEnabledIndex]?.focus(); + menuItemRefs.current[prevEnabled]?.focus(); break; + } - case 'Enter': - case ' ': - event.preventDefault(); - if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) { - const selectedOptionIndex = enabledIndices[focusedIndex]; - const selectedOption = options[selectedOptionIndex]; - if (selectedOption && selectedOption.callback) { - onOpenChange(false); - void selectedOption.callback(); - } - } - break; - - case 'Escape': + case 'Escape': { event.preventDefault(); onOpenChange(false); break; + } } }; if (isOpen) { document.addEventListener('keydown', handleKeyDown); } - - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; + return () => document.removeEventListener('keydown', handleKeyDown); }, [ isOpen, focusedIndex, diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx new file mode 100644 index 000000000..6d81e9dc6 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; + +export const useKeyboardActivation = ( + keys: string[], + enabled: boolean, + action: () => void, + capture = false, +) => { + useEffect(() => { + if (!enabled) { + return; + } + + const onKeyDown = (e: KeyboardEvent) => { + const modal = document.querySelector('.c__modal__scroller'); + + if (modal) { + e.stopPropagation(); + e.stopImmediatePropagation(); + return; + } + + if (keys.includes(e.key)) { + e.preventDefault(); + action(); + } + }; + + document.addEventListener('keydown', onKeyDown, capture); + return () => document.removeEventListener('keydown', onKeyDown, capture); + }, [keys, enabled, action, capture]); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeItemKeyboardActivate.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeItemKeyboardActivate.tsx index d0208af7f..2dbfbccff 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeItemKeyboardActivate.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeItemKeyboardActivate.tsx @@ -1,26 +1,8 @@ -import { useEffect } from 'react'; +import { useKeyboardActivation } from './useKeyboardActivation'; -/** - * While the node has keyboard focus, run `activate()` on Enter / Space. - * Gives tree-items the same "open on Enter" behaviour that clicks already have. - */ export const useTreeItemKeyboardActivate = ( focused: boolean, activate: () => void, ) => { - useEffect(() => { - if (!focused) { - return; - } - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - activate(); - } - }; - - document.addEventListener('keydown', onKeyDown, true); - return () => document.removeEventListener('keydown', onKeyDown, true); - }, [focused, activate]); + useKeyboardActivation(['Enter', ' '], focused, activate, true); };