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
+73 -8
View File
@@ -80,6 +80,71 @@ describe('completion feedback motion', () => {
})
})
describe('archived task-list disclosure', () => {
it('keeps one collapsed disclosure row even when the archive is empty', () => {
expect(app).toContain('const archivedListsExpanded = ref(false)')
expect(app).toContain('class="archived-lists-toggle"')
expect(app).toContain('已归档 {{ archivedLists.length }}')
expect(app).toContain(':aria-expanded="archivedLists.length > 0 && archivedListsExpanded"')
expect(app).toContain('aria-controls="archived-task-lists"')
expect(app).toContain(':disabled="archivedLists.length === 0"')
expect(app).toContain('id="archived-task-lists"')
})
it('renders compact archived rows with a restore and purge menu', () => {
expect(app).toContain('v-show="archivedListsExpanded"')
expect(app).toContain('class="list-row archived-row"')
expect(app).toContain('class="archived-row-menu-trigger"')
expect(app).toContain('恢复清单')
expect(app).toContain('永久删除清单')
expect(css).toContain('.archived-lists-toggle{min-height:44px;')
expect(css).toContain('.archived-row{min-height:44px;display:grid;grid-template-columns:minmax(0,1fr) 44px;align-items:center;')
expect(css).toContain('.archived-row-actions{position:fixed;z-index:70;')
expect(css).toContain('@media(max-width:930px){.archived-action-mask{display:block;position:fixed;')
expect(css).toContain('.archived-row-actions{position:fixed;z-index:61;left:0;right:0;top:auto;bottom:0;')
})
it('preserves the expanded state while restoring or deleting the last list', () => {
const restoreBlock = app.slice(app.indexOf('async function restoreList'), app.indexOf('function openPurgeList'))
const purgeBlock = app.slice(app.indexOf('async function confirmPurgeList'), app.indexOf('function toggleSidebarCreate'))
expect(restoreBlock).not.toContain('archivedListsExpanded.value = false')
expect(purgeBlock).not.toContain('archivedListsExpanded.value = false')
})
it('keeps archived and ordinary sidebar action menus mutually exclusive', () => {
const archivedActionBlock = app.slice(app.indexOf('function toggleArchivedListAction'), app.indexOf('function openPurgeList'))
const sidebarActionBlock = app.slice(app.indexOf('function openSidebarAction'), app.indexOf('function runSidebarCreate'))
expect(archivedActionBlock).toContain('closeSidebarAction()')
expect(sidebarActionBlock).toContain('closeArchivedListAction(false)')
})
it('closes the desktop archived menu from outside pointer input without replacing the mobile mask', () => {
expect(app).toContain("document.addEventListener('pointerdown', handleArchivedListOutsidePointer)")
expect(app).toContain("window.matchMedia('(min-width: 931px)').matches")
expect(app).toContain("target.closest('.archived-row-menu,.archived-row-actions')) closeArchivedListAction(false)")
expect(app).toContain('<Teleport to="body">')
expect(app).toContain('class="archived-action-mask" @click.self="closeArchivedListAction()"')
expect(app).toContain("document.addEventListener('scroll', handleArchivedListViewportChange, true)")
expect(app).toContain("window.addEventListener('resize', handleArchivedListViewportChange)")
expect(css).toContain('@media(max-width:930px){.archived-action-mask{display:block;position:fixed;')
})
it('restores archive menu focus after Escape and actions, with a fallback after row removal', () => {
expect(app).toContain('const archivedListsToggle = ref<HTMLButtonElement | null>(null)')
expect(app).toContain('let archivedListActionTrigger: HTMLElement | null = null')
expect(app).toContain("document.addEventListener('keydown', handleArchivedListEscape)")
expect(app).toContain('resolveArchivedMenuFocusTarget(archivedListActionTrigger, archivedListsToggle.value)')
expect(app).toContain('ref="archivedListsToggle"')
expect(app).toContain("@click=\"toggleArchivedListAction(list,$event.currentTarget)\"")
expect(app).toContain('purgeListTrigger = archivedListActionTrigger')
expect(app).toContain('focusArchivedListTrigger()')
})
it('disables archive disclosure motion for reduced-motion users', () => {
expect(css).toContain('@media(prefers-reduced-motion:reduce){.archived-lists-toggle svg,.archived-list-items{transition:none}}')
})
})
describe('mobile sheet contract', () => {
it('uses shared roles for details, creation and secondary actions', () => {
expect(app).toContain('class="task-compose-mask app-sheet-mask"')
@@ -629,7 +694,7 @@ describe('sidebar information hierarchy', () => {
expect(app).toContain('aria-label="打开清单操作"')
expect(app).toContain('class="sidebar-action-mask app-sheet-mask"')
expect(app).toContain('class="sidebar-action-sheet app-sheet app-sheet--actions"')
expect(app).toContain('@keydown.esc="sidebarCreateOpen=false;closeSidebarAction()"')
expect(app).toContain('@keydown.esc="sidebarCreateOpen=false;closeArchivedListAction();closeSidebarAction()"')
expect(app).toContain('sidebarCreateOpen.value = false; sidebarAction.value = null')
expect(app).not.toContain('aria-label="重命名文件夹" @click="renameEntity')
expect(app).not.toContain('aria-label="重命名清单" @click="renameEntity')
@@ -724,22 +789,22 @@ describe('sidebar layout', () => {
expect(css).not.toContain('.list-row.list-dragging{touch-action:none}')
})
it('only restores archived lists from the explicit restore action', () => {
expect(app).toContain('class="list-row-main archived-row-label"')
it('only restores archived lists from the explicit restore menu action', () => {
expect(app).toContain('class="archived-row-label"')
expect(app).not.toContain(':aria-label="`恢复清单:${list.name}`" @click="restoreList(list)"')
expect(app).toContain('aria-label="恢复清单" @click="restoreList(list)"')
expect(app).toContain('@click="restoreList(archivedListAction)"><ArchiveRestore/>恢复清单')
})
it('loads archived lists during bootstrap so archived rows are visible after refresh', () => {
expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n void preloadCountdowns()\n await loadRestoredView()')
})
it('styles archived rows through the new list-row-main structure', () => {
expect(css).toContain('.archived-row .list-row-main>svg{color:#c9a45c}')
it('styles archived rows through the compact disclosure structure', () => {
expect(css).toContain('.archived-row-label{min-width:0;overflow:hidden;text-overflow:ellipsis;')
})
it('offers permanent deletion only beside archived lists', () => {
expect(app).toContain('aria-label="永久删除清单" @click="openPurgeList(list,$event.currentTarget)"')
it('offers permanent deletion only inside archived list menus', () => {
expect(app).toContain('@click="openPurgeList(archivedListAction)"><Trash2/>永久删除清单')
expect(app).toContain("api(`/lists/${purgeListTarget.value.id}/purge`, { method: 'DELETE' })")
expect(app).not.toMatch(/list\.is_inbox[^\n]*openPurgeList/)
})