Trap keyboard focus in popovers
This commit is contained in:
1 parent
2432113c7f
commit
411fb03c22
6 files changed
+61
-5
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '#lib/ui/focusTrap';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { musicPlayer } from '#lib/music/player.svelte';
|
||||
import MusicNowPlaying from './MusicNowPlaying.svelte';
|
||||
@@ -19,7 +20,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<button class="backdrop" aria-label="Close music player" onclick={onclose}></button>
|
||||
<div class="popover" role="dialog" aria-label="Music player">
|
||||
<div class="popover" role="dialog" aria-modal="true" aria-label="Music player" tabindex="-1" use:focusTrap>
|
||||
<div class="popover-surface">
|
||||
{#if track}
|
||||
<MusicNowPlaying {track} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '#lib/ui/focusTrap';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import {
|
||||
@@ -41,7 +42,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<button class="backdrop" aria-label="Close battery details" onclick={onclose}></button>
|
||||
<div class="popover" role="dialog" aria-label="Battery details">
|
||||
<div class="popover" role="dialog" aria-modal="true" aria-label="Battery details" tabindex="-1" use:focusTrap>
|
||||
<div class="popover-surface" class:charging={battery.charging} class:low={isLow}>
|
||||
<header class="summary">
|
||||
<span class="battery-icon">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '#lib/ui/focusTrap';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { activityLabels, type DiscordPresence } from '#lib/discord/types';
|
||||
|
||||
@@ -12,7 +13,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<button class="backdrop" aria-label="Close Discord status" onclick={onclose}></button>
|
||||
<div class="popover" role="dialog" aria-label="Discord status">
|
||||
<div class="popover" role="dialog" aria-modal="true" aria-label="Discord status" tabindex="-1" use:focusTrap>
|
||||
<div class="popover-surface">
|
||||
<header class="profile">
|
||||
<div class="avatar">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '#lib/ui/focusTrap';
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
let { donateUrl, discordUrl, onclose }: { donateUrl: string; discordUrl: string; onclose: () => void } = $props();
|
||||
@@ -11,7 +12,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<button class="backdrop" aria-label="Close donate card" onclick={onclose}></button>
|
||||
<div class="popover" role="dialog" aria-label="Support me">
|
||||
<div class="popover" role="dialog" aria-modal="true" aria-label="Support me" tabindex="-1" use:focusTrap>
|
||||
<div class="popover-surface">
|
||||
<header class="header">
|
||||
<Icon icon="simple-icons:kofi" width="22" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from '#lib/ui/focusTrap';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { title, onclose, children }: { title: string; onclose: () => void; children: Snippet } = $props();
|
||||
@@ -11,7 +12,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<button class="backdrop" aria-label="Close {title}" onclick={onclose}></button>
|
||||
<div class="popup" role="dialog" aria-label={title}>
|
||||
<div class="popup" role="dialog" aria-modal="true" aria-label={title} tabindex="-1" use:focusTrap>
|
||||
<div class="popup-title">{title}</div>
|
||||
<div class="popup-body">
|
||||
{@render children()}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const focusableSelector = [
|
||||
'a[href]',
|
||||
'button:not([disabled])',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])'
|
||||
].join(', ');
|
||||
|
||||
function focusableElements(container: HTMLElement): HTMLElement[] {
|
||||
return [...container.querySelectorAll<HTMLElement>(focusableSelector)].filter(
|
||||
(element) => element.getClientRects().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function focusTrap(container: HTMLElement) {
|
||||
const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
(focusableElements(container)[0] ?? container).focus({ preventScroll: true });
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Tab') return;
|
||||
|
||||
const elements = focusableElements(container);
|
||||
const first = elements[0];
|
||||
const last = elements.at(-1);
|
||||
const active = document.activeElement;
|
||||
|
||||
if (!first || !last) {
|
||||
event.preventDefault();
|
||||
container.focus({ preventScroll: true });
|
||||
} else if (!container.contains(active)) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
} else if (event.shiftKey && active === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && active === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user