Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward the ref to all components #1116

Merged
merged 3 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased - @headlessui/react]

- Nothing yet!
### Fixed

- Forward the `ref` to all components ([#1116](https://github.com/tailwindlabs/headlessui/pull/1116))

## [Unreleased - @headlessui/vue]

Expand Down
34 changes: 14 additions & 20 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -687,11 +687,13 @@ interface LabelRenderPropArg {
}
type LabelPropsWeControl = 'id' | 'ref' | 'onClick'

function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>
let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>,
ref: Ref<HTMLLabelElement>
) {
let [state] = useComboboxContext('Combobox.Label')
let id = `headlessui-combobox-label-${useId()}`
let labelRef = useSyncRefs(state.labelRef, ref)

let handleClick = useCallback(
() => state.inputRef.current?.focus({ preventScroll: true }),
Expand All @@ -702,14 +704,14 @@ function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
() => ({ open: state.comboboxState === ComboboxStates.Open, disabled: state.disabled }),
[state]
)
let propsWeControl = { ref: state.labelRef, id, onClick: handleClick }
let propsWeControl = { ref: labelRef, id, onClick: handleClick }
return render({
props: { ...props, ...propsWeControl },
slot,
defaultTag: DEFAULT_LABEL_TAG,
name: 'Combobox.Label',
})
}
})

// ---

Expand Down Expand Up @@ -821,7 +823,7 @@ type ComboboxOptionPropsWeControl =
| 'onPointerMove'
| 'onMouseMove'

function Option<
let Option = forwardRefWithAs(function Option<
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
// TODO: One day we will be able to infer this type from the generic in Combobox itself.
// But today is not that day..
Expand All @@ -830,7 +832,8 @@ function Option<
props: Props<TTag, OptionRenderPropArg, ComboboxOptionPropsWeControl | 'value'> & {
disabled?: boolean
value: TType
}
},
ref: Ref<HTMLLIElement>
) {
let { disabled = false, value, ...passthroughProps } = props
let [state, dispatch] = useComboboxContext('Combobox.Option')
Expand All @@ -840,6 +843,7 @@ function Option<
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false
let selected = state.comboboxPropsRef.current.value === value
let bag = useRef<ComboboxOptionDataRef['current']>({ disabled, value })
let optionRef = useSyncRefs(ref)

useIsoMorphicEffect(() => {
bag.current.disabled = disabled
Expand Down Expand Up @@ -883,12 +887,7 @@ function Option<
document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' })
})
return d.dispose
}, [
id,
active,
state.comboboxState,
/* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex,
])
}, [id, active, state.comboboxState, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex])

let handleClick = useCallback(
(event: { preventDefault: Function }) => {
Expand Down Expand Up @@ -925,6 +924,7 @@ function Option<

let propsWeControl = {
id,
ref: optionRef,
role: 'option',
tabIndex: disabled === true ? undefined : -1,
'aria-disabled': disabled === true ? true : undefined,
Expand All @@ -944,14 +944,8 @@ function Option<
defaultTag: DEFAULT_OPTION_TAG,
name: 'Combobox.Option',
})
}
})

// ---

export let Combobox = Object.assign(ComboboxRoot, {
Input,
Button,
Label,
Options,
Option,
})
export let Combobox = Object.assign(ComboboxRoot, { Input, Button, Label, Options, Option })
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import React, {
// Types
ElementType,
ReactNode,
Ref,
} from 'react'

import { Props } from '../../types'
import { useId } from '../../hooks/use-id'
import { render } from '../../utils/render'
import { forwardRefWithAs, render } from '../../utils/render'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useSyncRefs } from '../../hooks/use-sync-refs'

// ---

Expand Down Expand Up @@ -86,24 +88,23 @@ export function useDescriptions(): [
// ---

let DEFAULT_DESCRIPTION_TAG = 'p' as const
interface DescriptionRenderPropArg {}
type DescriptionPropsWeControl = 'id'

export function Description<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG>(
props: Props<TTag, DescriptionRenderPropArg, DescriptionPropsWeControl>
) {
export let Description = forwardRefWithAs(function Description<
TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG
>(props: Props<TTag, {}, 'id'>, ref: Ref<HTMLParagraphElement>) {
let context = useDescriptionContext()
let id = `headlessui-description-${useId()}`
let descriptionRef = useSyncRefs(ref)

useIsoMorphicEffect(() => context.register(id), [id, context.register])

let passThroughProps = props
let propsWeControl = { ...context.props, id }
let propsWeControl = { ref: descriptionRef, ...context.props, id }

return render({
props: { ...passThroughProps, ...propsWeControl },
slot: context.slot || {},
defaultTag: DEFAULT_DESCRIPTION_TAG,
name: context.name || 'Description',
})
}
})
12 changes: 7 additions & 5 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ DialogContext.displayName = 'DialogContext'
function useDialogContext(component: string) {
let context = useContext(DialogContext)
if (context === null) {
let err = new Error(`<${component} /> is missing a parent <${Dialog.displayName} /> component.`)
let err = new Error(`<${component} /> is missing a parent <Dialog /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useDialogContext)
throw err
}
Expand Down Expand Up @@ -394,12 +394,14 @@ interface TitleRenderPropArg {
}
type TitlePropsWeControl = 'id'

function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
props: Props<TTag, TitleRenderPropArg, TitlePropsWeControl>
let Title = forwardRefWithAs(function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
props: Props<TTag, TitleRenderPropArg, TitlePropsWeControl>,
ref: Ref<HTMLHeadingElement>
) {
let [{ dialogState, setTitleId }] = useDialogContext('Dialog.Title')

let id = `headlessui-dialog-title-${useId()}`
let titleRef = useSyncRefs(ref)

useEffect(() => {
setTitleId(id)
Expand All @@ -414,12 +416,12 @@ function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
let passthroughProps = props

return render({
props: { ...passthroughProps, ...propsWeControl },
props: { ref: titleRef, ...passthroughProps, ...propsWeControl },
slot,
defaultTag: DEFAULT_TITLE_TAG,
name: 'Dialog.Title',
})
}
})

// ---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ DisclosureContext.displayName = 'DisclosureContext'
function useDisclosureContext(component: string) {
let context = useContext(DisclosureContext)
if (context === null) {
let err = new Error(`<${component} /> is missing a parent <${Disclosure.name} /> component.`)
let err = new Error(`<${component} /> is missing a parent <Disclosure /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useDisclosureContext)
throw err
}
Expand All @@ -118,7 +118,7 @@ DisclosureAPIContext.displayName = 'DisclosureAPIContext'
function useDisclosureAPIContext(component: string) {
let context = useContext(DisclosureAPIContext)
if (context === null) {
let err = new Error(`<${component} /> is missing a parent <${Disclosure.name} /> component.`)
let err = new Error(`<${component} /> is missing a parent <Disclosure /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useDisclosureAPIContext)
throw err
}
Expand All @@ -144,14 +144,18 @@ interface DisclosureRenderPropArg {
close(focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>): void
}

export function Disclosure<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
let DisclosureRoot = forwardRefWithAs(function Disclosure<
TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG
>(
props: Props<TTag, DisclosureRenderPropArg> & {
defaultOpen?: boolean
}
},
ref: Ref<TTag>
) {
let { defaultOpen = false, ...passthroughProps } = props
let buttonId = `headlessui-disclosure-button-${useId()}`
let panelId = `headlessui-disclosure-panel-${useId()}`
let disclosureRef = useSyncRefs(ref)

let reducerBag = useReducer(stateReducer, {
disclosureState: defaultOpen ? DisclosureStates.Open : DisclosureStates.Closed,
Expand Down Expand Up @@ -198,7 +202,7 @@ export function Disclosure<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_
})}
>
{render({
props: passthroughProps,
props: { ref: disclosureRef, ...passthroughProps },
slot,
defaultTag: DEFAULT_DISCLOSURE_TAG,
name: 'Disclosure',
Expand All @@ -207,7 +211,7 @@ export function Disclosure<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_
</DisclosureAPIContext.Provider>
</DisclosureContext.Provider>
)
}
})

// ---

Expand Down Expand Up @@ -387,5 +391,4 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE

// ---

Disclosure.Button = Button
Disclosure.Panel = Panel
export let Disclosure = Object.assign(DisclosureRoot, { Button, Panel })
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,37 @@ import {
// Types
ElementType,
MutableRefObject,
Ref,
} from 'react'

import { Props } from '../../types'
import { render } from '../../utils/render'
import { forwardRefWithAs, render } from '../../utils/render'
import { useFocusTrap, Features as FocusTrapFeatures } from '../../hooks/use-focus-trap'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { useSyncRefs } from '../../hooks/use-sync-refs'

let DEFAULT_FOCUS_TRAP_TAG = 'div' as const

export function FocusTrap<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
props: Props<TTag> & { initialFocus?: MutableRefObject<HTMLElement | null> }
export let FocusTrap = forwardRefWithAs(function FocusTrap<
TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG
>(
props: Props<TTag> & { initialFocus?: MutableRefObject<HTMLElement | null> },
ref: Ref<HTMLElement>
) {
let container = useRef<HTMLElement | null>(null)
let focusTrapRef = useSyncRefs(container, ref)
let { initialFocus, ...passthroughProps } = props

let ready = useServerHandoffComplete()
useFocusTrap(container, ready ? FocusTrapFeatures.All : FocusTrapFeatures.None, { initialFocus })

let propsWeControl = {
ref: container,
ref: focusTrapRef,
}

return render({
props: { ...passthroughProps, ...propsWeControl },
defaultTag: DEFAULT_FOCUS_TRAP_TAG,
name: 'FocusTrap',
})
}
})
20 changes: 12 additions & 8 deletions packages/@headlessui-react/src/components/label/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import React, {
// Types
ElementType,
ReactNode,
Ref,
} from 'react'

import { Props } from '../../types'
import { useId } from '../../hooks/use-id'
import { render } from '../../utils/render'
import { forwardRefWithAs, render } from '../../utils/render'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useSyncRefs } from '../../hooks/use-sync-refs'

// ---

Expand Down Expand Up @@ -77,21 +79,23 @@ export function useLabels(): [string | undefined, (props: LabelProviderProps) =>
// ---

let DEFAULT_LABEL_TAG = 'label' as const
interface LabelRenderPropArg {}
type LabelPropsWeControl = 'id'

export function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl> & {
export let Label = forwardRefWithAs(function Label<
TTag extends ElementType = typeof DEFAULT_LABEL_TAG
>(
props: Props<TTag, {}, 'id'> & {
passive?: boolean
}
},
ref: Ref<HTMLLabelElement>
) {
let { passive = false, ...passThroughProps } = props
let context = useLabelContext()
let id = `headlessui-label-${useId()}`
let labelRef = useSyncRefs(ref)

useIsoMorphicEffect(() => context.register(id), [id, context.register])

let propsWeControl = { ...context.props, id }
let propsWeControl = { ref: labelRef, ...context.props, id }

let allProps = { ...passThroughProps, ...propsWeControl }
// @ts-expect-error props are dynamic via context, some components will
Expand All @@ -104,4 +108,4 @@ export function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
defaultTag: DEFAULT_LABEL_TAG,
name: context.name || 'Label',
})
}
})
Loading