8e93cd0630
* export component interfaces, and mark them as internal This is not ideal because we don't want these to be public. However, if you are creating components on top of Headless UI, the TypeScript compiler needs access to them. So now they are public in a sense, but you shouldn't be interacting with them directly. Co-authored-by: Jordan Pittman <jordan@cryptica.me> * Update changelog --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
129 lines
3.4 KiB
TypeScript
129 lines
3.4 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
|
|
// Types
|
|
ElementType,
|
|
ReactNode,
|
|
Ref,
|
|
} from 'react'
|
|
|
|
import { Props } from '../../types'
|
|
import { useId } from '../../hooks/use-id'
|
|
import { forwardRefWithAs, HasDisplayName, RefProp, render } from '../../utils/render'
|
|
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
|
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
|
import { useEvent } from '../../hooks/use-event'
|
|
|
|
// ---
|
|
|
|
interface SharedData {
|
|
slot?: {}
|
|
name?: string
|
|
props?: {}
|
|
}
|
|
|
|
let DescriptionContext = createContext<
|
|
({ register(value: string): () => void } & SharedData) | null
|
|
>(null)
|
|
|
|
function useDescriptionContext() {
|
|
let context = useContext(DescriptionContext)
|
|
if (context === null) {
|
|
let err = new Error(
|
|
'You used a <Description /> component, but it is not inside a relevant parent.'
|
|
)
|
|
if (Error.captureStackTrace) Error.captureStackTrace(err, useDescriptionContext)
|
|
throw err
|
|
}
|
|
return context
|
|
}
|
|
|
|
interface DescriptionProviderProps extends SharedData {
|
|
children: ReactNode
|
|
}
|
|
|
|
export function useDescriptions(): [
|
|
string | undefined,
|
|
(props: DescriptionProviderProps) => JSX.Element
|
|
] {
|
|
let [descriptionIds, setDescriptionIds] = useState<string[]>([])
|
|
|
|
return [
|
|
// The actual id's as string or undefined
|
|
descriptionIds.length > 0 ? descriptionIds.join(' ') : undefined,
|
|
|
|
// The provider component
|
|
useMemo(() => {
|
|
return function DescriptionProvider(props: DescriptionProviderProps) {
|
|
let register = useEvent((value: string) => {
|
|
setDescriptionIds((existing) => [...existing, value])
|
|
|
|
return () =>
|
|
setDescriptionIds((existing) => {
|
|
let clone = existing.slice()
|
|
let idx = clone.indexOf(value)
|
|
if (idx !== -1) clone.splice(idx, 1)
|
|
return clone
|
|
})
|
|
})
|
|
|
|
let contextBag = useMemo(
|
|
() => ({ register, slot: props.slot, name: props.name, props: props.props }),
|
|
[register, props.slot, props.name, props.props]
|
|
)
|
|
|
|
return (
|
|
<DescriptionContext.Provider value={contextBag}>
|
|
{props.children}
|
|
</DescriptionContext.Provider>
|
|
)
|
|
}
|
|
}, [setDescriptionIds]),
|
|
]
|
|
}
|
|
|
|
// ---
|
|
|
|
let DEFAULT_DESCRIPTION_TAG = 'p' as const
|
|
|
|
export type DescriptionProps<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG> =
|
|
Props<TTag>
|
|
|
|
function DescriptionFn<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG>(
|
|
props: DescriptionProps<TTag>,
|
|
ref: Ref<HTMLParagraphElement>
|
|
) {
|
|
let internalId = useId()
|
|
let { id = `headlessui-description-${internalId}`, ...theirProps } = props
|
|
let context = useDescriptionContext()
|
|
let descriptionRef = useSyncRefs(ref)
|
|
|
|
useIsoMorphicEffect(() => context.register(id), [id, context.register])
|
|
|
|
let ourProps = { ref: descriptionRef, ...context.props, id }
|
|
|
|
return render({
|
|
ourProps,
|
|
theirProps,
|
|
slot: context.slot || {},
|
|
defaultTag: DEFAULT_DESCRIPTION_TAG,
|
|
name: context.name || 'Description',
|
|
})
|
|
}
|
|
|
|
// ---
|
|
export interface _internal_ComponentDescription extends HasDisplayName {
|
|
<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG>(
|
|
props: DescriptionProps<TTag> & RefProp<typeof DescriptionFn>
|
|
): JSX.Element
|
|
}
|
|
|
|
let DescriptionRoot = forwardRefWithAs(DescriptionFn) as unknown as _internal_ComponentDescription
|
|
|
|
export let Description = Object.assign(DescriptionRoot, {
|
|
//
|
|
})
|