feat(host): blur cancels path editing; scrollbar clearance in the miller columns

This commit is contained in:
creatixchu
2026-07-29 13:42:11 +08:00
parent c747f721ba
commit 60383efef9
3 files changed
+48 -19

No files matched your search

@@ -55,7 +55,9 @@
align-items: stretch;
flex: 1 1 0;
min-height: 0;
gap: 20px;
/* Columns already end in an 8px scrollbar clearance, so the divider only
* needs a slim gap of its own on each side. */
gap: 12px;
overflow-x: auto;
scrollbar-width: none;
}
@@ -136,7 +138,9 @@
flex-direction: column;
flex: 1 1 0;
min-height: 0;
padding: 16px 24px;
/* Right inset is slimmer than the left: the trailing column's own 8px
* scrollbar clearance makes up the optical difference. */
padding: 16px 16px 16px 24px;
}
/* Two-pane columns split the row evenly around the divider; 256px is the
@@ -149,6 +153,9 @@
flex: 1 1 0;
min-width: 256px;
overflow-y: auto;
/* The overlay scrollbar paints at the column's edge; keep the row pills
* clear of the thumb. */
padding-right: 8px;
}
.columnWide {
@@ -200,6 +200,25 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
})
}, [launchListing])
/** Abandon path editing (Escape or clicking away) and restore the crumb view. */
const cancelPathEdit = useCallback(() => {
// Cancel also withdraws a navigation the editor already launched: its
// late success must not jump to the cancelled path, so the pending
// request is superseded and the view leaves the loading state.
supersede()
setLoading(false)
setPathDraft(null)
setError(null)
// Editing may have superseded the selection's preview request; a
// selection with no preview would render a 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()
}, [supersede, child, parent, navigate])
/** A right-column pick advances the view one level: child becomes the level. */
const advance = useCallback((entry: DirectoryEntry) => {
/* v8 ignore next -- narrowing guard: the right column only renders with a child listing. */
@@ -382,25 +401,14 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
}
if (event.key === 'Escape') {
event.stopPropagation()
// Cancel also withdraws a navigation the editor already
// launched: its late success must not jump to the
// cancelled path, so the pending request is superseded
// and the view leaves the loading state.
supersede()
setLoading(false)
setPathDraft(null)
setError(null)
// Editing may have superseded the selection's preview
// request; a selection with no preview would render a
// 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()
cancelPathEdit()
}
}}
// Clicking anywhere outside the editor reads as leaving it:
// focus loss cancels the edit like Escape. Enter keeps focus
// in the input while its navigation is in flight, so a
// submitted path is never withdrawn by this handler.
onBlur={cancelPathEdit}
/>
)}
</div>
@@ -220,6 +220,20 @@ describe('DirectoryBrowser', () => {
expect(screen.queryByLabelText('browser.editPath', { selector: 'input' })).toBeNull()
})
it('clicking away from the path editor cancels it back to the crumb view', async () => {
mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: '/somewhere/else' } })
// Focus moving anywhere outside the editor abandons the draft like Escape.
fireEvent.blur(input)
expect(screen.queryByLabelText('browser.editPath', { selector: 'input' })).toBeNull()
// The crumb view is back and the abandoned draft was never navigated to.
expect(screen.getByRole('button', { name: 'browser.editPath' })).toBeTruthy()
expect(screen.getByRole('listitem').textContent).toBe('Documents')
})
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.