fix(host): restart home on early-edit cancel; clamp the dialog to short viewports

Escape canceling a path edit opened before any level listed relaunches
the home listing instead of stranding a blank picker (the editor had
superseded the initial request while parent was still null). The card's
height clamps to the viewport (min(420px, 100dvh - 32px)); header and
footer are flex-none and the columns scroll, so Open/Cancel stay
reachable on landscape phones and short embedded windows.
This commit is contained in:
creatixchu
2026-07-29 04:46:49 +08:00
parent 48141b0007
commit 1a9b84b19e
4 changed files with 34 additions and 5 deletions

View File

@@ -320,13 +320,13 @@ describe('createFixtureApi', () => {
const created = await api.host.createDirectory(req({ path: '/', name: 'srv' }))
if (!created.result.ok) throw new Error('create failed')
expect(created.result.value.path).toBe('/srv')
const listed = await api.host.listDirectory(req({ path: '/srv' }))
const listed = await api.host.listDirectory(req({ path: '/srv' }), new AbortController().signal)
if (!listed.result.ok) throw new Error('list failed')
expect(listed.result.value.crumbs).toEqual([
{ name: '/', path: '/', hidden: false },
{ name: 'srv', path: '/srv', hidden: false },
])
const root = await api.host.listDirectory(req({ path: '/' }))
const root = await api.host.listDirectory(req({ path: '/' }), new AbortController().signal)
if (!root.result.ok) throw new Error('root list failed')
expect(root.result.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false })
})

View File

@@ -1,12 +1,15 @@
/* Directory-browser dialog (figma 813-23126 family). The shared Modal renders
* headless here — mask, card, Escape only — and this module owns the figma
* frame exactly: fixed 600×420 card, header (title + crumbs, l3 separator),
* frame: 600×420 card (viewport-clamped), header (title + crumbs, l3 separator),
* the one-or-two-column Miller content, and the bordered footer. */
/* Doubled class beats Modal's own .dialog regardless of stylesheet order. */
/* Short viewports clamp the card: header/footer are flex-none and the
* columns scroll, so shrinking the height keeps Open/Cancel reachable
* instead of clipping them below a fixed overlay. */
.dialog.dialog {
width: min(600px, 100%);
height: 420px;
height: min(420px, calc(100dvh - 32px));
padding: 0;
gap: 0;
}

View File

@@ -1,6 +1,7 @@
/**
* The in-app workspace-directory browser (figma Harness 813-23126 family): a
* fixed 600×420 dialog whose header carries the title, the selection-path
* 600×420 dialog (clamped to short/narrow viewports — the Miller row scrolls
* sideways, the columns scroll down) whose header carries the title, the selection-path
* breadcrumb, and a click-to-edit path zone; below it a Miller view — one
* full-width level until a row is selected, then two 256px columns (level |
* selected folder's children) around a hairline divider. Selecting in the
@@ -364,6 +365,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
// half-empty two-pane view, so cancel falls back to the
// single-pane level.
if (child === null) setSelected(null)
// With no level listed yet (the editor superseded the
// initial home listing), plain cancellation would leave a
// permanently blank picker: restart the home listing.
if (parent === null) navigate()
}
}}
/>

View File

@@ -177,6 +177,27 @@ describe('DirectoryBrowser', () => {
expect(screen.queryByLabelText('browser.editPath', { selector: 'input' })).toBeNull()
})
it('restarts the home listing when Escape cancels an edit opened before any level listed', async () => {
// The initial home listing hangs; Edit Path supersedes it while parent
// is still null, and Escape must not strand a blank picker.
let settled = false
const gate = new Promise<never>(() => {})
const listDirectory = vi.fn(async (path?: string) => {
if (!settled) { settled = true; return gate }
return listingFor(path)
})
mount({ listDirectory })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
expect(input.value).toBe('')
fireEvent.keyDown(input, { key: 'Escape' })
// Cancellation relaunched the home listing instead of leaving neither
// rows nor status behind.
await waitFor(() => { expect(screen.getByRole('listitem').textContent).toBe('Documents') })
expect(listDirectory).toHaveBeenCalledTimes(2)
expect(listDirectory).toHaveBeenLastCalledWith(undefined)
})
it('surfaces an unreadable target as an alert and keeps the edit open for correction', async () => {
mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })