fix: sheet Escape & focus management, detail a11y, 44px touch targets
- AppSheet: Escape closes any sheet branch; non-modal/inline sheets move focus into panel on open; modal<->inline transition keeps overlay stack - Task detail: initial focus on close button; note textarea aria-label - Global sr-only status region for toast announcements (visual toast hidden from AT to avoid double reading) - Focus view: drop duplicate topbar h1 (page keeps its own single h1) - Archived lists toggle hidden until loaded (no more 'archived 0' flash) - Touch: date-clear/time-remove 44px; edit/preview switch hit area 44px with 3px gap; calendar filter row gets a scroll-right mask hint
This commit is contained in:
@@ -39,6 +39,13 @@ function focusables() {
|
||||
.filter(isVisibleFocusable)
|
||||
}
|
||||
function keydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
if (props.busy || (props.modal && !isTopOverlay(overlayId))) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('close')
|
||||
return
|
||||
}
|
||||
if (!props.modal || event.key !== 'Tab' || !isTopOverlay(overlayId)) return
|
||||
const controls = focusables()
|
||||
if (!controls.length) { event.preventDefault(); panel.value?.focus(); return }
|
||||
@@ -54,19 +61,26 @@ function focusIntoPanel() {
|
||||
}
|
||||
}
|
||||
async function activate() {
|
||||
if (!props.open || !props.modal || overlayId) return
|
||||
overlayId = pushOverlay(requestClose, () => props.busy, focusIntoPanel)
|
||||
layerZIndex.value = overlayZIndex(overlayId)
|
||||
if (!props.open) return
|
||||
if (!props.modal && overlayId) deactivate()
|
||||
if (props.modal) {
|
||||
if (!overlayId) {
|
||||
overlayId = pushOverlay(requestClose, () => props.busy, focusIntoPanel)
|
||||
layerZIndex.value = overlayZIndex(overlayId)
|
||||
}
|
||||
await nextTick()
|
||||
if (props.open && overlayId) focusIntoPanel()
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
if (!props.open || !props.modal || !overlayId) return
|
||||
focusIntoPanel()
|
||||
if (props.open) focusIntoPanel()
|
||||
}
|
||||
function deactivate() {
|
||||
if (overlayId) popOverlay(overlayId)
|
||||
overlayId = null
|
||||
}
|
||||
watch([() => props.open, () => props.modal], ([open, modal]) => {
|
||||
if (open && modal) void activate()
|
||||
watch([() => props.open, () => props.modal], ([open]) => {
|
||||
if (open) void activate()
|
||||
else deactivate()
|
||||
}, { immediate: true })
|
||||
onBeforeUnmount(deactivate)
|
||||
@@ -83,11 +97,11 @@ onBeforeUnmount(deactivate)
|
||||
</Transition>
|
||||
</Teleport>
|
||||
<Teleport v-else-if="inlineTarget" :to="inlineTarget">
|
||||
<component v-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" :aria-busy="busy || undefined" v-bind="$attrs">
|
||||
<component v-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" :aria-busy="busy || undefined" tabindex="-1" v-bind="$attrs" @keydown="keydown">
|
||||
<slot />
|
||||
</component>
|
||||
</Teleport>
|
||||
<component v-else-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" v-bind="$attrs">
|
||||
<component v-else-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" tabindex="-1" v-bind="$attrs" @keydown="keydown">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user