diff --git a/packages/@headlessui-react/src/test-utils/interactions.ts b/packages/@headlessui-react/src/test-utils/interactions.ts index 784d021..a742a94 100644 --- a/packages/@headlessui-react/src/test-utils/interactions.ts +++ b/packages/@headlessui-react/src/test-utils/interactions.ts @@ -234,6 +234,7 @@ export async function click( ) { try { if (element === null) return expect(element).not.toBe(null) + if (element.disabled) return let options = { button } diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index b9a8057..9df9b89 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix getting Vue dom elements ([#1610](https://github.com/tailwindlabs/headlessui/pull/1610)) - Ensure `CMD`+`Backspace` works in nullable mode for `Combobox` component ([#1617](https://github.com/tailwindlabs/headlessui/pull/1617)) +- Properly merge incoming props with own props ([#1651](https://github.com/tailwindlabs/headlessui/pull/1651)) ## [1.6.5] - 2022-06-20 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index 77c9eb3..fafa583 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -4832,7 +4832,7 @@ describe('Mouse interactions', () => { Trigger alice - bob + bob charlie @@ -4849,7 +4849,7 @@ describe('Mouse interactions', () => { // We should not be able to focus the first option await focus(options[1]) - assertNoActiveComboboxOption() + assertNotActiveComboboxOption(options[1]) }) ) diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 18d0bcd..df95ff8 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -439,7 +439,7 @@ export let Combobox = defineComponent({ ) return () => { - let { name, modelValue, disabled, ...incomingProps } = props + let { name, modelValue, disabled, ...theirProps } = props let slot = { open: comboboxState.value === ComboboxStates.Open, disabled, @@ -466,10 +466,11 @@ export let Combobox = defineComponent({ ) : []), render({ - props: { + theirProps: { ...attrs, - ...omit(incomingProps, ['nullable', 'multiple', 'onUpdate:modelValue', 'by']), + ...omit(theirProps, ['nullable', 'multiple', 'onUpdate:modelValue', 'by']), }, + ourProps: {}, slot, slots, attrs, @@ -500,9 +501,11 @@ export let ComboboxLabel = defineComponent({ } let ourProps = { id, ref: api.labelRef, onClick: handleClick } + let theirProps = props return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -601,9 +604,11 @@ export let ComboboxButton = defineComponent({ onKeydown: handleKeydown, onClick: handleClick, } + let theirProps = props return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -755,10 +760,11 @@ export let ComboboxInput = defineComponent({ tabIndex: 0, ref: api.inputRef, } - let incomingProps = omit(props, ['displayValue']) + let theirProps = omit(props, ['displayValue']) return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -827,10 +833,11 @@ export let ComboboxOptions = defineComponent({ ref: api.optionsRef, role: 'listbox', } - let incomingProps = omit(props, ['hold']) + let theirProps = omit(props, ['hold']) return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -937,8 +944,11 @@ export let ComboboxOption = defineComponent({ onMouseleave: handleLeave, } + let theirProps = props + return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/description/description.ts b/packages/@headlessui-vue/src/components/description/description.ts index 65ab4d0..99f1dc7 100644 --- a/packages/@headlessui-vue/src/components/description/description.ts +++ b/packages/@headlessui-vue/src/components/description/description.ts @@ -78,7 +78,7 @@ export let Description = defineComponent({ return () => { let { name = 'Description', slot = ref({}), props = {} } = context - let incomingProps = myProps + let theirProps = myProps let ourProps = { ...Object.entries(props).reduce( (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), @@ -88,7 +88,8 @@ export let Description = defineComponent({ } return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot: slot.value, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 90181f6..76f82fe 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -280,7 +280,7 @@ export let Dialog = defineComponent({ 'aria-labelledby': titleId.value, 'aria-describedby': describedby.value, } - let { open: _, initialFocus, ...incomingProps } = props + let { open: _, initialFocus, ...theirProps } = props let slot = { open: dialogState.value === DialogStates.Open } @@ -302,7 +302,8 @@ export let Dialog = defineComponent({ }, () => render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -344,10 +345,11 @@ export let DialogOverlay = defineComponent({ 'aria-hidden': true, onClick: handleClick, } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot: { open: api.dialogState.value === DialogStates.Open }, attrs, slots, @@ -381,7 +383,7 @@ export let DialogBackdrop = defineComponent({ }) return () => { - let incomingProps = props + let theirProps = props let ourProps = { id, ref: internalBackdropRef, @@ -391,7 +393,8 @@ export let DialogBackdrop = defineComponent({ return h(ForcePortalRoot, { force: true }, () => h(Portal, () => render({ - props: { ...attrs, ...incomingProps, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...theirProps }, slot: { open: api.dialogState.value === DialogStates.Open }, attrs, slots, @@ -426,10 +429,11 @@ export let DialogPanel = defineComponent({ ref: api.panelRef, onClick: handleClick, } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot: { open: api.dialogState.value === DialogStates.Open }, attrs, slots, @@ -457,10 +461,11 @@ export let DialogTitle = defineComponent({ return () => { let ourProps = { id } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot: { open: api.dialogState.value === DialogStates.Open }, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts index 46adf52..49f8a4d 100644 --- a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts +++ b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts @@ -118,9 +118,16 @@ export let Disclosure = defineComponent({ ) return () => { - let { defaultOpen: _, ...incomingProps } = props + let { defaultOpen: _, ...theirProps } = props let slot = { open: disclosureState.value === DisclosureStates.Open, close: api.close } - return render({ props: incomingProps, slot, slots, attrs, name: 'Disclosure' }) + return render({ + theirProps, + ourProps: {}, + slot, + slots, + attrs, + name: 'Disclosure', + }) } }, }) @@ -223,7 +230,8 @@ export let DisclosureButton = defineComponent({ } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, @@ -263,7 +271,8 @@ export let DisclosurePanel = defineComponent({ let ourProps = { id: api.panelId, ref: api.panel } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts index b488b4e..d9d5b34 100644 --- a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts +++ b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts @@ -103,7 +103,7 @@ export let FocusTrap = Object.assign( return () => { let slot = {} let ourProps = { 'data-hi': 'container', ref: container } - let { features, initialFocus, containers: _containers, ...incomingProps } = props + let { features, initialFocus, containers: _containers, ...theirProps } = props return h(Fragment, [ Boolean(features & Features.TabLock) && @@ -114,7 +114,8 @@ export let FocusTrap = Object.assign( features: HiddenFeatures.Focusable, }), render({ - props: { ...attrs, ...incomingProps, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...theirProps }, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/label/label.ts b/packages/@headlessui-vue/src/components/label/label.ts index 623b75f..7edb1b9 100644 --- a/packages/@headlessui-vue/src/components/label/label.ts +++ b/packages/@headlessui-vue/src/components/label/label.ts @@ -77,7 +77,7 @@ export let Label = defineComponent({ return () => { let { name = 'Label', slot = {}, props = {} } = context - let { passive, ...incomingProps } = myProps + let { passive, ...theirProps } = myProps let ourProps = { ...Object.entries(props).reduce( (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), @@ -85,14 +85,19 @@ export let Label = defineComponent({ ), id, } - let allProps = { ...incomingProps, ...ourProps } - // @ts-expect-error props are dynamic via context, some components will - // provide an onClick then we can delete it. - if (passive) delete allProps['onClick'] + if (passive) { + // @ts-expect-error props are dynamic via context, some components will provide an onClick + // then we can delete it. + delete ourProps['onClick'] + // @ts-expect-error props are dynamic via context, some components will provide an onClick + // then we can delete it. + delete theirProps['onClick'] + } return render({ - props: allProps, + ourProps, + theirProps, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 5bd9062..4c08c10 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -323,7 +323,7 @@ export let Listbox = defineComponent({ ) return () => { - let { name, modelValue, disabled, ...incomingProps } = props + let { name, modelValue, disabled, ...theirProps } = props let slot = { open: listboxState.value === ListboxStates.Open, disabled } @@ -346,9 +346,10 @@ export let Listbox = defineComponent({ ) : []), render({ - props: { + ourProps: {}, + theirProps: { ...attrs, - ...omit(incomingProps, ['onUpdate:modelValue', 'horizontal', 'multiple', 'by']), + ...omit(theirProps, ['onUpdate:modelValue', 'horizontal', 'multiple', 'by']), }, slot, slots, @@ -381,7 +382,8 @@ export let ListboxLabel = defineComponent({ let ourProps = { id, ref: api.labelRef, onClick: handleClick } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, @@ -480,7 +482,8 @@ export let ListboxButton = defineComponent({ } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, @@ -604,10 +607,11 @@ export let ListboxOptions = defineComponent({ tabIndex: 0, ref: api.optionsRef, } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -753,7 +757,8 @@ export let ListboxOption = defineComponent({ } return render({ - props: { ...omit(props, ['value', 'disabled']), ...ourProps }, + ourProps, + theirProps: omit(props, ['value', 'disabled']), slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/menu/menu.test.tsx b/packages/@headlessui-vue/src/components/menu/menu.test.tsx index 4ef874c..201fa2b 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-vue/src/components/menu/menu.test.tsx @@ -317,16 +317,16 @@ describe('Rendering', () => { '', 'The current component is rendering a "template".', 'However we need to passthrough the following props:', - ' - disabled', - ' - ref', - ' - id', - ' - type', - ' - aria-haspopup', ' - aria-controls', ' - aria-expanded', + ' - aria-haspopup', + ' - disabled', + ' - id', + ' - onClick', ' - onKeydown', ' - onKeyup', - ' - onClick', + ' - ref', + ' - type', '', 'You can apply a few solutions:', ' - Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".', @@ -518,9 +518,9 @@ describe('Rendering', () => { ' - id', ' - onKeydown', ' - onKeyup', + ' - ref', ' - role', ' - tabIndex', - ' - ref', '', 'You can apply a few solutions:', ' - Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".', @@ -680,18 +680,18 @@ describe('Rendering', () => { '', 'The current component is rendering a "template".', 'However we need to passthrough the following props:', + ' - aria-disabled', ' - disabled', ' - id', + ' - onClick', + ' - onFocus', + ' - onMouseleave', + ' - onMousemove', + ' - onPointerleave', + ' - onPointermove', ' - ref', ' - role', ' - tabIndex', - ' - aria-disabled', - ' - onClick', - ' - onFocus', - ' - onPointermove', - ' - onMousemove', - ' - onPointerleave', - ' - onMouseleave', '', 'You can apply a few solutions:', ' - Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".', @@ -3452,11 +3452,11 @@ describe('Mouse interactions', () => { Trigger - alice - + alice + bob - + @@ -3472,13 +3472,11 @@ describe('Mouse interactions', () => { let items = getMenuItems() await focus(items[0]) - await focus(items[1]) - await press(Keys.Enter) + await click(items[1]) expect(clickHandler).not.toHaveBeenCalled() // Activate the last item - await focus(items[2]) - await press(Keys.Enter) + await click(getMenuItems()[2]) expect(clickHandler).not.toHaveBeenCalled() }) }) diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 4a4a543..59a5ca1 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -228,7 +228,7 @@ export let Menu = defineComponent({ return () => { let slot = { open: menuState.value === MenuStates.Open } - return render({ props, slot, slots, attrs, name: 'Menu' }) + return render({ ourProps: {}, theirProps: props, slot, slots, attrs, name: 'Menu' }) } }, }) @@ -314,9 +314,11 @@ export let MenuButton = defineComponent({ onKeyup: handleKeyUp, onClick: handleClick, } + let theirProps = props return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -458,10 +460,11 @@ export let MenuItems = defineComponent({ ref: api.itemsRef, } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -551,9 +554,11 @@ export let MenuItem = defineComponent({ onPointerleave: handleLeave, onMouseleave: handleLeave, } + let theirProps = props return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts index d79f645..595ad99 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.ts @@ -227,10 +227,8 @@ export let Popover = defineComponent({ return () => { let slot = { open: popoverState.value === PopoverStates.Open, close: api.close } return render({ - props: { - ...props, - ref: internalPopoverRef, - }, + theirProps: props, + ourProps: { ref: internalPopoverRef }, slot, slots, attrs, @@ -390,7 +388,8 @@ export let PopoverButton = defineComponent({ return h(Fragment, [ render({ - props: { ...attrs, ...props, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...props }, slot, attrs: attrs, slots: slots, @@ -446,7 +445,8 @@ export let PopoverOverlay = defineComponent({ } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, @@ -630,7 +630,8 @@ export let PopoverPanel = defineComponent({ onFocus: handleBeforeFocus, }), render({ - props: { ...attrs, ...props, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...props }, slot, attrs, slots, @@ -712,7 +713,8 @@ export let PopoverGroup = defineComponent({ let ourProps = { ref: groupRef } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot: {}, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/portal/portal.ts b/packages/@headlessui-vue/src/components/portal/portal.ts index fc8e1aa..d1aee87 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.ts @@ -88,7 +88,8 @@ export let Portal = defineComponent({ Teleport, { to: myTarget.value }, render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot: {}, attrs, slots, @@ -121,9 +122,16 @@ export let PortalGroup = defineComponent({ provide(PortalGroupContext, api) return () => { - let { target: _, ...incomingProps } = props + let { target: _, ...theirProps } = props - return render({ props: incomingProps, slot: {}, attrs, slots, name: 'PortalGroup' }) + return render({ + theirProps, + ourProps: {}, + slot: {}, + attrs, + slots, + name: 'PortalGroup', + }) } }, }) 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 d80917e..c6c05df 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -211,7 +211,7 @@ export let RadioGroup = defineComponent({ let id = `headlessui-radiogroup-${useId()}` return () => { - let { modelValue, disabled, name, ...incomingProps } = props + let { modelValue, disabled, name, ...theirProps } = props let ourProps = { ref: radioGroupRef, @@ -241,7 +241,8 @@ export let RadioGroup = defineComponent({ ) : []), render({ - props: { ...attrs, ...incomingProps, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...theirProps }, slot: {}, attrs, slots, @@ -307,7 +308,7 @@ export let RadioGroupOption = defineComponent({ } return () => { - let incomingProps = omit(props, ['value', 'disabled']) + let theirProps = omit(props, ['value', 'disabled']) let slot = { checked: checked.value, @@ -330,7 +331,8 @@ export let RadioGroupOption = defineComponent({ } return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index a78a702..262a6dc 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -55,7 +55,8 @@ export let SwitchGroup = defineComponent({ provide(GroupContext, api) - return () => render({ props, slot: {}, slots, attrs, name: 'SwitchGroup' }) + return () => + render({ theirProps: props, ourProps: {}, slot: {}, slots, attrs, name: 'SwitchGroup' }) }, }) @@ -108,7 +109,7 @@ export let Switch = defineComponent({ } return () => { - let { name, value, modelValue, ...incomingProps } = props + let { name, value, modelValue, ...theirProps } = props let slot = { checked: modelValue } let ourProps = { id, @@ -141,7 +142,8 @@ export let Switch = defineComponent({ ) : null, render({ - props: { ...attrs, ...incomingProps, ...ourProps }, + ourProps, + theirProps: { ...attrs, ...theirProps }, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.ts b/packages/@headlessui-vue/src/components/tabs/tabs.ts index 3699a37..ed9e547 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.ts @@ -154,10 +154,11 @@ export let TabGroup = defineComponent({ }, }), render({ - props: { + theirProps: { ...attrs, ...omit(props, ['selectedIndex', 'defaultIndex', 'manual', 'vertical', 'onChange']), }, + ourProps: {}, slot, slots, attrs, @@ -185,10 +186,11 @@ export let TabList = defineComponent({ role: 'tablist', 'aria-orientation': api.orientation.value, } - let incomingProps = props + let theirProps = props return render({ - props: { ...incomingProps, ...ourProps }, + ourProps, + theirProps, slot, attrs, slots, @@ -307,7 +309,8 @@ export let Tab = defineComponent({ } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, @@ -331,7 +334,8 @@ export let TabPanels = defineComponent({ let slot = { selectedIndex: api.selectedIndex.value } return render({ - props, + theirProps: props, + ourProps: {}, slot, attrs, slots, @@ -373,7 +377,8 @@ export let TabPanel = defineComponent({ } return render({ - props: { ...props, ...ourProps }, + ourProps, + theirProps: props, slot, attrs, slots, diff --git a/packages/@headlessui-vue/src/components/transitions/transition.ts b/packages/@headlessui-vue/src/components/transitions/transition.ts index b2f1c06..b7b7914 100644 --- a/packages/@headlessui-vue/src/components/transitions/transition.ts +++ b/packages/@headlessui-vue/src/components/transitions/transition.ts @@ -327,10 +327,11 @@ export let TransitionChild = defineComponent({ } = props let ourProps = { ref: container } - let incomingProps = rest + let theirProps = rest return render({ - props: { ...incomingProps, ...ourProps }, + theirProps, + ourProps, slot: {}, slots, attrs, @@ -416,7 +417,7 @@ export let TransitionRoot = defineComponent({ provide(TransitionContext, transitionBag) return () => { - let incomingProps = omit(props, [ + let theirProps = omit(props, [ 'show', 'appear', 'unmount', @@ -428,10 +429,11 @@ export let TransitionRoot = defineComponent({ let sharedProps = { unmount: props.unmount } return render({ - props: { + ourProps: { ...sharedProps, as: 'template', }, + theirProps: {}, slot: {}, slots: { ...slots, @@ -445,7 +447,7 @@ export let TransitionRoot = defineComponent({ onAfterLeave: () => emit('afterLeave'), ...attrs, ...sharedProps, - ...incomingProps, + ...theirProps, }, slots.default ), diff --git a/packages/@headlessui-vue/src/internal/hidden.ts b/packages/@headlessui-vue/src/internal/hidden.ts index 7401a5d..a4e655e 100644 --- a/packages/@headlessui-vue/src/internal/hidden.ts +++ b/packages/@headlessui-vue/src/internal/hidden.ts @@ -39,7 +39,8 @@ export let Hidden = defineComponent({ } return render({ - props: { ...theirProps, ...ourProps }, + ourProps, + theirProps, slot: {}, attrs, slots, diff --git a/packages/@headlessui-vue/src/internal/portal-force-root.ts b/packages/@headlessui-vue/src/internal/portal-force-root.ts index 7857091..b4349bb 100644 --- a/packages/@headlessui-vue/src/internal/portal-force-root.ts +++ b/packages/@headlessui-vue/src/internal/portal-force-root.ts @@ -24,8 +24,15 @@ export let ForcePortalRoot = defineComponent({ provide(ForcePortalRootContext, props.force) return () => { - let { force, ...incomingProps } = props - return render({ props: incomingProps, slot: {}, slots, attrs, name: 'ForcePortalRoot' }) + let { force, ...theirProps } = props + return render({ + theirProps, + ourProps: {}, + slot: {}, + slots, + attrs, + name: 'ForcePortalRoot', + }) } }, }) diff --git a/packages/@headlessui-vue/src/test-utils/interactions.ts b/packages/@headlessui-vue/src/test-utils/interactions.ts index 681208f..37f8169 100644 --- a/packages/@headlessui-vue/src/test-utils/interactions.ts +++ b/packages/@headlessui-vue/src/test-utils/interactions.ts @@ -232,6 +232,7 @@ export async function click( ) { try { if (element === null) return expect(element).not.toBe(null) + if (element.disabled) return let options = { button } diff --git a/packages/@headlessui-vue/src/utils/render.test.ts b/packages/@headlessui-vue/src/utils/render.test.ts index 55dfb93..fd3552f 100644 --- a/packages/@headlessui-vue/src/utils/render.test.ts +++ b/packages/@headlessui-vue/src/utils/render.test.ts @@ -9,7 +9,7 @@ let Dummy = defineComponent({ as: { type: [Object, String], default: 'div' }, }, setup(props, { attrs, slots }) { - return () => render({ props, slots, attrs, slot: {}, name: 'Dummy' }) + return () => render({ theirProps: props, ourProps: {}, slots, attrs, slot: {}, name: 'Dummy' }) }, }) @@ -60,7 +60,8 @@ describe('Validation', () => { PassThrough(props, context) { props.as = props.as ?? 'template' return render({ - props, + theirProps: props, + ourProps: {}, attrs: context.attrs, slots: context.slots, slot: {}, diff --git a/packages/@headlessui-vue/src/utils/render.ts b/packages/@headlessui-vue/src/utils/render.ts index 172bd0d..247a8db 100644 --- a/packages/@headlessui-vue/src/utils/render.ts +++ b/packages/@headlessui-vue/src/utils/render.ts @@ -29,9 +29,12 @@ export enum RenderStrategy { export function render({ visible = true, features = Features.None, + ourProps, + theirProps, ...main }: { - props: Record + ourProps: Record + theirProps: Record slot: Record attrs: Record slots: Slots @@ -40,16 +43,19 @@ export function render({ features?: Features visible?: boolean }) { + let props = mergeProps(theirProps, ourProps) + let mainWithProps = Object.assign(main, { props }) + // Visible always render - if (visible) return _render(main) + if (visible) return _render(mainWithProps) if (features & Features.Static) { // When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else - if (main.props.static) return _render(main) + if (props.static) return _render(mainWithProps) } if (features & Features.RenderStrategy) { - let strategy = main.props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden + let strategy = props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden return match(strategy, { [RenderStrategy.Unmount]() { @@ -58,14 +64,14 @@ export function render({ [RenderStrategy.Hidden]() { return _render({ ...main, - props: { ...main.props, hidden: true, style: { display: 'none' } }, + props: { ...props, hidden: true, style: { display: 'none' } }, }) }, }) } // No features enabled, just render - return _render(main) + return _render(mainWithProps) } function _render({ @@ -116,6 +122,7 @@ function _render({ `However we need to passthrough the following props:`, Object.keys(incomingProps) .concat(Object.keys(attrs)) + .sort((a, z) => a.localeCompare(z)) .map((line) => ` - ${line}`) .join('\n'), '', @@ -130,10 +137,7 @@ function _render({ ) } - return cloneVNode( - firstChild, - Object.assign({}, incomingProps as Record, dataAttributes) - ) + return cloneVNode(firstChild, Object.assign({}, incomingProps, dataAttributes)) } if (Array.isArray(children) && children.length === 1) { @@ -173,6 +177,60 @@ function flattenFragments(children: VNode[]): VNode[] { }) } +function mergeProps(...listOfProps: Record[]) { + if (listOfProps.length === 0) return {} + if (listOfProps.length === 1) return listOfProps[0] + + let target: Record = {} + + let eventHandlers: Record< + string, + ((event: { defaultPrevented: boolean }, ...args: any[]) => void | undefined)[] + > = {} + + for (let props of listOfProps) { + for (let prop in props) { + // Collect event handlers + if (prop.startsWith('on') && typeof props[prop] === 'function') { + eventHandlers[prop] ??= [] + eventHandlers[prop].push(props[prop]) + } else { + // Override incoming prop + target[prop] = props[prop] + } + } + } + + // Do not attach any event handlers when there is a `disabled` or `aria-disabled` prop set. + if (target.disabled || target['aria-disabled']) { + return Object.assign( + target, + // Set all event listeners that we collected to `undefined`. This is + // important because of the `cloneElement` from above, which merges the + // existing and new props, they don't just override therefore we have to + // explicitly nullify them. + Object.fromEntries(Object.keys(eventHandlers).map((eventName) => [eventName, undefined])) + ) + } + + // Merge event handlers + for (let eventName in eventHandlers) { + Object.assign(target, { + [eventName](event: { defaultPrevented: boolean }, ...args: any[]) { + let handlers = eventHandlers[eventName] + + for (let handler of handlers) { + if (event?.defaultPrevented) return + + handler(event, ...args) + } + }, + }) + } + + return target +} + export function compact>(object: T) { let clone = Object.assign({}, object) for (let key in clone) {