Merge pull request #13695 from itk-dev/8908-focus-trap-modal

feat: focus trap modal
This commit is contained in:
Tim Jaeryang Baek 2025-05-08 20:44:36 +04:00 committed by GitHub
commit 68f5ff540c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 19 additions and 4 deletions

8
package-lock.json generated
View File

@ -36,6 +36,7 @@
"dompurify": "^3.2.5", "dompurify": "^3.2.5",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"focus-trap": "^7.6.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"html-entities": "^2.5.3", "html-entities": "^2.5.3",
@ -6801,9 +6802,10 @@
"dev": true "dev": true
}, },
"node_modules/focus-trap": { "node_modules/focus-trap": {
"version": "7.5.4", "version": "7.6.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
"license": "MIT",
"dependencies": { "dependencies": {
"tabbable": "^6.2.0" "tabbable": "^6.2.0"
} }

View File

@ -79,6 +79,7 @@
"dompurify": "^3.2.5", "dompurify": "^3.2.5",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"focus-trap": "^7.6.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"html-entities": "^2.5.3", "html-entities": "^2.5.3",

View File

@ -3,7 +3,7 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
import * as FocusTrap from 'focus-trap';
export let show = true; export let show = true;
export let size = 'md'; export let size = 'md';
export let containerClassName = 'p-3'; export let containerClassName = 'p-3';
@ -11,6 +11,10 @@
let modalElement = null; let modalElement = null;
let mounted = false; let mounted = false;
// Create focus trap to trap user tabs inside modal
// https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html
// https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html
let focusTrap: FocusTrap.FocusTrap | null = null;
const sizeToWidth = (size) => { const sizeToWidth = (size) => {
if (size === 'full') { if (size === 'full') {
@ -45,9 +49,12 @@
$: if (show && modalElement) { $: if (show && modalElement) {
document.body.appendChild(modalElement); document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} else if (modalElement) { } else if (modalElement) {
focusTrap.deactivate();
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
document.body.removeChild(modalElement); document.body.removeChild(modalElement);
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
@ -55,6 +62,9 @@
onDestroy(() => { onDestroy(() => {
show = false; show = false;
if (focusTrap) {
focusTrap.deactivate();
}
if (modalElement) { if (modalElement) {
document.body.removeChild(modalElement); document.body.removeChild(modalElement);
} }
@ -66,6 +76,8 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={modalElement} bind:this={modalElement}
aria-modal="true"
role="dialog"
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain" class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
in:fade={{ duration: 10 }} in:fade={{ duration: 10 }}
on:mousedown={() => { on:mousedown={() => {