feat: collapse archived lists in sidebar
ci / gitleaks (push) Successful in 6s
ci / docker (push) Successful in 3m32s

This commit is contained in:
2026-09-09 22:23:12 +08:00
parent d49c81212d
commit 63f066681f
5 changed files with 241 additions and 18 deletions
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { positionArchivedMenu, resolveArchivedMenuFocusTarget } from './archived-list-menu'
describe('resolveArchivedMenuFocusTarget', () => {
it('returns the archived row trigger while it remains connected', () => {
const trigger = { isConnected: true } as HTMLElement
const fallback = { isConnected: true } as HTMLButtonElement
expect(resolveArchivedMenuFocusTarget(trigger, fallback)).toBe(trigger)
})
it('falls back to the archived-list disclosure after the row is removed', () => {
const trigger = { isConnected: false } as HTMLElement
const fallback = { isConnected: true } as HTMLButtonElement
expect(resolveArchivedMenuFocusTarget(trigger, fallback)).toBe(fallback)
})
it('returns null when neither focus target is available', () => {
expect(resolveArchivedMenuFocusTarget(null, null)).toBeNull()
})
})
describe('positionArchivedMenu', () => {
it('places the desktop menu below the trigger when it fits', () => {
expect(positionArchivedMenu({ left: 180, right: 224, top: 100, bottom: 144 }, 164, 102, 1280, 800)).toEqual({ left: 60, top: 150 })
})
it('flips above when the menu does not fit below', () => {
expect(positionArchivedMenu({ left: 4, right: 48, top: 730, bottom: 774 }, 164, 102, 320, 800)).toEqual({ left: 8, top: 622 })
})
it('clamps below-placement when the trigger is above the viewport', () => {
expect(positionArchivedMenu({ left: 180, right: 224, top: -200, bottom: -156 }, 164, 102, 1280, 800)).toEqual({ left: 60, top: 8 })
})
it('clamps above-placement when the trigger is below the viewport', () => {
expect(positionArchivedMenu({ left: 180, right: 224, top: 850, bottom: 894 }, 164, 102, 1280, 800)).toEqual({ left: 60, top: 690 })
})
it.each([
{ menuHeight: 104, label: 'nearly fills' },
{ menuHeight: 130, label: 'exceeds' },
])('uses the top margin when the menu $label the available height', ({ menuHeight }) => {
expect(positionArchivedMenu({ left: 180, right: 224, top: 50, bottom: 94 }, 164, menuHeight, 1280, 120)).toEqual({ left: 60, top: 8 })
})
})
+27
View File
@@ -0,0 +1,27 @@
export type ArchivedMenuRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>
export function positionArchivedMenu(
trigger: ArchivedMenuRect,
menuWidth: number,
menuHeight: number,
viewportWidth: number,
viewportHeight: number,
gap = 6,
margin = 8,
): { left: number; top: number } {
const left = Math.min(Math.max(margin, trigger.right - menuWidth), viewportWidth - menuWidth - margin)
const below = trigger.bottom + gap
const preferredTop = below + menuHeight <= viewportHeight - margin
? below
: trigger.top - menuHeight - gap
const maxTop = Math.max(margin, viewportHeight - menuHeight - margin)
const top = Math.min(Math.max(margin, preferredTop), maxTop)
return { left, top }
}
export function resolveArchivedMenuFocusTarget(
trigger: HTMLElement | null,
fallback: HTMLButtonElement | null,
): HTMLElement | null {
return trigger?.isConnected ? trigger : fallback
}