From f31a424c5083011fc3191007a7cb730c0fe1340c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 28 Mar 2024 17:18:51 +0100 Subject: [PATCH 01/14] remove `nullable` prop --- .../src/components/combobox/combobox.test.tsx | 24 +--------- .../src/components/combobox/combobox.tsx | 47 +++++-------------- 2 files changed, 13 insertions(+), 58 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 8e24d69d0e..3b33b2fc9a 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -692,7 +692,7 @@ describe('Rendering', () => { return ( <> - + @@ -4100,27 +4100,6 @@ describe.each([{ virtual: true }, { virtual: false }])( let [value, setValue] = useState('bob') let [, setQuery] = useState('') - // return ( - // { - // setValue(value) - // handleChange(value) - // }, - // nullable: true, - // }} - // inputProps={{ - // onChange: (event: any) => setQuery(event.target.value), - // }} - // /> - // ) - return ( setQuery(event.target.value)} /> Trigger diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 42bd0446fb..aa16af78e7 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -502,7 +502,6 @@ let ComboboxDataContext = createContext< disabled: boolean mode: ValueMode activeOptionIndex: number | null - nullable: boolean immediate: boolean virtual: { options: unknown[]; disabled: (value: unknown) => boolean } | null @@ -552,18 +551,16 @@ type ComboboxRenderPropArg = { value: TValue } -type O = 'value' | 'defaultValue' | 'nullable' | 'multiple' | 'onChange' | 'by' +type O = 'value' | 'defaultValue' | 'multiple' | 'onChange' | 'by' type ComboboxValueProps< TValue, - TNullable extends boolean | undefined, TMultiple extends boolean | undefined, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG, > = Extract< | ({ value?: EnsureArray defaultValue?: EnsureArray - nullable: true // We ignore `nullable` in multiple mode multiple: true onChange?(value: EnsureArray): void by?: ByComparator @@ -571,7 +568,6 @@ type ComboboxValueProps< | ({ value?: TValue | null defaultValue?: TValue | null - nullable: true multiple?: false onChange?(value: TValue | null): void by?: ByComparator @@ -579,28 +575,25 @@ type ComboboxValueProps< | ({ value?: EnsureArray defaultValue?: EnsureArray - nullable?: false multiple: true onChange?(value: EnsureArray): void by?: ByComparator ? U : TValue> } & Expand, TValue>, O>>) | ({ value?: TValue - nullable?: false multiple?: false defaultValue?: TValue onChange?(value: TValue): void by?: ByComparator } & Props, O>), - { nullable?: TNullable; multiple?: TMultiple } + { multiple?: TMultiple } > export type ComboboxProps< TValue, - TNullable extends boolean | undefined, TMultiple extends boolean | undefined, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG, -> = ComboboxValueProps & { +> = ComboboxValueProps & { disabled?: boolean __demoMode?: boolean form?: string @@ -613,24 +606,16 @@ export type ComboboxProps< } function ComboboxFn( - props: ComboboxProps, + props: ComboboxProps, ref: Ref ): JSX.Element function ComboboxFn( - props: ComboboxProps, - ref: Ref -): JSX.Element -function ComboboxFn( - props: ComboboxProps, - ref: Ref -): JSX.Element -function ComboboxFn( - props: ComboboxProps, + props: ComboboxProps, ref: Ref ): JSX.Element function ComboboxFn( - props: ComboboxProps, + props: ComboboxProps, ref: Ref ) { let providedDisabled = useDisabled() @@ -643,7 +628,6 @@ function ComboboxFn { @@ -1214,7 +1197,7 @@ function InputFn< event.stopPropagation() } - if (data.nullable && data.mode === ValueMode.Single) { + if (data.mode === ValueMode.Single) { // We want to clear the value when the user presses escape if and only if the current // value is not set (aka, they didn't select anything yet, or they cleared the input which // caused the value to be set to `null`). If the current value is set, then we want to @@ -1252,7 +1235,7 @@ function InputFn< // // This is can happen when you press backspace, but also when you select all the text and press // ctrl/cmd+x. - if (data.nullable && data.mode === ValueMode.Single) { + if (data.mode === ValueMode.Single) { if (event.target.value === '') { clear() } @@ -1285,7 +1268,7 @@ function InputFn< // caused the value to be set to `null`). If the current value is set, then we want to // fallback to that value when we press escape (this part is handled in the watcher that // syncs the value with the input field again). - if (data.nullable && data.value === null) { + if (data.value === null) { clear() } @@ -1827,16 +1810,10 @@ function OptionFn< export interface _internal_ComponentCombobox extends HasDisplayName { ( - props: ComboboxProps & RefProp - ): JSX.Element - ( - props: ComboboxProps & RefProp - ): JSX.Element - ( - props: ComboboxProps & RefProp + props: ComboboxProps & RefProp ): JSX.Element ( - props: ComboboxProps & RefProp + props: ComboboxProps & RefProp ): JSX.Element } From 70d0173108a96ce2b70f5dbf8b475af4bd9d6654 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 28 Mar 2024 18:23:05 +0100 Subject: [PATCH 02/14] prevent selecting active option on blur + cleanup and adjust comments --- .../src/components/combobox/combobox.tsx | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index aa16af78e7..6d00d4c095 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1251,32 +1251,28 @@ function InputFn< isTyping.current = false // Focus is moved into the list, we don't want to close yet. - if (data.optionsRef.current?.contains(relatedTarget)) { - return - } + if (data.optionsRef.current?.contains(relatedTarget)) return - if (data.buttonRef.current?.contains(relatedTarget)) { - return - } + // Focus is moved to the button, we don't want to close yet. + if (data.buttonRef.current?.contains(relatedTarget)) return + // Focus is moved, but the combobox is not open. This can mean two things: + // + // 1. The combobox was never opened, so we don't have to do anything. + // 2. The combobox was closed and focus was moved already. At that point we + // don't need to try and select the active option. if (data.comboboxState !== ComboboxState.Open) return - event.preventDefault() - if (data.mode === ValueMode.Single) { - // We want to clear the value when the user presses escape if and only if the current - // value is not set (aka, they didn't select anything yet, or they cleared the input which - // caused the value to be set to `null`). If the current value is set, then we want to - // fallback to that value when we press escape (this part is handled in the watcher that - // syncs the value with the input field again). - if (data.value === null) { - clear() - } + event.preventDefault() - // We do have a value, so let's select the active option, unless we were just going through - // the form and we opened it due to the focus event. - else if (data.activationTrigger !== ActivationTrigger.Focus) { - actions.selectActiveOption() - } + // We want to clear the value when the user presses escape or clicks outside + // the combobox if and only if the current value is not set (aka, they + // didn't select anything yet, or they cleared the input which caused the + // value to be set to `null`). If the current value is set, then we want to + // fallback to that value when we press escape (this part is handled in the + // watcher that syncs the value with the input field again). + if (data.mode === ValueMode.Single && data.value === null) { + clear() } return actions.closeCombobox() From 6aa53480b387e3ed90ba7bbb7f11d5ad68021bec Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 28 Mar 2024 18:26:25 +0100 Subject: [PATCH 03/14] remove nullable from comments --- .../src/components/combobox/combobox.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 6d00d4c095..6934bd9a56 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1230,15 +1230,13 @@ function InputFn< // options while typing won't work at all because we are still in "composing" mode. onChange?.(event) - // When the value becomes empty in a single value mode while being nullable then we want to clear + // When the value becomes empty in a single value mode then we want to clear // the option entirely. // // This is can happen when you press backspace, but also when you select all the text and press // ctrl/cmd+x. - if (data.mode === ValueMode.Single) { - if (event.target.value === '') { - clear() - } + if (data.mode === ValueMode.Single && event.target.value === '') { + clear() } // Open the combobox to show the results based on what the user has typed From 21c409410ddb16b3b6917d966d16e368087dcddf Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 28 Mar 2024 19:24:09 +0100 Subject: [PATCH 04/14] bump TypeScript to 5.4 This gives us `NoInfer`! --- package-lock.json | 20 +++++++++++++++++--- package.json | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c2e013223..ed9f8defe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "prettier-plugin-tailwindcss": "^0.5.7", "rimraf": "^3.0.2", "tslib": "^2.3.1", - "typescript": "^5.3.2" + "typescript": "^5.4.3" } }, "node_modules/@adobe/css-tools": { @@ -100,6 +100,19 @@ "node": ">=18" } }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "dev": true, @@ -9653,9 +9666,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 8f89cf0c7d..68164048dd 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,6 @@ "prettier-plugin-tailwindcss": "^0.5.7", "rimraf": "^3.0.2", "tslib": "^2.3.1", - "typescript": "^5.3.2" + "typescript": "^5.4.3" } } From d8163006fc89a9b7dae403931cdd2d4283206994 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 28 Mar 2024 20:14:43 +0100 Subject: [PATCH 05/14] simplify types of `Combobox` Now that `nullable` is gone, we can take another look at the type definition. This in combination with the new `NoInfer` type makes types drastically simpler and more correct. --- .../src/components/combobox/combobox.tsx | 82 ++++++------------- 1 file changed, 24 insertions(+), 58 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 6934bd9a56..31cb44455d 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -48,7 +48,7 @@ import { import { FormFields } from '../../internal/form-fields' import { useProvidedId } from '../../internal/id' import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed' -import type { EnsureArray, Expand, Props } from '../../types' +import type { EnsureArray, Props } from '../../types' import { history } from '../../utils/active-element-history' import { isDisabledReactIssue7711 } from '../../utils/bugs' import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index' @@ -551,68 +551,34 @@ type ComboboxRenderPropArg = { value: TValue } -type O = 'value' | 'defaultValue' | 'multiple' | 'onChange' | 'by' - -type ComboboxValueProps< - TValue, - TMultiple extends boolean | undefined, - TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG, -> = Extract< - | ({ - value?: EnsureArray - defaultValue?: EnsureArray - multiple: true - onChange?(value: EnsureArray): void - by?: ByComparator - } & Props, TValue>, O>) - | ({ - value?: TValue | null - defaultValue?: TValue | null - multiple?: false - onChange?(value: TValue | null): void - by?: ByComparator - } & Expand, O>>) - | ({ - value?: EnsureArray - defaultValue?: EnsureArray - multiple: true - onChange?(value: EnsureArray): void - by?: ByComparator ? U : TValue> - } & Expand, TValue>, O>>) - | ({ - value?: TValue - multiple?: false - defaultValue?: TValue - onChange?(value: TValue): void - by?: ByComparator - } & Props, O>), - { multiple?: TMultiple } -> - export type ComboboxProps< TValue, TMultiple extends boolean | undefined, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG, -> = ComboboxValueProps & { - disabled?: boolean - __demoMode?: boolean - form?: string - name?: string - immediate?: boolean - virtual?: { - options: TValue[] - disabled?: (value: TValue) => boolean - } | null -} +> = Props< + TTag, + ComboboxRenderPropArg>, + 'value' | 'defaultValue' | 'multiple' | 'onChange' | 'by', + { + value?: TMultiple extends true ? EnsureArray : TValue + defaultValue?: TMultiple extends true ? EnsureArray> : NoInfer -function ComboboxFn( - props: ComboboxProps, - ref: Ref -): JSX.Element -function ComboboxFn( - props: ComboboxProps, - ref: Ref -): JSX.Element + onChange?(value: TMultiple extends true ? EnsureArray> : NoInfer): void + by?: ByComparator> + + multiple?: TMultiple + disabled?: boolean + form?: string + name?: string + immediate?: boolean + virtual?: { + options: NoInfer[] + disabled?: (value: NoInfer) => boolean + } | null + + __demoMode?: boolean + } +> function ComboboxFn( props: ComboboxProps, From 953316ee760291efd2d26a31d967d716f2f1e499 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 00:26:37 +0100 Subject: [PATCH 06/14] re-add `nullable` to prevent type issues But let's mark it as deprecated to hint that something changed. --- .../@headlessui-react/src/components/combobox/combobox.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 31cb44455d..53ccb9101b 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -566,6 +566,9 @@ export type ComboboxProps< onChange?(value: TMultiple extends true ? EnsureArray> : NoInfer): void by?: ByComparator> + /** @deprecated The `` is now nullable default */ + nullable?: boolean + multiple?: TMultiple disabled?: boolean form?: string From e85627c0ab2197ce6dae2459467de6f62aebf65f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 00:46:09 +0100 Subject: [PATCH 07/14] update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index c8291c4ebc..9086dd5168 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Attempt form submission when pressing `Enter` on the `` component ([#2972](https://github.com/tailwindlabs/headlessui/pull/2972)) +- Make the `Combobox` component `nullable` by default ([#3064](https://github.com/tailwindlabs/headlessui/pull/3064)) ### Added From fe6e4aaabb678357cdb0018a397a3aaa57b70c46 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 01:00:57 +0100 Subject: [PATCH 08/14] improve `ByComparator` type If we are just checking for `T extends null`, then `{id:1,name:string}|null` will also be true and therefore we would eventually return `string` instead of `"id" | "name"`. To solve this, we first check if `NonNullable extends never`, this would be the case if `T` is `null`. Otherwise, we know it's not just `null` but it can be something else with or without `null`. To be sure, we use `keyof NonNullable` to get rid of the `null` part and to only keep the rest of the object (if it's an object). --- packages/@headlessui-react/src/hooks/use-by-comparator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@headlessui-react/src/hooks/use-by-comparator.ts b/packages/@headlessui-react/src/hooks/use-by-comparator.ts index fe5dd20b6e..f1f78003e4 100644 --- a/packages/@headlessui-react/src/hooks/use-by-comparator.ts +++ b/packages/@headlessui-react/src/hooks/use-by-comparator.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' export type ByComparator = - | (T extends null ? string : keyof T & string) + | (NonNullable extends never ? string : keyof NonNullable & string) | ((a: T, z: T) => boolean) function defaultBy(a: T, z: T) { From 31bf4d9295a933a440774d33406ebf7cd2cd446b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 01:04:42 +0100 Subject: [PATCH 09/14] ensure the `by` prop type handles `multiple` values correctly This way the `by` prop will still compare single values that are present inside the array. This now also solves a pending TypeScript issue that we used to `// @ts-expect-error` before. --- .../@headlessui-react/src/components/combobox/combobox.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 53ccb9101b..4f3efb2047 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -564,7 +564,9 @@ export type ComboboxProps< defaultValue?: TMultiple extends true ? EnsureArray> : NoInfer onChange?(value: TMultiple extends true ? EnsureArray> : NoInfer): void - by?: ByComparator> + by?: ByComparator< + TMultiple extends true ? EnsureArray>[number] : NoInfer + > /** @deprecated The `` is now nullable default */ nullable?: boolean @@ -628,7 +630,6 @@ function ComboboxFn(null) type TActualValue = true extends typeof multiple ? EnsureArray[number] : TValue - // @ts-expect-error Eventually we'll want to tackle this, but for now this will do. let compare = useByComparator(by) let calculateIndex = useEvent((value: TValue) => { From 89a302710aee8c34952707043acd23d2f3623969 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 01:29:05 +0100 Subject: [PATCH 10/14] type uncontrolled `Combobox` components correctly We have some tests that use uncontrolled components which means that we can't infer the type from the `value` type. --- .../src/components/combobox/combobox.test.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 3b33b2fc9a..c45443ff84 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -387,7 +387,7 @@ describe('Rendering', () => { 'should be possible to use completely new objects while rendering (single mode)', suppressConsoleLogs(async () => { function Example() { - let [value, setValue] = useState({ id: 2, name: 'Bob' }) + let [value, setValue] = useState<{ id: number; name: string }>({ id: 2, name: 'Bob' }) return ( setValue(value)} by="id"> @@ -472,7 +472,7 @@ describe('Rendering', () => { ] render( - + name="assignee" by="id"> value.name} onChange={NOOP} @@ -546,7 +546,7 @@ describe('Rendering', () => { ] render( - + name="assignee" by="id"> @@ -1608,7 +1608,11 @@ describe('Rendering', () => { handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement))) }} > - + + name="assignee" + defaultValue={{ id: 2, name: 'bob', label: 'Bob' }} + by="id" + > {({ value }) => value?.name ?? 'Trigger'} Date: Fri, 29 Mar 2024 01:31:26 +0100 Subject: [PATCH 11/14] simplify `onChange` calls Now that we don't infer the type when using the generic inside of `onChange`, it means that we can use `onChange={setValue}` directly because we don't have to worry about the updater function of `setValue` anymore. --- .../src/components/combobox/combobox.test.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index c45443ff84..df4de3c742 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -390,7 +390,7 @@ describe('Rendering', () => { let [value, setValue] = useState<{ id: number; name: string }>({ id: 2, name: 'Bob' }) return ( - setValue(value)} by="id"> + Trigger alice @@ -432,7 +432,7 @@ describe('Rendering', () => { let [value, setValue] = useState([{ id: 2, name: 'Bob' }]) return ( - setValue(value)} by="id" multiple> + Trigger alice @@ -4189,7 +4189,7 @@ describe.each([{ virtual: true }, { virtual: false }])( }} value={value} by="value" - onChange={(value) => setValue(value)} + onChange={setValue} > setQuery(event.target.value)} /> Trigger @@ -5484,7 +5484,7 @@ describe('Multi-select', () => { let [value, setValue] = useState(['bob', 'charlie']) return ( - setValue(value)} multiple> + {}} /> Trigger @@ -5520,7 +5520,7 @@ describe('Multi-select', () => { let [value, setValue] = useState(['bob', 'charlie']) return ( - setValue(value)} multiple> + {}} /> Trigger @@ -5549,7 +5549,7 @@ describe('Multi-select', () => { let [value, setValue] = useState(['bob', 'charlie']) return ( - setValue(value)} multiple> + {}} /> Trigger @@ -5582,7 +5582,7 @@ describe('Multi-select', () => { let [value, setValue] = useState(['bob', 'charlie']) return ( - setValue(value)} multiple> + {}} /> Trigger @@ -5630,7 +5630,7 @@ describe('Multi-select', () => { let [value, setValue] = useState([]) return ( - setValue(value)} multiple> + {}} /> Trigger From 1b9efefbf85dd63a171ffffe460d1daddf2eb4ad Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Mar 2024 01:41:59 +0100 Subject: [PATCH 12/14] correctly type `onChange`, by adding `null` If you are in single value mode, then the `onChange` can (and will) receive `null` as a value (when you clear the input field). We never properly typed it so this fixes that. In multiple value mode this won't happen, if anything the value will be `[]` but not `null`. --- .../src/components/combobox/combobox.test.tsx | 17 ++++++++++------- .../src/components/combobox/combobox.tsx | 4 +++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index df4de3c742..b1d938bcd1 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -387,7 +387,10 @@ describe('Rendering', () => { 'should be possible to use completely new objects while rendering (single mode)', suppressConsoleLogs(async () => { function Example() { - let [value, setValue] = useState<{ id: number; name: string }>({ id: 2, name: 'Bob' }) + let [value, setValue] = useState<{ id: number; name: string } | null>({ + id: 2, + name: 'Bob', + }) return ( @@ -499,7 +502,7 @@ describe('Rendering', () => { ] function Example() { - let [person, setPerson] = useState(data[1]) + let [person, setPerson] = useState<(typeof data)[number] | null>(data[1]) return ( @@ -647,7 +650,7 @@ describe('Rendering', () => { 'selecting an option puts the display value into Combobox.Input when displayValue is provided (when value is undefined)', suppressConsoleLogs(async () => { function Example() { - let [value, setValue] = useState(undefined) + let [value, setValue] = useState(undefined) return ( @@ -766,7 +769,7 @@ describe('Rendering', () => { 'should reflect the value in the input when the value changes and when you are typing', suppressConsoleLogs(async () => { function Example() { - let [value, setValue] = useState('bob') + let [value, setValue] = useState('bob') let [_query, setQuery] = useState('') return ( @@ -4171,7 +4174,7 @@ describe.each([{ virtual: true }, { virtual: false }])( describe('`Any` key aka search', () => { type Option = { value: string; name: string; disabled: boolean } function Example(props: { people: { value: string; name: string; disabled: boolean }[] }) { - let [value, setValue] = useState