Files
Kanban/src/components/SideDrawer.vue
T
chenshoulin 48f178e5bb style(ui): 抽屉遮罩改为半透明加背景模糊,透出底下的看板
原来的遮罩压得太重(浅色 slate-900/40、深色 black/60),打开抽屉后左边几乎
是一片灰,看不出底下是什么。改为浅色 slate-900/25、深色 black/40,并加
backdrop-blur-sm:背景仍然看得见轮廓,模糊把它推远,不和面板抢焦点。

补三条测试盯住这个观感:遮罩必须半透明、必须带模糊、必须铺满视口且在
面板之下。
2026-10-18 19:51:38 +08:00

133 lines
3.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps<{ open: boolean; title?: string }>()
const emit = defineEmits<{ close: [] }>()
const panelRef = ref<HTMLElement | null>(null)
let previouslyFocused: HTMLElement | null = null
const FOCUSABLE_SELECTOR = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')
function focusableElements(): HTMLElement[] {
const root = panelRef.value
if (!root) return []
return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
}
/** ESC 关闭 + Tab 焦点陷阱 */
function onDocumentKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
event.preventDefault()
emit('close')
return
}
if (event.key !== 'Tab') return
const items = focusableElements()
if (items.length === 0) {
event.preventDefault()
panelRef.value?.focus()
return
}
const first = items[0]
const last = items[items.length - 1]
const active = document.activeElement as HTMLElement | null
const inside = active !== null && items.includes(active)
if (event.shiftKey) {
if (!inside || active === first) {
event.preventDefault()
last?.focus()
}
} else if (!inside || active === last) {
event.preventDefault()
first?.focus()
}
}
function stopListening(): void {
document.removeEventListener('keydown', onDocumentKeydown, true)
}
watch(
() => props.open,
(isOpen) => {
if (isOpen) {
previouslyFocused = document.activeElement as HTMLElement | null
document.addEventListener('keydown', onDocumentKeydown, true)
void nextTick(() => {
const target = focusableElements()[0] ?? panelRef.value
target?.focus()
})
} else {
stopListening()
previouslyFocused?.focus?.()
previouslyFocused = null
}
},
{ immediate: true },
)
onBeforeUnmount(stopListening)
</script>
<template>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-40">
<!--
遮罩:压暗 + 轻微模糊。刻意压得比较轻,让底下的看板仍然看得见轮廓,
只把注意力让给抽屉;模糊负责把背景「推远」,避免和面板内容抢焦点。
-->
<div
class="absolute inset-0 bg-slate-900/25 backdrop-blur-sm dark:bg-black/40"
@click="emit('close')"
/>
<aside
ref="panelRef"
class="absolute inset-y-0 right-0 flex w-full max-w-md flex-col bg-white shadow-panel outline-none dark:bg-zinc-900"
role="dialog"
aria-modal="true"
:aria-label="title"
tabindex="-1"
>
<header
class="flex items-center justify-between border-b border-slate-200 px-4 py-3 dark:border-zinc-800"
>
<h2 class="text-base font-medium text-slate-800 dark:text-zinc-100">{{ title }}</h2>
<button
type="button"
class="rounded px-2 py-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
aria-label="关闭"
@click="emit('close')"
>
✕
</button>
</header>
<div class="min-h-0 flex-1 overflow-y-auto p-4">
<slot />
</div>
<footer
v-if="$slots.footer"
class="border-t border-slate-200 px-4 py-3 dark:border-zinc-800"
>
<slot name="footer" />
</footer>
</aside>
</div>
</Teleport>
</template>