diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 5541baa171..73b5e866f7 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Forward `disabled` state to hidden inputs in form-like components ([#3004](https://github.com/tailwindlabs/headlessui/pull/3004)) - Prefer incoming `data-*` attributes, over the ones set by Headless UI ([#3035](https://github.com/tailwindlabs/headlessui/pull/3035)) - Respect `selectedIndex` for controlled `` components ([#3037](https://github.com/tailwindlabs/headlessui/pull/3037)) +- Prevent unnecessary execution of the `displayValue` callback in the `ComboboxInput` component ([#3048](https://github.com/tailwindlabs/headlessui/pull/3048)) ### Changed diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 562a33ea87..8e24d69d0e 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -489,6 +489,53 @@ describe('Rendering', () => { }) ) + it( + 'should keep the defaultValue when the Combobox state changes', + suppressConsoleLogs(async () => { + let data = [ + { id: 1, name: 'alice', label: 'Alice' }, + { id: 2, name: 'bob', label: 'Bob' }, + { id: 3, name: 'charlie', label: 'Charlie' }, + ] + + function Example() { + let [person, setPerson] = useState(data[1]) + + return ( + + String(Math.random())} /> + + + {data.map((person) => ( + + {person.label} + + ))} + + + ) + } + + render() + + let value = getComboboxInput()?.value + + // Toggle the state a few times combobox + await click(getComboboxButton()) + await click(getComboboxButton()) + await click(getComboboxButton()) + + // Verify the value is still the same + expect(getComboboxInput()?.value).toBe(value) + + // Choose an option, which should update the value + await click(getComboboxOptions()[2]) + + // Verify the value changed + expect(getComboboxInput()?.value).not.toBe(value) + }) + ) + it( 'should close the Combobox when the input is blurred', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 7971322b05..1c4c88f17a 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1005,7 +1005,7 @@ function InputFn< // which should always result in a string (since we are filling in the value of the text input), // you don't have to use this at all, a more common UI is a "tag" based UI, which you can render // yourself using the selected option(s). - let currentDisplayValue = (function () { + let currentDisplayValue = useMemo(() => { if (typeof displayValue === 'function' && data.value !== undefined) { return displayValue(data.value as unknown as TType) ?? '' } else if (typeof data.value === 'string') { @@ -1013,7 +1013,7 @@ function InputFn< } else { return '' } - })() + }, [data.value, displayValue]) // Syncing the input value has some rules attached to it to guarantee a smooth and expected user // experience: diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index ee56f2d21d..d1e7772b79 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -483,6 +483,48 @@ describe('Rendering', () => { ) }) + it( + 'should not crash when a defaultValue is not given', + suppressConsoleLogs(async () => { + let data = [ + { id: 1, name: 'alice', label: 'Alice' }, + { id: 2, name: 'bob', label: 'Bob' }, + { id: 3, name: 'charlie', label: 'Charlie' }, + ] + + renderTemplate({ + template: html` + + + + + + {{ person.label }} + + + + `, + setup: () => ({ person: ref(data[0]), data, displayValue: () => String(Math.random()) }), + }) + + let value = getComboboxInput()?.value + + // Toggle the state a few times combobox + await click(getComboboxButton()) + await click(getComboboxButton()) + await click(getComboboxButton()) + + // Verify the value is still the same + expect(getComboboxInput()?.value).toBe(value) + + // Choose an option, which should update the value + await click(getComboboxOptions()[1]) + + // Verify the value changed + expect(getComboboxInput()?.value).not.toBe(value) + }) + ) + it( 'should not crash when a defaultValue is not given', suppressConsoleLogs(async () => {