From e2e6e5e936f4bc598f23077f2a9fd213e2027b3c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 02:58:53 +0000 Subject: [PATCH] fix(components): a field's Esc in the record drawer no longer steps the 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 Claude-Session: https://claude.ai/code/session_01L9qibwcrcmDwuHqtrvwstB --- .changeset/28156-record-pane-field-escape.md | 14 ++ ...gation-overlay-field-escape-28156.test.tsx | 209 ++++++++++++++++++ .../src/custom/navigation-overlay.tsx | 28 ++- 3 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 .changeset/28156-record-pane-field-escape.md create mode 100644 ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx diff --git a/.changeset/28156-record-pane-field-escape.md b/.changeset/28156-record-pane-field-escape.md new file mode 100644 index 00000000000..cc62362e8b1 --- /dev/null +++ b/.changeset/28156-record-pane-field-escape.md @@ -0,0 +1,14 @@ +--- +'@object-ui/components': patch +--- + +fix(components): in the record drawer, an `Esc` a field takes stays the field's — below the stack's root it no longer steps the pane back, and maximized it no longer restores the pane + +Clause-②: no +Metadata-protocol: none + +The drawer's dialog layer hears `Esc` on the document in the capture phase, before any field in the record does, and its handler stepped back one level (below the root) or restored the pane (maximized) on every `Esc`. So closing a mention list in the composer, cancelling a cell edit, or leaving an enlarged face also lost the reader's level or the maximized width. + +- The drawer now asks what the split pane beside a list already asks: an `Esc` already marked handled, or pressed in a text field, an editable region, or an open menu, list box or dialog inside the record, is not the pane's. The pane stays where it is and the field gets the key. +- A bare `Esc`, or one pressed on a control that owns no key (a button in the record), still steps back below the root and still restores from maximized; at the root it still closes the drawer. +- With focus in a text field, `Esc` never steps the pane back, as in the split pane: move focus out of the field, or use `←`. diff --git a/ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx b/ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx new file mode 100644 index 00000000000..995bfd0a1c1 --- /dev/null +++ b/ui/packages/components/src/__tests__/navigation-overlay-field-escape-28156.test.tsx @@ -0,0 +1,209 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectstack#28156 — an `Esc` a field in the record takes is the field's: + * below the stack's root it does not step the pane back, and maximized it + * does not restore the pane. A bare `Esc` still does both (the controls). + * + * The drawer's Radix dialog layer hears `Esc` on the document in the capture + * phase, ahead of the record's own handlers, so the fields are pinned in the + * shapes they really take the key in: + * - a cell editor cancels in a React `onKeyDown` and marks the key handled; + * - the composer closes its mention list in a React `onKeyDown` and marks + * nothing; + * - an enlarged face, which marks the key handled from a window capture + * listener, ahead of the layer: both its own `Esc` (it closes itself) and + * one pressed in a field inside it. + * The split pane beside a list answers the same, from its document listener. + */ + +import * as React from 'react'; +import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { + NavigationOverlay, + RECORD_SPLIT_MIN_CONTENT_PX, + type NavigationOverlayProps, +} from '../custom/navigation-overlay'; + +/** What each field did with the `Esc` it got. */ +let fieldLog: string[] = []; + +/** The record's body: the fields, the face, and a control that owns no key. */ +function RecordBody() { + const faceRef = React.useRef(null); + React.useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Escape' || !(e.target instanceof Node)) return; + if (!faceRef.current?.contains(e.target)) return; + fieldLog.push('face'); + e.preventDefault(); + }; + window.addEventListener('keydown', onKeyDown, true); + return () => window.removeEventListener('keydown', onKeyDown, true); + }, []); + return ( +
+ { + if (e.key !== 'Escape') return; + fieldLog.push('cell editor'); + e.preventDefault(); + }} + /> +