Repository navigation
Commit aaa5c1d
fix(components): a field's Esc in the record drawer no longer steps the pane back or restores it from maximized (#28294)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao9Er3hy5TVuvBiu4EsvhoLJ)_
Fixes #28156
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:记录抽屉里,在字段内按
Esc(关提及列表、取消单元格编辑、退出动态输入框或放大面)只归该字段:在下层时不再退回上一层,最大化时不再还原。直接按
Esc(焦点不在字段里)仍退一层、仍还原,根层仍关闭抽屉。
- 为什么改:读者在关联记录里按 Esc 只想关掉字段里的东西,却连带丢了当前层级或最大化宽度。Radix 的对话层在 document
捕获阶段先于任何字段收到 Esc,所以卡片写的那一行 `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 on `main` 69ce01d against
the installed `@radix-ui/react-dismissable-layer` 1.1.19, that guard is
not enough:
- The layer listens for `keydown` on the document in the **capture**
phase (only while it is the highest layer) and calls `onEscapeKeyDown`
from there, then reads `defaultPrevented` for its own dismiss.
- So it hears `Esc` before any React `onKeyDown` in the record. A probe
through the real `NavigationOverlay` drawer with `onBack`: an input
whose React handler calls `preventDefault()` logs `onBack` first and
then the field (which sees `defaultPrevented` already true); a field
marked handled from a **window** capture listener (the shape #28116's
enlarged face uses) logs the face first and then `onBack`.
- With the one-line guard, the window-capture case stops stepping back
and the React case still does. On `main` the composer's mention list
(`RichTextCommentInput`), the grid's `InlineEditing` and the data
table's cell editor all take `Esc` in a React `onKeyDown`.
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's `onEscapeKeyDown` now:
1. returns when the key is already handled (`defaultPrevented`): an
enlarged face or anything else that takes the key ahead of the layer;
2. otherwise holds the sheet open (`preventDefault`, so the layer does
not dismiss the whole drawer);
3. leaves the key to its target when `escapeOwnedByTarget` says a text
field, an editable region, or an open menu, list box or dialog in the
record owns it;
4. only then restores from maximized or steps back one level.
This is the question the split pane beside a list already asks
(`useSplitEscape`), so the drawer and the split pane now answer `Esc`
the 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's `role="dialog"`, so without this a button in the
record would read as owned and a bare `Esc` there would stop stepping
back. The split pane passes nothing and reads as before.
**Cost.** With focus in a text field, `Esc` never 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 on `Esc` as 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 real `NavigationOverlay` (its Radix sheet and its
split listener). The record body holds the shapes the fields really take
the key in: a cell editor (React `onKeyDown`, `preventDefault`), the
composer closing its mention list (React `onKeyDown`, nothing marked), a
field inside an enlarged face and the face's own `Esc` (window capture
listener, marks handled).
- Drawer below the root: `Esc` in each of the four keeps the level
(`onBack`, `setIsOpen` and `close` never called). CONTROL: a bare `Esc`
steps back once and does not close; an `Esc` on a button in the record
steps back too.
- Drawer maximized: `Esc` in each of the four keeps it maximized and
open. CONTROL: a bare `Esc` restores, and only the next one closes the
root.
- Split pane beside the list, below the root and maximized: the same
four keep the level / the maximized width, and a bare `Esc` steps back /
restores (CONTROL; these held before this PR and are pinned for parity).
<details><summary>Ablations (each through <code>node
scripts/ablation-replace.mjs</code>, restore proven blob == HEAD; the
test imports the component from source, so no dist leg)</summary>
On head e2e6e5e:
- A0, the drawer handler back to `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.
- A1, the `defaultPrevented` return removed: `2 failed`, the enlarged
face's own `Esc`, below the root and maximized.
- A2, the ownership test removed: `4 failed`, the cell editor and the
composer, below the root and maximized.
- A3, `owner !== pane` removed: `1 failed`, the CONTROL that an `Esc` on
a button in the record still steps back.
</details>
## Acceptance notes
- Acceptance item 1 (below the root, field keeps the level; bare `Esc`
steps back): pinned in the drawer and in the split pane.
- Acceptance item 2 (maximized, field keeps it maximized; bare `Esc`
restores): pinned in the drawer and in the split pane.
- Acceptance item 3 (against the real `NavigationOverlay`, ablation reds
the field case): A0 above.
- The root drawer is outside this card: there an `Esc` in a field still
dismisses the whole drawer unless something marked it handled ahead of
the layer (#28116's face does). Noted, not filed: the card scopes the
levels below the root and the maximized pane.
## Verification
<details><summary>Tests, type-check and the derived gates on
e2e6e5e</summary>
Head `e2e6e5e9`, change set vs the merge base `69ce01d7`: 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 was
`share-filter-sort-spec-parity.test.ts`, which reads
`@objectstack/spec`'s `.d.ts` and met a closure built with
`OS_SKIP_DTS=1` (no declarations). After `env -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; `--ran` reconcile: `34 derived, 34 run, 0
NOT-MEASURED, 0 UNRUN`. `pnpm --dir ui exec vitest run scripts/`: `Test
Files 169 passed | 2 skipped (171)`.
- Not run locally, CI's own: the whole `ui/` suite (Test UI), the other
type-check lanes, the repo-wide scans. Dogfood not owed (no `!` in the
title, no `(narrowing)` arm).
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibwcrcmDwuHqtrvwstB)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent a8d2fd7 commit aaa5c1d
3 files changed
Lines changed: 245 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
433 | 433 | | |
434 | 434 | | |
435 | 435 | | |
436 | | - | |
437 | | - | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
438 | 439 | | |
439 | | - | |
| 440 | + | |
440 | 441 | | |
441 | 442 | | |
442 | 443 | | |
443 | 444 | | |
444 | | - | |
| 445 | + | |
| 446 | + | |
445 | 447 | | |
446 | 448 | | |
447 | 449 | | |
| |||
647 | 649 | | |
648 | 650 | | |
649 | 651 | | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
650 | 664 | | |
651 | 665 | | |
652 | 666 | | |
| |||
801 | 815 | | |
802 | 816 | | |
803 | 817 | | |
804 | | - | |
805 | | - | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
806 | 822 | | |
807 | 823 | | |
808 | 824 | | |
| |||
0 commit comments