From 6d4712739575d631815dd998458acb9327517b3e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 15:09:28 +0000 Subject: [PATCH] fix(TextInput): merge caller inputProps with the React Aria props Every field rendering TextInputBase passed its hook's props down under the same `inputProps` name as the caller's, so one side overwrote the other wholesale. TextInput, TextArea, NumberInput and CommandTextArea dropped the caller's `inputProps`; PasswordInput and SearchInput did the reverse and replaced the hook's value tracking, ids and ARIA attributes with it. Each now merges the two with `mergeProps`, so a key the caller sets wins and event handlers are chained rather than replaced. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NcSWqpSaC4Mvn4Fdd2mDat --- .changeset/text-input-merge-input-props.md | 5 + .../CommandTextArea/CommandTextArea.tsx | 37 +++--- .../fields/NumberInput/NumberInput.docs.mdx | 2 +- .../fields/NumberInput/NumberInput.tsx | 7 +- .../PasswordInput/PasswordInput.docs.mdx | 2 +- .../fields/PasswordInput/PasswordInput.tsx | 7 +- .../fields/SearchInput/SearchInput.docs.mdx | 2 +- .../fields/SearchInput/SearchInput.tsx | 7 +- .../fields/TextArea/TextArea.docs.mdx | 2 +- src/components/fields/TextArea/TextArea.tsx | 7 +- .../fields/TextInput/TextInput.docs.mdx | 2 +- src/components/fields/TextInput/TextInput.tsx | 3 +- src/components/fields/input-props.test.tsx | 120 ++++++++++++++++++ 13 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 .changeset/text-input-merge-input-props.md create mode 100644 src/components/fields/input-props.test.tsx diff --git a/.changeset/text-input-merge-input-props.md b/.changeset/text-input-merge-input-props.md new file mode 100644 index 000000000..5bdf6b746 --- /dev/null +++ b/.changeset/text-input-merge-input-props.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Fix `inputProps` on `TextInput`, `TextArea`, `PasswordInput`, `SearchInput`, `NumberInput` and `CommandTextArea`. Each of these passes its own React Aria props down under the same `inputProps` name as the caller's, and one side was overwriting the other wholesale: `TextInput`, `TextArea`, `NumberInput` and `CommandTextArea` dropped the caller's `inputProps` entirely, while `PasswordInput` and `SearchInput` did the opposite and replaced the hook's value tracking, ids and ARIA attributes with it. The two are now merged, so a key you set wins and event handlers are chained rather than replaced. diff --git a/src/components/fields/CommandTextArea/CommandTextArea.tsx b/src/components/fields/CommandTextArea/CommandTextArea.tsx index a9731e8ca..cb0b6febd 100644 --- a/src/components/fields/CommandTextArea/CommandTextArea.tsx +++ b/src/components/fields/CommandTextArea/CommandTextArea.tsx @@ -220,6 +220,7 @@ function CommandTextArea( maxRows = 10, rows = 3, labelProps: userLabelProps, + inputProps: userInputProps, inputRef: propsInputRef, value, defaultValue, @@ -650,22 +651,26 @@ function CommandTextArea( }; // ---- assemble input props --------------------------------------------- - const commandInputProps = mergeProps(inputProps, { - role: 'combobox', - 'aria-autocomplete': 'list', - 'aria-expanded': shouldShowPopover, - 'aria-haspopup': 'listbox', - 'aria-controls': shouldShowPopover ? listBoxId : undefined, - 'aria-activedescendant': - shouldShowPopover && focusedKey != null - ? `ListBoxItem-${focusedKey}` - : undefined, - onKeyDown: onKeyDownHandler, - onSelect: syncCaret, - onKeyUp: syncCaret, - onClick: syncCaret, - 'data-input-type': 'command-textarea', - }); + const commandInputProps = mergeProps( + inputProps, + { + role: 'combobox', + 'aria-autocomplete': 'list', + 'aria-expanded': shouldShowPopover, + 'aria-haspopup': 'listbox', + 'aria-controls': shouldShowPopover ? listBoxId : undefined, + 'aria-activedescendant': + shouldShowPopover && focusedKey != null + ? `ListBoxItem-${focusedKey}` + : undefined, + onKeyDown: onKeyDownHandler, + onSelect: syncCaret, + onKeyUp: syncCaret, + onClick: syncCaret, + 'data-input-type': 'command-textarea', + }, + userInputProps, + ); // ---- render ----------------------------------------------------------- const field = ( diff --git a/src/components/fields/NumberInput/NumberInput.docs.mdx b/src/components/fields/NumberInput/NumberInput.docs.mdx index c1028d92f..456cd32a3 100644 --- a/src/components/fields/NumberInput/NumberInput.docs.mdx +++ b/src/components/fields/NumberInput/NumberInput.docs.mdx @@ -41,7 +41,7 @@ A number input component that allows users to enter numeric values and increment - **`isWheelDisabled`** `boolean` — Whether scrolling the mouse wheel changes the value - **`decrementAriaLabel`** `string` — Custom aria-label for the decrement button - **`incrementAriaLabel`** `string` — Custom aria-label for the increment button -- **`inputProps`** `Props` — Direct props passed to the native input element +- **`inputProps`** `Props` — Direct props passed to the native input element, merged over the props React Aria generates: a key you set wins, and event handlers are chained rather than replaced - **`wrapperProps`** `Props` — Direct props passed to the input wrapper element - **`onChange`** `function` — Callback fired when the input value changes - **`onBlur`** `function` — Callback fired when the input loses focus diff --git a/src/components/fields/NumberInput/NumberInput.tsx b/src/components/fields/NumberInput/NumberInput.tsx index 191b3690a..1eac87d19 100644 --- a/src/components/fields/NumberInput/NumberInput.tsx +++ b/src/components/fields/NumberInput/NumberInput.tsx @@ -52,6 +52,7 @@ function NumberInput( onChange, inputRef, labelProps: userLabelProps, + inputProps: userInputProps, ...otherProps } = props; let showStepper = !hideStepper; @@ -94,7 +95,11 @@ function NumberInput( {...otherProps} ref={ref} labelProps={mergedLabelProps} - inputProps={{ ...inputProps, 'data-input-type': 'numberinput' }} + inputProps={mergeProps( + inputProps, + { 'data-input-type': 'numberinput' }, + userInputProps, + )} inputRef={inputRef} wrapperProps={groupProps} suffixPosition="after" diff --git a/src/components/fields/PasswordInput/PasswordInput.docs.mdx b/src/components/fields/PasswordInput/PasswordInput.docs.mdx index 4c1fce811..ffd0b67a5 100644 --- a/src/components/fields/PasswordInput/PasswordInput.docs.mdx +++ b/src/components/fields/PasswordInput/PasswordInput.docs.mdx @@ -34,7 +34,7 @@ A password input is a specialized text field that masks user input to protect se - **`suffixPosition`** `'before' | 'after'` (default: `after`) — Position of suffix relative to validation/loading icons - **`size`** `'small' | 'medium' | 'large'` (default: `medium`) — Input size - **`autoComplete`** `string` — HTML [`autocomplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) token that tells the browser what to autofill (`current-password`, `new-password`, `off`, …). The lowercase `autocomplete` alias is deprecated. -- **`inputProps`** `Props` — Direct props passed to the native input element +- **`inputProps`** `Props` — Direct props passed to the native input element, merged over the props React Aria generates: a key you set wins, and event handlers are chained rather than replaced - **`wrapperProps`** `Props` — Direct props passed to the input wrapper element - **`onChange`** `function` — Callback fired when the input value changes - **`onBlur`** `function` — Callback fired when the input loses focus diff --git a/src/components/fields/PasswordInput/PasswordInput.tsx b/src/components/fields/PasswordInput/PasswordInput.tsx index 8b7d861bd..8484aecfc 100644 --- a/src/components/fields/PasswordInput/PasswordInput.tsx +++ b/src/components/fields/PasswordInput/PasswordInput.tsx @@ -39,6 +39,7 @@ function PasswordInput( let [type, setType] = useState('password'); let { labelProps: userLabelProps, + inputProps: userInputProps, suffix, multiLine, inputRef: propsInputRef, @@ -101,7 +102,11 @@ function PasswordInput( } diff --git a/src/components/fields/TextArea/TextArea.docs.mdx b/src/components/fields/TextArea/TextArea.docs.mdx index 9556cec36..eabdb568f 100644 --- a/src/components/fields/TextArea/TextArea.docs.mdx +++ b/src/components/fields/TextArea/TextArea.docs.mdx @@ -66,7 +66,7 @@ These properties allow direct style application without using the `styles` prop: - **`autoComplete`** `string` — HTML [`autocomplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) token that tells the browser what to autofill (`street-address`, `off`, …). The lowercase `autocomplete` alias is deprecated. - **`maxLength`** `number` — Maximum number of characters allowed - **`minLength`** `number` — Minimum number of characters required -- **`inputProps`** `Props` — Direct HTML props passed to the inner textarea element +- **`inputProps`** `Props` — Direct HTML props passed to the inner textarea element, merged over the props React Aria generates: a key you set wins, and event handlers are chained rather than replaced - **`wrapperProps`** `Props` — Additional HTML props passed to the input wrapper container - **`inputRef`** `RefObject` — Ref to the inner textarea element - **`wrapperRef`** `RefObject` — Ref to the input wrapper container diff --git a/src/components/fields/TextArea/TextArea.tsx b/src/components/fields/TextArea/TextArea.tsx index dc1e48530..0db6b1e4d 100644 --- a/src/components/fields/TextArea/TextArea.tsx +++ b/src/components/fields/TextArea/TextArea.tsx @@ -47,6 +47,7 @@ function TextArea( maxRows = 10, rows = 3, labelProps: userLabelProps, + inputProps: userInputProps, inputRef: propsInputRef, value, isBuffered, @@ -99,7 +100,11 @@ function TextArea( multiLine inputRef={inputRef} labelProps={mergedLabelProps} - inputProps={{ ...inputProps, 'data-input-type': 'textarea' }} + inputProps={mergeProps( + inputProps, + { 'data-input-type': 'textarea' }, + userInputProps, + )} isDisabled={isDisabled} isReadOnly={isReadOnly} isRequired={isRequired} diff --git a/src/components/fields/TextInput/TextInput.docs.mdx b/src/components/fields/TextInput/TextInput.docs.mdx index 23bd32c65..5e690528b 100644 --- a/src/components/fields/TextInput/TextInput.docs.mdx +++ b/src/components/fields/TextInput/TextInput.docs.mdx @@ -40,7 +40,7 @@ A text input component that allows users to input custom text entries with a key - **`autoComplete`** `string` — HTML [`autocomplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) token that tells the browser what to autofill (`email`, `username`, `off`, …). The lowercase `autocomplete` alias is deprecated. - **`maxLength`** `number` — Maximum number of characters allowed - **`minLength`** `number` — Minimum number of characters required -- **`inputProps`** `Props` — Direct HTML props passed to the inner input element +- **`inputProps`** `Props` — Direct HTML props passed to the inner input element, merged over the props React Aria generates: a key you set wins, and event handlers are chained rather than replaced - **`inputRef`** `RefObject` — Ref to the inner input element - **`wrapperRef`** `RefObject` — Ref to the input wrapper container - **`loadingIndicator`** `ReactNode` — Custom loading indicator element diff --git a/src/components/fields/TextInput/TextInput.tsx b/src/components/fields/TextInput/TextInput.tsx index 4772a8df2..b71015e7a 100644 --- a/src/components/fields/TextInput/TextInput.tsx +++ b/src/components/fields/TextInput/TextInput.tsx @@ -34,6 +34,7 @@ export const TextInput = forwardRef(function TextInput( let { labelProps: userLabelProps, + inputProps: userInputProps, inputRef: propsInputRef, form, isBuffered, @@ -68,7 +69,7 @@ export const TextInput = forwardRef(function TextInput( {...restProps} ref={ref} labelProps={mergedLabelProps} - inputProps={inputProps} + inputProps={mergeProps(inputProps, userInputProps)} inputRef={inputRef} /> ); diff --git a/src/components/fields/input-props.test.tsx b/src/components/fields/input-props.test.tsx new file mode 100644 index 000000000..180208faf --- /dev/null +++ b/src/components/fields/input-props.test.tsx @@ -0,0 +1,120 @@ +import { + CommandTextArea, + NumberInput, + PasswordInput, + SearchInput, + TextArea, + TextInput, +} from '../../index'; +import { act, renderWithRoot, userEvent } from '../../test'; + +import type { Props } from '../../props'; + +/** Every field built on `TextInputBase` marks its control with `data-input-type`. */ +function getInput(container: HTMLElement) { + return container.querySelector( + 'input[data-input-type], textarea[data-input-type]', + )!; +} + +const FIELDS = [ + { + name: 'TextInput', + render: (props: Props) => , + }, + { + name: 'TextArea', + render: (props: Props) =>