diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2830616f..964c48e906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix Tree-shaking support ([#1247](https://github.com/tailwindlabs/headlessui/pull/1247)) - Stop propagation on the Popover Button ([#1263](https://github.com/tailwindlabs/headlessui/pull/1263)) - Fix incorrect `active` option in the Listbox/Combobox component ([#1264](https://github.com/tailwindlabs/headlessui/pull/1264)) +- Properly merge incoming props ([#1265](https://github.com/tailwindlabs/headlessui/pull/1265)) ### Added diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index e7ee8a32d3..83a0e626e3 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -330,7 +330,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< }, ref: Ref ) { - let { name, value, onChange, disabled = false, __demoMode = false, ...passThroughProps } = props + let { name, value, onChange, disabled = false, __demoMode = false, ...theirProps } = props let comboboxPropsRef = useRef({ value, @@ -481,9 +481,11 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< // Ensure that we update the inputRef if the value changes useIsoMorphicEffect(syncInputValue, [syncInputValue]) + let ourProps = ref === null ? {} : { ref } let renderConfiguration = { - props: ref === null ? passThroughProps : { ...passThroughProps, ref }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_COMBOBOX_TAG, name: 'Combobox', @@ -556,7 +558,7 @@ let Input = forwardRefWithAs(function Input< }, ref: Ref ) { - let { value, onChange, displayValue, ...passThroughProps } = props + let { value, onChange, displayValue, ...theirProps } = props let [state] = useComboboxContext('Combobox.Input') let data = useComboboxData() let actions = useComboboxActions() @@ -677,7 +679,7 @@ let Input = forwardRefWithAs(function Input< [state] ) - let propsWeControl = { + let ourProps = { ref: inputRef, id, role: 'combobox', @@ -694,7 +696,8 @@ let Input = forwardRefWithAs(function Input< } return render({ - props: { ...passThroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_INPUT_TAG, name: 'Combobox.Input', @@ -806,8 +809,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.comboboxState === ComboboxStates.Open, disabled: state.disabled }), [state] ) - let passthroughProps = props - let propsWeControl = { + let theirProps = props + let ourProps = { ref: buttonRef, id, type: useResolveButtonType(props, state.buttonRef), @@ -822,7 +825,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.comboboxState === ComboboxStates.Open, disabled: state.disabled }), [state] ) - let propsWeControl = { ref: labelRef, id, onClick: handleClick } + + let theirProps = props + let ourProps = { ref: labelRef, id, onClick: handleClick } + return render({ - props: { ...props, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_LABEL_TAG, name: 'Combobox.Label', @@ -890,7 +898,7 @@ let Options = forwardRefWithAs(function Options< }, ref: Ref ) { - let { hold = false, ...passthroughProps } = props + let { hold = false, ...theirProps } = props let [state] = useComboboxContext('Combobox.Options') let { optionsPropsRef } = state @@ -936,7 +944,7 @@ let Options = forwardRefWithAs(function Options< () => ({ open: state.comboboxState === ComboboxStates.Open }), [state] ) - let propsWeControl = { + let ourProps = { 'aria-activedescendant': state.activeOptionIndex === null ? undefined : state.options[state.activeOptionIndex]?.id, 'aria-labelledby': labelledby, @@ -946,7 +954,8 @@ let Options = forwardRefWithAs(function Options< } return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OPTIONS_TAG, features: OptionsRenderFeatures, @@ -986,7 +995,7 @@ let Option = forwardRefWithAs(function Option< }, ref: Ref ) { - let { disabled = false, value, ...passthroughProps } = props + let { disabled = false, value, ...theirProps } = props let [state] = useComboboxContext('Combobox.Option') let data = useComboboxData() let actions = useComboboxActions() @@ -1072,7 +1081,7 @@ let Option = forwardRefWithAs(function Option< [active, selected, disabled] ) - let propsWeControl = { + let ourProps = { id, ref: optionRef, role: 'option', @@ -1092,7 +1101,8 @@ let Option = forwardRefWithAs(function Option< } return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OPTION_TAG, name: 'Combobox.Option', diff --git a/packages/@headlessui-react/src/components/description/description.tsx b/packages/@headlessui-react/src/components/description/description.tsx index 2b4336aef9..ce8a5a0fd7 100644 --- a/packages/@headlessui-react/src/components/description/description.tsx +++ b/packages/@headlessui-react/src/components/description/description.tsx @@ -98,11 +98,12 @@ export let Description = forwardRefWithAs(function Description< useIsoMorphicEffect(() => context.register(id), [id, context.register]) - let passThroughProps = props - let propsWeControl = { ref: descriptionRef, ...context.props, id } + let theirProps = props + let ourProps = { ref: descriptionRef, ...context.props, id } return render({ - props: { ...passThroughProps, ...propsWeControl }, + ourProps, + theirProps, slot: context.slot || {}, defaultTag: DEFAULT_DESCRIPTION_TAG, name: context.name || 'Description', diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 3de5c0ffb3..10521c59d1 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -119,7 +119,7 @@ let DialogRoot = forwardRefWithAs(function Dialog< }, ref: Ref ) { - let { open, onClose, initialFocus, __demoMode = false, ...rest } = props + let { open, onClose, initialFocus, __demoMode = false, ...theirProps } = props let [nestedDialogCount, setNestedDialogCount] = useState(0) let usesOpenClosedState = useOpenClosed() @@ -292,7 +292,7 @@ let DialogRoot = forwardRefWithAs(function Dialog< [dialogState] ) - let propsWeControl = { + let ourProps = { ref: dialogRef, id, role: 'dialog', @@ -303,7 +303,6 @@ let DialogRoot = forwardRefWithAs(function Dialog< event.stopPropagation() }, } - let passthroughProps = rest return ( {render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_DIALOG_TAG, features: DialogRenderFeatures, @@ -379,16 +379,18 @@ let Overlay = forwardRefWithAs(function Overlay< () => ({ open: dialogState === DialogStates.Open }), [dialogState] ) - let propsWeControl = { + + let theirProps = props + let ourProps = { ref: overlayRef, id, 'aria-hidden': true, onClick: handleClick, } - let passthroughProps = props return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OVERLAY_TAG, name: 'Dialog.Overlay', @@ -421,11 +423,13 @@ let Title = forwardRefWithAs(function Title ({ open: dialogState === DialogStates.Open }), [dialogState] ) - let propsWeControl = { id } - let passthroughProps = props + + let theirProps = props + let ourProps = { ref: titleRef, id } return render({ - props: { ref: titleRef, ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_TITLE_TAG, name: 'Dialog.Title', diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx index 7cd4488853..ac85802d96 100644 --- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx +++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx @@ -156,7 +156,7 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure< }, ref: Ref ) { - let { defaultOpen = false, ...passthroughProps } = props + let { defaultOpen = false, ...theirProps } = props let buttonId = `headlessui-disclosure-button-${useId()}` let panelId = `headlessui-disclosure-panel-${useId()}` let internalDisclosureRef = useRef(null) @@ -214,6 +214,10 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure< [disclosureState, close] ) + let ourProps = { + ref: disclosureRef, + } + return ( @@ -224,7 +228,8 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure< })} > {render({ - props: { ref: disclosureRef, ...passthroughProps }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_DISCLOSURE_TAG, name: 'Disclosure', @@ -320,8 +325,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.disclosureState === DisclosureStates.Open, close }), [state, close] ) - let propsWeControl = { + + let theirProps = props + let ourProps = { ref: panelRef, id: state.panelId, } - let passthroughProps = props return ( {render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_PANEL_TAG, features: PanelRenderFeatures, diff --git a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx index 15e882a5f7..92dd61a6e2 100644 --- a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx +++ b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx @@ -23,17 +23,18 @@ export let FocusTrap = forwardRefWithAs(function FocusTrap< ) { let container = useRef(null) let focusTrapRef = useSyncRefs(container, ref) - let { initialFocus, ...passthroughProps } = props + let { initialFocus, ...theirProps } = props let ready = useServerHandoffComplete() useFocusTrap(container, ready ? FocusTrapFeatures.All : FocusTrapFeatures.None, { initialFocus }) - let propsWeControl = { + let ourProps = { ref: focusTrapRef, } return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, defaultTag: DEFAULT_FOCUS_TRAP_TAG, name: 'FocusTrap', }) diff --git a/packages/@headlessui-react/src/components/label/label.tsx b/packages/@headlessui-react/src/components/label/label.tsx index 4da9864126..15f75d8f22 100644 --- a/packages/@headlessui-react/src/components/label/label.tsx +++ b/packages/@headlessui-react/src/components/label/label.tsx @@ -88,22 +88,28 @@ export let Label = forwardRefWithAs(function Label< }, ref: Ref ) { - let { passive = false, ...passThroughProps } = props + let { passive = false, ...theirProps } = props let context = useLabelContext() let id = `headlessui-label-${useId()}` let labelRef = useSyncRefs(ref) useIsoMorphicEffect(() => context.register(id), [id, context.register]) - let propsWeControl = { ref: labelRef, ...context.props, id } + let ourProps = { ref: labelRef, ...context.props, id } - let allProps = { ...passThroughProps, ...propsWeControl } - // @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) { + if ('onClick' in ourProps) { + delete (ourProps as any)['onClick'] + } + + if ('onClick' in theirProps) { + delete (theirProps as any)['onClick'] + } + } return render({ - props: allProps, + ourProps, + theirProps, slot: context.slot || {}, defaultTag: DEFAULT_LABEL_TAG, name: context.name || 'Label', diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 9da7c7ca2d..7e3938bd0d 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -314,7 +314,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox< }, ref: Ref ) { - let { value, name, onChange, disabled = false, horizontal = false, ...passThroughProps } = props + let { value, name, onChange, disabled = false, horizontal = false, ...theirProps } = props const orientation = horizontal ? 'horizontal' : 'vertical' let listboxRef = useSyncRefs(ref) @@ -382,8 +382,11 @@ let ListboxRoot = forwardRefWithAs(function Listbox< [listboxState, disabled] ) + let ourProps = { ref: listboxRef } + let renderConfiguration = { - props: { ref: listboxRef, ...passThroughProps }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_LISTBOX_TAG, name: 'Listbox', @@ -513,8 +516,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }), [state] ) - let passthroughProps = props - let propsWeControl = { + let theirProps = props + let ourProps = { ref: buttonRef, id, type: useResolveButtonType(props, state.buttonRef), @@ -529,7 +532,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }), [state] ) - let propsWeControl = { ref: labelRef, id, onClick: handleClick } + let theirProps = props + let ourProps = { ref: labelRef, id, onClick: handleClick } + return render({ - props: { ...props, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_LABEL_TAG, name: 'Listbox.Label', @@ -702,7 +709,9 @@ let Options = forwardRefWithAs(function Options< () => ({ open: state.listboxState === ListboxStates.Open }), [state] ) - let propsWeControl = { + + let theirProps = props + let ourProps = { 'aria-activedescendant': state.activeOptionIndex === null ? undefined : state.options[state.activeOptionIndex]?.id, 'aria-multiselectable': state.propsRef.current.mode === ValueMode.Multi ? true : undefined, @@ -714,10 +723,10 @@ let Options = forwardRefWithAs(function Options< tabIndex: 0, ref: optionsRef, } - let passthroughProps = props return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OPTIONS_TAG, features: OptionsRenderFeatures, @@ -758,7 +767,7 @@ let Option = forwardRefWithAs(function Option< }, ref: Ref ) { - let { disabled = false, value, ...passthroughProps } = props + let { disabled = false, value, ...theirProps } = props let [state, dispatch] = useListboxContext('Listbox.Option') let id = `headlessui-listbox-option-${useId()}` let active = @@ -839,7 +848,7 @@ let Option = forwardRefWithAs(function Option< () => ({ active, selected, disabled }), [active, selected, disabled] ) - let propsWeControl = { + let ourProps = { id, ref: optionRef, role: 'option', @@ -859,7 +868,8 @@ let Option = forwardRefWithAs(function Option< } return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OPTION_TAG, name: 'Listbox.Option', diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx index f8c36f60b1..bf087b26f6 100644 --- a/packages/@headlessui-react/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx @@ -1005,7 +1005,7 @@ describe('Keyboard interactions', () => { // Click the menu button again await click(getMenuButton()) - // Active the last menu item + // Activate the last menu item await mouseMove(getMenuItems()[2]) // Close menu, and invoke the item diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index cfeb84c9c5..094a3c4b3f 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -255,6 +255,9 @@ let MenuRoot = forwardRefWithAs(function Menu {render({ - props: { ref: menuRef, ...props }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_MENU_TAG, name: 'Menu', @@ -355,8 +359,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.menuState === MenuStates.Open }), [state] ) - let passthroughProps = props - let propsWeControl = { + let theirProps = props + let ourProps = { ref: buttonRef, id, type: useResolveButtonType(props, state.buttonRef), @@ -369,7 +373,8 @@ let Button = forwardRefWithAs(function Button ({ open: state.menuState === MenuStates.Open }), [state] ) - let propsWeControl = { + + let theirProps = props + let ourProps = { 'aria-activedescendant': state.activeItemIndex === null ? undefined : state.items[state.activeItemIndex]?.id, 'aria-labelledby': state.buttonRef.current?.id, @@ -532,10 +539,10 @@ let Items = forwardRefWithAs(function Items( props: Props & { disabled?: boolean - onClick?: (event: { preventDefault: Function }) => void }, ref: Ref ) { - let { disabled = false, onClick, ...passthroughProps } = props + let { disabled = false, ...theirProps } = props let [state, dispatch] = useMenuContext('Menu.Item') let id = `headlessui-menu-item-${useId()}` let active = state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false @@ -606,9 +612,8 @@ let Item = forwardRefWithAs(function Item state.buttonRef.current?.focus({ preventScroll: true })) - if (onClick) return onClick(event) }, - [dispatch, state.buttonRef, disabled, onClick] + [dispatch, state.buttonRef, disabled] ) let handleFocus = useCallback(() => { @@ -634,7 +639,7 @@ let Item = forwardRefWithAs(function Item(() => ({ active, disabled }), [active, disabled]) - let propsWeControl = { + let ourProps = { id, ref: itemRef, role: 'menuitem', @@ -650,7 +655,8 @@ let Item = forwardRefWithAs(function Item @@ -271,7 +274,8 @@ let PopoverRoot = forwardRefWithAs(function Popover< })} > {render({ - props: { ref: popoverRef, ...props }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_POPOVER_TAG, name: 'Popover', @@ -485,8 +489,8 @@ let Button = forwardRefWithAs(function Button ({ open: popoverState === PopoverStates.Open }), [popoverState] ) - let propsWeControl = { + + let theirProps = props + let ourProps = { ref: overlayRef, id, 'aria-hidden': true, onClick: handleClick, } - let passthroughProps = props return render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OVERLAY_TAG, features: OverlayRenderFeatures, @@ -591,7 +598,7 @@ let Panel = forwardRefWithAs(function Panel ) { - let { focus = false, ...passthroughProps } = props + let { focus = false, ...theirProps } = props let [state, dispatch] = usePopoverContext('Popover.Panel') let { close } = usePopoverAPIContext('Popover.Panel') @@ -721,7 +728,7 @@ let Panel = forwardRefWithAs(function Panel ({ open: state.popoverState === PopoverStates.Open, close }), [state, close] ) - let propsWeControl = { + let ourProps = { ref: panelRef, id: state.panelId, onKeyDown: handleKeyDown, @@ -730,7 +737,8 @@ let Panel = forwardRefWithAs(function Panel {render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_PANEL_TAG, features: PanelRenderFeatures, @@ -814,13 +822,15 @@ let Group = forwardRefWithAs(function Group(() => ({}), []) - let propsWeControl = { ref: groupRef } - let passthroughProps = props + + let theirProps = props + let ourProps = { ref: groupRef } return ( {render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_GROUP_TAG, name: 'Popover.Group', diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx index 992ee15dd2..54959e395e 100644 --- a/packages/@headlessui-react/src/components/portal/portal.tsx +++ b/packages/@headlessui-react/src/components/portal/portal.tsx @@ -69,7 +69,7 @@ interface PortalRenderPropArg {} let PortalRoot = forwardRefWithAs(function Portal< TTag extends ElementType = typeof DEFAULT_PORTAL_TAG >(props: Props, ref: Ref) { - let passthroughProps = props + let theirProps = props let internalPortalRootRef = useRef(null) let portalRef = useSyncRefs( optionalRef((ref) => { @@ -105,11 +105,14 @@ let PortalRoot = forwardRefWithAs(function Portal< if (!ready) return null + let ourProps = { ref: portalRef } + return !target || !element ? null : createPortal( render({ - props: { ref: portalRef, ...passthroughProps }, + ourProps, + theirProps, defaultTag: DEFAULT_PORTAL_TAG, name: 'Portal', }), @@ -130,13 +133,16 @@ let Group = forwardRefWithAs(function Group ) { - let { target, ...passthroughProps } = props + let { target, ...theirProps } = props let groupRef = useSyncRefs(ref) + let ourProps = { ref: groupRef } + return ( {render({ - props: { ref: groupRef, ...passthroughProps }, + ourProps, + theirProps, defaultTag: DEFAULT_GROUP_TAG, name: 'Popover.Group', })} 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 1788946225..c244a29e57 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx @@ -121,7 +121,7 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< }, ref: Ref ) { - let { value, name, onChange, disabled = false, ...passThroughProps } = props + let { value, name, onChange, disabled = false, ...theirProps } = props let [{ options }, dispatch] = useReducer(stateReducer, { options: [], } as StateDefinition) @@ -252,7 +252,7 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< [registerOption, firstOption, containsCheckedOption, triggerChange, disabled, value] ) - let propsWeControl = { + let ourProps = { ref: radioGroupRef, id, role: 'radiogroup', @@ -262,7 +262,8 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< } let renderConfiguration = { - props: { ...passThroughProps, ...propsWeControl }, + ourProps, + theirProps, defaultTag: DEFAULT_RADIO_GROUP_TAG, name: 'RadioGroup', } @@ -341,7 +342,7 @@ let Option = forwardRefWithAs(function Option< let [describedby, DescriptionProvider] = useDescriptions() let { addFlag, removeFlag, hasFlag } = useFlags(OptionState.Empty) - let { value, disabled = false, ...passThroughProps } = props + let { value, disabled = false, ...theirProps } = props let propsRef = useRef({ value, disabled }) useIsoMorphicEffect(() => { @@ -379,7 +380,7 @@ let Option = forwardRefWithAs(function Option< let isDisabled = radioGroupDisabled || disabled let checked = radioGroupValue === value - let propsWeControl = { + let ourProps = { ref: optionRef, id, role: 'radio', @@ -406,7 +407,8 @@ let Option = forwardRefWithAs(function Option< {render({ - props: { ...passThroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_OPTION_TAG, name: 'RadioGroup.Option', diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 16e86b48d8..2ada6c9e28 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -49,6 +49,9 @@ function Group(props: Props [switchElement, setSwitchElement, labelledby, describedby] ) + let ourProps = {} + let theirProps = props + return ( (props: Props }} > - {render({ props, defaultTag: DEFAULT_GROUP_TAG, name: 'Switch.Group' })} + {render({ + ourProps, + theirProps, + defaultTag: DEFAULT_GROUP_TAG, + name: 'Switch.Group', + })} @@ -101,7 +109,7 @@ let SwitchRoot = forwardRefWithAs(function Switch< }, ref: Ref ) { - let { checked, onChange, name, value, ...passThroughProps } = props + let { checked, onChange, name, value, ...theirProps } = props let id = `headlessui-switch-${useId()}` let groupContext = useContext(GroupContext) let internalSwitchRef = useRef(null) @@ -136,7 +144,7 @@ let SwitchRoot = forwardRefWithAs(function Switch< ) let slot = useMemo(() => ({ checked }), [checked]) - let propsWeControl = { + let ourProps = { id, ref: switchRef, role: 'switch', @@ -151,7 +159,8 @@ let SwitchRoot = forwardRefWithAs(function Switch< } let renderConfiguration = { - props: { ...passThroughProps, ...propsWeControl }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_SWITCH_TAG, name: 'Switch', diff --git a/packages/@headlessui-react/src/components/tabs/tabs.tsx b/packages/@headlessui-react/src/components/tabs/tabs.tsx index 10b249517d..8058faba0d 100644 --- a/packages/@headlessui-react/src/components/tabs/tabs.tsx +++ b/packages/@headlessui-react/src/components/tabs/tabs.tsx @@ -146,7 +146,7 @@ let Tabs = forwardRefWithAs(function Tabs @@ -244,7 +248,8 @@ let Tabs = forwardRefWithAs(function Tabs {render({ - props: { ref: tabsRef, ...passThroughProps }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_TABS_TAG, name: 'Tabs', @@ -270,15 +275,17 @@ let List = forwardRefWithAs(function List ({ selected }), [selected]) - let propsWeControl = { + + let theirProps = props + let ourProps = { ref: tabRef, onKeyDown: handleKeyDown, onFocus: activation === 'manual' ? handleFocus : handleSelection, @@ -390,10 +399,10 @@ let TabRoot = forwardRefWithAs(function Tab ({ selectedIndex }), [selectedIndex]) + let theirProps = props + let ourProps = { ref: panelsRef } + return render({ - props: { ref: panelsRef, ...props }, + ourProps, + theirProps, slot, defaultTag: DEFAULT_PANELS_TAG, name: 'Tabs.Panels', @@ -462,7 +475,9 @@ let Panel = forwardRefWithAs(function Panel ({ selected }), [selected]) - let propsWeControl = { + + let theirProps = props + let ourProps = { ref: panelRef, id, role: 'tabpanel', @@ -470,10 +485,9 @@ let Panel = forwardRefWithAs(function Panel @@ -360,7 +360,8 @@ let TransitionChild = forwardRefWithAs(function TransitionChild< })} > {render({ - props: { ...passthroughProps, ...propsWeControl }, + ourProps, + theirProps, defaultTag: DEFAULT_TRANSITION_CHILD_TAG, features: TransitionChildRenderFeatures, visible: state === TreeStates.Visible, @@ -375,7 +376,7 @@ let TransitionRoot = forwardRefWithAs(function Transition< TTag extends ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG >(props: TransitionChildProps & { show?: boolean; appear?: boolean }, ref: Ref) { // @ts-expect-error - let { show, appear = false, unmount, ...passthroughProps } = props as typeof props + let { show, appear = false, unmount, ...theirProps } = props as typeof props let transitionRef = useSyncRefs(ref) let usesOpenClosedState = useOpenClosed() @@ -417,13 +418,12 @@ let TransitionRoot = forwardRefWithAs(function Transition< {render({ - props: { + ourProps: { ...sharedProps, as: Fragment, - children: ( - - ), + children: , }, + theirProps: {}, defaultTag: Fragment, features: TransitionChildRenderFeatures, visible: state === TreeStates.Visible, diff --git a/packages/@headlessui-react/src/internal/visually-hidden.tsx b/packages/@headlessui-react/src/internal/visually-hidden.tsx index 9aceeff5a9..8ae0fb5d8f 100644 --- a/packages/@headlessui-react/src/internal/visually-hidden.tsx +++ b/packages/@headlessui-react/src/internal/visually-hidden.tsx @@ -7,22 +7,25 @@ let DEFAULT_VISUALLY_HIDDEN_TAG = 'div' as const export let VisuallyHidden = forwardRefWithAs(function VisuallyHidden< TTag extends ElementType = typeof DEFAULT_VISUALLY_HIDDEN_TAG >(props: Props, ref: Ref) { - return render({ - props: { - ...props, - ref, - style: { - position: 'absolute', - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: 'hidden', - clip: 'rect(0, 0, 0, 0)', - whiteSpace: 'nowrap', - borderWidth: '0', - }, + let theirProps = props + let ourProps = { + ref, + style: { + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: '0', }, + } + + return render({ + ourProps, + theirProps, slot: {}, defaultTag: DEFAULT_VISUALLY_HIDDEN_TAG, name: 'VisuallyHidden', diff --git a/packages/@headlessui-react/src/utils/render.test.tsx b/packages/@headlessui-react/src/utils/render.test.tsx index 9c743c4520..64f40a85f2 100644 --- a/packages/@headlessui-react/src/utils/render.test.tsx +++ b/packages/@headlessui-react/src/utils/render.test.tsx @@ -19,7 +19,8 @@ describe('Default functionality', () => { return (
{render({ - props, + ourProps: {}, + theirProps: props, slot, defaultTag: 'div', name: 'Dummy', @@ -80,7 +81,8 @@ describe('Default functionality', () => { return (
{render({ - props: { ...props, ref }, + ourProps: { ref }, + theirProps: props, slot, defaultTag: 'div', name: 'OtherDummy', @@ -323,7 +325,8 @@ describe('Features.Static', () => { return (
{render({ - props: rest, + ourProps: {}, + theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, @@ -428,7 +431,8 @@ describe('Features.RenderStrategy', () => { return (
{render({ - props: rest, + ourProps: {}, + theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, @@ -455,7 +459,8 @@ describe('Features.Static | Features.RenderStrategy', () => { return (
{render({ - props: rest, + ourProps: {}, + theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts index eedd1e5ab9..ce3047d109 100644 --- a/packages/@headlessui-react/src/utils/render.ts +++ b/packages/@headlessui-react/src/utils/render.ts @@ -47,20 +47,24 @@ export type PropsForFeatures = XOR< > export function render({ - props, + ourProps, + theirProps, slot, defaultTag, features, visible = true, name, }: { - props: Expand & PropsForFeatures> + ourProps: Expand & PropsForFeatures> + theirProps: Expand> slot?: TSlot defaultTag: ElementType features?: TFeature visible?: boolean name: string }) { + let props = mergeProps(theirProps, ourProps) + // Visible always render if (visible) return _render(props, slot, defaultTag, name) @@ -106,7 +110,7 @@ function _render( as: Component = tag, children, refName = 'ref', - ...passThroughProps + ...rest } = omit(props, ['unmount', 'static']) // This allows us to use `` @@ -117,12 +121,12 @@ function _render( | ReactElement[] // Allow for className to be a function with the slot as the contents - if (passThroughProps.className && typeof passThroughProps.className === 'function') { - ;(passThroughProps as any).className = passThroughProps.className(slot) + if (rest.className && typeof rest.className === 'function') { + ;(rest as any).className = rest.className(slot) } if (Component === Fragment) { - if (Object.keys(compact(passThroughProps)).length > 0) { + if (Object.keys(compact(rest)).length > 0) { if ( !isValidElement(resolvedChildren) || (Array.isArray(resolvedChildren) && resolvedChildren.length > 1) @@ -133,7 +137,7 @@ function _render( '', `The current component <${name} /> is rendering a "Fragment".`, `However we need to passthrough the following props:`, - Object.keys(passThroughProps) + Object.keys(rest) .map((line) => ` - ${line}`) .join('\n'), '', @@ -153,9 +157,7 @@ function _render( Object.assign( {}, // Filter out undefined values so that they don't override the existing values - mergeEventFunctions(compact(omit(passThroughProps, ['ref'])), resolvedChildren.props, [ - 'onClick', - ]), + mergeProps(resolvedChildren.props, compact(omit(rest, ['ref']))), refRelatedProps ) ) @@ -164,46 +166,63 @@ function _render( return createElement( Component, - Object.assign({}, omit(passThroughProps, ['ref']), Component !== Fragment && refRelatedProps), + Object.assign({}, omit(rest, ['ref']), Component !== Fragment && refRelatedProps), resolvedChildren ) } -/** - * We can use this function for the following useCase: - * - *