Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use correct value when resetting <Listbox multiple> and <Combobox multiple> #2626

Merged
merged 3 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))

## [1.7.16] - 2023-07-27

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,50 @@ describe('Rendering', () => {
assertActiveComboboxOption(getComboboxOptions()[1])
})

it('should be possible to reset to the default value in multiple mode', async () => {
let handleSubmission = jest.fn()
let data = ['alice', 'bob', 'charlie']

render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Combobox name="assignee" defaultValue={['bob'] as string[]} multiple>
<Combobox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Combobox.Button>
<Combobox.Options>
{data.map((person) => (
<Combobox.Option key={person} value={person}>
{person}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
<button id="submit">submit</button>
<button type="reset" id="reset">
reset
</button>
</form>
)

await click(document.getElementById('submit'))

// Bob is the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})

await click(document.getElementById('reset'))
await click(document.getElementById('submit'))

// Bob is still the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})
})

it('should still call the onChange listeners when choosing new values', async () => {
let handleChange = jest.fn()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,9 +642,9 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
if (defaultValue === undefined) return

d.addEventListener(form.current, 'reset', () => {
onChange(defaultValue)
theirOnChange?.(defaultValue)
})
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])

return (
<ComboboxActionsContext.Provider value={actions}>
Expand Down
44 changes: 44 additions & 0 deletions packages/@headlessui-react/src/components/listbox/listbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,50 @@ describe('Rendering', () => {
assertActiveListboxOption(getListboxOptions()[1])
})

it('should be possible to reset to the default value in multiple mode', async () => {
let handleSubmission = jest.fn()
let data = ['alice', 'bob', 'charlie']

render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Listbox name="assignee" defaultValue={['bob'] as string[]} multiple>
<Listbox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Listbox.Button>
<Listbox.Options>
{data.map((person) => (
<Listbox.Option key={person} value={person}>
{person}
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
<button id="submit">submit</button>
<button type="reset" id="reset">
reset
</button>
</form>
)

await click(document.getElementById('submit'))

// Bob is the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})

await click(document.getElementById('reset'))
await click(document.getElementById('submit'))

// Bob is still the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})
})

it('should still call the onChange listeners when choosing new values', async () => {
let handleChange = jest.fn()

Expand Down
4 changes: 2 additions & 2 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ function ListboxFn<
if (defaultValue === undefined) return

d.addEventListener(form.current, 'reset', () => {
onChange(defaultValue)
theirOnChange?.(defaultValue)
})
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])

return (
<ListboxActionsContext.Provider value={actions}>
Expand Down
5 changes: 4 additions & 1 deletion packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Fix form elements for uncontrolled `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))

## [1.7.15] - 2023-07-27

Expand Down
46 changes: 46 additions & 0 deletions packages/@headlessui-vue/src/components/combobox/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,52 @@ describe('Rendering', () => {
})
)

it('should be possible to reset to the default value in multiple mode', async () => {
let data = ['alice', 'bob', 'charlie']
let handleSubmission = jest.fn()

renderTemplate({
template: html`
<form @submit="handleSubmit">
<Combobox name="assignee" :defaultValue="['bob']" multiple>
<ComboboxButton v-slot="{ value }"
>{{ value.join(', ') || 'Trigger' }}</ComboboxButton
>
<ComboboxOptions>
<ComboboxOption v-for="person in data" :key="person" :value="person">
{{ person }}
</ComboboxOption>
</ComboboxOptions>
</Combobox>
<button id="submit">submit</button>
<button type="reset" id="reset">reset</button>
</form>
`,
setup: () => ({
data,
handleSubmit(e: SubmitEvent) {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
},
}),
})

await click(document.getElementById('submit'))

// Bob is the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})

await click(document.getElementById('reset'))
await click(document.getElementById('submit'))

// Bob is still the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})
})

it('should still call the onChange listeners when choosing new values', async () => {
let handleChange = jest.fn()

Expand Down
20 changes: 11 additions & 9 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,21 @@ 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 === undefined
? match(mode.value, {
[ValueMode.Multi]: [],
[ValueMode.Single]: undefined,
})
: props.modelValue
),
let [directValue, theirOnChange] = useControllable(
computed(() => props.modelValue),
(value: unknown) => emit('update:modelValue', value),
computed(() => props.defaultValue)
)

let value = computed(() =>
directValue.value === undefined
? match(mode.value, {
[ValueMode.Multi]: [],
[ValueMode.Single]: undefined,
})
: directValue.value
)

let goToOptionRaf: ReturnType<typeof requestAnimationFrame> | null = null
let orderOptionsRaf: ReturnType<typeof requestAnimationFrame> | null = null

Expand Down
44 changes: 44 additions & 0 deletions packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,50 @@ describe('Rendering', () => {
})
)

it('should be possible to reset to the default value in multiple mode', async () => {
let data = ['alice', 'bob', 'charlie']
let handleSubmission = jest.fn()

renderTemplate({
template: html`
<form @submit="handleSubmit">
<Listbox name="assignee" :defaultValue="['bob']" multiple>
<ListboxButton v-slot="{ value }">{{ value.join(', ') || 'Trigger' }}</ListboxButton>
<ListboxOptions>
<ListboxOption v-for="person in data" :key="person" :value="person">
{{ person }}
</ListboxOption>
</ListboxOptions>
</Listbox>
<button id="submit">submit</button>
<button type="reset" id="reset">reset</button>
</form>
`,
setup: () => ({
data,
handleSubmit(e: SubmitEvent) {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
},
}),
})

await click(document.getElementById('submit'))

// Bob is the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})

await click(document.getElementById('reset'))
await click(document.getElementById('submit'))

// Bob is still the defaultValue
expect(handleSubmission).toHaveBeenLastCalledWith({
'assignee[0]': 'bob',
})
})

it('should still call the onChange listeners when choosing new values', async () => {
let handleChange = jest.fn()

Expand Down
27 changes: 17 additions & 10 deletions packages/@headlessui-vue/src/components/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,22 @@ export let Listbox = defineComponent({
}

let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
let [value, theirOnChange] = useControllable(
computed(() =>
props.modelValue === undefined
? match(mode.value, {
[ValueMode.Multi]: [],
[ValueMode.Single]: undefined,
})
: props.modelValue
),

let [directValue, theirOnChange] = useControllable(
computed(() => props.modelValue),
(value: unknown) => emit('update:modelValue', value),
computed(() => props.defaultValue)
)

let value = computed(() =>
directValue.value === undefined
? match(mode.value, {
[ValueMode.Multi]: [],
[ValueMode.Single]: undefined,
})
: directValue.value
)

let api = {
listboxState,
value,
Expand Down Expand Up @@ -300,6 +303,10 @@ export let Listbox = defineComponent({
activeOptionIndex.value = adjustedState.activeOptionIndex
activationTrigger.value = ActivationTrigger.Other
},
theirOnChange(value: unknown) {
if (props.disabled) return
theirOnChange(value)
},
select(value: unknown) {
if (props.disabled) return
theirOnChange(
Expand Down Expand Up @@ -357,7 +364,7 @@ export let Listbox = defineComponent({
if (props.defaultValue === undefined) return

function handle() {
api.select(props.defaultValue)
api.theirOnChange(props.defaultValue)
}

form.value.addEventListener('reset', handle)
Expand Down
Loading