Commit
•
fe5e801
1
Parent(s):
54abee8
Improve modal a11y (#177)
Browse files* add Portal component so we can render modals at root for focus trap
* make modals closeable with ESC + click outside + focus trap
* remove unecessary check on inert prop
Co-authored-by: Eliott C. <coyotte508@gmail.com>
---------
Co-authored-by: Eliott C. <coyotte508@gmail.com>
src/app.html
CHANGED
@@ -20,7 +20,7 @@
|
|
20 |
%sveltekit.head%
|
21 |
</head>
|
22 |
<body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
|
23 |
-
<div class="contents h-full">%sveltekit.body%</div>
|
24 |
|
25 |
<!-- Google Tag Manager -->
|
26 |
<script>
|
|
|
20 |
%sveltekit.head%
|
21 |
</head>
|
22 |
<body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
|
23 |
+
<div id="app" class="contents h-full">%sveltekit.body%</div>
|
24 |
|
25 |
<!-- Google Tag Manager -->
|
26 |
<script>
|
src/lib/components/Modal.svelte
CHANGED
@@ -1,15 +1,59 @@
|
|
1 |
<script lang="ts">
|
|
|
2 |
import { cubicOut } from "svelte/easing";
|
3 |
import { fade } from "svelte/transition";
|
|
|
|
|
4 |
|
5 |
export let width = "max-w-sm";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
</script>
|
7 |
|
8 |
-
<
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
</div>
|
15 |
-
</
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
3 |
import { cubicOut } from "svelte/easing";
|
4 |
import { fade } from "svelte/transition";
|
5 |
+
import Portal from "./Portal.svelte";
|
6 |
+
import { browser } from "$app/environment";
|
7 |
|
8 |
export let width = "max-w-sm";
|
9 |
+
|
10 |
+
let backdropEl: HTMLDivElement;
|
11 |
+
let modalEl: HTMLDivElement;
|
12 |
+
|
13 |
+
const dispatch = createEventDispatcher<{ close: void }>();
|
14 |
+
|
15 |
+
function handleKeydown(event: KeyboardEvent) {
|
16 |
+
// close on ESC
|
17 |
+
if (event.key === "Escape") {
|
18 |
+
event.preventDefault();
|
19 |
+
dispatch("close");
|
20 |
+
}
|
21 |
+
}
|
22 |
+
|
23 |
+
function handleBackdropClick(event: MouseEvent) {
|
24 |
+
if (event.target === backdropEl) {
|
25 |
+
dispatch("close");
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
onMount(() => {
|
30 |
+
document.getElementById("app")?.setAttribute("inert", "true");
|
31 |
+
modalEl.focus();
|
32 |
+
});
|
33 |
+
|
34 |
+
onDestroy(() => {
|
35 |
+
if (!browser) return;
|
36 |
+
document.getElementById("app")?.removeAttribute("inert");
|
37 |
+
});
|
38 |
</script>
|
39 |
|
40 |
+
<Portal>
|
41 |
+
<div
|
42 |
+
role="presentation"
|
43 |
+
tabindex="-1"
|
44 |
+
bind:this={backdropEl}
|
45 |
+
on:click={handleBackdropClick}
|
46 |
+
transition:fade={{ easing: cubicOut, duration: 300 }}
|
47 |
+
class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
|
48 |
+
>
|
49 |
+
<div
|
50 |
+
role="dialog"
|
51 |
+
tabindex="-1"
|
52 |
+
bind:this={modalEl}
|
53 |
+
on:keydown={handleKeydown}
|
54 |
+
class="-mt-10 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none md:-mt-20 {width}"
|
55 |
+
>
|
56 |
+
<slot />
|
57 |
+
</div>
|
58 |
</div>
|
59 |
+
</Portal>
|
src/lib/components/ModelsModal.svelte
CHANGED
@@ -18,7 +18,7 @@
|
|
18 |
const dispatch = createEventDispatcher<{ close: void }>();
|
19 |
</script>
|
20 |
|
21 |
-
<Modal width="max-w-lg">
|
22 |
<form
|
23 |
action="{base}/settings"
|
24 |
method="post"
|
|
|
18 |
const dispatch = createEventDispatcher<{ close: void }>();
|
19 |
</script>
|
20 |
|
21 |
+
<Modal width="max-w-lg" on:close>
|
22 |
<form
|
23 |
action="{base}/settings"
|
24 |
method="post"
|
src/lib/components/Portal.svelte
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { onMount, onDestroy } from "svelte";
|
3 |
+
|
4 |
+
let el: HTMLElement;
|
5 |
+
|
6 |
+
onMount(() => {
|
7 |
+
el.ownerDocument.body.appendChild(el);
|
8 |
+
});
|
9 |
+
|
10 |
+
onDestroy(() => {
|
11 |
+
if (el?.parentNode) {
|
12 |
+
el.parentNode.removeChild(el);
|
13 |
+
}
|
14 |
+
});
|
15 |
+
</script>
|
16 |
+
|
17 |
+
<div bind:this={el} class="contents" hidden>
|
18 |
+
<slot />
|
19 |
+
</div>
|
src/lib/components/SettingsModal.svelte
CHANGED
@@ -13,7 +13,7 @@
|
|
13 |
const dispatch = createEventDispatcher<{ close: void }>();
|
14 |
</script>
|
15 |
|
16 |
-
<Modal>
|
17 |
<form
|
18 |
class="flex w-full flex-col gap-5 p-6"
|
19 |
use:enhance={() => {
|
@@ -24,7 +24,7 @@
|
|
24 |
>
|
25 |
<div class="flex items-start justify-between text-xl font-semibold text-gray-800">
|
26 |
<h2>Settings</h2>
|
27 |
-
<button class="group" on:click={() => dispatch("close")}>
|
28 |
<CarbonClose class="text-gray-900 group-hover:text-gray-500" />
|
29 |
</button>
|
30 |
</div>
|
|
|
13 |
const dispatch = createEventDispatcher<{ close: void }>();
|
14 |
</script>
|
15 |
|
16 |
+
<Modal on:close>
|
17 |
<form
|
18 |
class="flex w-full flex-col gap-5 p-6"
|
19 |
use:enhance={() => {
|
|
|
24 |
>
|
25 |
<div class="flex items-start justify-between text-xl font-semibold text-gray-800">
|
26 |
<h2>Settings</h2>
|
27 |
+
<button type="button" class="group" on:click={() => dispatch("close")}>
|
28 |
<CarbonClose class="text-gray-900 group-hover:text-gray-500" />
|
29 |
</button>
|
30 |
</div>
|