Skip to content

Commit

Permalink
feat: Add TextField.onEscapeBubble. (#1062)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenh authored Aug 31, 2024
1 parent e111a76 commit f4f8fbb
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const FilterableStaticHeight = () => (
export const HeaderWithComponents = () => <ModalExample size="lg" withTag />;
export const WithDatePicker = () => <ModalExample withDateField />;
export const WithFieldInHeader = () => <ModalExample withTextArea />;
export const WithTextFieldInHeader = () => <ModalExample withTextField />;
export const WithDrawHeaderBorder = () => <ModalExample drawHeaderBorder={true} />;
export const VirtualizedTableInBody = () => {
const { openModal } = useModal();
Expand Down
12 changes: 12 additions & 0 deletions src/components/Modal/TestModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -39,6 +40,17 @@ export function TestModalContent(props: TestModalContentProps) {
<span>Modal Title with Tag</span>
<Tag text="In progress" type="info" xss={Css.ml1.$} />
</div>
) : props.withTextField ? (
<TextField
label="Title"
placeholder="Test title"
value={internalValue}
onChange={(v) => setValue(v)}
labelStyle="hidden"
onEscapeBubble
borderless
xss={Css.xl.$}
/>
) : props.withTextArea ? (
<TextAreaField
label="Title"
Expand Down
14 changes: 14 additions & 0 deletions src/inputs/TextField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ describe("TextFieldTest", () => {
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(
<div onKeyDown={onEscape}>
<TestTextField value="foo" onEscapeBubble />
</div>,
);
// When hitting the Escape key
fireEvent.keyDown(r.name, { key: "Escape" });
// Then onEscape callback should be called
expect(onEscape).toHaveBeenCalledTimes(1);
});
});

function TestTextField<X extends Only<TextFieldXss, X>>(props: Omit<TextFieldProps<X>, "onChange" | "label">) {
Expand Down
14 changes: 14 additions & 0 deletions src/inputs/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export interface TextFieldProps<X> extends BeamTextFieldProps<X> {
clearable?: boolean;
api?: MutableRefObject<TextFieldApi | undefined>;
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;
Expand All @@ -28,6 +38,7 @@ export function TextField<X extends Only<TextFieldXss, X>>(props: TextFieldProps
onFocus,
api,
onEnter,
onEscapeBubble,
hideErrorMessage,
...otherProps
} = props;
Expand All @@ -50,6 +61,9 @@ export function TextField<X extends Only<TextFieldXss, X>>(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();
}
},
},
Expand Down

0 comments on commit f4f8fbb

Please sign in to comment.