00cc8c50e3
* 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
87 lines
2.2 KiB
TypeScript
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)
|
|
}
|