diff --git a/packages/client/connection/tests/fixture.spec.ts b/packages/client/connection/tests/fixture.spec.ts index 3c8256ac3e..15fecad404 100644 --- a/packages/client/connection/tests/fixture.spec.ts +++ b/packages/client/connection/tests/fixture.spec.ts @@ -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 }) }) diff --git a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css index 4e9378290b..82be42efbe 100644 --- a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css +++ b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css @@ -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; } diff --git a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx index fe8b7ff5c7..1c50eb8167 100644 --- a/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx +++ b/packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx @@ -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() } }} /> diff --git a/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx b/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx index a590bd3b17..5221e627f8 100644 --- a/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx +++ b/packages/host/directory-picker-browse/tests/directory-browser.spec.tsx @@ -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(() => {}) + 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('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() })