Add by prop for Listbox, Combobox and RadioGroup (#1482)
* Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` * update changelog
This commit is contained in:
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Allow to override the `type` on the `ComboboxInput` ([#1476](https://github.com/tailwindlabs/headlessui/pull/1476))
|
||||
- Ensure the the `<PopoverPanel focus>` closes correctly ([#1477](https://github.com/tailwindlabs/headlessui/pull/1477))
|
||||
|
||||
### Added
|
||||
|
||||
- Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` ([#1482](https://github.com/tailwindlabs/headlessui/pull/1482))
|
||||
|
||||
## [Unreleased - @headlessui/react]
|
||||
|
||||
### Fixed
|
||||
@@ -19,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Allow to override the `type` on the `Combobox.Input` ([#1476](https://github.com/tailwindlabs/headlessui/pull/1476))
|
||||
- Ensure the the `<Popover.Panel focus>` closes correctly ([#1477](https://github.com/tailwindlabs/headlessui/pull/1477))
|
||||
|
||||
### Added
|
||||
|
||||
- Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` ([#1482](https://github.com/tailwindlabs/headlessui/pull/1482))
|
||||
|
||||
## [@headlessui/vue@v1.6.2] - 2022-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -170,6 +170,108 @@ describe('Rendering', () => {
|
||||
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Combobox value={options[1]} onChange={console.log}>
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
{options.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
)
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Combobox value={{ id: 2, name: 'Bob' }} onChange={console.log} by="id">
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
{options.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
)
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Combobox
|
||||
value={{ id: 2, name: 'Bob' }}
|
||||
onChange={console.log}
|
||||
by={(a, z) => a.id === z.id}
|
||||
>
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
{options.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
)
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Combobox.Input', () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useTreeWalker } from '../../hooks/use-tree-walker'
|
||||
import { sortByDomNode } from '../../utils/focus-management'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { objectToFormEntries } from '../../utils/form'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
enum ComboboxStates {
|
||||
Open,
|
||||
@@ -69,6 +70,7 @@ interface StateDefinition {
|
||||
mode: ValueMode
|
||||
onChange(value: unknown): void
|
||||
nullable: boolean
|
||||
compare(a: unknown, z: unknown): boolean
|
||||
__demoMode: boolean
|
||||
}>
|
||||
inputPropsRef: MutableRefObject<{
|
||||
@@ -160,12 +162,13 @@ let reducers: {
|
||||
|
||||
// Check if we have a selected value that we can make active
|
||||
let activeOptionIndex = state.activeOptionIndex
|
||||
let { value, mode } = state.comboboxPropsRef.current
|
||||
let { value, mode, compare } = state.comboboxPropsRef.current
|
||||
let optionIdx = state.options.findIndex((option) => {
|
||||
let optionValue = option.dataRef.current.value
|
||||
let selected = match(mode, {
|
||||
[ValueMode.Multi]: () => (value as unknown[]).includes(optionValue),
|
||||
[ValueMode.Single]: () => value === optionValue,
|
||||
[ValueMode.Multi]: () =>
|
||||
(value as unknown[]).some((option) => compare(option, optionValue)),
|
||||
[ValueMode.Single]: () => compare(value, optionValue),
|
||||
})
|
||||
|
||||
return selected
|
||||
@@ -226,11 +229,12 @@ let reducers: {
|
||||
|
||||
// Check if we need to make the newly registered option active.
|
||||
if (state.activeOptionIndex === null) {
|
||||
let { value, mode } = state.comboboxPropsRef.current
|
||||
let { value, mode, compare } = state.comboboxPropsRef.current
|
||||
let optionValue = action.dataRef.current.value
|
||||
let selected = match(mode, {
|
||||
[ValueMode.Multi]: () => (value as unknown[]).includes(optionValue),
|
||||
[ValueMode.Single]: () => value === optionValue,
|
||||
[ValueMode.Multi]: () =>
|
||||
(value as unknown[]).some((option) => compare(option, optionValue)),
|
||||
[ValueMode.Single]: () => compare(value, optionValue),
|
||||
})
|
||||
if (selected) {
|
||||
adjustedState.activeOptionIndex = adjustedState.options.indexOf(option)
|
||||
@@ -340,10 +344,11 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
props: Props<
|
||||
TTag,
|
||||
ComboboxRenderPropArg<TType>,
|
||||
'value' | 'onChange' | 'disabled' | 'name' | 'nullable' | 'multiple'
|
||||
'value' | 'onChange' | 'disabled' | 'name' | 'nullable' | 'multiple' | 'by'
|
||||
> & {
|
||||
value: TType
|
||||
onChange(value: TType): void
|
||||
by?: (keyof TType & string) | ((a: TType, z: TType) => boolean)
|
||||
disabled?: boolean
|
||||
__demoMode?: boolean
|
||||
name?: string
|
||||
@@ -356,6 +361,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
by = (a, z) => a === z,
|
||||
disabled = false,
|
||||
__demoMode = false,
|
||||
nullable = false,
|
||||
@@ -367,6 +373,14 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
let comboboxPropsRef = useRef<StateDefinition['comboboxPropsRef']['current']>({
|
||||
value,
|
||||
mode: multiple ? ValueMode.Multi : ValueMode.Single,
|
||||
compare: useEvent(
|
||||
typeof by === 'string'
|
||||
? (a: TType, z: TType) => {
|
||||
let property = by as unknown as keyof TType
|
||||
return a[property] === z[property]
|
||||
}
|
||||
: by
|
||||
),
|
||||
onChange,
|
||||
nullable,
|
||||
__demoMode,
|
||||
@@ -1093,9 +1107,13 @@ let Option = forwardRefWithAs(function Option<
|
||||
let id = `headlessui-combobox-option-${useId()}`
|
||||
let active =
|
||||
data.activeOptionIndex !== null ? state.options[data.activeOptionIndex].id === id : false
|
||||
|
||||
let selected = match(data.mode, {
|
||||
[ValueMode.Multi]: () => (data.value as TType[]).includes(value),
|
||||
[ValueMode.Single]: () => data.value === value,
|
||||
[ValueMode.Multi]: () =>
|
||||
(data.value as TType[]).some((option) =>
|
||||
state.comboboxPropsRef.current.compare(option, value)
|
||||
),
|
||||
[ValueMode.Single]: () => state.comboboxPropsRef.current.compare(data.value, value),
|
||||
})
|
||||
let internalOptionRef = useRef<HTMLLIElement | null>(null)
|
||||
let bag = useRef<ComboboxOptionDataRef['current']>({ disabled, value, domRef: internalOptionRef })
|
||||
|
||||
@@ -162,6 +162,108 @@ describe('Rendering', () => {
|
||||
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Listbox value={options[1]} onChange={console.log}>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
{options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
)
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Listbox value={{ id: 2, name: 'Bob' }} onChange={console.log} by="id">
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
{options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
)
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<Listbox
|
||||
value={{ id: 2, name: 'Bob' }}
|
||||
onChange={console.log}
|
||||
by={(a, z) => a.id === z.id}
|
||||
>
|
||||
<Listbox.Button>Trigger</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
{options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
)
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Listbox.Label', () => {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { objectToFormEntries } from '../../utils/form'
|
||||
import { getOwnerDocument } from '../../utils/owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
enum ListboxStates {
|
||||
Open,
|
||||
@@ -65,7 +66,12 @@ interface StateDefinition {
|
||||
|
||||
orientation: 'horizontal' | 'vertical'
|
||||
|
||||
propsRef: MutableRefObject<{ value: unknown; onChange(value: unknown): void; mode: ValueMode }>
|
||||
propsRef: MutableRefObject<{
|
||||
value: unknown
|
||||
onChange(value: unknown): void
|
||||
mode: ValueMode
|
||||
compare(a: unknown, z: unknown): boolean
|
||||
}>
|
||||
labelRef: MutableRefObject<HTMLLabelElement | null>
|
||||
buttonRef: MutableRefObject<HTMLButtonElement | null>
|
||||
optionsRef: MutableRefObject<HTMLUListElement | null>
|
||||
@@ -154,12 +160,13 @@ let reducers: {
|
||||
|
||||
// Check if we have a selected value that we can make active
|
||||
let activeOptionIndex = state.activeOptionIndex
|
||||
let { value, mode } = state.propsRef.current
|
||||
let { value, mode, compare } = state.propsRef.current
|
||||
let optionIdx = state.options.findIndex((option) => {
|
||||
let optionValue = option.dataRef.current.value
|
||||
let selected = match(mode, {
|
||||
[ValueMode.Multi]: () => (value as unknown[]).includes(optionValue),
|
||||
[ValueMode.Single]: () => value === optionValue,
|
||||
[ValueMode.Multi]: () =>
|
||||
(value as unknown[]).some((option) => compare(option, optionValue)),
|
||||
[ValueMode.Single]: () => compare(value, optionValue),
|
||||
})
|
||||
|
||||
return selected
|
||||
@@ -243,11 +250,12 @@ let reducers: {
|
||||
|
||||
// Check if we need to make the newly registered option active.
|
||||
if (state.activeOptionIndex === null) {
|
||||
let { value, mode } = state.propsRef.current
|
||||
let { value, mode, compare } = state.propsRef.current
|
||||
let optionValue = action.dataRef.current.value
|
||||
let selected = match(mode, {
|
||||
[ValueMode.Multi]: () => (value as unknown[]).includes(optionValue),
|
||||
[ValueMode.Single]: () => value === optionValue,
|
||||
[ValueMode.Multi]: () =>
|
||||
(value as unknown[]).some((option) => compare(option, optionValue)),
|
||||
[ValueMode.Single]: () => compare(value, optionValue),
|
||||
})
|
||||
if (selected) {
|
||||
adjustedState.activeOptionIndex = adjustedState.options.indexOf(option)
|
||||
@@ -304,10 +312,11 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
|
||||
props: Props<
|
||||
TTag,
|
||||
ListboxRenderPropArg,
|
||||
'value' | 'onChange' | 'disabled' | 'horizontal' | 'name' | 'multiple'
|
||||
'value' | 'onChange' | 'disabled' | 'horizontal' | 'name' | 'multiple' | 'by'
|
||||
> & {
|
||||
value: TType
|
||||
onChange(value: TType): void
|
||||
by?: (keyof TType & string) | ((a: TType, z: TType) => boolean)
|
||||
disabled?: boolean
|
||||
horizontal?: boolean
|
||||
name?: string
|
||||
@@ -319,6 +328,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
by = (a, z) => a === z,
|
||||
disabled = false,
|
||||
horizontal = false,
|
||||
multiple = false,
|
||||
@@ -330,7 +340,19 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
|
||||
let reducerBag = useReducer(stateReducer, {
|
||||
listboxState: ListboxStates.Closed,
|
||||
propsRef: {
|
||||
current: { value, onChange, mode: multiple ? ValueMode.Multi : ValueMode.Single },
|
||||
current: {
|
||||
value,
|
||||
onChange,
|
||||
mode: multiple ? ValueMode.Multi : ValueMode.Single,
|
||||
compare: useEvent(
|
||||
typeof by === 'string'
|
||||
? (a: TType, z: TType) => {
|
||||
let property = by as unknown as keyof TType
|
||||
return a[property] === z[property]
|
||||
}
|
||||
: by
|
||||
),
|
||||
},
|
||||
},
|
||||
labelRef: createRef(),
|
||||
buttonRef: createRef(),
|
||||
@@ -770,9 +792,12 @@ let Option = forwardRefWithAs(function Option<
|
||||
let id = `headlessui-listbox-option-${useId()}`
|
||||
let active =
|
||||
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false
|
||||
|
||||
let { value: optionValue, compare } = state.propsRef.current
|
||||
|
||||
let selected = match(state.propsRef.current.mode, {
|
||||
[ValueMode.Multi]: () => (state.propsRef.current.value as TType[]).includes(value),
|
||||
[ValueMode.Single]: () => state.propsRef.current.value === value,
|
||||
[ValueMode.Multi]: () => (optionValue as TType[]).some((option) => compare(option, value)),
|
||||
[ValueMode.Single]: () => compare(optionValue, value),
|
||||
})
|
||||
|
||||
let internalOptionRef = useRef<HTMLLIElement | null>(null)
|
||||
|
||||
@@ -321,6 +321,93 @@ describe('Rendering', () => {
|
||||
assertActiveElement(getByText('Option 3'))
|
||||
})
|
||||
)
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<RadioGroup value={options[1]} onChange={console.log}>
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<RadioGroup value={{ id: 2, name: 'Bob' }} onChange={console.log} by="id">
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
<RadioGroup
|
||||
value={{ id: 2, name: 'Bob' }}
|
||||
onChange={console.log}
|
||||
by={(a, z) => a.id === z.id}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.id}
|
||||
value={option}
|
||||
className={(info) => JSON.stringify(info)}
|
||||
>
|
||||
{option.name}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveAttribute(
|
||||
'class',
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Keyboard interactions', () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { attemptSubmit, objectToFormEntries } from '../../utils/form'
|
||||
import { getOwnerDocument } from '../../utils/owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
interface Option {
|
||||
id: string
|
||||
@@ -82,6 +83,7 @@ let RadioGroupContext = createContext<{
|
||||
firstOption?: Option
|
||||
containsCheckedOption: boolean
|
||||
disabled: boolean
|
||||
compare(a: unknown, z: unknown): boolean
|
||||
} | null>(null)
|
||||
RadioGroupContext.displayName = 'RadioGroupContext'
|
||||
|
||||
@@ -112,16 +114,25 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
props: Props<
|
||||
TTag,
|
||||
RadioGroupRenderPropArg,
|
||||
RadioGroupPropsWeControl | 'value' | 'onChange' | 'disabled' | 'name'
|
||||
RadioGroupPropsWeControl | 'value' | 'onChange' | 'disabled' | 'name' | 'by'
|
||||
> & {
|
||||
value: TType
|
||||
onChange(value: TType): void
|
||||
by?: (keyof TType & string) | ((a: TType, z: TType) => boolean)
|
||||
disabled?: boolean
|
||||
name?: string
|
||||
},
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let { value, name, onChange, disabled = false, ...theirProps } = props
|
||||
let { value, name, onChange, by = (a, z) => a === z, disabled = false, ...theirProps } = props
|
||||
let compare = useEvent(
|
||||
typeof by === 'string'
|
||||
? (a: TType, z: TType) => {
|
||||
let property = by as unknown as keyof TType
|
||||
return a[property] === z[property]
|
||||
}
|
||||
: by
|
||||
)
|
||||
let [{ options }, dispatch] = useReducer(stateReducer, {
|
||||
options: [],
|
||||
} as StateDefinition)
|
||||
@@ -140,16 +151,17 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
[options]
|
||||
)
|
||||
let containsCheckedOption = useMemo(
|
||||
() => options.some((option) => option.propsRef.current.value === value),
|
||||
() => options.some((option) => compare(option.propsRef.current.value as TType, value)),
|
||||
[options, value]
|
||||
)
|
||||
|
||||
let triggerChange = useCallback(
|
||||
(nextValue) => {
|
||||
if (disabled) return false
|
||||
if (nextValue === value) return false
|
||||
let nextOption = options.find((option) => option.propsRef.current.value === nextValue)
|
||||
?.propsRef.current
|
||||
if (compare(nextValue, value)) return false
|
||||
let nextOption = options.find((option) =>
|
||||
compare(option.propsRef.current.value as TType, nextValue)
|
||||
)?.propsRef.current
|
||||
if (nextOption?.disabled) return false
|
||||
|
||||
onChange(nextValue)
|
||||
@@ -251,8 +263,9 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
change: triggerChange,
|
||||
disabled,
|
||||
value,
|
||||
compare,
|
||||
}),
|
||||
[registerOption, firstOption, containsCheckedOption, triggerChange, disabled, value]
|
||||
[registerOption, firstOption, containsCheckedOption, triggerChange, disabled, value, compare]
|
||||
)
|
||||
|
||||
let ourProps = {
|
||||
@@ -357,6 +370,7 @@ let Option = forwardRefWithAs(function Option<
|
||||
firstOption,
|
||||
containsCheckedOption,
|
||||
value: radioGroupValue,
|
||||
compare,
|
||||
} = useRadioGroupContext('RadioGroup.Option')
|
||||
|
||||
useIsoMorphicEffect(
|
||||
@@ -377,7 +391,7 @@ let Option = forwardRefWithAs(function Option<
|
||||
let isFirstOption = firstOption?.id === id
|
||||
let isDisabled = radioGroupDisabled || disabled
|
||||
|
||||
let checked = radioGroupValue === value
|
||||
let checked = compare(radioGroupValue as TType, value)
|
||||
let ourProps = {
|
||||
ref: optionRef,
|
||||
id,
|
||||
|
||||
@@ -225,6 +225,113 @@ describe('Rendering', () => {
|
||||
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value">
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ComboboxOption
|
||||
>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref(options[1])
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value" by="id">
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ComboboxOption
|
||||
>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value" :by="compare">
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ComboboxOption
|
||||
>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value, compare: (a: any, z: any) => a.id === z.id }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
let bob = getComboboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Combobox.Input', () => {
|
||||
|
||||
@@ -35,6 +35,10 @@ import { useOutsideClick } from '../../hooks/use-outside-click'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { objectToFormEntries } from '../../utils/form'
|
||||
|
||||
function defaultComparator<T>(a: T, z: T): boolean {
|
||||
return a === z
|
||||
}
|
||||
|
||||
enum ComboboxStates {
|
||||
Open,
|
||||
Closed,
|
||||
@@ -63,6 +67,8 @@ type StateDefinition = {
|
||||
mode: ComputedRef<ValueMode>
|
||||
nullable: ComputedRef<boolean>
|
||||
|
||||
compare: (a: unknown, z: unknown) => boolean
|
||||
|
||||
inputPropsRef: Ref<{ displayValue?: (item: unknown) => string }>
|
||||
optionsPropsRef: Ref<{ static: boolean; hold: boolean }>
|
||||
|
||||
@@ -110,6 +116,7 @@ export let Combobox = defineComponent({
|
||||
props: {
|
||||
as: { type: [Object, String], default: 'template' },
|
||||
disabled: { type: [Boolean], default: false },
|
||||
by: { type: [String, Function], default: () => defaultComparator },
|
||||
modelValue: { type: [Object, String, Number, Boolean] },
|
||||
name: { type: String },
|
||||
nullable: { type: Boolean, default: false },
|
||||
@@ -172,6 +179,13 @@ export let Combobox = defineComponent({
|
||||
comboboxState,
|
||||
value,
|
||||
mode,
|
||||
compare(a: any, z: any) {
|
||||
if (typeof props.by === 'string') {
|
||||
let property = props.by as unknown as any
|
||||
return a[property] === z[property]
|
||||
}
|
||||
return props.by(a, z)
|
||||
},
|
||||
nullable,
|
||||
inputRef,
|
||||
labelRef,
|
||||
@@ -217,9 +231,11 @@ export let Combobox = defineComponent({
|
||||
let optionIdx = options.value.findIndex((option) => {
|
||||
let optionValue = toRaw(option.dataRef.value)
|
||||
let selected = match(mode.value, {
|
||||
[ValueMode.Single]: () => toRaw(api.value.value) === toRaw(optionValue),
|
||||
[ValueMode.Single]: () => api.compare(toRaw(api.value.value), toRaw(optionValue)),
|
||||
[ValueMode.Multi]: () =>
|
||||
(toRaw(api.value.value) as unknown[]).includes(toRaw(optionValue)),
|
||||
(toRaw(api.value.value) as unknown[]).some((value) =>
|
||||
api.compare(toRaw(value), toRaw(optionValue))
|
||||
),
|
||||
})
|
||||
|
||||
return selected
|
||||
@@ -350,9 +366,11 @@ export let Combobox = defineComponent({
|
||||
if (activeOptionIndex.value === null) {
|
||||
let optionValue = (dataRef.value as any).value
|
||||
let selected = match(mode.value, {
|
||||
[ValueMode.Single]: () => toRaw(api.value.value) === toRaw(optionValue),
|
||||
[ValueMode.Single]: () => api.compare(toRaw(api.value.value), toRaw(optionValue)),
|
||||
[ValueMode.Multi]: () =>
|
||||
(toRaw(api.value.value) as unknown[]).includes(toRaw(optionValue)),
|
||||
(toRaw(api.value.value) as unknown[]).some((value) =>
|
||||
api.compare(toRaw(value), toRaw(optionValue))
|
||||
),
|
||||
})
|
||||
|
||||
if (selected) {
|
||||
@@ -449,7 +467,7 @@ export let Combobox = defineComponent({
|
||||
render({
|
||||
props: {
|
||||
...attrs,
|
||||
...omit(incomingProps, ['nullable', 'multiple', 'onUpdate:modelValue']),
|
||||
...omit(incomingProps, ['nullable', 'multiple', 'onUpdate:modelValue', 'by']),
|
||||
},
|
||||
slot,
|
||||
slots,
|
||||
@@ -859,8 +877,11 @@ export let ComboboxOption = defineComponent({
|
||||
|
||||
let selected = computed(() =>
|
||||
match(api.mode.value, {
|
||||
[ValueMode.Single]: () => toRaw(api.value.value) === toRaw(props.value),
|
||||
[ValueMode.Multi]: () => (toRaw(api.value.value) as unknown[]).includes(toRaw(props.value)),
|
||||
[ValueMode.Single]: () => api.compare(toRaw(api.value.value), toRaw(props.value)),
|
||||
[ValueMode.Multi]: () =>
|
||||
(toRaw(api.value.value) as unknown[]).some((value) =>
|
||||
api.compare(toRaw(value), toRaw(props.value))
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -198,6 +198,113 @@ describe('Rendering', () => {
|
||||
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value">
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ListboxOption
|
||||
>
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref(options[1])
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value" by="id">
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ListboxOption
|
||||
>
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Listbox v-model="value" :by="compare">
|
||||
<ListboxButton>Trigger</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</ListboxOption
|
||||
>
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value, compare: (a: any, z: any) => a.id === z.id }
|
||||
},
|
||||
})
|
||||
|
||||
await click(getListboxButton())
|
||||
|
||||
let bob = getListboxOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ active: true, selected: true, disabled: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ListboxLabel', () => {
|
||||
|
||||
@@ -33,6 +33,10 @@ import { useOutsideClick } from '../../hooks/use-outside-click'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { objectToFormEntries } from '../../utils/form'
|
||||
|
||||
function defaultComparator<T>(a: T, z: T): boolean {
|
||||
return a === z
|
||||
}
|
||||
|
||||
enum ListboxStates {
|
||||
Open,
|
||||
Closed,
|
||||
@@ -67,6 +71,8 @@ type StateDefinition = {
|
||||
|
||||
mode: ComputedRef<ValueMode>
|
||||
|
||||
compare: (a: unknown, z: unknown) => boolean
|
||||
|
||||
labelRef: Ref<HTMLLabelElement | null>
|
||||
buttonRef: Ref<HTMLButtonElement | null>
|
||||
optionsRef: Ref<HTMLDivElement | null>
|
||||
@@ -110,6 +116,7 @@ export let Listbox = defineComponent({
|
||||
props: {
|
||||
as: { type: [Object, String], default: 'template' },
|
||||
disabled: { type: [Boolean], default: false },
|
||||
by: { type: [String, Function], default: () => defaultComparator },
|
||||
horizontal: { type: [Boolean], default: false },
|
||||
modelValue: { type: [Object, String, Number, Boolean] },
|
||||
name: { type: String, optional: true },
|
||||
@@ -164,6 +171,13 @@ export let Listbox = defineComponent({
|
||||
listboxState,
|
||||
value,
|
||||
mode,
|
||||
compare(a: any, z: any) {
|
||||
if (typeof props.by === 'string') {
|
||||
let property = props.by as unknown as any
|
||||
return a[property] === z[property]
|
||||
}
|
||||
return props.by(a, z)
|
||||
},
|
||||
orientation: computed(() => (props.horizontal ? 'horizontal' : 'vertical')),
|
||||
labelRef,
|
||||
buttonRef,
|
||||
@@ -269,7 +283,7 @@ export let Listbox = defineComponent({
|
||||
let copy = toRaw(api.value.value as unknown[]).slice()
|
||||
let raw = toRaw(value)
|
||||
|
||||
let idx = copy.indexOf(raw)
|
||||
let idx = copy.findIndex((value) => api.compare(raw, toRaw(value)))
|
||||
if (idx === -1) {
|
||||
copy.push(raw)
|
||||
} else {
|
||||
@@ -332,7 +346,7 @@ export let Listbox = defineComponent({
|
||||
render({
|
||||
props: {
|
||||
...attrs,
|
||||
...omit(incomingProps, ['onUpdate:modelValue', 'horizontal', 'multiple']),
|
||||
...omit(incomingProps, ['onUpdate:modelValue', 'horizontal', 'multiple', 'by']),
|
||||
},
|
||||
slot,
|
||||
slots,
|
||||
@@ -625,8 +639,11 @@ export let ListboxOption = defineComponent({
|
||||
|
||||
let selected = computed(() =>
|
||||
match(api.mode.value, {
|
||||
[ValueMode.Single]: () => toRaw(api.value.value) === toRaw(props.value),
|
||||
[ValueMode.Multi]: () => (toRaw(api.value.value) as unknown[]).includes(toRaw(props.value)),
|
||||
[ValueMode.Single]: () => api.compare(toRaw(api.value.value), toRaw(props.value)),
|
||||
[ValueMode.Multi]: () =>
|
||||
(toRaw(api.value.value) as unknown[]).some((value) =>
|
||||
api.compare(toRaw(value), toRaw(props.value))
|
||||
),
|
||||
})
|
||||
)
|
||||
let isFirstSelected = computed(() => {
|
||||
@@ -635,8 +652,9 @@ export let ListboxOption = defineComponent({
|
||||
let currentValues = toRaw(api.value.value) as unknown[]
|
||||
|
||||
return (
|
||||
api.options.value.find((option) => currentValues.includes(option.dataRef.value))?.id ===
|
||||
id
|
||||
api.options.value.find((option) =>
|
||||
currentValues.some((value) => api.compare(toRaw(value), toRaw(option.dataRef.value)))
|
||||
)?.id === id
|
||||
)
|
||||
},
|
||||
[ValueMode.Single]: () => selected.value,
|
||||
|
||||
@@ -504,6 +504,101 @@ describe('Rendering', () => {
|
||||
// Verify that the third radio group option is active
|
||||
assertActiveElement(getByText('Option 3'))
|
||||
})
|
||||
|
||||
describe('Equality', () => {
|
||||
let options = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]
|
||||
|
||||
it(
|
||||
'should use object equality by default',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<RadioGroup v-model="value">
|
||||
<RadioGroupButton>Trigger</RadioGroupButton>
|
||||
<RadioGroupOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</RadioGroupOption
|
||||
>
|
||||
</RadioGroup>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref(options[1])
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a field',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<RadioGroup v-model="value" by="id">
|
||||
<RadioGroupButton>Trigger</RadioGroupButton>
|
||||
<RadioGroupOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</RadioGroupOption
|
||||
>
|
||||
</RadioGroup>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value }
|
||||
},
|
||||
})
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to compare objects by a comparator function',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<RadioGroup v-model="value" :by="compare">
|
||||
<RadioGroupButton>Trigger</RadioGroupButton>
|
||||
<RadioGroupOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option"
|
||||
v-slot="data"
|
||||
>{{ JSON.stringify(data) }}</RadioGroupOption
|
||||
>
|
||||
</RadioGroup>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref({ id: 2, name: 'Bob' })
|
||||
return { options, value, compare: (a: any, z: any) => a.id === z.id }
|
||||
},
|
||||
})
|
||||
|
||||
let bob = getRadioGroupOptions()[1]
|
||||
expect(bob).toHaveTextContent(
|
||||
JSON.stringify({ checked: true, disabled: false, active: false })
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Keyboard interactions', () => {
|
||||
|
||||
@@ -27,6 +27,10 @@ import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { attemptSubmit, objectToFormEntries } from '../../utils/form'
|
||||
import { getOwnerDocument } from '../../utils/owner'
|
||||
|
||||
function defaultComparator<T>(a: T, z: T): boolean {
|
||||
return a === z
|
||||
}
|
||||
|
||||
interface Option {
|
||||
id: string
|
||||
element: Ref<HTMLElement | null>
|
||||
@@ -41,6 +45,8 @@ interface StateDefinition {
|
||||
firstOption: Ref<Option | undefined>
|
||||
containsCheckedOption: Ref<boolean>
|
||||
|
||||
compare(a: unknown, z: unknown): boolean
|
||||
|
||||
// State mutators
|
||||
change(nextValue: unknown): boolean
|
||||
registerOption(action: Option): void
|
||||
@@ -69,6 +75,7 @@ export let RadioGroup = defineComponent({
|
||||
props: {
|
||||
as: { type: [Object, String], default: 'div' },
|
||||
disabled: { type: [Boolean], default: false },
|
||||
by: { type: [String, Function], default: () => defaultComparator },
|
||||
modelValue: { type: [Object, String, Number, Boolean] },
|
||||
name: { type: String, optional: true },
|
||||
},
|
||||
@@ -94,13 +101,22 @@ export let RadioGroup = defineComponent({
|
||||
})
|
||||
),
|
||||
containsCheckedOption: computed(() =>
|
||||
options.value.some((option) => toRaw(option.propsRef.value) === toRaw(props.modelValue))
|
||||
options.value.some((option) =>
|
||||
api.compare(toRaw(option.propsRef.value), toRaw(props.modelValue))
|
||||
)
|
||||
),
|
||||
compare(a: any, z: any) {
|
||||
if (typeof props.by === 'string') {
|
||||
let property = props.by as unknown as any
|
||||
return a[property] === z[property]
|
||||
}
|
||||
return props.by(a, z)
|
||||
},
|
||||
change(nextValue: unknown) {
|
||||
if (props.disabled) return false
|
||||
if (value.value === nextValue) return false
|
||||
let nextOption = options.value.find(
|
||||
(option) => toRaw(option.propsRef.value) === toRaw(nextValue)
|
||||
if (api.compare(toRaw(value.value), toRaw(nextValue))) return false
|
||||
let nextOption = options.value.find((option) =>
|
||||
api.compare(toRaw(option.propsRef.value), toRaw(nextValue))
|
||||
)?.propsRef
|
||||
if (nextOption?.disabled) return false
|
||||
emit('update:modelValue', nextValue)
|
||||
@@ -267,7 +283,7 @@ export let RadioGroupOption = defineComponent({
|
||||
|
||||
let isFirstOption = computed(() => api.firstOption.value?.id === id)
|
||||
let disabled = computed(() => api.disabled.value || props.disabled)
|
||||
let checked = computed(() => toRaw(api.value.value) === toRaw(props.value))
|
||||
let checked = computed(() => api.compare(toRaw(api.value.value), toRaw(props.value)))
|
||||
let tabIndex = computed(() => {
|
||||
if (disabled.value) return -1
|
||||
if (checked.value) return 0
|
||||
|
||||
Reference in New Issue
Block a user