mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 02:22:17 +00:00
fix(a11y): trap focus inside the pokédex modal
Tab moved to the page behind the open dialog and closing it left focus nowhere. Keep Tab within the dialog, move focus to the close button on open, restore it to the trigger on close, and give the dialog an accessible name.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let isOpen: boolean;
|
export let isOpen: boolean;
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
@@ -14,24 +14,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dialog: HTMLDivElement;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
const previous = document.activeElement as HTMLElement | null;
|
||||||
});
|
const close = dialog.querySelector<HTMLButtonElement>('.close-button');
|
||||||
|
close?.focus();
|
||||||
onDestroy(() => {
|
const trapFocus = (event: KeyboardEvent) => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
handleKeyDown(event);
|
||||||
|
if (event.key !== 'Tab') return;
|
||||||
|
const nodes = Array.from(
|
||||||
|
dialog.querySelectorAll<HTMLElement>(
|
||||||
|
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), a[href]'
|
||||||
|
)
|
||||||
|
).filter((node) => node.getClientRects().length);
|
||||||
|
const first = nodes[0],
|
||||||
|
last = nodes.at(-1);
|
||||||
|
if (event.shiftKey && document.activeElement === first) {
|
||||||
|
event.preventDefault();
|
||||||
|
last?.focus();
|
||||||
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||||||
|
event.preventDefault();
|
||||||
|
first?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', trapFocus);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', trapFocus);
|
||||||
|
if (previous?.isConnected) previous.focus();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="modal modal-open" role="dialog" aria-modal="true">
|
<div
|
||||||
|
bind:this={dialog}
|
||||||
|
class="modal modal-open"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Pokémon details"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
<div class="modal-box-custom bg-primary text-primary-content">
|
<div class="modal-box-custom bg-primary text-primary-content">
|
||||||
<button class="close-button" on:click={onClose} aria-label="Close"> ✕ </button>
|
<button data-offline-action class="close-button" on:click={onClose} aria-label="Close">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
data-offline-action
|
||||||
type="button"
|
type="button"
|
||||||
class="modal-backdrop bg-black/50"
|
class="modal-backdrop bg-black/50"
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
|
|||||||
Reference in New Issue
Block a user