Skip to content

Commit 90828f1

Browse files
committed
fix(plugin-detail): a text field in an open face keeps its Esc, and the record pane stays open around it
The face let an Esc pressed in a text field go on to the field, unmarked, so inside the record pane the drawer's dialog layer took it next and closed the whole pane: Esc in the 动态 tab's composer dropped the reader on the list with `?related=timeline` still in the URL. A field in the face still gets the key (it closes its mention list, cancels an inline edit), and the face marks it handled, so the drawer does not dismiss on it and the face stays open. A field outside the face, and an open floating layer (a select, a popover), keep the key unmarked as before. Claude-Session: https://claude.ai/code/session_01Utp6CaA9ZdTcj97mzGtHYt Co-authored-by: Claude <noreply@anthropic.com>
1 parent d98543c commit 90828f1

2 files changed

Lines changed: 50 additions & 21 deletions

File tree

‎ui/packages/plugin-detail/src/RecordPageColumns.tsx‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,29 +84,30 @@ function relatedNodeId(node: any): RelatedCardId | null {
8484
}
8585

8686
/**
87-
* A panel's whole face, open in place (objectstack#27055, Target 9): under the
88-
* record header, at the page's full width, with ‹ to return. Esc returns too;
89-
* a dialog or menu open over the page keeps its own Esc.
90-
*/
91-
/**
92-
* Whether an `Esc` belongs to something other than the open face: a field the
93-
* reader types in, an open menu or alert, or a dialog the face is not inside
94-
* (a popover, a confirm, a drawer opened over the page that holds the face).
95-
* The dialog that holds the face — the record pane's drawer — is not one.
87+
* Whether an `Esc` belongs to a layer over the open face: an open menu, alert
88+
* or floating content (a select, a tooltip), or a dialog the face is not
89+
* inside (a popover, a confirm, a drawer opened over the page that holds the
90+
* face). The dialog that holds the face — the record pane's drawer — is not one.
9691
*/
97-
function escapeOwnedElsewhere(target: EventTarget | null, face: Element | null): boolean {
98-
if (!face) return true;
99-
if (
100-
target instanceof Element &&
101-
target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"]')
102-
) {
103-
return true;
104-
}
92+
function escapeOwnedByLayer(face: Element): boolean {
10593
const doc = face.ownerDocument;
106-
if (doc.querySelector('[role="alertdialog"], [role="menu"]')) return true;
94+
if (doc.querySelector('[role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]')) return true;
10795
return Array.from(doc.querySelectorAll('[role="dialog"]')).some((dialog) => !dialog.contains(face));
10896
}
10997

98+
function isTypingTarget(target: EventTarget | null): target is Element {
99+
return (
100+
target instanceof Element &&
101+
!!target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"]')
102+
);
103+
}
104+
105+
/**
106+
* A panel's whole face, open in place (objectstack#27055, Target 9): under the
107+
* record header, at the page's full width, with ‹ to return. Esc returns too;
108+
* a dialog or menu open over the page keeps its own Esc, and so does a field
109+
* the reader types in.
110+
*/
110111
const EnlargedFace: React.FC<{ id: string; title: string; onClose?: () => void; children: React.ReactNode }> = ({
111112
id,
112113
title,
@@ -123,7 +124,15 @@ const EnlargedFace: React.FC<{ id: string; title: string; onClose?: () => void;
123124
// The face takes the key, and keeps it from the pane.
124125
const onKeyDown = (event: KeyboardEvent) => {
125126
if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing) return;
126-
if (escapeOwnedElsewhere(event.target, faceRef.current)) return;
127+
const face = faceRef.current;
128+
if (!face || escapeOwnedByLayer(face)) return;
129+
if (isTypingTarget(event.target)) {
130+
// A field keeps its own `Esc` (it closes a mention list, cancels an
131+
// inline edit), so the key goes on to it; pressed in the face, it is
132+
// marked handled, and the pane around the face stays open.
133+
if (face.contains(event.target)) event.preventDefault();
134+
return;
135+
}
127136
event.preventDefault();
128137
event.stopPropagation();
129138
onClose();

‎ui/packages/plugin-detail/src/__tests__/RecordPageColumns.enlargedEscapeInPane-27524.test.tsx‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,31 @@ describe('Esc on an open face inside the record pane (objectstack#27524)', () =>
127127
menu.remove();
128128
});
129129

130-
it('a text field owns Esc: the face stays open while the reader types in it', () => {
131-
render(pane({}));
130+
it('a text field in the face keeps its own Esc, and the drawer stays open around the face', () => {
131+
const setIsOpen = vi.fn();
132+
render(pane({ setIsOpen }));
132133
const input = document.createElement('input');
133134
face()!.appendChild(input);
135+
const fieldEsc = vi.fn();
136+
input.addEventListener('keydown', fieldEsc);
134137
escape(input);
138+
// The key reaches the field (a mention list, an inline edit's cancel), and
139+
// it is not the pane's: the face and the drawer stay.
140+
expect(fieldEsc).toHaveBeenCalledTimes(1);
135141
expect(face()).not.toBeNull();
142+
expect(setIsOpen).not.toHaveBeenCalled();
143+
escape(face()!);
144+
expect(face()).toBeNull();
145+
expect(setIsOpen).not.toHaveBeenCalled();
146+
});
147+
148+
it('a text field in the face, in the split pane beside a list: the face and the pane stay', () => {
149+
const close = vi.fn();
150+
render(pane({ mode: 'split', mainContent: <div>list</div>, close }));
151+
const input = document.createElement('textarea');
152+
face()!.appendChild(input);
153+
escape(input);
154+
expect(face()).not.toBeNull();
155+
expect(close).not.toHaveBeenCalled();
136156
});
137157
});

0 commit comments

Comments
 (0)