text
diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts
index 6ce5ad6d5b..d92d8e037d 100644
--- a/web/src/lib/actions/__test__/focus-trap.spec.ts
+++ b/web/src/lib/actions/__test__/focus-trap.spec.ts
@@ -12,6 +12,12 @@ describe('focusTrap action', () => {
expect(document.activeElement).toEqual(screen.getByTestId('one'));
});
+ it('should not set focus if inactive', async () => {
+ render(FocusTrapTest, { show: true, active: false });
+ await tick();
+ expect(document.activeElement).toBe(document.body);
+ });
+
it('supports backward focus wrapping', async () => {
render(FocusTrapTest, { show: true });
await tick();
diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts
index 92775546aa..599a97af75 100644
--- a/web/src/lib/actions/click-outside.ts
+++ b/web/src/lib/actions/click-outside.ts
@@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
}
};
- document.addEventListener('mousedown', handleClick, true);
+ document.addEventListener('mousedown', handleClick, false);
node.addEventListener('keydown', handleKey, false);
return {
destroy() {
- document.removeEventListener('mousedown', handleClick, true);
+ document.removeEventListener('mousedown', handleClick, false);
node.removeEventListener('keydown', handleKey, false);
},
};
diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts
index 7483e76099..1a84f21729 100644
--- a/web/src/lib/actions/focus-trap.ts
+++ b/web/src/lib/actions/focus-trap.ts
@@ -1,16 +1,34 @@
import { shortcuts } from '$lib/actions/shortcut';
import { tick } from 'svelte';
-const selectors =
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+interface Options {
+ /**
+ * Set whether the trap is active or not.
+ */
+ active?: boolean;
+}
-export function focusTrap(container: HTMLElement) {
+const selectors =
+ 'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';
+
+export function focusTrap(container: HTMLElement, options?: Options) {
const triggerElement = document.activeElement;
- const focusableElement = container.querySelector
(selectors);
+ const withDefaults = (options?: Options) => {
+ return {
+ active: options?.active ?? true,
+ };
+ };
- // Use tick() to ensure focus trap works correctly inside
- void tick().then(() => focusableElement?.focus());
+ const setInitialFocus = () => {
+ const focusableElement = container.querySelector(selectors);
+ // Use tick() to ensure focus trap works correctly inside
+ void tick().then(() => focusableElement?.focus());
+ };
+
+ if (withDefaults(options).active) {
+ setInitialFocus();
+ }
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
const focusableElements = container.querySelectorAll(selectors);
@@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab' },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
- if (document.activeElement === lastElement) {
+ if (document.activeElement === lastElement && withDefaults(options).active) {
event.preventDefault();
firstElement?.focus();
}
@@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
- if (document.activeElement === firstElement) {
+ if (document.activeElement === firstElement && withDefaults(options).active) {
event.preventDefault();
lastElement?.focus();
}
@@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
]);
return {
+ update(newOptions?: Options) {
+ options = newOptions;
+ if (withDefaults(options).active) {
+ setInitialFocus();
+ }
+ },
destroy() {
destroyShortcuts?.();
if (triggerElement instanceof HTMLElement) {
diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte
index b80e7d1a44..a1a24634c4 100644
--- a/web/src/lib/components/elements/buttons/skip-link.svelte
+++ b/web/src/lib/components/elements/buttons/skip-link.svelte
@@ -7,10 +7,17 @@
* Target for the skip link to move focus to.
*/
target?: string;
+ /**
+ * Text for the skip link button.
+ */
text?: string;
+ /**
+ * Breakpoint at which the skip link is visible. Defaults to always being visible.
+ */
+ breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
}
- let { target = 'main', text = $t('skip_to_content') }: Props = $props();
+ let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();
let isFocused = $state(false);
@@ -18,6 +25,29 @@
const targetEl = document.querySelector(target);
targetEl?.focus();
};
+
+ const getBreakpoint = () => {
+ if (!breakpoint) {
+ return '';
+ }
+ switch (breakpoint) {
+ case 'sm': {
+ return 'hidden sm:block';
+ }
+ case 'md': {
+ return 'hidden md:block';
+ }
+ case 'lg': {
+ return 'hidden lg:block';
+ }
+ case 'xl': {
+ return 'hidden xl:block';
+ }
+ case '2xl': {
+ return 'hidden 2xl:block';
+ }
+ }
+ };