Files
immich/web/src/lib/utils/focus-util.ts
Min Idzelis f029910dc7 feat: keyboard navigation to timeline (#17798)
* feat: improve focus

* feat: keyboard nav

* feat: improve focus

* typo

* test

* fix test

* lint

* bad merge

* lint

* inadvertent

* lint

* fix: flappy e2e test

* bad merge and fix tests

* use modulus in loop

* tests

* react to modal dialog refactor

* regression due to deferLayout

* Review comments

* Re-use change-date instead of new component

* bad merge

* Review comments

* rework moveFocus

* lint

* Fix outline

* use Date

* Finish up removing/reducing date parsing

* lint

* title

* strings

* Rework dates, rework earlier/later algorithm

* bad merge

* fix tests

* Fix race in scroll comp

* consolidate scroll methods

* Review comments

* console.log

* Edge cases in scroll compensation

* edge case, optimizations

* review comments

* lint

* lint

* More edge cases

* lint

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-05-28 08:55:14 -05:00

55 lines
1.8 KiB
TypeScript

import { focusable, isTabbable, tabbable, type CheckOptions, type TabbableOptions } from 'tabbable';
type TabbableOpts = TabbableOptions & CheckOptions;
let defaultOpts: TabbableOpts = {
includeContainer: false,
};
export const setDefaultTabbleOptions = (options: TabbableOpts) => {
defaultOpts = options;
};
export const getTabbable = (container: Element, includeContainer: boolean = false) =>
tabbable(container, { ...defaultOpts, includeContainer });
export const moveFocus = (
selector: (element: HTMLElement | SVGElement) => boolean,
direction: 'previous' | 'next',
): void => {
const focusableElements = focusable(document.body, { includeContainer: true });
if (focusableElements.length === 0) {
return;
}
const currentElement = document.activeElement as HTMLElement | null;
const currentIndex = currentElement ? focusableElements.indexOf(currentElement) : -1;
// If no element is focused, focus the first matching element or the first focusable element
if (currentIndex === -1) {
const firstMatchingElement = focusableElements.find((element) => selector(element));
if (firstMatchingElement) {
firstMatchingElement.focus();
} else if (focusableElements[0]) {
focusableElements[0].focus();
}
return;
}
// Calculate the step direction
const step = direction === 'next' ? 1 : -1;
const totalElements = focusableElements.length;
// Search for the next focusable element that matches the selector
let nextIndex = currentIndex;
do {
nextIndex = (nextIndex + step + totalElements) % totalElements;
const candidateElement = focusableElements[nextIndex];
if (isTabbable(candidateElement) && selector(candidateElement)) {
candidateElement.focus();
break;
}
} while (nextIndex !== currentIndex);
};