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:
Robin Malfait
2022-04-22 18:55:55 +02:00
committed by GitHub
parent 591b32861a
commit 0c34fe802c
13 changed files with 49 additions and 34 deletions
+2
View File
@@ -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>