refactor(web): focus trap (#10915)
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
|
||||
export let show: boolean;
|
||||
</script>
|
||||
|
||||
<button type="button" on:click={() => (show = true)}>Open</button>
|
||||
|
||||
{#if show}
|
||||
<div use:focusTrap>
|
||||
<div>
|
||||
<span>text</span>
|
||||
<button data-testid="one" type="button" on:click={() => (show = false)}>Close</button>
|
||||
</div>
|
||||
<input data-testid="two" disabled />
|
||||
<input data-testid="three" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<HTMLElement>(selectors);
|
||||
focusableElement?.focus();
|
||||
|
||||
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
|
||||
const focusableElements = container.querySelectorAll<HTMLElement>(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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user