feat(host): blur cancels path editing; scrollbar clearance in the miller columns
This commit is contained in:
@@ -55,7 +55,9 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 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;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
@@ -136,7 +138,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 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
|
/* Two-pane columns split the row evenly around the divider; 256px is the
|
||||||
@@ -149,6 +153,9 @@
|
|||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 256px;
|
min-width: 256px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
/* The overlay scrollbar paints at the column's edge; keep the row pills
|
||||||
|
* clear of the thumb. */
|
||||||
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnWide {
|
.columnWide {
|
||||||
|
|||||||
@@ -200,6 +200,25 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
})
|
})
|
||||||
}, [launchListing])
|
}, [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. */
|
/** A right-column pick advances the view one level: child becomes the level. */
|
||||||
const advance = useCallback((entry: DirectoryEntry) => {
|
const advance = useCallback((entry: DirectoryEntry) => {
|
||||||
/* v8 ignore next -- narrowing guard: the right column only renders with a child listing. */
|
/* 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') {
|
if (event.key === 'Escape') {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Cancel also withdraws a navigation the editor already
|
cancelPathEdit()
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
// 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>
|
</div>
|
||||||
|
|||||||
@@ -220,6 +220,20 @@ describe('DirectoryBrowser', () => {
|
|||||||
expect(screen.queryByLabelText('browser.editPath', { selector: 'input' })).toBeNull()
|
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 () => {
|
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
|
// The initial home listing hangs; Edit Path supersedes it while parent
|
||||||
// is still null, and Escape must not strand a blank picker.
|
// is still null, and Escape must not strand a blank picker.
|
||||||
|
|||||||
Reference in New Issue
Block a user