Files
immich/web/src/lib/actions/focus-trap.ts
Min Idzelis 4b1ced439b feat: improve/refactor focus handling (#17796)
* feat: improve focus

* test

* lint

* use modulus in loop
2025-04-30 00:19:38 -04:00

84 lines
2.1 KiB
TypeScript

import { shortcuts } from '$lib/actions/shortcut';
import { getTabbable } from '$lib/utils/focus-util';
import { tick } from 'svelte';
interface Options {
/**
* Set whether the trap is active or not.
*/
active?: boolean;
}
export function focusTrap(container: HTMLElement, options?: Options) {
const triggerElement = document.activeElement;
const withDefaults = (options?: Options) => {
return {
active: options?.active ?? true,
};
};
const setInitialFocus = async () => {
const focusableElement = getTabbable(container, false)[0];
if (focusableElement) {
// Use tick() to ensure focus trap works correctly inside <Portal />
await tick();
focusableElement?.focus();
}
};
if (withDefaults(options).active) {
void setInitialFocus();
}
const getFocusableElements = () => {
const focusableElements = getTabbable(container);
return [
focusableElements.at(0), //
focusableElements.at(-1),
];
};
const { destroy: destroyShortcuts } = shortcuts(container, [
{
ignoreInputFields: false,
preventDefault: false,
shortcut: { key: 'Tab' },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === lastElement && withDefaults(options).active) {
event.preventDefault();
firstElement?.focus();
}
},
},
{
ignoreInputFields: false,
preventDefault: false,
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === firstElement && withDefaults(options).active) {
event.preventDefault();
lastElement?.focus();
}
},
},
]);
return {
update(newOptions?: Options) {
options = newOptions;
if (withDefaults(options).active) {
void setInitialFocus();
}
},
destroy() {
destroyShortcuts?.();
if (triggerElement instanceof HTMLElement) {
triggerElement.focus();
}
},
};
}