diff --git a/frontend/src/CountdownPanel.test.ts b/frontend/src/CountdownPanel.test.ts index 40d1a5f..a495ab8 100644 --- a/frontend/src/CountdownPanel.test.ts +++ b/frontend/src/CountdownPanel.test.ts @@ -13,13 +13,18 @@ describe('countdown modal accessibility', () => { expect(source).toContain(':inert="open || Boolean(detailItem)"') }) - it('uses a compact timeline and moves row actions into a detail sheet', () => { + it('uses one time-capsule focus and moves row actions into a detail sheet', () => { expect(source).toContain('class="countdown-timeline"') expect(source).toContain('class="countdown-row"') - expect(source).toContain('@click="openDetail(item)"') + expect(source).toContain('class="countdown-focus"') + expect(source).toContain('@click="openDetail(focusItem)"') + expect(source).toContain('item.id !== focusItem.value?.id') + expect(source).toContain('`kind-${item.kind}`') + expect(source).toContain(':class="`kind-${focusItem.kind}`"') expect(source).toContain('class="countdown-detail-sheet"') expect(source).toContain('ref="detailCloseButton"') expect(source).toContain('detailCloseButton.value?.focus()') + expect(source).toContain('@keydown="trapDetailFocus"') expect(source).not.toContain('class="countdown-actions"') }) diff --git a/frontend/src/CountdownPanel.vue b/frontend/src/CountdownPanel.vue index d03cb86..1d1fc8b 100644 --- a/frontend/src/CountdownPanel.vue +++ b/frontend/src/CountdownPanel.vue @@ -70,16 +70,17 @@ function sorted(itemsToSort: Countdown[]) { return [...itemsToSort].sort((a, b) => Number(b.pinned) - Number(a.pinned) || a.days - b.days || a.title.localeCompare(b.title, 'zh-Hans-CN')) } const upcomingItems = computed(() => sorted(items.value.filter((item) => item.days >= 0))) -const focusItem = computed(() => upcomingItems.value[0] ?? sorted(items.value)[0] ?? null) +const focusItem = computed(() => sorted(items.value).find((item) => item.pinned) ?? upcomingItems.value[0] ?? sorted(items.value)[0] ?? null) const countdownSummary = computed(() => { if (!items.value.length) return '还没有记录重要日子' const pinned = items.value.filter((item) => item.pinned).length return pinned ? `${items.value.length} 个重要日子 · ${pinned} 个置顶` : `${items.value.length} 个重要日子` }) const countdownGroups = computed(() => { - const recent = sorted(items.value.filter((item) => item.days >= 0 && item.days <= 30)) - const later = sorted(items.value.filter((item) => item.days > 30)) - const past = sorted(items.value.filter((item) => item.days < 0)) + const withoutFocus = items.value.filter((item) => item.id !== focusItem.value?.id) + const recent = sorted(withoutFocus.filter((item) => item.days >= 0 && item.days <= 30)) + const later = sorted(withoutFocus.filter((item) => item.days > 30)) + const past = sorted(withoutFocus.filter((item) => item.days < 0)) return [ { key:'recent', title:'最近', items:recent }, { key:'later', title:'稍后', items:later }, @@ -149,6 +150,17 @@ function formatDateShort(value:string){const [y,m,d]=value.split('-');return `${ function repeatLabel(value:Countdown['repeat_rule']){return({none:'不重复',weekly:'每周',monthly:'每月',yearly:'每年'})[value]} function openDetail(item:Countdown){detailItem.value=item;previousFocus=document.activeElement instanceof HTMLElement ? document.activeElement : null;void nextTick(() => detailCloseButton.value?.focus())} function closeDetail(){detailItem.value=null;void nextTick(() => previousFocus?.focus())} +function trapDetailFocus(event: KeyboardEvent) { + if (event.key !== 'Tab') return + const dialog = event.currentTarget as HTMLElement + const controls = Array.from(dialog.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')) + .filter((item) => !item.hasAttribute('disabled')) + if (!controls.length) return + const first = controls[0] + const last = controls[controls.length - 1] + if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() } +} function openFromEmpty(){openCountdownComposer()} function openCountdownComposer(origin?: { x: number; y: number }){if(origin)composerOrigin.value=origin;detailItem.value=null;editingId.value=null;showAdvanced.value=false;form.value=freshForm();open.value=true;focusDialog()} defineExpose({ openCountdownComposer }) @@ -162,28 +174,28 @@ onBeforeUnmount(() => { previousFocus = null })
{{countdownSummary}}

{{error}}

-
+ +

{{group.title}}

-
-
还没有重要日子生日、纪念日,或一场期待已久的旅行
{{item.title}}{{formatDateShort(item.display_date)}}
-