fix(host): review round 5 — draft-pending action gating, in-flow errors, IME guards

- Open and New folder disable while a path draft is uncommitted: targetPath
  still names the previous selection/listing, and committing against it
  while a different path shows in the header adopts the wrong directory.
- The Miller columns keep their own row so a status/error line renders below
  them inside the card instead of competing as a third flex item the dialog
  clips off-screen.
- Both text inputs (path editor, folder name) carry the IME composition
  guard the workspace-name inputs already had: a composing Enter confirms
  the candidate, never submits.
This commit is contained in:
creatixchu
2026-07-29 00:15:30 +08:00
parent d2b16380d1
commit f8cd2bf749
3 changed files with 100 additions and 25 deletions

View File

@@ -41,6 +41,16 @@
/* Deep chains scroll inside the trail (the effect pins the tail into view)
* so the edit zone to the right never leaves the bar. */
/* The Miller columns keep their own row so a status/error line below never
* competes with the fixed column widths for horizontal space. */
.millerRow {
display: flex;
align-items: stretch;
flex: 1 1 0;
min-height: 0;
gap: 20px;
}
.crumbTrail {
display: flex;
align-items: center;
@@ -113,10 +123,9 @@
* the hairline divider centered between them; each column scrolls alone. */
.content {
display: flex;
align-items: stretch;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
gap: 20px;
padding: 16px 24px 0;
}

View File

@@ -120,6 +120,9 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
// Deep ancestry overflows the trail; keep its tail (the current directory
// and the edit zone beside it) in view whenever the chain changes.
const crumbTrailRef = useRef<HTMLSpanElement | null>(null)
// IME confirmation (Enter selecting a candidate) must not submit either
// text input; the same guard the workspace-name inputs carry.
const composingRef = useRef(false)
/** Replace the whole view with one freshly listed level (no selection). */
const navigate = useCallback((path?: string) => {
@@ -242,6 +245,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
// focus trap, so every parent control goes inert (Shift-Tab or AT must not
// close, adopt, or retarget underneath the child).
const parentInert = busy || folderDraft !== null
// An uncommitted path draft makes targetPath stale relative to the header:
// committing actions must not act on the previous selection/listing while
// a different path is displayed.
const draftPending = pathDraft !== null
return (
<Modal
@@ -298,8 +305,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
autoFocus
disabled={parentInert}
onChange={(event) => { setPathDraft(event.target.value) }}
onCompositionStart={() => { composingRef.current = true }}
onCompositionEnd={() => { composingRef.current = false }}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.key === 'Enter' && !composingRef.current) {
event.preventDefault()
const target = pathDraft.trim()
if (target !== '') navigate(target)
@@ -315,25 +324,27 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
</div>
</div>
<div className={css.content}>
{parent !== null && (
<LevelColumn
entries={parent.entries}
selectedPath={selected?.path ?? null}
busy={parentInert}
onPick={select}
wide={!twoPane}
/>
)}
{twoPane && <span className={css.divider} />}
{twoPane && child !== null && (
<LevelColumn
entries={child.entries}
selectedPath={null}
busy={parentInert}
onPick={advance}
wide={false}
/>
)}
<div className={css.millerRow}>
{parent !== null && (
<LevelColumn
entries={parent.entries}
selectedPath={selected?.path ?? null}
busy={parentInert}
onPick={select}
wide={!twoPane}
/>
)}
{twoPane && <span className={css.divider} />}
{twoPane && child !== null && (
<LevelColumn
entries={child.entries}
selectedPath={null}
busy={parentInert}
onPick={advance}
wide={false}
/>
)}
</div>
{loading && <div className={css.status} role="status">{t('browser.loading')}</div>}
{error !== null && <div className={css.error} role="alert">{error}</div>}
</div>
@@ -341,7 +352,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
<Button
variant="outline"
icon={<IconPlusOutline16 size={14} />}
disabled={parent === null || loading || parentInert}
disabled={parent === null || loading || parentInert || draftPending}
onClick={() => {
setFolderDraft('')
setCreateError(null)
@@ -354,7 +365,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
<Button
variant="primary"
className={clsx(css.footerAction)}
disabled={targetPath === null || loading || parentInert}
disabled={targetPath === null || loading || parentInert || draftPending}
/* v8 ignore next -- narrowing guard: Open disables while no target exists. */
onClick={() => { if (targetPath !== null) onOpen(targetPath) }}
>
@@ -380,8 +391,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
autoFocus
disabled={creatingFolder}
onChange={(event) => { setFolderDraft(event.target.value) }}
onCompositionStart={() => { composingRef.current = true }}
onCompositionEnd={() => { composingRef.current = false }}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.key === 'Enter' && !composingRef.current) {
event.preventDefault()
confirmCreate()
}