Files
headlessui/packages/@headlessui-react/src/components/description/description.tsx
T
Jordan Pittman 8e93cd0630 Export component interfaces and mark them as internal (#2313)
* 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>
2023-08-31 11:38:18 -04:00

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, {
//
})