Add invalid prop to Combobox component (#3677)

This PR adds the `invalid` prop to the `Combobox` component. This will
also expose the `invalid` value as a render prop to the `Combobox.Input`
and `Combobox.Button` components.

It will also expose the `data-invalid` attribute on these components
when the `invalid` prop is set to `true`.

```tsx
<Combobox invalid>
 <Combobox.Input />
 <Combobox.Button />
</Combobox>
```

Without `invalid` prop:
<img width="916" alt="image"
src="https://github.com/user-attachments/assets/2c199691-7b29-450f-89a5-4b84e6704c6a"
/>


With invalid prop:
<img width="913" alt="image"
src="https://github.com/user-attachments/assets/4bdde518-39b3-4998-b353-604a818a3c99"
/>

Notice the `data-invalid` prop on the `<input>` and the `<button>`.
This commit is contained in:
Robin Malfait
2025-04-04 11:46:50 +02:00
committed by GitHub
parent 0a8de016e8
commit 9a4c030003
3 changed files with 19 additions and 4 deletions
+2 -1
View File
@@ -15,8 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Use correct `ownerDocument` when using internal `Portal` component ([#3594](https://github.com/tailwindlabs/headlessui/pull/3594))
- Bump `@tanstack/react-virtual` to be fix warnings in React 19 projects ([#3588](https://github.com/tailwindlabs/headlessui/pull/3588))
- Bump `@tanstack/react-virtual` to fix warnings in React 19 projects ([#3588](https://github.com/tailwindlabs/headlessui/pull/3588))
- Fix `aria-invalid` attributes to have a valid `'true'` value ([#3639](https://github.com/tailwindlabs/headlessui/pull/3639))
- Add missing `invalid` prop to `Combobox` component ([#3677](https://github.com/tailwindlabs/headlessui/pull/3677))
## [2.2.0] - 2024-10-25
@@ -1045,6 +1045,7 @@ describe('Rendering', () => {
open: false,
active: false,
disabled: false,
invalid: false,
value: 'test',
hover: false,
focus: false,
@@ -1061,6 +1062,7 @@ describe('Rendering', () => {
open: true,
active: true,
disabled: false,
invalid: false,
value: 'test',
hover: false,
focus: false,
@@ -1094,6 +1096,7 @@ describe('Rendering', () => {
open: false,
active: false,
disabled: false,
invalid: false,
value: 'test',
hover: false,
focus: false,
@@ -1110,6 +1113,7 @@ describe('Rendering', () => {
open: true,
active: true,
disabled: false,
invalid: false,
value: 'test',
hover: false,
focus: false,
@@ -577,6 +577,7 @@ let ComboboxDataContext = createContext<
value: unknown
defaultValue: unknown
disabled: boolean
invalid: boolean
mode: ValueMode
activeOptionIndex: number | null
immediate: boolean
@@ -619,6 +620,7 @@ let DEFAULT_COMBOBOX_TAG = Fragment
type ComboboxRenderPropArg<TValue, TActive = TValue> = {
open: boolean
disabled: boolean
invalid: boolean
activeIndex: number | null
activeOption: TActive | null
value: TValue
@@ -648,6 +650,7 @@ export type ComboboxProps<
multiple?: TMultiple
disabled?: boolean
invalid?: boolean
form?: string
name?: string
immediate?: boolean
@@ -676,6 +679,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
form,
name,
by,
invalid = false,
disabled = providedDisabled || false,
onClose,
__demoMode = false,
@@ -751,6 +755,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
value,
defaultValue,
disabled,
invalid,
mode: multiple ? ValueMode.Multi : ValueMode.Single,
virtual: virtual ? state.virtual : null,
get activeOptionIndex() {
@@ -785,7 +790,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
isSelected,
isActive,
}),
[value, defaultValue, disabled, multiple, __demoMode, state, virtual]
[value, defaultValue, disabled, invalid, multiple, __demoMode, state, virtual]
)
useIsoMorphicEffect(() => {
@@ -813,6 +818,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
return {
open: data.comboboxState === ComboboxState.Open,
disabled,
invalid,
activeIndex: data.activeOptionIndex,
activeOption:
data.activeOptionIndex === null
@@ -822,7 +828,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
: (data.options[data.activeOptionIndex]?.dataRef.current.value as TValue) ?? null,
value,
} satisfies ComboboxRenderPropArg<unknown>
}, [data, disabled, value])
}, [data, disabled, value, invalid])
let selectActiveOption = useEvent(() => {
if (data.activeOptionIndex === null) return
@@ -999,6 +1005,7 @@ let DEFAULT_INPUT_TAG = 'input' as const
type InputRenderPropArg = {
open: boolean
disabled: boolean
invalid: boolean
hover: boolean
focus: boolean
autofocus: boolean
@@ -1398,11 +1405,12 @@ function InputFn<
return {
open: data.comboboxState === ComboboxState.Open,
disabled,
invalid: data.invalid,
hover,
focus,
autofocus: autoFocus,
} satisfies InputRenderPropArg
}, [data, hover, focus, autoFocus, disabled])
}, [data, hover, focus, autoFocus, disabled, data.invalid])
let ourProps = mergeProps(
{
@@ -1463,6 +1471,7 @@ type ButtonRenderPropArg = {
open: boolean
active: boolean
disabled: boolean
invalid: boolean
value: any
focus: boolean
hover: boolean
@@ -1587,6 +1596,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
open: data.comboboxState === ComboboxState.Open,
active: active || data.comboboxState === ComboboxState.Open,
disabled,
invalid: data.invalid,
value: data.value,
hover,
focus,