feat: strengthen backup and mobile workflows
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
type OverlayEntry = {
|
||||
id: symbol
|
||||
close: () => void
|
||||
busy: () => boolean
|
||||
restoreFocus: HTMLElement | null
|
||||
focusPanel: () => void
|
||||
}
|
||||
|
||||
const stack: OverlayEntry[] = []
|
||||
const background = new Map<HTMLElement, { inert: boolean; ariaHidden: string | null }>()
|
||||
let listening = false
|
||||
|
||||
function root() {
|
||||
let element = document.getElementById('overlay-root')
|
||||
if (!element) {
|
||||
element = document.createElement('div')
|
||||
element.id = 'overlay-root'
|
||||
document.body.appendChild(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
function syncBackground() {
|
||||
const overlayRoot = root()
|
||||
if (stack.length) {
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!(child instanceof HTMLElement) || child === overlayRoot || background.has(child)) continue
|
||||
background.set(child, { inert: child.hasAttribute('inert'), ariaHidden: child.getAttribute('aria-hidden') })
|
||||
child.setAttribute('inert', '')
|
||||
child.setAttribute('aria-hidden', 'true')
|
||||
}
|
||||
return
|
||||
}
|
||||
for (const [element, state] of background) {
|
||||
if (!state.inert) element.removeAttribute('inert')
|
||||
if (state.ariaHidden === null) element.removeAttribute('aria-hidden')
|
||||
else element.setAttribute('aria-hidden', state.ariaHidden)
|
||||
}
|
||||
background.clear()
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Escape' || event.defaultPrevented) return
|
||||
const entry = stack.at(-1)
|
||||
if (!entry || entry.busy()) return
|
||||
event.preventDefault()
|
||||
entry.close()
|
||||
}
|
||||
|
||||
export function overlayRoot() { return root() }
|
||||
|
||||
export function pushOverlay(close: () => void, busy: () => boolean, focusPanel: () => void) {
|
||||
const entry: OverlayEntry = {
|
||||
id: Symbol('overlay'), close, busy, focusPanel,
|
||||
restoreFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null,
|
||||
}
|
||||
stack.push(entry)
|
||||
if (!listening) { document.addEventListener('keydown', onKeydown); listening = true }
|
||||
syncBackground()
|
||||
return entry.id
|
||||
}
|
||||
|
||||
export function popOverlay(id: symbol) {
|
||||
const index = stack.findIndex((entry) => entry.id === id)
|
||||
if (index < 0) return
|
||||
const wasTop = index === stack.length - 1
|
||||
const [entry] = stack.splice(index, 1)
|
||||
if (!stack.length && listening) { document.removeEventListener('keydown', onKeydown); listening = false }
|
||||
syncBackground()
|
||||
if (!wasTop) return
|
||||
const newTop = stack.at(-1)
|
||||
void nextTick(() => {
|
||||
if (newTop) newTop.focusPanel()
|
||||
else if (entry.restoreFocus?.isConnected) entry.restoreFocus.focus()
|
||||
})
|
||||
}
|
||||
|
||||
export function isTopOverlay(id: symbol | null) {
|
||||
return Boolean(id && stack.at(-1)?.id === id)
|
||||
}
|
||||
Reference in New Issue
Block a user