Properly merge incoming props (#1265)

* rename inconsistent `passThroughProps` and `passthroughProps` to more
concise `incomingProps`

This is going to make a bit more sense in the next commits of this
branch, hold on!

* split props into `propsWeControl` and `propsTheyControl`

This will allow us to merge the props with a bit more control. Instead
of overriding every prop from the user' props with our props, we can now
merge event listeners.

* update `render` API to accept `propsWeControl` and `propsTheyControl`

* improve the merge logic

This will essentially do the exact same thing we were doing before:
```js
let props = { ...propsTheyControl, ...propsWeControl }
```

But instead of overriding everything, we will merge the event listener
related props like `onClick`, `onKeyDown`, ...

* fix typo in tests

* simplify naming

- Rename `propsWeControl` to `ourProps`
- Rename `propsTheyControl` to `theirProps`

* update changelog
This commit is contained in:
Robin Malfait
2022-03-22 17:32:11 +01:00
committed by GitHub
parent 4f8c615245
commit 3e19aa5c97
37 changed files with 398 additions and 283 deletions
+1
View File
@@ -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
@@ -330,7 +330,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
},
ref: Ref<TTag>
) {
let { name, value, onChange, disabled = false, __demoMode = false, ...passThroughProps } = props
let { name, value, onChange, disabled = false, __demoMode = false, ...theirProps } = props
let comboboxPropsRef = useRef<StateDefinition['comboboxPropsRef']['current']>({
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<HTMLInputElement>
) {
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<TTag extends ElementType = typeof
() => ({ 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<TTag extends ElementType = typeof
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Combobox.Button',
@@ -855,9 +859,13 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
() => ({ 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<HTMLUListElement>
) {
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<HTMLLIElement>
) {
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',
@@ -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',
@@ -119,7 +119,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
},
ref: Ref<HTMLDivElement>
) {
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 (
<StackProvider
@@ -331,7 +330,8 @@ let DialogRoot = forwardRefWithAs(function Dialog<
<ForcePortalRoot force={false}>
<DescriptionProvider slot={slot} name="Dialog.Description">
{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<TTag extends ElementType = typeof DE
() => ({ 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',
@@ -156,7 +156,7 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
},
ref: Ref<TTag>
) {
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<HTMLElement | null>(null)
@@ -214,6 +214,10 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
[disclosureState, close]
)
let ourProps = {
ref: disclosureRef,
}
return (
<DisclosureContext.Provider value={reducerBag}>
<DisclosureAPIContext.Provider value={api}>
@@ -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<TTag extends ElementType = typeof
)
let type = useResolveButtonType(props, internalButtonRef)
let passthroughProps = props
let propsWeControl = isWithinPanel
let theirProps = props
let ourProps = isWithinPanel
? { ref: buttonRef, type, onKeyDown: handleKeyDown, onClick: handleClick }
: {
ref: buttonRef,
@@ -337,7 +342,8 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Disclosure.Button',
@@ -391,16 +397,18 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
() => ({ open: state.disclosureState === DisclosureStates.Open, close }),
[state, close]
)
let propsWeControl = {
let theirProps = props
let ourProps = {
ref: panelRef,
id: state.panelId,
}
let passthroughProps = props
return (
<DisclosurePanelContext.Provider value={state.panelId}>
{render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_PANEL_TAG,
features: PanelRenderFeatures,
@@ -23,17 +23,18 @@ export let FocusTrap = forwardRefWithAs(function FocusTrap<
) {
let container = useRef<HTMLElement | null>(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',
})
@@ -88,22 +88,28 @@ export let Label = forwardRefWithAs(function Label<
},
ref: Ref<HTMLLabelElement>
) {
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',
@@ -314,7 +314,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
},
ref: Ref<TTag>
) {
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<TTag extends ElementType = typeof
() => ({ 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<TTag extends ElementType = typeof
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Listbox.Button',
@@ -562,9 +566,12 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
() => ({ 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<HTMLElement>
) {
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',
@@ -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
@@ -255,6 +255,9 @@ let MenuRoot = forwardRefWithAs(function Menu<TTag extends ElementType = typeof
[menuState]
)
let theirProps = props
let ourProps = { ref: menuRef }
return (
<MenuContext.Provider value={reducerBag}>
<OpenClosedProvider
@@ -264,7 +267,8 @@ let MenuRoot = forwardRefWithAs(function Menu<TTag extends ElementType = typeof
})}
>
{render({
props: { ref: menuRef, ...props },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_MENU_TAG,
name: 'Menu',
@@ -355,8 +359,8 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
() => ({ 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<TTag extends ElementType = typeof
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Menu.Button',
@@ -521,7 +526,9 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
() => ({ 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<TTag extends ElementType = typeof DE
tabIndex: 0,
ref: itemsRef,
}
let passthroughProps = props
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_ITEMS_TAG,
features: ItemsRenderFeatures,
@@ -565,11 +572,10 @@ type MenuItemPropsWeControl =
let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
props: Props<TTag, ItemRenderPropArg, MenuItemPropsWeControl> & {
disabled?: boolean
onClick?: (event: { preventDefault: Function }) => void
},
ref: Ref<HTMLElement>
) {
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<TTag extends ElementType = typeof DEFA
if (disabled) return event.preventDefault()
dispatch({ type: ActionTypes.CloseMenu })
disposables().nextFrame(() => 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<TTag extends ElementType = typeof DEFA
}, [disabled, active, dispatch])
let slot = useMemo<ItemRenderPropArg>(() => ({ active, disabled }), [active, disabled])
let propsWeControl = {
let ourProps = {
id,
ref: itemRef,
role: 'menuitem',
@@ -650,7 +655,8 @@ let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFA
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_ITEM_TAG,
name: 'Menu.Item',
@@ -261,6 +261,9 @@ let PopoverRoot = forwardRefWithAs(function Popover<
[popoverState, close]
)
let theirProps = props
let ourProps = { ref: popoverRef }
return (
<PopoverContext.Provider value={reducerBag}>
<PopoverAPIContext.Provider value={api}>
@@ -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<TTag extends ElementType = typeof
)
let type = useResolveButtonType(props, internalButtonRef)
let passthroughProps = props
let propsWeControl = isWithinPanel
let theirProps = props
let ourProps = isWithinPanel
? {
ref: withinPanelButtonRef,
type,
@@ -505,7 +509,8 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
}
return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Popover.Button',
@@ -555,16 +560,18 @@ let Overlay = forwardRefWithAs(function Overlay<
() => ({ 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<TTag extends ElementType = typeof DE
},
ref: Ref<HTMLDivElement>
) {
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<TTag extends ElementType = typeof DE
() => ({ 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<TTag extends ElementType = typeof DE
return (
<PopoverPanelContext.Provider value={state.panelId}>
{render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_PANEL_TAG,
features: PanelRenderFeatures,
@@ -814,13 +822,15 @@ let Group = forwardRefWithAs(function Group<TTag extends ElementType = typeof DE
)
let slot = useMemo<GroupRenderPropArg>(() => ({}), [])
let propsWeControl = { ref: groupRef }
let passthroughProps = props
let theirProps = props
let ourProps = { ref: groupRef }
return (
<PopoverGroupContext.Provider value={contextBag}>
{render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_GROUP_TAG,
name: 'Popover.Group',
@@ -69,7 +69,7 @@ interface PortalRenderPropArg {}
let PortalRoot = forwardRefWithAs(function Portal<
TTag extends ElementType = typeof DEFAULT_PORTAL_TAG
>(props: Props<TTag, PortalRenderPropArg>, ref: Ref<HTMLElement>) {
let passthroughProps = props
let theirProps = props
let internalPortalRootRef = useRef<HTMLElement | null>(null)
let portalRef = useSyncRefs(
optionalRef<typeof internalPortalRootRef['current']>((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<TTag extends ElementType = typeof DE
},
ref: Ref<HTMLElement>
) {
let { target, ...passthroughProps } = props
let { target, ...theirProps } = props
let groupRef = useSyncRefs(ref)
let ourProps = { ref: groupRef }
return (
<PortalGroupContext.Provider value={target}>
{render({
props: { ref: groupRef, ...passthroughProps },
ourProps,
theirProps,
defaultTag: DEFAULT_GROUP_TAG,
name: 'Popover.Group',
})}
@@ -121,7 +121,7 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
},
ref: Ref<HTMLElement>
) {
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<
<DescriptionProvider name="RadioGroup.Description">
<LabelProvider name="RadioGroup.Label">
{render({
props: { ...passThroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_OPTION_TAG,
name: 'RadioGroup.Option',
@@ -49,6 +49,9 @@ function Group<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(props: Props
[switchElement, setSwitchElement, labelledby, describedby]
)
let ourProps = {}
let theirProps = props
return (
<DescriptionProvider name="Switch.Description">
<LabelProvider
@@ -62,7 +65,12 @@ function Group<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(props: Props
}}
>
<GroupContext.Provider value={context}>
{render({ props, defaultTag: DEFAULT_GROUP_TAG, name: 'Switch.Group' })}
{render({
ourProps,
theirProps,
defaultTag: DEFAULT_GROUP_TAG,
name: 'Switch.Group',
})}
</GroupContext.Provider>
</LabelProvider>
</DescriptionProvider>
@@ -101,7 +109,7 @@ let SwitchRoot = forwardRefWithAs(function Switch<
},
ref: Ref<HTMLElement>
) {
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<HTMLButtonElement | null>(null)
@@ -136,7 +144,7 @@ let SwitchRoot = forwardRefWithAs(function Switch<
)
let slot = useMemo<SwitchRenderPropArg>(() => ({ 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',
@@ -146,7 +146,7 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
manual = false,
onChange,
selectedIndex = null,
...passThroughProps
...theirProps
} = props
const orientation = vertical ? 'vertical' : 'horizontal'
const activation = manual ? 'manual' : 'auto'
@@ -228,6 +228,10 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
let SSRCounter = useRef(0)
let ourProps = {
ref: tabsRef,
}
return (
<TabsSSRContext.Provider value={typeof window === 'undefined' ? SSRCounter : null}>
<TabsContext.Provider value={providerBag}>
@@ -244,7 +248,8 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
}}
/>
{render({
props: { ref: tabsRef, ...passThroughProps },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_TABS_TAG,
name: 'Tabs',
@@ -270,15 +275,17 @@ let List = forwardRefWithAs(function List<TTag extends ElementType = typeof DEFA
let listRef = useSyncRefs(ref)
let slot = { selectedIndex }
let propsWeControl = {
let theirProps = props
let ourProps = {
ref: listRef,
role: 'tablist',
'aria-orientation': orientation,
}
let passThroughProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_LIST_TAG,
name: 'Tabs.List',
@@ -377,7 +384,9 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
}, [])
let slot = useMemo(() => ({ 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<TTag extends ElementType = typeof DE
'aria-selected': selected,
tabIndex: selected ? 0 : -1,
}
let passThroughProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_TAB_TAG,
name: 'Tabs.Tab',
@@ -416,8 +425,12 @@ let Panels = forwardRefWithAs(function Panels<TTag extends ElementType = typeof
let slot = useMemo(() => ({ 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<TTag extends ElementType = typeof DE
SSRContext === null ? myIndex === selectedIndex : SSRContext.current++ === selectedIndex
let slot = useMemo(() => ({ selected }), [selected])
let propsWeControl = {
let theirProps = props
let ourProps = {
ref: panelRef,
id,
role: 'tabpanel',
@@ -470,10 +485,9 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
tabIndex: selected ? 0 : -1,
}
let passThroughProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_PANEL_TAG,
features: PanelRenderFeatures,
@@ -348,8 +348,8 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
}
}, [show, skip, state])
let propsWeControl = { ref: transitionRef }
let passthroughProps = rest
let theirProps = rest
let ourProps = { ref: transitionRef }
return (
<NestingContext.Provider value={nesting}>
@@ -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<TTag> & { show?: boolean; appear?: boolean }, ref: Ref<HTMLElement>) {
// @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<
<NestingContext.Provider value={nestingBag}>
<TransitionContext.Provider value={transitionBag}>
{render({
props: {
ourProps: {
...sharedProps,
as: Fragment,
children: (
<TransitionChild ref={transitionRef} {...sharedProps} {...passthroughProps} />
),
children: <TransitionChild ref={transitionRef} {...sharedProps} {...theirProps} />,
},
theirProps: {},
defaultTag: Fragment,
features: TransitionChildRenderFeatures,
visible: state === TreeStates.Visible,
@@ -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<TTag>, ref: Ref<HTMLElement>) {
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',
@@ -19,7 +19,8 @@ describe('Default functionality', () => {
return (
<div data-testid="wrapper">
{render({
props,
ourProps: {},
theirProps: props,
slot,
defaultTag: 'div',
name: 'Dummy',
@@ -80,7 +81,8 @@ describe('Default functionality', () => {
return (
<div data-testid="wrapper">
{render({
props: { ...props, ref },
ourProps: { ref },
theirProps: props,
slot,
defaultTag: 'div',
name: 'OtherDummy',
@@ -323,7 +325,8 @@ describe('Features.Static', () => {
return (
<div data-testid="wrapper">
{render({
props: rest,
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
@@ -428,7 +431,8 @@ describe('Features.RenderStrategy', () => {
return (
<div data-testid="wrapper">
{render({
props: rest,
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
@@ -455,7 +459,8 @@ describe('Features.Static | Features.RenderStrategy', () => {
return (
<div data-testid="wrapper">
{render({
props: rest,
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
+61 -42
View File
@@ -47,20 +47,24 @@ export type PropsForFeatures<T extends Features> = XOR<
>
export function render<TFeature extends Features, TTag extends ElementType, TSlot>({
props,
ourProps,
theirProps,
slot,
defaultTag,
features,
visible = true,
name,
}: {
props: Expand<Props<TTag, TSlot, any> & PropsForFeatures<TFeature>>
ourProps: Expand<Props<TTag, TSlot, any> & PropsForFeatures<TFeature>>
theirProps: Expand<Props<TTag, TSlot, any>>
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<TTag extends ElementType, TSlot>(
as: Component = tag,
children,
refName = 'ref',
...passThroughProps
...rest
} = omit(props, ['unmount', 'static'])
// This allows us to use `<HeadlessUIComponent as={MyComponent} refName="innerRef" />`
@@ -117,12 +121,12 @@ function _render<TTag extends ElementType, TSlot>(
| 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<TTag extends ElementType, TSlot>(
'',
`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<TTag extends ElementType, TSlot>(
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<TTag extends ElementType, TSlot>(
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:
*
* <Menu.Item> <button onClick={console.log} /> </Menu.Item>
*
* Our `Menu.Item` will have an internal `onClick`, if you passthrough an `onClick` to the actual
* `Menu.Item` component we will call it correctly. However, when we have an `onClick` on the actual
* first child, that one should _also_ be called (but before this implementation, it was just
* overriding the `onClick`). But it is only when we *render* that we have access to the existing
* props of this component.
*
* It's a bit hacky, and not that clean, but it is something internal and we have tests to rely on
* so that we can refactor this later (if needed).
*/
function mergeEventFunctions(
passThroughProps: Record<string, any>,
existingProps: Record<string, any>,
functionsToMerge: string[]
) {
let clone = Object.assign({}, passThroughProps)
for (let func of functionsToMerge) {
if (passThroughProps[func] !== undefined && existingProps[func] !== undefined) {
Object.assign(clone, {
[func](event: { defaultPrevented: boolean }) {
// Props we control
if (!event.defaultPrevented) passThroughProps[func](event)
function mergeProps(...listOfProps: Props<any, any>[]) {
if (listOfProps.length === 0) return {}
if (listOfProps.length === 1) return listOfProps[0]
// Existing props on the component
if (!event.defaultPrevented) existingProps[func](event)
},
})
let target: Props<any, any> = {}
let eventHandlers: Record<
string,
((event: { defaultPrevented: boolean }) => 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]
}
}
}
return clone
// 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 }) {
let handlers = eventHandlers[eventName]
for (let handler of handlers) {
if (event.defaultPrevented) return
handler(event)
}
},
})
}
return target
}
/**
@@ -342,7 +342,7 @@ export let Combobox = defineComponent({
)
return () => {
let { name, modelValue, disabled, ...passThroughProps } = props
let { name, modelValue, disabled, ...incomingProps } = props
let slot = {
open: comboboxState.value === ComboboxStates.Open,
disabled,
@@ -351,7 +351,7 @@ export let Combobox = defineComponent({
}
let renderConfiguration = {
props: omit(passThroughProps, ['onUpdate:modelValue']),
props: omit(incomingProps, ['onUpdate:modelValue']),
slot,
slots,
attrs,
@@ -402,10 +402,10 @@ export let ComboboxLabel = defineComponent({
disabled: api.disabled.value,
}
let propsWeControl = { id, ref: api.labelRef, onClick: handleClick }
let ourProps = { id, ref: api.labelRef, onClick: handleClick }
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -500,7 +500,7 @@ export let ComboboxButton = defineComponent({
open: api.comboboxState.value === ComboboxStates.Open,
disabled: api.disabled.value,
}
let propsWeControl = {
let ourProps = {
ref: api.buttonRef,
id,
type: type.value,
@@ -517,7 +517,7 @@ export let ComboboxButton = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -629,7 +629,7 @@ export let ComboboxInput = defineComponent({
return () => {
let slot = { open: api.comboboxState.value === ComboboxStates.Open }
let propsWeControl = {
let ourProps = {
'aria-controls': api.optionsRef.value?.id,
'aria-expanded': api.disabled ? undefined : api.comboboxState.value === ComboboxStates.Open,
'aria-activedescendant':
@@ -647,10 +647,10 @@ export let ComboboxInput = defineComponent({
tabIndex: 0,
ref: api.inputRef,
}
let passThroughProps = omit(props, ['displayValue'])
let incomingProps = omit(props, ['displayValue'])
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -709,7 +709,7 @@ export let ComboboxOptions = defineComponent({
return () => {
let slot = { open: api.comboboxState.value === ComboboxStates.Open }
let propsWeControl = {
let ourProps = {
'aria-activedescendant':
api.activeOptionIndex.value === null
? undefined
@@ -719,10 +719,10 @@ export let ComboboxOptions = defineComponent({
ref: api.optionsRef,
role: 'listbox',
}
let passThroughProps = omit(props, ['hold'])
let incomingProps = omit(props, ['hold'])
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -839,7 +839,7 @@ export let ComboboxOption = defineComponent({
return () => {
let { disabled } = props
let slot = { active: active.value, selected: selected.value, disabled }
let propsWeControl = {
let ourProps = {
id,
ref: internalOptionRef,
role: 'option',
@@ -859,7 +859,7 @@ export let ComboboxOption = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -78,8 +78,8 @@ export let Description = defineComponent({
return () => {
let { name = 'Description', slot = ref({}), props = {} } = context
let passThroughProps = myProps
let propsWeControl = {
let incomingProps = myProps
let ourProps = {
...Object.entries(props).reduce(
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),
{}
@@ -88,7 +88,7 @@ export let Description = defineComponent({
}
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot: slot.value,
attrs,
slots,
@@ -263,7 +263,7 @@ export let Dialog = defineComponent({
}
return () => {
let propsWeControl = {
let ourProps = {
// Manually passthrough the attributes, because Vue can't automatically pass
// it to the underlying div because of all the wrapper components below.
...attrs,
@@ -275,7 +275,7 @@ export let Dialog = defineComponent({
'aria-describedby': describedby.value,
onClick: handleClick,
}
let { open: _, initialFocus, ...passThroughProps } = props
let { open: _, initialFocus, ...incomingProps } = props
let slot = { open: dialogState.value === DialogStates.Open }
@@ -284,7 +284,7 @@ export let Dialog = defineComponent({
h(PortalGroup, { target: internalDialogRef.value }, () =>
h(ForcePortalRoot, { force: false }, () =>
render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -319,15 +319,15 @@ export let DialogOverlay = defineComponent({
}
return () => {
let propsWeControl = {
let ourProps = {
id,
'aria-hidden': true,
onClick: handleClick,
}
let passThroughProps = props
let incomingProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot: { open: api.dialogState.value === DialogStates.Open },
attrs,
slots,
@@ -354,11 +354,11 @@ export let DialogTitle = defineComponent({
})
return () => {
let propsWeControl = { id }
let passThroughProps = props
let ourProps = { id }
let incomingProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot: { open: api.dialogState.value === DialogStates.Open },
attrs,
slots,
@@ -118,9 +118,9 @@ export let Disclosure = defineComponent({
)
return () => {
let { defaultOpen: _, ...passThroughProps } = props
let { defaultOpen: _, ...incomingProps } = props
let slot = { open: disclosureState.value === DisclosureStates.Open, close: api.close }
return render({ props: passThroughProps, slot, slots, attrs, name: 'Disclosure' })
return render({ props: incomingProps, slot, slots, attrs, name: 'Disclosure' })
}
},
})
@@ -201,7 +201,7 @@ export let DisclosureButton = defineComponent({
return () => {
let slot = { open: api.disclosureState.value === DisclosureStates.Open }
let propsWeControl = isWithinPanel
let ourProps = isWithinPanel
? {
ref: internalButtonRef,
type: type.value,
@@ -223,7 +223,7 @@ export let DisclosureButton = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -260,10 +260,10 @@ export let DisclosurePanel = defineComponent({
return () => {
let slot = { open: api.disclosureState.value === DisclosureStates.Open, close: api.close }
let propsWeControl = { id: api.panelId, ref: api.panel }
let ourProps = { id: api.panelId, ref: api.panel }
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -25,11 +25,11 @@ export let FocusTrap = defineComponent({
return () => {
let slot = {}
let propsWeControl = { ref: container }
let { initialFocus, ...passThroughProps } = props
let ourProps = { ref: container }
let { initialFocus, ...incomingProps } = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -77,15 +77,15 @@ export let Label = defineComponent({
return () => {
let { name = 'Label', slot = {}, props = {} } = context
let { passive, ...passThroughProps } = myProps
let propsWeControl = {
let { passive, ...incomingProps } = myProps
let ourProps = {
...Object.entries(props).reduce(
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),
{}
),
id,
}
let allProps = { ...passThroughProps, ...propsWeControl }
let allProps = { ...incomingProps, ...ourProps }
// @ts-expect-error props are dynamic via context, some components will
// provide an onClick then we can delete it.
@@ -305,11 +305,11 @@ export let Listbox = defineComponent({
)
return () => {
let { name, modelValue, disabled, ...passThroughProps } = props
let { name, modelValue, disabled, ...incomingProps } = props
let slot = { open: listboxState.value === ListboxStates.Open, disabled }
let renderConfiguration = {
props: omit(passThroughProps, ['onUpdate:modelValue', 'horizontal']),
props: omit(incomingProps, ['onUpdate:modelValue', 'horizontal']),
slot,
slots,
attrs,
@@ -359,10 +359,10 @@ export let ListboxLabel = defineComponent({
open: api.listboxState.value === ListboxStates.Open,
disabled: api.disabled.value,
}
let propsWeControl = { id, ref: api.labelRef, onClick: handleClick }
let ourProps = { id, ref: api.labelRef, onClick: handleClick }
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -444,7 +444,7 @@ export let ListboxButton = defineComponent({
open: api.listboxState.value === ListboxStates.Open,
disabled: api.disabled.value,
}
let propsWeControl = {
let ourProps = {
ref: api.buttonRef,
id,
type: type.value,
@@ -461,7 +461,7 @@ export let ListboxButton = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -571,7 +571,7 @@ export let ListboxOptions = defineComponent({
return () => {
let slot = { open: api.listboxState.value === ListboxStates.Open }
let propsWeControl = {
let ourProps = {
'aria-activedescendant':
api.activeOptionIndex.value === null
? undefined
@@ -585,10 +585,10 @@ export let ListboxOptions = defineComponent({
tabIndex: 0,
ref: api.optionsRef,
}
let passThroughProps = props
let incomingProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -710,7 +710,7 @@ export let ListboxOption = defineComponent({
return () => {
let { disabled } = props
let slot = { active: active.value, selected: selected.value, disabled }
let propsWeControl = {
let ourProps = {
id,
ref: internalOptionRef,
role: 'option',
@@ -730,7 +730,7 @@ export let ListboxOption = defineComponent({
}
return render({
props: { ...omit(props, ['value', 'disabled']), ...propsWeControl },
props: { ...omit(props, ['value', 'disabled']), ...ourProps },
slot,
attrs,
slots,
@@ -1358,7 +1358,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
@@ -302,7 +302,7 @@ export let MenuButton = defineComponent({
return () => {
let slot = { open: api.menuState.value === MenuStates.Open }
let propsWeControl = {
let ourProps = {
ref: api.buttonRef,
id,
type: type.value,
@@ -315,7 +315,7 @@ export let MenuButton = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -443,7 +443,7 @@ export let MenuItems = defineComponent({
return () => {
let slot = { open: api.menuState.value === MenuStates.Open }
let propsWeControl = {
let ourProps = {
'aria-activedescendant':
api.activeItemIndex.value === null
? undefined
@@ -457,10 +457,10 @@ export let MenuItems = defineComponent({
ref: api.itemsRef,
}
let passThroughProps = props
let incomingProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -537,7 +537,7 @@ export let MenuItem = defineComponent({
return () => {
let { disabled } = props
let slot = { active: active.value, disabled }
let propsWeControl = {
let ourProps = {
id,
ref: internalItemRef,
role: 'menuitem',
@@ -552,7 +552,7 @@ export let MenuItem = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -379,7 +379,7 @@ export let PopoverButton = defineComponent({
return () => {
let slot = { open: api.popoverState.value === PopoverStates.Open }
let propsWeControl = isWithinPanel
let ourProps = isWithinPanel
? {
ref: elementRef,
type: type.value,
@@ -401,7 +401,7 @@ export let PopoverButton = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs: attrs,
slots: slots,
@@ -439,14 +439,14 @@ export let PopoverOverlay = defineComponent({
return () => {
let slot = { open: api.popoverState.value === PopoverStates.Open }
let propsWeControl = {
let ourProps = {
id,
'aria-hidden': true,
onClick: handleClick,
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -581,14 +581,14 @@ export let PopoverPanel = defineComponent({
close: api.close,
}
let propsWeControl = {
let ourProps = {
ref: api.panel,
id: api.panelId,
onKeydown: handleKeyDown,
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -656,10 +656,10 @@ export let PopoverGroup = defineComponent({
})
return () => {
let propsWeControl = { ref: groupRef }
let ourProps = { ref: groupRef }
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot: {},
attrs,
slots,
@@ -73,7 +73,7 @@ export let Portal = defineComponent({
return () => {
if (myTarget.value === null) return null
let propsWeControl = {
let ourProps = {
ref: element,
}
@@ -83,7 +83,7 @@ export let Portal = defineComponent({
Teleport,
{ to: myTarget.value },
render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot: {},
attrs,
slots,
@@ -116,9 +116,9 @@ export let PortalGroup = defineComponent({
provide(PortalGroupContext, api)
return () => {
let { target: _, ...passThroughProps } = props
let { target: _, ...incomingProps } = props
return render({ props: passThroughProps, slot: {}, attrs, slots, name: 'PortalGroup' })
return render({ props: incomingProps, slot: {}, attrs, slots, name: 'PortalGroup' })
}
},
})
@@ -191,9 +191,9 @@ export let RadioGroup = defineComponent({
let id = `headlessui-radiogroup-${useId()}`
return () => {
let { modelValue, disabled, name, ...passThroughProps } = props
let { modelValue, disabled, name, ...incomingProps } = props
let propsWeControl = {
let ourProps = {
ref: radioGroupRef,
id,
role: 'radiogroup',
@@ -203,7 +203,7 @@ export let RadioGroup = defineComponent({
}
let renderConfiguration = {
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot: {},
attrs,
slots,
@@ -290,7 +290,7 @@ export let RadioGroupOption = defineComponent({
}
return () => {
let passThroughProps = omit(props, ['value', 'disabled'])
let incomingProps = omit(props, ['value', 'disabled'])
let slot = {
checked: checked.value,
@@ -298,7 +298,7 @@ export let RadioGroupOption = defineComponent({
active: Boolean(state.value & OptionState.Active),
}
let propsWeControl = {
let ourProps = {
id,
ref: optionRef,
role: 'radio',
@@ -313,7 +313,7 @@ export let RadioGroupOption = defineComponent({
}
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -103,9 +103,9 @@ export let Switch = defineComponent({
}
return () => {
let { name, value, modelValue, ...passThroughProps } = props
let { name, value, modelValue, ...incomingProps } = props
let slot = { checked: modelValue }
let propsWeControl = {
let ourProps = {
id,
ref: switchRef,
role: 'switch',
@@ -120,7 +120,7 @@ export let Switch = defineComponent({
}
let renderConfiguration = {
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -176,14 +176,14 @@ export let TabList = defineComponent({
return () => {
let slot = { selectedIndex: api.selectedIndex.value }
let propsWeControl = {
let ourProps = {
role: 'tablist',
'aria-orientation': api.orientation.value,
}
let passThroughProps = props
let incomingProps = props
return render({
props: { ...passThroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot,
attrs,
slots,
@@ -281,7 +281,7 @@ export let Tab = defineComponent({
return () => {
let slot = { selected: selected.value }
let propsWeControl = {
let ourProps = {
ref: internalTabRef,
onKeydown: handleKeyDown,
onFocus: api.activation.value === 'manual' ? handleFocus : handleSelection,
@@ -297,7 +297,7 @@ export let Tab = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -354,7 +354,7 @@ export let TabPanel = defineComponent({
return () => {
let slot = { selected: selected.value }
let propsWeControl = {
let ourProps = {
ref: internalPanelRef,
id,
role: 'tabpanel',
@@ -363,7 +363,7 @@ export let TabPanel = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot,
attrs,
slots,
@@ -326,11 +326,11 @@ export let TransitionChild = defineComponent({
...rest
} = props
let propsWeControl = { ref: container }
let passthroughProps = rest
let ourProps = { ref: container }
let incomingProps = rest
return render({
props: { ...passthroughProps, ...propsWeControl },
props: { ...incomingProps, ...ourProps },
slot: {},
slots,
attrs,
@@ -416,7 +416,7 @@ export let TransitionRoot = defineComponent({
provide(TransitionContext, transitionBag)
return () => {
let passThroughProps = omit(props, ['show', 'appear', 'unmount'])
let incomingProps = omit(props, ['show', 'appear', 'unmount'])
let sharedProps = { unmount: props.unmount }
return render({
@@ -437,7 +437,7 @@ export let TransitionRoot = defineComponent({
onAfterLeave: () => emit('afterLeave'),
...attrs,
...sharedProps,
...passThroughProps,
...incomingProps,
},
slots.default
),
@@ -24,8 +24,8 @@ export let ForcePortalRoot = defineComponent({
provide(ForcePortalRootContext, props.force)
return () => {
let { force, ...passThroughProps } = props
return render({ props: passThroughProps, slot: {}, slots, attrs, name: 'ForcePortalRoot' })
let { force, ...incomingProps } = props
return render({ props: incomingProps, slot: {}, slots, attrs, name: 'ForcePortalRoot' })
}
},
})
@@ -8,7 +8,7 @@ export let VisuallyHidden = defineComponent({
},
setup(props, { slots, attrs }) {
return () => {
let propsWeControl = {
let ourProps = {
style: {
position: 'absolute',
width: 1,
@@ -23,7 +23,7 @@ export let VisuallyHidden = defineComponent({
}
return render({
props: { ...props, ...propsWeControl },
props: { ...props, ...ourProps },
slot: {},
attrs,
slots,
+5 -5
View File
@@ -81,12 +81,12 @@ function _render({
slots: Slots
name: string
}) {
let { as, ...passThroughProps } = omit(props, ['unmount', 'static'])
let { as, ...incomingProps } = omit(props, ['unmount', 'static'])
let children = slots.default?.(slot)
if (as === 'template') {
if (Object.keys(passThroughProps).length > 0 || Object.keys(attrs).length > 0) {
if (Object.keys(incomingProps).length > 0 || Object.keys(attrs).length > 0) {
let [firstChild, ...other] = children ?? []
if (!isValidElement(firstChild) || other.length > 0) {
@@ -96,7 +96,7 @@ function _render({
'',
`The current component <${name} /> is rendering a "template".`,
`However we need to passthrough the following props:`,
Object.keys(passThroughProps)
Object.keys(incomingProps)
.concat(Object.keys(attrs))
.map((line) => ` - ${line}`)
.join('\n'),
@@ -112,7 +112,7 @@ function _render({
)
}
return cloneVNode(firstChild, passThroughProps as Record<string, any>)
return cloneVNode(firstChild, incomingProps as Record<string, any>)
}
if (Array.isArray(children) && children.length === 1) {
@@ -122,7 +122,7 @@ function _render({
return children
}
return h(as, passThroughProps, children)
return h(as, incomingProps, children)
}
export function compact<T extends Record<any, any>>(object: T) {