/** * The in-app workspace-directory browser (figma Harness 813-23126 family): a * 680×500 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 of one * or two columns splitting the row evenly (256px floor; level | selected * folder's children) around a hairline divider — the display root and * degraded landings keep the single wide level, while any selection opens * the second pane, including the one a navigation lands with: a crumb jump * or a submitted path commits the target immediately, then re-selects it * in its parent level once that level arrives, so stepping back keeps two * panes away from the display root. Selecting in the * right column shifts the view one level deeper. "New folder" opens a nested * create dialog targeting the selected folder (or the level itself) and * selects the created folder — unless a newer pick or crumb jump supersedes * the post-create relist, in which case neither the level nor the selection * refreshes (see closeCreateDialog's two-stage parking for the matching * focus story). Open adopts the selected folder, falling back * to the listed level. Pure consumer of the injected browse calls — the * owning flow decides what "Open" means and owns the workspace-creation * error surface. Hidden entries are host-flagged and hidden by default; the * footer's fixed-label "Show hidden files" toggle (aria-pressed, check when * on) reveals them (client-side only). The path editor opens seeded with a * trailing separator, and while the draft's directory part names a listed * level, its final segment prefix-filters that level's rows (a dot-led * prefix also reveals the hidden entries it names). */ import { useCallback, useEffect, useRef, useState } from 'react' import clsx from 'clsx' import { Button, IconCheckOutline16, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, Modal, } from '@deepseek-ai/dsh-client-ui-primitives' import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client' import { DirectoryBrowseError } from '@deepseek-ai/dsh-client-runtime/client' import type { Translate } from '@deepseek-ai/dsh-client-locale/client' import css from './DirectoryBrowser.module.css' /** Owner-supplied browser props: browse calls, pick semantics, and copy. */ export interface DirectoryBrowserProps { /** Dialog visibility (owner-local; closed unmounts nothing but resets on reopen). */ open: boolean /** List one directory level (absent path = the Host home directory); the signal aborts a superseded scan on the wire. */ listDirectory: (path?: string, signal?: AbortSignal) => Promise /** Create one child directory under an existing parent. */ createDirectory: (path: string, name: string) => Promise /** The operator confirmed a directory (the selection, else the listed level). */ onOpen: (path: string) => void /** Close without picking (mask, Escape, Cancel). */ onClose: () => void /** The owner's confirm is in flight: Open disables, the view freezes. */ busy: boolean /** Localized copy. */ t: Translate } /** Failure text: the Host business message when typed, else the throw's text. */ function failureText(error: unknown): string { if (error instanceof DirectoryBrowseError) return error.rpcError.message return error instanceof Error ? error.message : String(error) } /** * Case-folds a path for comparisons under the given separator's platform: * backslash (Windows) paths compare case-insensitively — a typed path * legally differs in case from the host's stamped one — while slash * platforms compare exactly (the filesystem may be case-sensitive; only a * FINAL-segment macOS case drift misses parent-entry matching and keeps * the single-pane landing, since parent entry paths inherit the typed * prefix). */ function foldPathFor(sep: '\\' | '/'): (value: string) => string { return value => (sep === '\\' ? value.toLowerCase() : value) } /** * Folds separators to the platform's canonical one: win32 treats a forward * slash as a separator too (resolve() folds them the same way), while * POSIX must not — a backslash there is a name character. */ function foldSeparatorsFor(sep: '\\' | '/'): (value: string) => string { return value => (sep === '\\' ? value.replaceAll('/', sep) : value) } /** * Lexically normalizes a typed absolute path for comparisons against the * backend's resolved ones (every listing path arrives in the * DirectoryListing contract's canonical shape; only the DRAFT side, the * one path a user types, needs this): collapses * repeated and trailing separators, drops `.` segments, and applies `..` * without ever crossing the root — POSIX's `/`, a drive's `C:`, or UNC's * `\\server\share` pair — mirroring resolve()'s lexical behavior. Expects * separators already folded to `sep` (foldSeparatorsFor); a lexical mirror * only, symlinks are the backend's business. */ function normalizePathFor(sep: '\\' | '/'): (value: string) => string { return (value) => { const unc = sep === '\\' && value.startsWith(`${sep}${sep}`) const rawSegments = (unc ? value.slice(2) : value).split(sep) // Empty segments are separator noise everywhere except POSIX's leading // root marker, which must survive as the first segment; scrubbing them // up front keeps a doubled separator from being locked into the UNC // server + share root below. const segments = unc ? rawSegments.filter(segment => segment !== '') : rawSegments // The unpoppable root: POSIX's leading empty segment / the drive // segment, or UNC's server + share pair. const rootLength = unc ? 2 : 1 const out = segments.slice(0, rootLength) for (const segment of segments.slice(rootLength)) { if (segment === '' || segment === '.') continue if (segment === '..') { if (out.length > rootLength) out.pop() continue } out.push(segment) } // A bare root keeps (or regains) the trailing separator resolve() // emits for `/`, `C:\`, and `\\server\share\`. return `${unc ? sep + sep : ''}${out.join(sep)}${out.length === rootLength ? sep : ''}` } } /** * Breadcrumb rows for display: inside the home subtree the chain starts at a * localized Home crumb; outside it the full ancestry shows, the root labeled * by its own path. `home` and every crumb path arrive in the same resolved * shape (the wire contract), so only the platform case fold remains — a * typed-case Windows chain still collapses to the Home crumb. */ function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryEntry[] { const fold = foldPathFor(separatorOf(listing)) const home = fold(listing.home) const homeIndex = listing.crumbs.findIndex(crumb => fold(crumb.path) === home) if (homeIndex === -1) return listing.crumbs const tail = listing.crumbs.slice(homeIndex + 1) return [{ name: homeLabel, path: listing.home, hidden: false }, ...tail] } /** * The listing's platform separator, read from the host-resolved root crumb * (`/`, `C:\`, `\\server\share\`) — exact for every root form the backend * emits, and immune to backslashes inside POSIX names (which the home text * may legally carry; the wire contract already excludes non-canonical * shapes elsewhere). * TODO: replace with a host-stamped `separator` field on the wire * DirectoryListing so the platform fact travels verbatim (the trade-off is * recorded in the directory-picker capability seam Agent Note). */ function separatorOf(listing: DirectoryListing): '\\' | '/' { const rootCrumb = listing.crumbs.at(0) // The seam type allows an empty chain (this backend never emits one, but // create-target naming supports it, see targetName): degrade to a // best-effort read of the home text — the pre-root-crumb heuristic, with // its backslash-in-a-POSIX-name blind spot. if (rootCrumb === undefined) return listing.home.includes('\\') ? '\\' : '/' return rootCrumb.path.includes('\\') ? '\\' : '/' } /** * The path draft's final segment, when its directory part names the level * `listing` lists — the segment the level prefix-filters on while the user * types. Any other draft (no separator yet, or naming some other directory) * leaves the level unfiltered. Only the directory part is lexically * normalized (dot segments, repeated separators, and win32 forward slashes * all match what Enter would navigate to) and platform-case-folded (exact * on slash platforms; Windows folds, since an upgraded selection may carry * the actual entry's case while the level below still carries the typed * one); the FINAL segment stays a literal name prefix — a lone `.` reads * as the dot-reveal, `..` matches no entry (Enter still navigates it) — * and the name filter downstream is case-insensitive everywhere. */ function draftPrefixFor(listing: DirectoryListing, draft: string | null): string | null { if (draft === null) return null const sep = separatorOf(listing) const folded = foldSeparatorsFor(sep)(draft) const cut = folded.lastIndexOf(sep) if (cut === -1) return null const fold = foldPathFor(sep) const normalize = normalizePathFor(sep) return fold(normalize(folded.slice(0, cut + 1))) === fold(listing.path) ? folded.slice(cut + 1) : null } /** One column of folder rows (the Miller view renders one or two of these). */ function LevelColumn({ entries, selectedPath, busy, onPick, showHidden, filterPrefix, pathEditing }: { entries: readonly DirectoryEntry[] selectedPath: string | null busy: boolean onPick: (entry: DirectoryEntry) => void showHidden: boolean filterPrefix: string | null pathEditing: boolean }) { const visible = entries.filter((entry) => { // The selection is exempt from both filters: it anchors the two-pane // view (crumbs and the child pane point at it), so neither the hidden // filter after a dot-reveal pick nor a prefix miss may orphan it. if (entry.path === selectedPath) return true if (filterPrefix !== null && !entry.name.toLowerCase().startsWith(filterPrefix.toLowerCase())) return false // A dot-led prefix names hidden entries explicitly, so matching ones // surface even while the toggle keeps the rest hidden. return showHidden || !entry.hidden || filterPrefix?.startsWith('.') === true }) return (
{visible.map((entry) => { const selected = entry.path === selectedPath return ( // The wrapper carries the list semantics; the row keeps its NATIVE // button role so assistive technology exposes an actionable control. ) })}
) } /** * Render the directory-browser dialog. * @param props - owner-controlled browser props. * @returns the dialog element (null while closed, via Modal). */ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen, onClose, busy, t }: DirectoryBrowserProps) { // Miller state: the listed level, the selected row in it, and the selected // folder's own listing (the right column; null while nothing is selected). const [parent, setParent] = useState(null) const [selected, setSelected] = useState(null) const [child, setChild] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Path-edit state: null = breadcrumb mode; a string = the draft being typed. const [pathDraft, setPathDraft] = useState(null) // Show-hidden toggle state (pure client-side filter, reset on each open). const [showHidden, setShowHidden] = useState(false) // Create-folder state: null = closed; a string = the nested dialog's draft. const [folderDraft, setFolderDraft] = useState(null) const [creatingFolder, setCreatingFolder] = useState(false) const [createError, setCreateError] = useState(null) const requestSeq = useRef(0) // The in-flight listing's controller: superseding intent aborts the wire // request too — the Host stops scanning — instead of only discarding the // eventual result while the scan keeps consuming host resources. Always // holds a controller so no consumer needs a null guard: initially a // placeholder that the first supersede aborts unused (minted lazily — // useRef evaluates its argument every render), afterwards the latest // scan's, settled or aborted between scans. const [initialScanController] = useState(() => new AbortController()) const scanController = useRef(initialScanController) // Bumped on every open/close edge: settlements from a previous open (a // pending creation included) must never mutate a reopened dialog. const openGeneration = useRef(0) // 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(null) // IME confirmation (Enter selecting a candidate) must not submit either // text input; the same guard the workspace-name inputs carry, shared by // the path editor and the folder-name input. const composingRef = useRef(false) // HMR/unmount invalidation: a completion from a disposed flow must not // update state or issue follow-up requests from a dead component. useEffect(() => () => { requestSeq.current += 1 openGeneration.current += 1 scanController.current.abort() }, []) const compositionGuard = { onCompositionStart: () => { composingRef.current = true }, onCompositionEnd: () => { composingRef.current = false }, } /** Newer intent wins: invalidate the pending listing's settlement AND abort its wire request. */ const supersede = useCallback((): number => { scanController.current.abort() return ++requestSeq.current }, []) /** Launch one listing under a fresh controller so a later supersession can abort it. */ const launchListing = useCallback((path: string | undefined): { seq: number; scan: Promise } => { const seq = supersede() const controller = new AbortController() scanController.current = controller return { seq, scan: listDirectory(path, controller.signal) } }, [supersede, listDirectory]) // The miller row's scroll host, shared by the pin and refocus effects // below and read by navigate's upgrade leg (declared ahead of both). const millerRowRef = useRef(null) // Focus parking (consumed by the refocus effect below): a pick — and a // parent-leg upgrade that displaces focused rows — parks on the // selection's row; every other displacing exit (Enter, Escape, a landing // whose new level dropped the focused row, a failed pick, and every // create-dialog exit, whose close-time parking is also what a failed // relist inherits) parks on the crumb edit zone, each only when focus // actually fell to body. One parking bypasses both flags: the // show-hidden toggle's click reclaims focus onto itself, synchronously, // when the click finds focus among the rows. Pointer-out cancels never // set (or clear) these — yanking focus back from wherever the user // clicked would be worse than the fall. const refocusPick = useRef(false) const refocusEditZone = useRef(false) const editZoneRef = useRef(null) /** * Whether the focused element sits among the miller rows — probed before * a landing replaces the row nodes to decide focus parking, and by the * show-hidden toggle's click to decide whether to reclaim the native * focus outcome. A probe only: it never gates its caller — a torn-down * ref in a landing's close race merely skips the parking, and * committing the landing into a closing dialog is safe (the component * already renders null, and the open effect resets parent/selected/child * on the next open). * @returns true when `document.activeElement` is inside the miller row. */ const focusInMillerRows = useCallback((): boolean => { const rowHost = millerRowRef.current // Only the landing callers can race a close (commit precedes the reset // effect); the toggle's click caller always finds the host mounted. /* v8 ignore next -- close-race guard: not deterministically reproducible. */ if (rowHost === null) return false return rowHost.contains(document.activeElement) }, []) /** * Launch a follow-up listing under the CURRENT supersession seq: a newer * intent aborts it like the leg it continues, and it supersedes nothing. */ const continueScan = useCallback((path: string): Promise => { // Abort whatever the slot last tracked before overwriting it (the // caller's settled leg: a no-op) — the slot must never silently strand // a live scan, the exact waste supersede() exists to prevent. scanController.current.abort() const controller = new AbortController() scanController.current = controller return listDirectory(path, controller.signal) }, [listDirectory]) /** * Replace the whole view with a freshly navigated level. The target level * commits the moment it arrives (single wide level: the editor closes and * loading ends on this first settlement, so an Enter-submitted navigation * is never withdrawn waiting on anything further). Away from the display * root — the same collapse the crumb header renders, so crumbs and pane * shape never disagree — a parent leg then upgrades the landing in place: * the target's ACTUAL parent-level entry re-selected (left pane = parent, * right pane = the target), so a crumb jump reads as stepping back one * pane (Windows folds case; on slash platforms only a FINAL-segment case * drift misses the match and keeps the single-pane landing — parent * entries inherit the typed prefix, so ancestor-segment drift still * matches, at the cost of the Home collapse). A failed parent leg, or a * truncated parent window that lacks the target, leaves the committed * single-pane landing — the upgrade must never orphan the selection it * exists to anchor. */ const navigate = useCallback((path?: string) => { const { seq, scan } = launchListing(path) setLoading(true) setError(null) scan.then((target) => { if (seq !== requestSeq.current) return // The landing replaces every row key; a slow jump leaves the OLD // rows tabbable meanwhile (parentInert excludes loading), so focus // may live among them. With no selection yet the edit zone is the // park target (body-guarded, like every other exit). The probe never // gates the commit below — stranding the dialog in loading over a // focus check would be far worse than a skipped parking. if (focusInMillerRows()) refocusEditZone.current = true setParent(target) setSelected(null) setChild(null) setLoading(false) setPathDraft(null) // Arity is label-independent: only the collapsed chain's depth decides. if (displayCrumbs(target, '').length < 2) return const parentCrumb = target.crumbs.at(-2) /* v8 ignore next -- narrowing: a two-deep display chain implies a parent crumb (root-to-target inclusive). */ if (parentCrumb === undefined) return continueScan(parentCrumb.path).then((parentLevel) => { if (seq !== requestSeq.current) return // Windows resolves a typed path preserving its case; anchor on the // parent level's actual entry so selection comparisons hold (slash // platforms compare exactly — see foldPathFor). const fold = foldPathFor(separatorOf(parentLevel)) const match = parentLevel.entries.find(entry => fold(entry.path) === fold(target.path)) if (match === undefined) return // The upgrade replaces every committed row node; if focus lives // among them (Tab reached the rows during the parent leg), arm the // refocus effect so it re-parks on the re-selected row. if (focusInMillerRows()) refocusPick.current = true setParent(parentLevel) setSelected(match) setChild(target) }, () => { // Swallows the parent-leg failure (its abort included): the // committed single-pane landing stands, and nobody asked to see // the parent level. }) }, (reason: unknown) => { if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) }) }, [launchListing, continueScan, focusInMillerRows]) /** * Close the nested create dialog. Its unmount drops focus to body (the * Modal has no focus trap), so every exit — Escape, mask, Cancel, and a * successful create — arms the body-guarded edit-zone parking. A * successful create therefore parks in TWO stages: the edit zone on this * close, then the relist's select() re-parks on the created row one RTT * later — deliberately re-parking even focus the user moved during the * relist window, and doubling as the parking a failed relist inherits. */ const closeCreateDialog = useCallback(() => { setFolderDraft(null) refocusEditZone.current = true }, []) /** Select a row of the listed level and preview its children on the right. */ const select = useCallback((entry: DirectoryEntry) => { const { seq, scan } = launchListing(entry.path) // A pick while the path editor is open adopts the (filtered) row and // closes the editor — the draft served its purpose. EVERY pick re-parks // focus on the selection after commit (see the refocus effect below): // a left-pane pick lands on the very row that was clicked (a near // no-op), while a right-pane advance and a create landing replace the // picked button's column entirely and would otherwise drop focus to // body. refocusPick.current = true setPathDraft(null) setSelected(entry) setChild(null) setLoading(true) setError(null) scan.then((next) => { if (seq !== requestSeq.current) return setChild(next) setLoading(false) }, (reason: unknown) => { if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) // An unreadable selection cannot be the committing target while the // breadcrumb still names the level: fall back to the single pane. setSelected(null) // Clearing the selection can unmount the very row the pick parked // focus on (a dot-revealed hidden row re-hides); the refocus effect // re-parks on the edit zone only if focus actually fell to body. refocusEditZone.current = true }) }, [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. */ if (child === null) return setParent(child) select(entry) }, [child, select]) // Every open starts fresh at the Host home directory; closing invalidates // any in-flight response so a late arrival cannot repopulate a closed dialog. useEffect(() => { openGeneration.current += 1 if (open) { setParent(null) setSelected(null) setChild(null) setCreatingFolder(false) setShowHidden(false) navigate() return } supersede() setError(null) setPathDraft(null) setFolderDraft(null) setCreateError(null) // A close mid-flight (failed Enter, then Cancel) may leave refocus // flags armed; retire them so a later render cannot consume them. refocusPick.current = false refocusEditZone.current = false }, [open, navigate, supersede]) /** The folder a create or Open acts on: the selection, else the listed level. */ const targetPath = selected?.path ?? parent?.path ?? null const targetName = selected?.name ?? (parent === null ? '' : (displayCrumbs(parent, t('browser.home')).at(-1)?.name ?? parent.path)) const confirmCreate = (): void => { /* v8 ignore next -- reentry fence: the nested dialog only renders with a target and disables while creating. */ if (targetPath === null || folderDraft === null || creatingFolder) return // Trim only rejects an all-whitespace draft; the Host gets the original // spelling — the backend accepts any non-blank single segment verbatim, // and trimming here would create (and select) a different sibling. const name = folderDraft if (name.trim() === '') return setCreatingFolder(true) setCreateError(null) const generation = openGeneration.current createDirectory(targetPath, name).then((createdPath) => { // A settlement from a closed (possibly reopened) flow must not touch // the fresh dialog or issue a relist against the stale target. if (generation !== openGeneration.current) return setCreatingFolder(false) closeCreateDialog() // Land like a right-column pick (figma 802:57446 → 813:23278 flow): the // create target becomes the listed level and the new folder its selection. const { seq, scan } = launchListing(targetPath) setLoading(true) scan.then((level) => { // The nested dialog closed before this relist launched, so the card // is interactive meanwhile: a pick or crumb jump supersedes it. if (seq !== requestSeq.current) return setParent(level) setLoading(false) select({ name, path: createdPath, hidden: false }) }, (reason: unknown) => { // Same interactive-window fence as the success branch above. if (seq !== requestSeq.current) return setLoading(false) setError(failureText(reason)) }) }, (reason: unknown) => { if (generation !== openGeneration.current) return setCreatingFolder(false) setCreateError(failureText(reason)) }) } // After the hooks: a closed dialog renders nothing and evaluates no copy. const crumbSource = child ?? parent const crumbs = crumbSource === null ? [] : displayCrumbs(crumbSource, t('browser.home')) const crumbTail = crumbs.at(-1)?.path useEffect(() => { const trail = crumbTrailRef.current if (trail !== null) trail.scrollLeft = trail.scrollWidth }, [crumbTail]) // On viewports too narrow for both fixed panes the Miller row scrolls; // whenever a child preview lands, pin it into view the way the crumb tail // pins — otherwise descent is unreachable on a phone-width window. The // refocus effect's row.focus() and this pin can fight on such viewports, // and whichever commit runs later wins by design: on a parent-leg // upgrade (one commit) focus placement runs after the pin and keeps the // selected LEFT row in view; on a plain advance or create landing the // child arrives in a later commit, so the pin runs after the focus and // descent reachability wins. const childPath = child?.path useEffect(() => { const row = millerRowRef.current if (row !== null && childPath !== undefined) row.scrollLeft = row.scrollWidth }, [childPath]) // Every pick and editor exit that would drop focus to body re-parks it // after commit, so THIS DIALOG'S OWN node replacements never leak focus // out of the card: a pick lands on the selection's row — aria-current in // the freshly rendered left pane, which survives even a right-pane // advance or a create landing replacing the picked button's column — // while the edit-zone exits enumerated at the flag declarations fall // back to the crumb edit zone. Outside the guarantee: the Modal has no // focus trap, so tabbing past the card's edge legitimately leaves, and // the owner's adopt window (busy inerts every control; browsers blur // disabled elements to body) gets no parking — the owner closes the // dialog either way. useEffect(() => { if (pathDraft !== null) return if (refocusPick.current) { refocusPick.current = false refocusEditZone.current = false const rowHost = millerRowRef.current /* v8 ignore next -- narrowing guard: the miller row is mounted whenever a pick just committed. */ if (rowHost === null) return const row = rowHost.querySelector('button[aria-current="true"]') if (row !== null) { row.focus() return } // The pick lost its row (a truncated relist after Create can drop // the created directory outside the window): fall through to the // edit-zone parking below instead of leaving focus where it fell. refocusEditZone.current = true } if (refocusEditZone.current) { refocusEditZone.current = false // Re-park only when the close actually dropped focus to body; focus // the user parked elsewhere (a surviving row) stays theirs. if (document.activeElement !== document.body) return const zone = editZoneRef.current // The effect already returned while a draft is open, and the close // reset cleared both flags — so crumb mode's zone is always mounted. /* v8 ignore next -- narrowing guard: crumb mode always renders the edit zone. */ if (zone === null) return zone.focus() } }) if (!open) return null const twoPane = selected !== null // The nested create dialog owns the interaction while open: Modal has no // 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 ( { if (folderDraft === null && !busy) onClose() }} title={t('browser.title')} className={clsx(css.dialog)} headless > {/* Path-edit cancellation is observed at the card scope, not the * input: once Tab parks focus on a filtered row the input is off the * event path, yet Escape must still collapse the editor (not the * dialog) and a further focus move out of the card must still * cancel. display:contents keeps header/content/footer as direct * flex children of the Modal card. */}
{ if (event.key !== 'Escape' || pathDraft === null) return // stopPropagation keeps the card-scope Escape from the Modal's // document listener — the same containment the input previously // provided for itself. event.stopPropagation() // The cancel may unmount whatever holds focus — the input, or a // dot-revealed row the cleared draft re-hides. Arm the parking // unconditionally: the refocus effect's body guard already // distinguishes a surviving focused row (left alone) from focus // that actually fell. Assignment (not a conditional set) also // retires a stale flag a failed or still-upgrading Enter left. refocusEditZone.current = true cancelPathEdit() }} // Focus leaving THIS dialog card while editing cancels like Escape. // Guarded non-cancel paths: window/tab focus loss (document no // longer focused); a focus move that stays inside the card (Tab // onto the filtered rows or the footer toggle); and pointer paths, // where rows and the toggle suppress focus steal on mousedown while // editing so their click lands first. Enter keeps focus in the // input while its navigation is in flight, so a submitted path is // never withdrawn here. Anchored to this card via closest, not any // [role="dialog"], so focus escaping into a sibling overlay cancels. onBlur={(event) => { if (pathDraft === null) return if (!document.hasFocus()) return const card = event.currentTarget.closest('[role="dialog"]') /* v8 ignore next -- narrowing guard: this scope always renders inside the Modal card. */ if (card === null) return if (event.relatedTarget instanceof Node && card.contains(event.relatedTarget)) return // The user moved focus out of the card themselves: cancel without // re-parking (a lingering Enter-failure flag must not yank focus // back either). refocusEditZone.current = false cancelPathEdit() }} >

{t('browser.title')}

{pathDraft === null ? ( <> {crumbs.map((crumb, index) => ( {index > 0 && } ))} {/* The empty zone right of the crumbs is the path-edit affordance. */}
{parent !== null && ( )} {twoPane && } {twoPane && child !== null && ( )}
{loading &&
{t('browser.loading')}
} {/* The backend bounds a level at its complete-result limit; say so * whenever a visible pane was cut instead of letting the tail of a * huge directory go silently missing. */} {(parent?.truncated === true || child?.truncated === true) && !loading &&
{t('browser.truncated')}
} {error !== null &&
{error}
}
{/* Nested create dialog (figma 813:23278): names one folder inside the target. */} { if (!creatingFolder) closeCreateDialog() }} title={t('browser.newFolder')} className={clsx(css.createDialog)} headless >

{t('browser.newFolder')}

{t('browser.createIn', { name: targetName })}

{ setFolderDraft(event.target.value) }} {...compositionGuard} onKeyDown={(event) => { if (event.key === 'Enter' && !composingRef.current) { event.preventDefault() confirmCreate() } if (event.key === 'Escape') { event.stopPropagation() if (!creatingFolder) closeCreateDialog() } }} /> {createError !== null &&
{createError}
}
) }