Repository navigation
fix(components): a field's Esc in the record drawer no longer steps the pane back or restores it from maximized - #28294
Merged
Conversation
…he pane back or restores it from maximized The drawer's Radix dialog layer hears Esc on the document in the capture phase, ahead of the record's own handlers, so its onEscapeKeyDown cannot wait for a field to mark the key handled. It now asks the split pane's question itself: a key already handled, or pressed where a text field, an editable region or an open menu, list box or dialog owns it, is not the pane's. escapeOwnedByTarget learns to skip the sheet's own dialog. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwcrcmDwuHqtrvwstB
3 tasks
Contributor
Author
|
domain:ui#2 · os-tesla · session_01L9qibxgfM8p8t3fo6y9eFK 维护者速读
席位核对ACCEPT · head
|
This was referenced Oct 31, 2026
feat(plugin-detail)!: a tab row under the record header; an open panel face is the active tab
#28394
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Tesla · project thread
Fixes #28156
Clause-②: no
Metadata-protocol: none
维护者速读
defaultPrevented守卫只挡得住放大面那种窗口捕获监听,挡不住普通字段;本 PR 让抽屉用分屏面已经在用的同一条判断。@object-ui/components的navigation-overlay.tsx一个处理函数。代价:焦点停在文本框里时 Esc 永远不退层(与分屏面今天一致),要先移开焦点或点 ←。回滚即 revert 本 PR。ui/packages/components/src/custom/navigation-overlay.tsx的handleDrawerEscape与escapeOwnedByTarget(新增pane参数);ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx。pnpm --dir ui exec vitest run packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx,预期 1 个文件、19 个测试全绿。What changes
Declared deviation from the card's Wanted. The card asks for
if (e.defaultPrevented) return;alone. Measured onmain69ce01d against the installed@radix-ui/react-dismissable-layer1.1.19, that guard is not enough:keydownon the document in the capture phase (only while it is the highest layer) and callsonEscapeKeyDownfrom there, then readsdefaultPreventedfor its own dismiss.Escbefore any ReactonKeyDownin the record. A probe through the realNavigationOverlaydrawer withonBack: an input whose React handler callspreventDefault()logsonBackfirst and then the field (which seesdefaultPreventedalready true); a field marked handled from a window capture listener (the shape feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel #28116's enlarged face uses) logs the face first and thenonBack.mainthe composer's mention list (RichTextCommentInput), the grid'sInlineEditingand the data table's cell editor all takeEscin a ReactonKeyDown.Seat 2 chose option B, the guard plus the split pane's ownership test (seat ruling, 2026-10-31T02:56Z, in the dispatch thread).
The drawer's
Esc(handleDrawerEscape). Below the stack's root, or maximized, the drawer'sonEscapeKeyDownnow:defaultPrevented): an enlarged face or anything else that takes the key ahead of the layer;preventDefault, so the layer does not dismiss the whole drawer);escapeOwnedByTargetsays a text field, an editable region, or an open menu, list box or dialog in the record owns it;This is the question the split pane beside a list already asks (
useSplitEscape), so the drawer and the split pane now answerEscthe same way.escapeOwnedByTarget(target, pane?). It takes the pane's own dialog and never counts it as an owner. Every target inside the drawer sits inside the sheet'srole="dialog", so without this a button in the record would read as owned and a bareEscthere would stop stepping back. The split pane passes nothing and reads as before.Cost. With focus in a text field,
Escnever steps the pane back or restores it, as in the split pane today. The reader moves focus out of the field or uses←.Unchanged. At the root (no
onBack, not maximized) the drawer passes no handler and the layer dismisses onEscas before; this card does not cover the root. The split pane's listener is untouched.Tests
New:
ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx(19), through the realNavigationOverlay(its Radix sheet and its split listener). The record body holds the shapes the fields really take the key in: a cell editor (ReactonKeyDown,preventDefault), the composer closing its mention list (ReactonKeyDown, nothing marked), a field inside an enlarged face and the face's ownEsc(window capture listener, marks handled).Escin each of the four keeps the level (onBack,setIsOpenandclosenever called). CONTROL: a bareEscsteps back once and does not close; anEscon a button in the record steps back too.Escin each of the four keeps it maximized and open. CONTROL: a bareEscrestores, and only the next one closes the root.Escsteps back / restores (CONTROL; these held before this PR and are pinned for parity).Ablations (each through
node scripts/ablation-replace.mjs, restore proven blob == HEAD; the test imports the component from source, so no dist leg)On head e2e6e5e:
main's (e.preventDefault(); handleEscape();):Tests 8 failed | 11 passed (19), the eight drawer arms (four below the root, four maximized). Controls and split arms green.defaultPreventedreturn removed:2 failed, the enlarged face's ownEsc, below the root and maximized.4 failed, the cell editor and the composer, below the root and maximized.owner !== paneremoved:1 failed, the CONTROL that anEscon a button in the record still steps back.Acceptance notes
Escsteps back): pinned in the drawer and in the split pane.Escrestores): pinned in the drawer and in the split pane.NavigationOverlay, ablation reds the field case): A0 above.Escin a field still dismisses the whole drawer unless something marked it handled ahead of the layer (feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel #28116's face does). Noted, not filed: the card scopes the levels below the root and the maximized pane.Verification
Tests, type-check and the derived gates on e2e6e5e
Head
e2e6e5e9, change set vs the merge base69ce01d7: 3 paths, 251 changed lines.pnpm --dir ui exec vitest run packages/components/under the verify lock:Test Files 1 failed | 461 passed | 1 skipped (463),Tests 5 failed | 4422 passed | 24 skipped (4451). The one red file wasshare-filter-sort-spec-parity.test.ts, which reads@objectstack/spec's.d.tsand met a closure built withOS_SKIP_DTS=1(no declarations). Afterenv -u OS_SKIP_DTS pnpm turbo run build --filter='@object-ui/components^...'(exit 0) it and the new file pass:Test Files 2 passed (2).pnpm --filter @object-ui/components run type-check(tsc --noEmit && tsc -p tsconfig.test.json): exit 0. eslint on the two changed source files: exit 0.pnpm --dir ui run check:new-line-citations:0 new citation(s), exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 commands, each run with its exit code captured before any pipe;--ranreconcile:34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.pnpm --dir ui exec vitest run scripts/:Test Files 169 passed | 2 skipped (171).ui/suite (Test UI), the other type-check lanes, the repo-wide scans. Dogfood not owed (no!in the title, no(narrowing)arm).Generated by Claude Code