Files
headlessui/packages/@headlessui-react/src/components/description/description.tsx
T
Robin Malfait 00cc8c50e3 Add Alert & RadioGroup components (#274)
* add Alert component

* expose Alert

* rename forgotten FLYOUT to POPOVER

* use PopoverRenderPropArg

* organize imports in a consistent way

* ensure Portals behave as expected

Portals can be nested from a React perspective, however in the DOM they
are rendered as siblings, this is mostly fine.

However, when they are rendered inside a Dialog, the Dialog itself is
marked with `role="modal"` which makes all the other content inert. This
means that rendering Menu.Items in a Portal or an Alert in a portal
makes it non-interactable. Alerts are not even announced.

To fix  this, we ensure that we make the `root` of the Portal the actual
dialog. This allows you to still interact with it, because an open modal
is the "root" for the assistive technology.

But there is a catch, a Dialog in a Dialog *can* render as a sibling,
because you force the focus into the new Dialog. So we also ensured that
Dialogs are always rendered in the portal root, and not inside another
Dialog.

* add dialog with alert example

* add internal Description component

* add internal Label component

* add RadioGroup component

* expose RadioGroup

* add RadioGroup example

* ensure to include tha RadioGroup.Option own id

* update changelog

* split documentation
2021-04-02 15:55:40 +02:00

87 lines
2.2 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
// Types
ElementType,
ReactNode,
} from 'react'
import { Props } from '../../types'
import { useId } from '../../hooks/use-id'
import { render } from '../../utils/render'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
// ---
let DescriptionContext = createContext<{ register(value: string): () => void }>({
register() {
return () => {}
},
})
function useDescriptionContext() {
return useContext(DescriptionContext)
}
export function useDescriptions(): [
string | undefined,
(props: { children: ReactNode }) => 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: { children: ReactNode }) {
let register = useCallback((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 }), [register])
return (
<DescriptionContext.Provider value={contextBag}>
{props.children}
</DescriptionContext.Provider>
)
}
}, [setDescriptionIds]),
]
}
// ---
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>
) {
let { register } = useDescriptionContext()
let id = `headlessui-description-${useId()}`
useIsoMorphicEffect(() => register(id), [id, register])
let passThroughProps = props
let propsWeControl = { id }
let bag = useMemo<DescriptionRenderPropArg>(() => ({}), [])
return render({ ...passThroughProps, ...propsWeControl }, bag, DEFAULT_DESCRIPTION_TAG)
}