diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 26d6b9e08..53f7c00b8 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure Popover doesn't crash when `focus` is going to `window` ([#2019](https://github.com/tailwindlabs/headlessui/pull/2019)) - Ensure `shift+home` and `shift+end` works as expected in the `Combobox.Input` component ([#2024](https://github.com/tailwindlabs/headlessui/pull/2024)) - Improve syncing of the `Combobox.Input` value ([#2042](https://github.com/tailwindlabs/headlessui/pull/2042)) +- Fix crash when using `multiple` mode without `value` prop (uncontrolled) for `Listbox` and `Combobox` components ([#2058](https://github.com/tailwindlabs/headlessui/pull/2058)) ## [1.7.4] - 2022-11-03 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index d4e65258e..e3be24ce2 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -172,6 +172,29 @@ describe('Rendering', () => { }) ) + it( + 'should not crash in multiple mode', + suppressConsoleLogs(async () => { + render( + + Trigger + + alice + bob + charlie + + + ) + + await click(getComboboxButton()) + let [alice, bob, charlie] = getComboboxOptions() + + await click(alice) + await click(bob) + await click(charlie) + }) + ) + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 31776faa2..0d5f573b3 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -403,7 +403,7 @@ function ComboboxFn( + let [value = multiple ? [] : undefined, theirOnChange] = useControllable( controlledValue, controlledOnChange, defaultValue diff --git a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx index 84f415d3c..45feef66d 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx @@ -163,6 +163,29 @@ describe('Rendering', () => { }) ) + it( + 'should not crash in multiple mode', + suppressConsoleLogs(async () => { + render( + + Trigger + + alice + bob + charlie + + + ) + + await click(getListboxButton()) + let [alice, bob, charlie] = getListboxOptions() + + await click(alice) + await click(bob) + await click(charlie) + }) + ) + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index e7c52bc5a..f78a7231a 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -358,7 +358,11 @@ let ListboxRoot = forwardRefWithAs(function Listbox< const orientation = horizontal ? 'horizontal' : 'vertical' let listboxRef = useSyncRefs(ref) - let [value, theirOnChange] = useControllable(controlledValue, controlledOnChange, defaultValue) + let [value = multiple ? [] : undefined, theirOnChange] = useControllable( + controlledValue, + controlledOnChange, + defaultValue + ) let [state, dispatch] = useReducer(stateReducer, { dataRef: createRef(), diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 6a26cd303..3bb627ba8 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure Popover doesn't crash when `focus` is going to `window` ([#2019](https://github.com/tailwindlabs/headlessui/pull/2019)) - Ensure `shift+home` and `shift+end` works as expected in the `ComboboxInput` component ([#2024](https://github.com/tailwindlabs/headlessui/pull/2024)) - Improve syncing of the `ComboboxInput` value ([#2042](https://github.com/tailwindlabs/headlessui/pull/2042)) +- Fix crash when using `multiple` mode without `value` prop (uncontrolled) for `Listbox` and `Combobox` components ([#2058](https://github.com/tailwindlabs/headlessui/pull/2058)) ## [1.7.4] - 2022-11-03 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index 8b09e5546..4c608887d 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -203,6 +203,45 @@ describe('Rendering', () => { }) ) + it( + 'should not crash in multiple mode', + suppressConsoleLogs(async () => { + let options = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ] + + renderTemplate({ + template: html` + + Trigger + + {{ JSON.stringify(data) }} + + + `, + setup: () => { + let value = ref(options[1]) + return { options, value } + }, + }) + + await click(getComboboxButton()) + let [alice, bob, charlie] = getComboboxOptions() + + await click(alice) + await click(bob) + await click(charlie) + }) + ) + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 47dda4b54..d9786c975 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -176,7 +176,14 @@ export let Combobox = defineComponent({ let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single)) let nullable = computed(() => props.nullable) let [value, theirOnChange] = useControllable( - computed(() => props.modelValue), + computed(() => + props.modelValue === undefined + ? match(mode.value, { + [ValueMode.Multi]: [], + [ValueMode.Single]: undefined, + }) + : props.modelValue + ), (value: unknown) => emit('update:modelValue', value), computed(() => props.defaultValue) ) diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx index 210019190..0a5360a96 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx @@ -183,6 +183,45 @@ describe('Rendering', () => { }) ) + it( + 'should not crash in multiple mode', + suppressConsoleLogs(async () => { + let options = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ] + + renderTemplate({ + template: html` + + Trigger + + {{ JSON.stringify(data) }} + + + `, + setup: () => { + let value = ref(options[1]) + return { options, value } + }, + }) + + await click(getListboxButton()) + let [alice, bob, charlie] = getListboxOptions() + + await click(alice) + await click(bob) + await click(charlie) + }) + ) + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 8ba88190c..864456233 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -168,7 +168,14 @@ export let Listbox = defineComponent({ let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single)) let [value, theirOnChange] = useControllable( - computed(() => props.modelValue), + computed(() => + props.modelValue === undefined + ? match(mode.value, { + [ValueMode.Multi]: [], + [ValueMode.Single]: undefined, + }) + : props.modelValue + ), (value: unknown) => emit('update:modelValue', value), computed(() => props.defaultValue) )