* Move shared components to a packages/ directory so they can be publish more sensibly * Iterate towards split out shared-components module * Move shared component source into src/ subdir * Fix up imports * Include shared components in babel-ing (again) * Remove now unused dependencies * Update import in storybook preview * ...except of course they aren't unused if we import the shared components by source * Ignore shared components deps * Add shared-components to i18n paths and upgrade web-i18n to version that supports doing so * Move storybook stuff to shared-components * Seems we don't need this anymore... * Remove unused deps and remove storybook plugin from eslint * Presumably working-directory is only valid on run steps * Ignore dep & run prettier * Prettier on knips.ts * Hopefully run in right dir * Remember how to software write * Okay... how about THIS way? * Oh right, they were git ignored. Sigh. * Add concurrently * Ignore in knip * Better? * Paaaaaaaackageeeeeeees * More packages * Move playwright snapshots * Still need a custom snapshots dir * Add eslint back * Oh, now knip sees them * Fix another import * Don't lint shared-components with everything else Okay, eslint & tsconfig are tied too closely for this to work and running tsc on the shared components will need its deps installing * Maybe lint shared components please? * Not quite * Remove storybook again Re-check if it does work without it * Remove storybook eslint plugin as we're not linting storybook here anymore * Remove this too * We do need it here though
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
/*
|
|
* Copyright 2025 New Vector Ltd.
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
* Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
useCallback,
|
|
useRef,
|
|
type RefObject,
|
|
type KeyboardEvent,
|
|
type KeyboardEventHandler,
|
|
type FocusEventHandler,
|
|
type FocusEvent,
|
|
} from "react";
|
|
|
|
/**
|
|
* A hook that provides keyboard navigation for a list of options.
|
|
*/
|
|
export function useListKeyboardNavigation(): {
|
|
listRef: RefObject<HTMLUListElement | null>;
|
|
onKeyDown: KeyboardEventHandler<HTMLUListElement>;
|
|
onFocus: FocusEventHandler<HTMLUListElement>;
|
|
} {
|
|
const listRef = useRef<HTMLUListElement>(null);
|
|
|
|
const onFocus = useCallback((evt: FocusEvent<HTMLUListElement>) => {
|
|
if (!listRef.current) return;
|
|
|
|
if (evt.target === listRef.current) {
|
|
// By default, focus the selected item
|
|
let selectedChild = listRef.current?.firstElementChild;
|
|
|
|
// If there is a selected item, focus that instead
|
|
for (const child of listRef.current.children) {
|
|
if (child.getAttribute("aria-selected") === "true") {
|
|
selectedChild = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
(selectedChild as HTMLElement)?.focus();
|
|
}
|
|
}, []);
|
|
|
|
const onKeyDown = useCallback((evt: KeyboardEvent<HTMLUListElement>) => {
|
|
const { key } = evt;
|
|
|
|
let handled = false;
|
|
|
|
switch (key) {
|
|
case "Enter":
|
|
case " ": {
|
|
handled = true;
|
|
(document.activeElement as HTMLElement).click();
|
|
break;
|
|
}
|
|
case "ArrowDown": {
|
|
handled = true;
|
|
const currentFocus = document.activeElement;
|
|
if (listRef.current?.contains(currentFocus) && currentFocus) {
|
|
(currentFocus.nextElementSibling as HTMLElement)?.focus();
|
|
}
|
|
break;
|
|
}
|
|
case "ArrowUp": {
|
|
handled = true;
|
|
const currentFocus = document.activeElement;
|
|
if (listRef.current?.contains(currentFocus) && currentFocus) {
|
|
(currentFocus.previousElementSibling as HTMLElement)?.focus();
|
|
}
|
|
break;
|
|
}
|
|
case "Home": {
|
|
handled = true;
|
|
(listRef.current?.firstElementChild as HTMLElement)?.focus();
|
|
break;
|
|
}
|
|
case "End": {
|
|
handled = true;
|
|
(listRef.current?.lastElementChild as HTMLElement)?.focus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (handled) {
|
|
evt.preventDefault();
|
|
}
|
|
}, []);
|
|
return { listRef, onKeyDown, onFocus };
|
|
}
|