From cb40db955530642a050aa4105281797fe2e831c7 Mon Sep 17 00:00:00 2001
From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Date: Mon, 8 Jul 2024 03:33:07 +0200
Subject: [PATCH] refactor(web): focus trap (#10915)
---
.../actions/__test__/focus-trap-test.svelte | 18 +
.../lib/actions/__test__/focus-trap.spec.ts | 40 ++
web/src/lib/actions/focus-trap.ts | 55 +++
.../asset-viewer/asset-viewer.svelte | 451 +++++++++---------
.../shared-components/focus-trap.svelte | 64 ---
.../full-screen-modal.svelte | 45 +-
.../navigation-bar/account-info-panel.svelte | 99 ++--
7 files changed, 407 insertions(+), 365 deletions(-)
create mode 100644 web/src/lib/actions/__test__/focus-trap-test.svelte
create mode 100644 web/src/lib/actions/__test__/focus-trap.spec.ts
create mode 100644 web/src/lib/actions/focus-trap.ts
delete mode 100644 web/src/lib/components/shared-components/focus-trap.svelte
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 0000000000000..207c880cd9d8f
--- /dev/null
+++ b/web/src/lib/actions/__test__/focus-trap-test.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+{#if show}
+
+{/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 0000000000000..be3a97db3f6da
--- /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 0000000000000..c854199600e65
--- /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 ce4430edf1bff..f216d73382ae8 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 d0d629ee4d7f1..bc1253a546c01 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 @@
-
+
-
-
-
-
-
-
+
+
-
+
+
+
+
+
{#if isShowSelectAvatar}