diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue index 609072f6f..e2789c89b 100644 --- a/library/components/info/MapInfo.vue +++ b/library/components/info/MapInfo.vue @@ -134,7 +134,25 @@ async function hide() { await nextTick() btnEl.value?.focus() } -function onKey(e) { if (e.key === 'Escape') { e.stopPropagation(); hide() } } +// ★ aria-modal PROMISES THE READER THAT TAB STAYS INSIDE, so keep it there: Tab from the last control +// wraps to the first, Shift+Tab from the first to the last. The active element is read from the shadow +// root (document.activeElement would be the host element). Hidden controls are skipped. +const dialogEl = ref(null) +function trapTab(e) { + const box = dialogEl.value + if (!box) return + const items = [...box.querySelectorAll('button, a[href], input, [tabindex]:not([tabindex="-1"])')] + .filter((el) => !el.disabled && el.offsetParent !== null) + if (!items.length) return + const active = box.getRootNode().activeElement + const first = items[0], last = items[items.length - 1] + if (e.shiftKey && (active === first || !box.contains(active))) { e.preventDefault(); last.focus() } + else if (!e.shiftKey && (active === last || !box.contains(active))) { e.preventDefault(); first.focus() } +} +function onKey(e) { + if (e.key === 'Escape') { e.stopPropagation(); hide() } + else if (e.key === 'Tab') trapTab(e) +} function toggle(i) { expanded[i] = !expanded[i] } // ★ A MISSING EXAMPLE IMAGE DISAPPEARS, QUIETLY. The examples are pre-generated files that ship in the // tree; if one is absent (a shoot not yet landed, a tree copied without it) the reader must see neither @@ -260,7 +278,7 @@ useShadowStyles(`
-