From 68df5123ffd5a08cc0f44313464ee657e74bfb2f Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:37:00 -0400 Subject: [PATCH] fix(a11y): opening a Search menu moves focus to its options, and Escape returns focus to its button (#33450) --- src/lib/components/layout/SearchModal.svelte | 47 ++++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/lib/components/layout/SearchModal.svelte b/src/lib/components/layout/SearchModal.svelte index d62d5f40aa..96f83a61c7 100644 --- a/src/lib/components/layout/SearchModal.svelte +++ b/src/lib/components/layout/SearchModal.svelte @@ -2,7 +2,7 @@ import { getContext, onDestroy, tick } from 'svelte'; import { goto } from '$app/navigation'; import { toast } from 'svelte-sonner'; - import { folders, settings, tags } from '$lib/stores'; + import { folders, mobile, settings, tags } from '$lib/stores'; import DropdownMenu from '$lib/components/common/DropdownMenu.svelte'; import DropdownSub from '$lib/components/common/DropdownSub.svelte'; import Check from '$lib/components/icons/Check.svelte'; @@ -52,6 +52,8 @@ let panel: 'filters' | 'actions' | 'types' | null = null; let input: HTMLInputElement; let list: HTMLDivElement; + let panelMenu: HTMLDivElement; + let panelTrigger: HTMLElement | null = null; let request: AbortController | undefined; let timer: ReturnType; let generation = 0; @@ -193,6 +195,20 @@ input.focus(); } + async function togglePanel(name: 'filters' | 'actions' | 'types') { + if (panel === name) { + const restoreFocus = panelMenu?.contains(document.activeElement); + panel = null; + if (restoreFocus) panelTrigger?.focus(); + return; + } + panelTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null; + panel = name; + if ($mobile) return; + await tick(); + panelMenu?.querySelector('button:not([disabled])')?.focus(); + } + function close() { show = false; suggesting = false; @@ -235,11 +251,17 @@ return; } } + if (event.key === 'Escape' && panel && panelMenu?.contains(event.target as Node)) { + event.stopPropagation(); + panel = null; + panelTrigger?.focus(); + return; + } if (event.key === 'Escape') return; if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); event.stopPropagation(); - panel = panel === 'actions' ? null : 'actions'; + togglePanel('actions'); return; } if (panel || (event.target !== input && event.target !== list)) return; @@ -357,7 +379,7 @@ aria-label={`${$i18n.t('Content type')}: ${$i18n.t(kind === 'all' ? 'Everything' : labels[kind])}`} data-search-panel aria-expanded={panel === 'types'} - on:click={() => (panel = panel === 'types' ? null : 'types')} + on:click={() => togglePanel('types')} >{$i18n.t(kind === 'all' ? 'Everything' : labels[kind])} (panel = panel === 'filters' ? null : 'filters')} - > togglePanel('filters')}>