Commit ba92ed1b authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Migrate keyboard-navigation to TS

parent 22dd6fd6
import type { RefObject } from 'preact';
import { useEffect } from 'preact/hooks'; import { useEffect } from 'preact/hooks';
import { ListenerCollection } from './listener-collection'; import { ListenerCollection } from './listener-collection';
/** function isElementDisabled(
* @param {HTMLElement & { disabled?: boolean }} element element: HTMLElement & { disabled?: boolean }
*/ ): element is HTMLElement & { disabled: true } {
function isElementDisabled(element) {
return typeof element.disabled === 'boolean' && element.disabled; return typeof element.disabled === 'boolean' && element.disabled;
} }
/** @param {HTMLElement} element */ function isElementVisible(element: HTMLElement): boolean {
function isElementVisible(element) {
return element.offsetParent !== null; return element.offsetParent !== null;
} }
export type ArrowKeyNavigationOptions = {
/**
* Whether to focus the first element in the set of matching elements when the
* component is mounted
*/
autofocus?: boolean;
/** Enable navigating elements using left/right arrow keys */
horizontal?: boolean;
/** Enable navigating elements using up/down arrow keys */
vertical?: boolean;
/** CSS selector which specifies the elements that navigation moves between */
selector?: string;
};
/** /**
* Enable arrow key navigation between interactive descendants of a * Enable arrow key navigation between interactive descendants of a
* container element. * container element.
...@@ -45,24 +59,15 @@ function isElementVisible(element) { ...@@ -45,24 +59,15 @@ function isElementVisible(element) {
* *
* [1] https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex * [1] https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex
* [2] https://www.w3.org/TR/wai-aria-practices/#keyboard * [2] https://www.w3.org/TR/wai-aria-practices/#keyboard
*
* @param {import('preact').RefObject<HTMLElement>} containerRef
* @param {object} options
* @param {boolean} [options.autofocus] - Whether to focus the first element
* in the set of matching elements when the component is mounted
* @param {boolean} [options.horizontal] - Enable navigating elements using left/right arrow keys
* @param {boolean} [options.vertical] - Enable navigating elements using up/down arrow keys
* @param {string} [options.selector] - CSS selector which specifies the
* elements that navigation moves between
*/ */
export function useArrowKeyNavigation( export function useArrowKeyNavigation(
containerRef, containerRef: RefObject<HTMLElement>,
{ {
autofocus = false, autofocus = false,
horizontal = true, horizontal = true,
vertical = true, vertical = true,
selector = 'a,button', selector = 'a,button',
} = {} }: ArrowKeyNavigationOptions = {}
) { ) {
useEffect(() => { useEffect(() => {
if (!containerRef.current) { if (!containerRef.current) {
...@@ -71,8 +76,8 @@ export function useArrowKeyNavigation( ...@@ -71,8 +76,8 @@ export function useArrowKeyNavigation(
const container = containerRef.current; const container = containerRef.current;
const getNavigableElements = () => { const getNavigableElements = () => {
const elements = /** @type {HTMLElement[]} */ ( const elements: HTMLElement[] = Array.from(
Array.from(container.querySelectorAll(selector)) container.querySelectorAll(selector)
); );
return elements.filter( return elements.filter(
el => isElementVisible(el) && !isElementDisabled(el) el => isElementVisible(el) && !isElementDisabled(el)
...@@ -85,14 +90,13 @@ export function useArrowKeyNavigation( ...@@ -85,14 +90,13 @@ export function useArrowKeyNavigation(
* Exactly one element will have `tabindex=0` and all others will have * Exactly one element will have `tabindex=0` and all others will have
* `tabindex=1`. * `tabindex=1`.
* *
* @param {HTMLElement[]} elements * @param currentIndex - Index of element in `elements` to make current.
* @param {number} currentIndex - Index of element in `elements` to make current.
* Defaults to the current element if there is one, or the first element * Defaults to the current element if there is one, or the first element
* otherwise. * otherwise.
* @param {boolean} setFocus - Whether to focus the current element * @param setFocus - Whether to focus the current element
*/ */
const updateTabIndexes = ( const updateTabIndexes = (
elements = getNavigableElements(), elements: HTMLElement[] = getNavigableElements(),
currentIndex = -1, currentIndex = -1,
setFocus = false setFocus = false
) => { ) => {
...@@ -103,7 +107,7 @@ export function useArrowKeyNavigation( ...@@ -103,7 +107,7 @@ export function useArrowKeyNavigation(
} }
} }
for (let [index, element] of elements.entries()) { for (const [index, element] of elements.entries()) {
element.tabIndex = index === currentIndex ? 0 : -1; element.tabIndex = index === currentIndex ? 0 : -1;
if (index === currentIndex && setFocus) { if (index === currentIndex && setFocus) {
element.focus(); element.focus();
...@@ -111,8 +115,7 @@ export function useArrowKeyNavigation( ...@@ -111,8 +115,7 @@ export function useArrowKeyNavigation(
} }
}; };
/** @param {KeyboardEvent} event */ const onKeyDown = (event: KeyboardEvent) => {
const onKeyDown = event => {
const elements = getNavigableElements(); const elements = getNavigableElements();
let currentIndex = elements.findIndex(item => item.tabIndex === 0); let currentIndex = elements.findIndex(item => item.tabIndex === 0);
...@@ -164,19 +167,13 @@ export function useArrowKeyNavigation( ...@@ -164,19 +167,13 @@ export function useArrowKeyNavigation(
// is triggered. // is triggered.
listeners.add(container, 'focusin', event => { listeners.add(container, 'focusin', event => {
const elements = getNavigableElements(); const elements = getNavigableElements();
const targetIndex = elements.indexOf( const targetIndex = elements.indexOf(event.target as HTMLElement);
/** @type {HTMLElement} */ (event.target)
);
if (targetIndex >= 0) { if (targetIndex >= 0) {
updateTabIndexes(elements, targetIndex); updateTabIndexes(elements, targetIndex);
} }
}); });
listeners.add( listeners.add(container, 'keydown', onKeyDown);
container,
'keydown',
/** @type {EventListener} */ (onKeyDown)
);
// Update the tab indexes of elements as they are added, removed, enabled // Update the tab indexes of elements as they are added, removed, enabled
// or disabled. // or disabled.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment