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(); + }} + /> +