diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte new file mode 100644 index 0000000000..207c880cd9 --- /dev/null +++ b/web/src/lib/actions/__test__/focus-trap-test.svelte @@ -0,0 +1,18 @@ + + + + +{#if show} +
+
+ text + +
+ + +
+{/if} diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts new file mode 100644 index 0000000000..be3a97db3f --- /dev/null +++ b/web/src/lib/actions/__test__/focus-trap.spec.ts @@ -0,0 +1,40 @@ +import FocusTrapTest from '$lib/actions/__test__/focus-trap-test.svelte'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { tick } from 'svelte'; + +describe('focusTrap action', () => { + const user = userEvent.setup(); + + it('sets focus to the first focusable element', () => { + render(FocusTrapTest, { show: true }); + expect(document.activeElement).toEqual(screen.getByTestId('one')); + }); + + it('supports backward focus wrapping', async () => { + render(FocusTrapTest, { show: true }); + await user.keyboard('{Shift>}{Tab}{/Shift}'); + expect(document.activeElement).toEqual(screen.getByTestId('three')); + }); + + it('supports forward focus wrapping', async () => { + render(FocusTrapTest, { show: true }); + screen.getByTestId('three').focus(); + await user.keyboard('{Tab}'); + expect(document.activeElement).toEqual(screen.getByTestId('one')); + }); + + it('restores focus to the triggering element', async () => { + render(FocusTrapTest, { show: false }); + const openButton = screen.getByText('Open'); + + openButton.focus(); + openButton.click(); + await tick(); + expect(document.activeElement).toEqual(screen.getByTestId('one')); + + screen.getByText('Close').click(); + await tick(); + expect(document.activeElement).toEqual(openButton); + }); +}); diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts new file mode 100644 index 0000000000..c854199600 --- /dev/null +++ b/web/src/lib/actions/focus-trap.ts @@ -0,0 +1,55 @@ +import { shortcuts } from '$lib/actions/shortcut'; + +const selectors = + 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; + +export function focusTrap(container: HTMLElement) { + const triggerElement = document.activeElement; + + const focusableElement = container.querySelector(selectors); + focusableElement?.focus(); + + const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => { + const focusableElements = container.querySelectorAll(selectors); + return [ + focusableElements.item(0), // + focusableElements.item(focusableElements.length - 1), + ]; + }; + + const { destroy: destroyShortcuts } = shortcuts(container, [ + { + ignoreInputFields: false, + preventDefault: false, + shortcut: { key: 'Tab' }, + onShortcut: (event) => { + const [firstElement, lastElement] = getFocusableElements(); + if (document.activeElement === lastElement) { + event.preventDefault(); + firstElement?.focus(); + } + }, + }, + { + ignoreInputFields: false, + preventDefault: false, + shortcut: { key: 'Tab', shift: true }, + onShortcut: (event) => { + const [firstElement, lastElement] = getFocusableElements(); + if (document.activeElement === firstElement) { + event.preventDefault(); + lastElement?.focus(); + } + }, + }, + ]); + + return { + destroy() { + destroyShortcuts?.(); + if (triggerElement instanceof HTMLElement) { + triggerElement.focus(); + } + }, + }; +} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index ce4430edf1..f216d73382 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,7 +1,6 @@ - -
{ - trapFocus('forward', event); - }, - preventDefault: false, - }, - { - ignoreInputFields: false, - shortcut: { key: 'Tab', shift: true }, - onShortcut: (event) => { - trapFocus('backward', event); - }, - preventDefault: false, - }, - ]} -> - -
diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte index d0d629ee4d..bc1253a546 100644 --- a/web/src/lib/components/shared-components/full-screen-modal.svelte +++ b/web/src/lib/components/shared-components/full-screen-modal.svelte @@ -1,7 +1,7 @@ - +
-
-
- -
- (isShowSelectAvatar = true)} - /> +
+ +
+ (isShowSelectAvatar = true)} + /> +
+
+
+

+ {$user.name} +

+

{$user.email}

+
+ + dispatch('close')}> +
-
-

- {$user.name} -

-

{$user.email}

-
- -
dispatch('close')}> - - -
- -
- -
+ +
- + +
+ +
+
{#if isShowSelectAvatar}