Skip to content

Commit aa01532

Browse files
authored
add onKeyDown action in TextInput widget (#1045)
1 parent 013ae91 commit aa01532

File tree

4 files changed

+95
-15
lines changed

4 files changed

+95
-15
lines changed

.changeset/few-scissors-confess.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ensembleui/react-kitchen-sink": patch
3+
"@ensembleui/react-runtime": patch
4+
---
5+
6+
add onKeyDown action in TextInput widget

apps/kitchen-sink/src/ensemble/screens/forms.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ View:
177177
label:
178178
Text:
179179
text: Text mask input
180+
onChange: console.log("formTextInput onChange", value)
181+
onKeyDown: console.log("formTextInput onKeyDown", event)
180182
- TextInput:
181183
id: minMaxTextInput
182184
inputType: number
@@ -188,6 +190,8 @@ View:
188190
required: true
189191
maxLength: 3
190192
label: Text input with min and max length
193+
onChange: console.log("minMaxTextInput onChange", value)
194+
onKeyDown: console.log("minMaxTextInput onKeyDown", event)
191195
- TextInput:
192196
id: regexTextInput
193197
required: true
@@ -362,6 +366,34 @@ View:
362366
label:
363367
Text:
364368
text: Text input with max length enforcement
369+
onKeyDown:
370+
executeCode: |
371+
console.log("initial_formTextInput onKeyDown", event)
372+
- TextInput:
373+
label: Multiline text 1
374+
hintText: Press enter to submit
375+
multiLine: true
376+
maxLines: 2
377+
submitOnEnter: ${initial_formTextInput?.value?.length > 4}
378+
onChange: console.log("TextArea 1 onChange", value)
379+
onKeyDown: |
380+
if (event.key === 'Enter' && !event.shiftKey) {
381+
event.preventDefault();
382+
myForm2.submit();
383+
}
384+
- TextInput:
385+
id: textArea2
386+
label: Multiline text 2
387+
hintText: Type freely
388+
multiline: true
389+
maxLines: 2
390+
onChange: console.log("TextArea 2 onChange", value)
391+
onKeyDown:
392+
executeCode: |
393+
console.log("TextArea 2 onKeyDown", event)
394+
- Button:
395+
label: toggle 'Multiline text 2' to submit on Enter
396+
onChange: textArea2.setSubmitOnEnter(textArea2?.submitOnEnter ?? true)
365397
- MultiSelect:
366398
id: initial_multiselectoptions1
367399
label: Choose multiple from API or storage

packages/runtime/src/widgets/Form/TextInput.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import { Input, Form, ConfigProvider } from "antd";
22
import type { Expression, EnsembleAction } from "@ensembleui/react-framework";
33
import { useRegisterBindings } from "@ensembleui/react-framework";
4-
import {
5-
useEffect,
6-
useMemo,
7-
useState,
8-
useCallback,
9-
useRef,
10-
type FormEvent,
11-
RefCallback,
12-
} from "react";
4+
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
5+
import type { RefCallback, FormEvent } from "react";
136
import { runes } from "runes2";
147
import type { Rule } from "antd/es/form";
15-
import { forEach } from "lodash-es";
16-
import IMask, { InputMask } from "imask";
8+
import { forEach, isObject, omitBy } from "lodash-es";
9+
import IMask, { type InputMask } from "imask";
1710
import type { EnsembleWidgetProps } from "../../shared/types";
1811
import { WidgetRegistry } from "../../registry";
1912
import type { TextStyles } from "../Text";
@@ -26,7 +19,10 @@ const widgetName = "TextInput";
2619
export type TextInputProps = {
2720
hintStyle?: TextStyles;
2821
labelStyle?: TextStyles;
22+
/** @deprecated see {@link TextInputProps.multiline} */
2923
multiLine?: Expression<boolean>;
24+
/** Specify whether this Text Input should span multiple lines */
25+
multiline?: Expression<boolean>;
3026
maxLines?: number;
3127
maxLength?: Expression<number>;
3228
maxLengthEnforcement?: Expression<
@@ -42,6 +38,7 @@ export type TextInputProps = {
4238
regexError?: string;
4339
maskError?: string;
4440
};
41+
onKeyDown: EnsembleAction;
4542
} & EnsembleWidgetProps<TextStyles> &
4643
FormInputProps<string>;
4744

@@ -62,6 +59,7 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
6259
);
6360
const formInstance = Form.useFormInstance();
6461
const action = useEnsembleAction(props.onChange);
62+
const onKeyDownAction = useEnsembleAction(props.onKeyDown);
6563

6664
const handleChange = useCallback(
6765
(newValue: string) => {
@@ -76,7 +74,7 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
7674
rootRef(node);
7775
};
7876

79-
const handleKeyDown = useCallback((e: FormEvent<HTMLInputElement>) => {
77+
const sanitizeNumberInput = useCallback((e: FormEvent<HTMLInputElement>) => {
8078
const target = e.target as HTMLInputElement;
8179
target.value = target.value.replace(/[^0-9.]/g, "");
8280
}, []);
@@ -92,6 +90,17 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
9290
[handleChange, mask],
9391
);
9492

93+
const handleKeyDown = useCallback(
94+
(event: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) =>
95+
onKeyDownAction?.callback({
96+
event: {
97+
...omitBy(event, isObject),
98+
preventDefault: event.preventDefault.bind(event),
99+
},
100+
}),
101+
[onKeyDownAction],
102+
);
103+
95104
useEffect(() => {
96105
setValue(values?.initialValue);
97106
}, [values?.initialValue]);
@@ -252,12 +261,13 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
252261
theme={{ token: { colorTextPlaceholder: values?.hintStyle?.color } }}
253262
>
254263
<EnsembleFormItem rules={rules} valuePropName="value" values={values}>
255-
{values?.multiLine ? (
264+
{values?.multiLine || values?.multiline ? (
256265
<Input.TextArea
257266
count={maxLengthConfig}
258267
defaultValue={values.value}
259268
disabled={values.enabled === false}
260-
onChange={(event): void => setValue(event.target.value)}
269+
onChange={(event): void => handleChange(event.target.value)}
270+
onKeyDown={handleKeyDown}
261271
placeholder={values.hintText ?? ""}
262272
ref={rootRef}
263273
rows={values.maxLines ? Number(values.maxLines) : 4} // Adjust the number of rows as needed
@@ -276,8 +286,9 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
276286
disabled={values?.enabled === false}
277287
onChange={(event): void => handleChange(event.target.value)}
278288
{...(values?.inputType === "number" && {
279-
onInput: (event): void => handleKeyDown(event),
289+
onInput: (event): void => sanitizeNumberInput(event),
280290
})}
291+
onKeyDown={handleKeyDown}
281292
onPaste={handleInputPaste}
282293
placeholder={values?.hintText ?? ""}
283294
ref={handleRef}

packages/runtime/src/widgets/Form/__tests__/TextInput.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,5 +326,36 @@ describe("TextInput", () => {
326326
);
327327
});
328328
});
329+
330+
test("logs event object onKeyDown for numeric TextInput", async () => {
331+
render(
332+
<Form
333+
children={[
334+
{
335+
name: "TextInput",
336+
properties: {
337+
label: "Number input",
338+
id: "numberInput",
339+
type: "number",
340+
onKeyDown: {
341+
executeCode: "console.log(event)",
342+
},
343+
},
344+
},
345+
...defaultFormButton,
346+
]}
347+
id="form"
348+
/>,
349+
{ wrapper: FormTestWrapper },
350+
);
351+
const input = screen.getByLabelText("Number input");
352+
userEvent.type(input, "2");
353+
354+
await waitFor(() => {
355+
expect(logSpy).toHaveBeenCalledWith(
356+
expect.objectContaining({ key: "2" }),
357+
);
358+
});
359+
});
329360
});
330361
/* eslint-enable react/no-children-prop */

0 commit comments

Comments
 (0)