Skip to content

Commit

Permalink
Properly merge incoming props (#1265)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
RobinMalfait committed Mar 22, 2022
1 parent 4f8c615 commit 3e19aa5
Show file tree
Hide file tree
Showing 37 changed files with 399 additions and 284 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 26 additions & 16 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -677,7 +679,7 @@ let Input = forwardRefWithAs(function Input<
[state]
)

let propsWeControl = {
let ourProps = {
ref: inputRef,
id,
role: 'combobox',
Expand All @@ -694,7 +696,8 @@ let Input = forwardRefWithAs(function Input<
}

return render({
props: { ...passThroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_INPUT_TAG,
name: 'Combobox.Input',
Expand Down Expand Up @@ -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),
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -946,7 +954,8 @@ let Options = forwardRefWithAs(function Options<
}

return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_OPTIONS_TAG,
features: OptionsRenderFeatures,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -1072,7 +1081,7 @@ let Option = forwardRefWithAs(function Option<
[active, selected, disabled]
)

let propsWeControl = {
let ourProps = {
id,
ref: optionRef,
role: 'option',
Expand All @@ -1092,7 +1101,8 @@ let Option = forwardRefWithAs(function Option<
}

return render({
props: { ...passthroughProps, ...propsWeControl },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_OPTION_TAG,
name: 'Combobox.Option',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 14 additions & 10 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -292,7 +292,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
[dialogState]
)

let propsWeControl = {
let ourProps = {
ref: dialogRef,
id,
role: 'dialog',
Expand All @@ -303,7 +303,6 @@ let DialogRoot = forwardRefWithAs(function Dialog<
event.stopPropagation()
},
}
let passthroughProps = rest

return (
<StackProvider
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -214,6 +214,10 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
[disclosureState, close]
)

let ourProps = {
ref: disclosureRef,
}

return (
<DisclosureContext.Provider value={reducerBag}>
<DisclosureAPIContext.Provider value={api}>
Expand All @@ -224,7 +228,8 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
})}
>
{render({
props: { ref: disclosureRef, ...passthroughProps },
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_DISCLOSURE_TAG,
name: 'Disclosure',
Expand Down Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
Expand Down
20 changes: 13 additions & 7 deletions packages/@headlessui-react/src/components/label/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 3e19aa5

Please sign in to comment.