feat(host): blur cancels path editing; scrollbar clearance in the miller columns
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user