From f4f8fbbf0c7c0acc6159fbb75bd40909e8c04a5a Mon Sep 17 00:00:00 2001 From: Stephen Haberman Date: Sat, 31 Aug 2024 14:10:47 -0500 Subject: [PATCH] feat: Add TextField.onEscapeBubble. (#1062) --- src/components/Modal/Modal.stories.tsx | 1 + src/components/Modal/TestModalContent.tsx | 12 ++++++++++++ src/inputs/TextField.test.tsx | 14 ++++++++++++++ src/inputs/TextField.tsx | 14 ++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index 9812f8543..b128db6f0 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -36,6 +36,7 @@ export const FilterableStaticHeight = () => ( export const HeaderWithComponents = () => ; export const WithDatePicker = () => ; export const WithFieldInHeader = () => ; +export const WithTextFieldInHeader = () => ; export const WithDrawHeaderBorder = () => ; export const VirtualizedTableInBody = () => { const { openModal } = useModal(); diff --git a/src/components/Modal/TestModalContent.tsx b/src/components/Modal/TestModalContent.tsx index 97fb4f1d1..e2f47d0c0 100644 --- a/src/components/Modal/TestModalContent.tsx +++ b/src/components/Modal/TestModalContent.tsx @@ -18,6 +18,7 @@ export interface TestModalContentProps { withTag?: boolean; withDateField?: boolean; withTextArea?: boolean; + withTextField?: boolean; } /** A fake modal content component that we share across the modal and superdrawer stories. */ @@ -39,6 +40,17 @@ export function TestModalContent(props: TestModalContentProps) { Modal Title with Tag + ) : props.withTextField ? ( + setValue(v)} + labelStyle="hidden" + onEscapeBubble + borderless + xss={Css.xl.$} + /> ) : props.withTextArea ? ( { expect(onBlur).toHaveBeenCalledTimes(1); expect(r.name).not.toHaveFocus(); }); + + it("can bubble escape", async () => { + const onEscape = jest.fn(); + // Given a TextField + const r = await render( +
+ +
, + ); + // When hitting the Escape key + fireEvent.keyDown(r.name, { key: "Escape" }); + // Then onEscape callback should be called + expect(onEscape).toHaveBeenCalledTimes(1); + }); }); function TestTextField>(props: Omit, "onChange" | "label">) { diff --git a/src/inputs/TextField.tsx b/src/inputs/TextField.tsx index 8c1d5566a..159981feb 100644 --- a/src/inputs/TextField.tsx +++ b/src/inputs/TextField.tsx @@ -12,6 +12,16 @@ export interface TextFieldProps extends BeamTextFieldProps { clearable?: boolean; api?: MutableRefObject; onEnter?: VoidFunction; + /** + * Allows a TextField to opt-in to bubbling up the escape key event to its parent. + * + * Usually this is a bad idea, because escape-in-a-modal might lose the user's WIP (without + * sufficient "are you sure" checking), and so instead we let callers opt-in to this. + * + * Note that react-aria's `useSearchField` / `useComboBox` seems to have this built-in: + * https://github.com/adobe/react-spectrum/issues/5480 + */ + onEscapeBubble?: boolean; endAdornment?: ReactNode; startAdornment?: ReactNode; hideErrorMessage?: boolean; @@ -28,6 +38,7 @@ export function TextField>(props: TextFieldProps onFocus, api, onEnter, + onEscapeBubble, hideErrorMessage, ...otherProps } = props; @@ -50,6 +61,9 @@ export function TextField>(props: TextFieldProps if (e.key === "Enter") { maybeCall(onEnter); inputRef.current?.blur(); + } else if (e.key === "Escape" && onEscapeBubble) { + // Allow closing modals from within text fields... + e.continuePropagation(); } }, },