diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 0692d1e..8284245 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -171,7 +171,7 @@ describe('Rendering', () => { }) ) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 1e86ad0..df98d32 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -311,10 +311,11 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< props: Props< TTag, ComboboxRenderPropArg, - '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 @@ -327,6 +328,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< name, value, onChange: theirOnChange, + by = (a, z) => a === z, disabled = false, __demoMode = false, nullable = false, @@ -352,7 +354,14 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< let buttonRef = useRef<_Data['buttonRef']['current']>(null) let optionsRef = useRef<_Data['optionsRef']['current']>(null) - let compare = useEvent((a, z) => a === z) + 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 isSelected: (value: TType) => boolean = useCallback( (compareValue) => diff --git a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx index 0c9c487..8fe7eb1 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx @@ -163,7 +163,7 @@ describe('Rendering', () => { }) ) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index a4803b9..b664331 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -311,10 +311,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 @@ -326,6 +327,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox< value, name, onChange, + by = (a, z) => a === z, disabled = false, horizontal = false, multiple = false, @@ -341,7 +343,14 @@ let ListboxRoot = forwardRefWithAs(function Listbox< value, onChange, mode: multiple ? ValueMode.Multi : ValueMode.Single, - compare: useEvent((a, z) => a === z), + 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(), diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx index cf75a34..ca45345 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx @@ -322,7 +322,7 @@ describe('Rendering', () => { }) ) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx index d668fb6..cd848b7 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx @@ -113,17 +113,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 ) { - let { value, name, onChange, disabled = false, ...theirProps } = props - let compare = useEvent((a, z) => a === z) + 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 [state, dispatch] = useReducer(stateReducer, { options: [] } as StateDefinition) let options = state.options as unknown as Option[] let [labelledby, LabelProvider] = useLabels() diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts index 6c942f6..36369ea 100644 --- a/packages/@headlessui-react/src/utils/render.ts +++ b/packages/@headlessui-react/src/utils/render.ts @@ -126,20 +126,20 @@ function _render( } let dataAttributes: Record = {} - // if (slot) { - // let exposeState = false - // let states = [] - // for (let [k, v] of Object.entries(slot)) { - // if (typeof v === 'boolean') { - // exposeState = true - // } - // if (v === true) { - // states.push(k) - // } - // } - // - // if (exposeState) dataAttributes[`data-headlessui-state`] = states.join(' ') - // } + if (slot) { + let exposeState = false + let states = [] + for (let [k, v] of Object.entries(slot)) { + if (typeof v === 'boolean') { + exposeState = true + } + if (v === true) { + states.push(k) + } + } + + if (exposeState) dataAttributes[`data-headlessui-state`] = states.join(' ') + } if (Component === Fragment) { if (Object.keys(compact(rest)).length > 0) { diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index 50436cc..c579fb0 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -226,7 +226,7 @@ describe('Rendering', () => { }) ) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index e3ace3f..5e70ac2 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -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(a: T, z: T): boolean { + return a === z +} + enum ComboboxStates { Open, Closed, @@ -112,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 }, @@ -175,7 +180,11 @@ export let Combobox = defineComponent({ value, mode, compare(a: any, z: any) { - return a === z + 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, @@ -458,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, diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx index 1766504..7fa2814 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx @@ -199,7 +199,7 @@ describe('Rendering', () => { }) ) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 6d950c0..b3a123a 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -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(a: T, z: T): boolean { + return a === z +} + enum ListboxStates { Open, Closed, @@ -112,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 }, @@ -167,7 +172,11 @@ export let Listbox = defineComponent({ value, mode, compare(a: any, z: any) { - return a === z + 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, @@ -337,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, diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts index 88365f6..e6aca27 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts @@ -505,7 +505,7 @@ describe('Rendering', () => { assertActiveElement(getByText('Option 3')) }) - describe.skip('Equality', () => { + describe('Equality', () => { let options = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts index 37a933a..8b4204c 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -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(a: T, z: T): boolean { + return a === z +} + interface Option { id: string element: Ref @@ -71,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 }, }, @@ -101,7 +106,11 @@ export let RadioGroup = defineComponent({ ) ), compare(a: any, z: any) { - return a === z + 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 diff --git a/packages/@headlessui-vue/src/utils/render.ts b/packages/@headlessui-vue/src/utils/render.ts index ce154ef..10c7426 100644 --- a/packages/@headlessui-vue/src/utils/render.ts +++ b/packages/@headlessui-vue/src/utils/render.ts @@ -86,20 +86,20 @@ function _render({ let children = slots.default?.(slot) let dataAttributes: Record = {} - // if (slot) { - // let exposeState = false - // let states = [] - // for (let [k, v] of Object.entries(slot)) { - // if (typeof v === 'boolean') { - // exposeState = true - // } - // if (v === true) { - // states.push(k) - // } - // } - // - // if (exposeState) dataAttributes[`data-headlessui-state`] = states.join(' ') - // } + if (slot) { + let exposeState = false + let states = [] + for (let [k, v] of Object.entries(slot)) { + if (typeof v === 'boolean') { + exposeState = true + } + if (v === true) { + states.push(k) + } + } + + if (exposeState) dataAttributes[`data-headlessui-state`] = states.join(' ') + } if (as === 'template') { if (Object.keys(incomingProps).length > 0 || Object.keys(attrs).length > 0) {