Add explicit multiple prop (#1355)
* add explicit `multiple` prop to the `Combobox` This allows you to set the value to a **tuple** in `single-value` mode, which was not possible before the `multiple` prop was introduced, because then it resulted in `multi-value` mode instead of `single-value` mode. * add explicit `multiple` prop to the `Listbox` This allows you to set the value to a **tuple** in `single-value` mode, which was not possible before the `multiple` prop was introduced, because then it resulted in `multi-value` mode instead of `single-value` mode. * update changelog * update playground to use `multiple` prop
This commit is contained in:
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Ensure that there is always an active option in the `Combobox` ([#1279](https://github.com/tailwindlabs/headlessui/pull/1279), [#1281](https://github.com/tailwindlabs/headlessui/pull/1281))
|
||||
- Allow `Enter` for form submit in `RadioGroup`, `Switch` and `Combobox` improvements ([#1285](https://github.com/tailwindlabs/headlessui/pull/1285))
|
||||
- add React 18 compatibility ([#1326](https://github.com/tailwindlabs/headlessui/pull/1326))
|
||||
- Add explicit `multiple` prop ([#1355](https://github.com/tailwindlabs/headlessui/pull/1355))
|
||||
|
||||
### Added
|
||||
|
||||
@@ -75,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Resolve `initialFocusRef` correctly ([#1276](https://github.com/tailwindlabs/headlessui/pull/1276))
|
||||
- Ensure that there is always an active option in the `Combobox` ([#1279](https://github.com/tailwindlabs/headlessui/pull/1279), [#1281](https://github.com/tailwindlabs/headlessui/pull/1281))
|
||||
- Allow `Enter` for form submit in `RadioGroup`, `Switch` and `Combobox` improvements ([#1285](https://github.com/tailwindlabs/headlessui/pull/1285))
|
||||
- Add explicit `multiple` prop ([#1355](https://github.com/tailwindlabs/headlessui/pull/1355))
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4594,7 +4594,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue}>
|
||||
<Combobox value={value} onChange={setValue} multiple>
|
||||
<Combobox.Input onChange={() => {}} />
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
@@ -4630,7 +4630,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue}>
|
||||
<Combobox value={value} onChange={setValue} multiple>
|
||||
<Combobox.Input onChange={() => {}} />
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
@@ -4659,7 +4659,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue}>
|
||||
<Combobox value={value} onChange={setValue} multiple>
|
||||
<Combobox.Input onChange={() => {}} />
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
@@ -4692,7 +4692,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue}>
|
||||
<Combobox value={value} onChange={setValue} multiple>
|
||||
<Combobox.Input onChange={() => {}} />
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
|
||||
@@ -340,7 +340,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
props: Props<
|
||||
TTag,
|
||||
ComboboxRenderPropArg<TType>,
|
||||
'value' | 'onChange' | 'disabled' | 'name' | 'nullable'
|
||||
'value' | 'onChange' | 'disabled' | 'name' | 'nullable' | 'multiple'
|
||||
> & {
|
||||
value: TType
|
||||
onChange(value: TType): void
|
||||
@@ -348,6 +348,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
__demoMode?: boolean
|
||||
name?: string
|
||||
nullable?: boolean
|
||||
multiple?: boolean
|
||||
},
|
||||
ref: Ref<TTag>
|
||||
) {
|
||||
@@ -358,20 +359,21 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
disabled = false,
|
||||
__demoMode = false,
|
||||
nullable = false,
|
||||
multiple = false,
|
||||
...theirProps
|
||||
} = props
|
||||
let defaultToFirstOption = useRef(false)
|
||||
|
||||
let comboboxPropsRef = useRef<StateDefinition['comboboxPropsRef']['current']>({
|
||||
value,
|
||||
mode: Array.isArray(value) ? ValueMode.Multi : ValueMode.Single,
|
||||
mode: multiple ? ValueMode.Multi : ValueMode.Single,
|
||||
onChange,
|
||||
nullable,
|
||||
__demoMode,
|
||||
})
|
||||
|
||||
comboboxPropsRef.current.value = value
|
||||
comboboxPropsRef.current.mode = Array.isArray(value) ? ValueMode.Multi : ValueMode.Single
|
||||
comboboxPropsRef.current.mode = multiple ? ValueMode.Multi : ValueMode.Single
|
||||
comboboxPropsRef.current.nullable = nullable
|
||||
|
||||
let optionsPropsRef = useRef<StateDefinition['optionsPropsRef']['current']>({
|
||||
@@ -411,7 +413,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
let dataBag = useMemo<Exclude<ContextType<typeof ComboboxData>, null>>(
|
||||
() => ({
|
||||
value,
|
||||
mode: Array.isArray(value) ? ValueMode.Multi : ValueMode.Single,
|
||||
mode: multiple ? ValueMode.Multi : ValueMode.Single,
|
||||
get activeOptionIndex() {
|
||||
if (defaultToFirstOption.current && _activeOptionIndex === null && options.length > 0) {
|
||||
let localActiveOptionIndex = options.findIndex(
|
||||
|
||||
@@ -3963,7 +3963,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={setValue}>
|
||||
<Listbox value={value} onChange={setValue} multiple>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<Listbox.Option value="alice">alice</Listbox.Option>
|
||||
@@ -3998,7 +3998,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={setValue}>
|
||||
<Listbox value={value} onChange={setValue} multiple>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<Listbox.Option value="alice">alice</Listbox.Option>
|
||||
@@ -4026,7 +4026,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={setValue}>
|
||||
<Listbox value={value} onChange={setValue} multiple>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<Listbox.Option value="alice">alice</Listbox.Option>
|
||||
@@ -4058,7 +4058,7 @@ describe('Multi-select', () => {
|
||||
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={setValue}>
|
||||
<Listbox value={value} onChange={setValue} multiple>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<Listbox.Option value="alice">alice</Listbox.Option>
|
||||
|
||||
@@ -304,24 +304,33 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
|
||||
props: Props<
|
||||
TTag,
|
||||
ListboxRenderPropArg,
|
||||
'value' | 'onChange' | 'disabled' | 'horizontal' | 'name'
|
||||
'value' | 'onChange' | 'disabled' | 'horizontal' | 'name' | 'multiple'
|
||||
> & {
|
||||
value: TType
|
||||
onChange(value: TType): void
|
||||
disabled?: boolean
|
||||
horizontal?: boolean
|
||||
name?: string
|
||||
multiple?: boolean
|
||||
},
|
||||
ref: Ref<TTag>
|
||||
) {
|
||||
let { value, name, onChange, disabled = false, horizontal = false, ...theirProps } = props
|
||||
let {
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
disabled = false,
|
||||
horizontal = false,
|
||||
multiple = false,
|
||||
...theirProps
|
||||
} = props
|
||||
const orientation = horizontal ? 'horizontal' : 'vertical'
|
||||
let listboxRef = useSyncRefs(ref)
|
||||
|
||||
let reducerBag = useReducer(stateReducer, {
|
||||
listboxState: ListboxStates.Closed,
|
||||
propsRef: {
|
||||
current: { value, onChange, mode: Array.isArray(value) ? ValueMode.Multi : ValueMode.Single },
|
||||
current: { value, onChange, mode: multiple ? ValueMode.Multi : ValueMode.Single },
|
||||
},
|
||||
labelRef: createRef(),
|
||||
buttonRef: createRef(),
|
||||
@@ -336,7 +345,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
|
||||
let [{ listboxState, propsRef, optionsRef, buttonRef }, dispatch] = reducerBag
|
||||
|
||||
propsRef.current.value = value
|
||||
propsRef.current.mode = Array.isArray(value) ? ValueMode.Multi : ValueMode.Single
|
||||
propsRef.current.mode = multiple ? ValueMode.Multi : ValueMode.Single
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
propsRef.current.onChange = (value: unknown) => {
|
||||
|
||||
@@ -4821,7 +4821,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<Combobox v-model="value" multiple>
|
||||
<ComboboxInput />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
@@ -4854,7 +4854,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<Combobox v-model="value" multiple>
|
||||
<ComboboxInput />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
@@ -4880,7 +4880,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<Combobox v-model="value" multiple>
|
||||
<ComboboxInput />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
@@ -4910,7 +4910,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<Combobox v-model="value" multiple>
|
||||
<ComboboxInput />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
@@ -4954,7 +4954,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<Combobox v-model="value" multiple>
|
||||
<ComboboxInput />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
|
||||
@@ -113,6 +113,7 @@ export let Combobox = defineComponent({
|
||||
modelValue: { type: [Object, String, Number, Boolean] },
|
||||
name: { type: String },
|
||||
nullable: { type: Boolean, default: false },
|
||||
multiple: { type: [Boolean], default: false },
|
||||
},
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
let comboboxState = ref<StateDefinition['comboboxState']['value']>(ComboboxStates.Closed)
|
||||
@@ -163,7 +164,7 @@ export let Combobox = defineComponent({
|
||||
}
|
||||
|
||||
let value = computed(() => props.modelValue)
|
||||
let mode = computed(() => (Array.isArray(value.value) ? ValueMode.Multi : ValueMode.Single))
|
||||
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
|
||||
let nullable = computed(() => props.nullable)
|
||||
|
||||
let api = {
|
||||
@@ -444,7 +445,7 @@ export let Combobox = defineComponent({
|
||||
)
|
||||
: []),
|
||||
render({
|
||||
props: omit(incomingProps, ['nullable', 'onUpdate:modelValue']),
|
||||
props: omit(incomingProps, ['nullable', 'multiple', 'onUpdate:modelValue']),
|
||||
slot,
|
||||
slots,
|
||||
attrs,
|
||||
|
||||
@@ -4086,7 +4086,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<Listbox v-model="value" multiple>
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption value="alice">alice</ListboxOption>
|
||||
@@ -4118,7 +4118,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<Listbox v-model="value" multiple>
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption value="alice">alice</ListboxOption>
|
||||
@@ -4143,7 +4143,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<Listbox v-model="value" multiple>
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption value="alice">alice</ListboxOption>
|
||||
@@ -4172,7 +4172,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<Listbox v-model="value" multiple>
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption value="alice">alice</ListboxOption>
|
||||
@@ -4215,7 +4215,7 @@ describe('Multi-select', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<Listbox v-model="value" multiple>
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption v-for="person in people" :value="person"
|
||||
|
||||
@@ -113,6 +113,7 @@ export let Listbox = defineComponent({
|
||||
horizontal: { type: [Boolean], default: false },
|
||||
modelValue: { type: [Object, String, Number, Boolean] },
|
||||
name: { type: String, optional: true },
|
||||
multiple: { type: [Boolean], default: false },
|
||||
},
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
let listboxState = ref<StateDefinition['listboxState']['value']>(ListboxStates.Closed)
|
||||
@@ -156,7 +157,7 @@ export let Listbox = defineComponent({
|
||||
}
|
||||
|
||||
let value = computed(() => props.modelValue)
|
||||
let mode = computed(() => (Array.isArray(value.value) ? ValueMode.Multi : ValueMode.Single))
|
||||
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
|
||||
|
||||
let api = {
|
||||
listboxState,
|
||||
@@ -327,7 +328,7 @@ export let Listbox = defineComponent({
|
||||
)
|
||||
: []),
|
||||
render({
|
||||
props: omit(incomingProps, ['onUpdate:modelValue', 'horizontal']),
|
||||
props: omit(incomingProps, ['onUpdate:modelValue', 'horizontal', 'multiple']),
|
||||
slot,
|
||||
slots,
|
||||
attrs,
|
||||
|
||||
@@ -39,7 +39,7 @@ function MultiPeopleList() {
|
||||
console.log([...new FormData(e.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<Combobox value={activePersons} onChange={setActivePersons} name="people">
|
||||
<Combobox value={activePersons} onChange={setActivePersons} name="people" multiple>
|
||||
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
|
||||
Assigned to
|
||||
</Combobox.Label>
|
||||
|
||||
@@ -38,7 +38,7 @@ function MultiPeopleList() {
|
||||
console.log([...new FormData(e.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<Listbox value={activePersons} onChange={setActivePersons} name="people">
|
||||
<Listbox value={activePersons} onChange={setActivePersons} name="people" multiple>
|
||||
<Listbox.Label className="block text-sm font-medium leading-5 text-gray-700">
|
||||
Assigned to
|
||||
</Listbox.Label>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="w-full max-w-4xl">
|
||||
<div class="space-y-1">
|
||||
<form @submit="onSubmit">
|
||||
<Combobox v-model="activePersons" name="people">
|
||||
<Combobox v-model="activePersons" name="people" multiple>
|
||||
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
|
||||
Assigned to
|
||||
</ComboboxLabel>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="w-full max-w-4xl">
|
||||
<div class="space-y-1">
|
||||
<form @submit="onSubmit">
|
||||
<Listbox v-model="activePersons" name="people">
|
||||
<Listbox v-model="activePersons" name="people" multiple>
|
||||
<ListboxLabel class="block text-sm font-medium leading-5 text-gray-700">
|
||||
Assigned to
|
||||
</ListboxLabel>
|
||||
|
||||
Reference in New Issue
Block a user