Adrien Denat coyotte508 HF staff commited on
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. <[email protected]>

---------

Co-authored-by: Eliott C. <[email protected]>

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
- <div
9
- transition:fade={{ easing: cubicOut, duration: 300 }}
10
- class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
11
- >
12
- <div class="-mt-10 overflow-hidden rounded-2xl bg-white shadow-2xl md:-mt-20 {width}">
13
- <slot />
 
 
 
 
 
 
 
 
 
 
 
 
14
  </div>
15
- </div>
 
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>