mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-12 01:47:01 +02:00
fixup! ✨(frontend) add keyboard navigation for subdocs with focus activation
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user