feat: collapse archived lists in sidebar
This commit is contained in:
@@ -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 })
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user