07ba551e63
* add internal `ResetOpenClosedProvider`
This will allow us to reset the `OpenClosedProvider` and reset the
"boundary". This is important when we want to wrap a `Dialog` inside of
a `Transition` that exists in another component that is wrapped in a
transition itself.
This will be used in let's say a `DisclosurePanel`:
```tsx
<Disclosure> // OpenClosedProvider
<Transition>
<DisclosurePanel> // ResetOpenClosedProvider
<Dialog /> // Can safely wrap `<Dialog />` in `<Transition />`
</DisclosurePanel>
</Transition>
</Disclosure>
```
* use `ResetOpenClosedProvider` in `PopoverPanel` and `DisclosurePanel`
* add `transition` prop to `<Transition>` component
This prop allows us to enabled / disable the `Transition` functionality.
E.g.: expose the underlying data attributes.
But it will still setup a `Transition` boundary for coordinating the
`TransitionChild` components.
* always wrap `Dialog` in a `Transition` component
+ add `transition` props to the `Dialog`, `DialogPanel` and `DialogBackdrop`
This will allow us individually control the transition on each element,
but also setup the transition boundary on the `Dialog` for coordination
purposes.
* improve dialog playground example
* update built in transition playground example to use individual transition props
* speedup example transitions
* Add validations to DialogFn
This technically means most or all of them can be removed from InternalDialog but we can do that later
* Pass `unmount={false}` from the Dialog to the wrapping transition
* Only wrap Dialog in a Transition if it’s not `static`
I’m not 100% sure this is right but it seems like it might be given that `static` implies it’s always rendered.
* remove validations from `InternalDialog`
Already validated by `Dialog` itself
* use existing `usesOpenClosedState`
* reword comment
* remove flawed test
The reason this test is flawed and why it's safe to delete it:
This test opened the dialog, then clicked on an element outside of the
dialog to close it and prove that we correctly focused that new element
instead of going to the button that opened the dialog in the first
place.
This test used to work before marked the rest of the page as `inert`.
Right now we mark the rest of the page as `inert`, so running this in a
real browser means that we can't click or focus an element outside of
the `dialog` simply because the rest of the page is inert.
The reason it fails all of a sudden is that the introduction of
`<Transition>` around the `<Dialog>` by default purely delays the
mounting just enough to record different elements to try and restore
focus to.
That said, this test clicked outside of a dialog and focused that
element which can't work in a real browser because the element can't be
interacted with at all.
* update changelog
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
589 lines
17 KiB
TypeScript
589 lines
17 KiB
TypeScript
'use client'
|
|
|
|
// WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
|
|
import React, {
|
|
Fragment,
|
|
createContext,
|
|
createRef,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useReducer,
|
|
useRef,
|
|
type ContextType,
|
|
type ElementType,
|
|
type MutableRefObject,
|
|
type MouseEvent as ReactMouseEvent,
|
|
type Ref,
|
|
type RefObject,
|
|
} from 'react'
|
|
import { useEscape } from '../../hooks/use-escape'
|
|
import { useEvent } from '../../hooks/use-event'
|
|
import { useId } from '../../hooks/use-id'
|
|
import { useInertOthers } from '../../hooks/use-inert-others'
|
|
import { useIsTouchDevice } from '../../hooks/use-is-touch-device'
|
|
import { useOnDisappear } from '../../hooks/use-on-disappear'
|
|
import { useOutsideClick } from '../../hooks/use-outside-click'
|
|
import { useOwnerDocument } from '../../hooks/use-owner'
|
|
import { useRootContainers } from '../../hooks/use-root-containers'
|
|
import { useScrollLock } from '../../hooks/use-scroll-lock'
|
|
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
|
|
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
|
import { CloseProvider } from '../../internal/close-provider'
|
|
import { HoistFormFields } from '../../internal/form-fields'
|
|
import { ResetOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
|
|
import { ForcePortalRoot } from '../../internal/portal-force-root'
|
|
import type { Props } from '../../types'
|
|
import { match } from '../../utils/match'
|
|
import {
|
|
RenderFeatures,
|
|
forwardRefWithAs,
|
|
render,
|
|
type HasDisplayName,
|
|
type PropsForFeatures,
|
|
type RefProp,
|
|
} from '../../utils/render'
|
|
import {
|
|
Description,
|
|
useDescriptions,
|
|
type _internal_ComponentDescription,
|
|
} from '../description/description'
|
|
import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
|
|
import { Portal, PortalGroup, useNestedPortals } from '../portal/portal'
|
|
import { Transition, TransitionChild } from '../transition/transition'
|
|
|
|
enum DialogStates {
|
|
Open,
|
|
Closed,
|
|
}
|
|
|
|
interface StateDefinition {
|
|
titleId: string | null
|
|
panelRef: MutableRefObject<HTMLElement | null>
|
|
}
|
|
|
|
enum ActionTypes {
|
|
SetTitleId,
|
|
}
|
|
|
|
type Actions = { type: ActionTypes.SetTitleId; id: string | null }
|
|
|
|
let reducers: {
|
|
[P in ActionTypes]: (
|
|
state: StateDefinition,
|
|
action: Extract<Actions, { type: P }>
|
|
) => StateDefinition
|
|
} = {
|
|
[ActionTypes.SetTitleId](state, action) {
|
|
if (state.titleId === action.id) return state
|
|
return { ...state, titleId: action.id }
|
|
},
|
|
}
|
|
|
|
let DialogContext = createContext<
|
|
| [
|
|
{
|
|
dialogState: DialogStates
|
|
close(): void
|
|
setTitleId(id: string | null): void
|
|
},
|
|
StateDefinition,
|
|
]
|
|
| null
|
|
>(null)
|
|
DialogContext.displayName = 'DialogContext'
|
|
|
|
function useDialogContext(component: string) {
|
|
let context = useContext(DialogContext)
|
|
if (context === null) {
|
|
let err = new Error(`<${component} /> is missing a parent <Dialog /> component.`)
|
|
if (Error.captureStackTrace) Error.captureStackTrace(err, useDialogContext)
|
|
throw err
|
|
}
|
|
return context
|
|
}
|
|
|
|
function stateReducer(state: StateDefinition, action: Actions) {
|
|
return match(action.type, reducers, state, action)
|
|
}
|
|
|
|
// ---
|
|
|
|
let InternalDialog = forwardRefWithAs(function InternalDialog<
|
|
TTag extends ElementType = typeof DEFAULT_DIALOG_TAG,
|
|
>(props: DialogProps<TTag>, ref: Ref<HTMLElement>) {
|
|
let internalId = useId()
|
|
let {
|
|
id = `headlessui-dialog-${internalId}`,
|
|
open,
|
|
onClose,
|
|
initialFocus,
|
|
role = 'dialog',
|
|
autoFocus = true,
|
|
__demoMode = false,
|
|
...theirProps
|
|
} = props
|
|
|
|
let didWarnOnRole = useRef(false)
|
|
|
|
role = (function () {
|
|
if (role === 'dialog' || role === 'alertdialog') {
|
|
return role
|
|
}
|
|
|
|
if (!didWarnOnRole.current) {
|
|
didWarnOnRole.current = true
|
|
console.warn(
|
|
`Invalid role [${role}] passed to <Dialog />. Only \`dialog\` and and \`alertdialog\` are supported. Using \`dialog\` instead.`
|
|
)
|
|
}
|
|
|
|
return 'dialog'
|
|
})()
|
|
|
|
let usesOpenClosedState = useOpenClosed()
|
|
if (open === undefined && usesOpenClosedState !== null) {
|
|
// Update the `open` prop based on the open closed state
|
|
open = (usesOpenClosedState & State.Open) === State.Open
|
|
}
|
|
|
|
let internalDialogRef = useRef<HTMLElement | null>(null)
|
|
let dialogRef = useSyncRefs(internalDialogRef, ref)
|
|
|
|
let ownerDocument = useOwnerDocument(internalDialogRef)
|
|
|
|
let dialogState = open ? DialogStates.Open : DialogStates.Closed
|
|
|
|
let [state, dispatch] = useReducer(stateReducer, {
|
|
titleId: null,
|
|
descriptionId: null,
|
|
panelRef: createRef(),
|
|
} as StateDefinition)
|
|
|
|
let close = useEvent(() => onClose(false))
|
|
|
|
let setTitleId = useEvent((id: string | null) => dispatch({ type: ActionTypes.SetTitleId, id }))
|
|
|
|
let ready = useServerHandoffComplete()
|
|
let enabled = ready ? dialogState === DialogStates.Open : false
|
|
let [portals, PortalWrapper] = useNestedPortals()
|
|
|
|
// We use this because reading these values during initial render(s)
|
|
// can result in `null` rather then the actual elements
|
|
// This doesn't happen when using certain components like a
|
|
// `<Dialog.Title>` because they cause the parent to re-render
|
|
let defaultContainer: RefObject<HTMLElement> = {
|
|
get current() {
|
|
return state.panelRef.current ?? internalDialogRef.current
|
|
},
|
|
}
|
|
|
|
let {
|
|
resolveContainers: resolveRootContainers,
|
|
mainTreeNodeRef,
|
|
MainTreeNode,
|
|
} = useRootContainers({
|
|
portals,
|
|
defaultContainers: [defaultContainer],
|
|
})
|
|
|
|
// When the `Dialog` is wrapped in a `Transition` (or another Headless UI component that exposes
|
|
// the OpenClosed state) then we get some information via context about its state. When the
|
|
// `Transition` is about to close, then the `State.Closing` state will be exposed. This allows us
|
|
// to enable/disable certain functionality in the `Dialog` upfront instead of waiting until the
|
|
// `Transition` is done transitioning.
|
|
let isClosing =
|
|
usesOpenClosedState !== null ? (usesOpenClosedState & State.Closing) === State.Closing : false
|
|
|
|
// Ensure other elements can't be interacted with
|
|
let inertOthersEnabled = __demoMode ? false : isClosing ? false : enabled
|
|
useInertOthers(inertOthersEnabled, {
|
|
allowed: useEvent(() => [
|
|
// Allow the headlessui-portal of the Dialog to be interactive. This
|
|
// contains the current dialog and the necessary focus guard elements.
|
|
internalDialogRef.current?.closest<HTMLElement>('[data-headlessui-portal]') ?? null,
|
|
]),
|
|
disallowed: useEvent(() => [
|
|
// Disallow the "main" tree root node
|
|
mainTreeNodeRef.current?.closest<HTMLElement>('body > *:not(#headlessui-portal-root)') ??
|
|
null,
|
|
]),
|
|
})
|
|
|
|
// Close Dialog on outside click
|
|
useOutsideClick(enabled, resolveRootContainers, (event) => {
|
|
event.preventDefault()
|
|
close()
|
|
})
|
|
|
|
// Handle `Escape` to close
|
|
useEscape(enabled, ownerDocument?.defaultView, (event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
// Ensure that we blur the current activeElement to prevent maintaining
|
|
// focus and potentially scrolling the page to the end (because the Dialog
|
|
// is rendered in a Portal at the end of the document.body and the browser
|
|
// tries to keep the focused element in view)
|
|
//
|
|
// Typically only happens in Safari.
|
|
if (
|
|
document.activeElement &&
|
|
'blur' in document.activeElement &&
|
|
typeof document.activeElement.blur === 'function'
|
|
) {
|
|
document.activeElement.blur()
|
|
}
|
|
|
|
close()
|
|
})
|
|
|
|
// Scroll lock
|
|
let scrollLockEnabled = __demoMode ? false : isClosing ? false : enabled
|
|
useScrollLock(scrollLockEnabled, ownerDocument, resolveRootContainers)
|
|
|
|
// Ensure we close the dialog as soon as the dialog itself becomes hidden
|
|
useOnDisappear(enabled, internalDialogRef, close)
|
|
|
|
let [describedby, DescriptionProvider] = useDescriptions()
|
|
|
|
let contextBag = useMemo<ContextType<typeof DialogContext>>(
|
|
() => [{ dialogState, close, setTitleId }, state],
|
|
[dialogState, state, close, setTitleId]
|
|
)
|
|
|
|
let slot = useMemo(
|
|
() => ({ open: dialogState === DialogStates.Open }) satisfies DialogRenderPropArg,
|
|
[dialogState]
|
|
)
|
|
|
|
let ourProps = {
|
|
ref: dialogRef,
|
|
id,
|
|
role,
|
|
tabIndex: -1,
|
|
'aria-modal': __demoMode ? undefined : dialogState === DialogStates.Open ? true : undefined,
|
|
'aria-labelledby': state.titleId,
|
|
'aria-describedby': describedby,
|
|
}
|
|
|
|
let shouldMoveFocusInside = !useIsTouchDevice()
|
|
let focusTrapFeatures = FocusTrapFeatures.None
|
|
|
|
if (enabled && !__demoMode) {
|
|
focusTrapFeatures |= FocusTrapFeatures.RestoreFocus
|
|
focusTrapFeatures |= FocusTrapFeatures.TabLock
|
|
|
|
if (autoFocus) {
|
|
focusTrapFeatures |= FocusTrapFeatures.AutoFocus
|
|
}
|
|
|
|
if (shouldMoveFocusInside) {
|
|
focusTrapFeatures |= FocusTrapFeatures.InitialFocus
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ResetOpenClosedProvider>
|
|
<ForcePortalRoot force={true}>
|
|
<Portal>
|
|
<DialogContext.Provider value={contextBag}>
|
|
<PortalGroup target={internalDialogRef}>
|
|
<ForcePortalRoot force={false}>
|
|
<DescriptionProvider slot={slot}>
|
|
<PortalWrapper>
|
|
<FocusTrap
|
|
initialFocus={initialFocus}
|
|
initialFocusFallback={internalDialogRef}
|
|
containers={resolveRootContainers}
|
|
features={focusTrapFeatures}
|
|
>
|
|
<CloseProvider value={close}>
|
|
{render({
|
|
ourProps,
|
|
theirProps,
|
|
slot,
|
|
defaultTag: DEFAULT_DIALOG_TAG,
|
|
features: DialogRenderFeatures,
|
|
visible: dialogState === DialogStates.Open,
|
|
name: 'Dialog',
|
|
})}
|
|
</CloseProvider>
|
|
</FocusTrap>
|
|
</PortalWrapper>
|
|
</DescriptionProvider>
|
|
</ForcePortalRoot>
|
|
</PortalGroup>
|
|
</DialogContext.Provider>
|
|
</Portal>
|
|
</ForcePortalRoot>
|
|
<HoistFormFields>
|
|
<MainTreeNode />
|
|
</HoistFormFields>
|
|
</ResetOpenClosedProvider>
|
|
)
|
|
})
|
|
|
|
// ---
|
|
|
|
let DEFAULT_DIALOG_TAG = 'div' as const
|
|
type DialogRenderPropArg = {
|
|
open: boolean
|
|
}
|
|
type DialogPropsWeControl = 'aria-describedby' | 'aria-labelledby' | 'aria-modal'
|
|
|
|
let DialogRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
|
|
|
|
export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> = Props<
|
|
TTag,
|
|
DialogRenderPropArg,
|
|
DialogPropsWeControl,
|
|
PropsForFeatures<typeof DialogRenderFeatures> & {
|
|
open?: boolean
|
|
onClose(value: boolean): void
|
|
initialFocus?: MutableRefObject<HTMLElement | null>
|
|
role?: 'dialog' | 'alertdialog'
|
|
autoFocus?: boolean
|
|
transition?: boolean
|
|
__demoMode?: boolean
|
|
}
|
|
>
|
|
|
|
function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
|
props: DialogProps<TTag>,
|
|
ref: Ref<HTMLElement>
|
|
) {
|
|
let { transition = false, open, ...rest } = props
|
|
|
|
// Validations
|
|
let usesOpenClosedState = useOpenClosed()
|
|
let hasOpen = props.hasOwnProperty('open') || usesOpenClosedState !== null
|
|
let hasOnClose = props.hasOwnProperty('onClose')
|
|
|
|
if (!hasOpen && !hasOnClose) {
|
|
throw new Error(
|
|
`You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.`
|
|
)
|
|
}
|
|
|
|
if (!hasOpen) {
|
|
throw new Error(
|
|
`You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
|
|
)
|
|
}
|
|
|
|
if (!hasOnClose) {
|
|
throw new Error(
|
|
`You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
|
|
)
|
|
}
|
|
|
|
if (!usesOpenClosedState && typeof props.open !== 'boolean') {
|
|
throw new Error(
|
|
`You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${props.open}`
|
|
)
|
|
}
|
|
|
|
if (typeof props.onClose !== 'function') {
|
|
throw new Error(
|
|
`You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${props.onClose}`
|
|
)
|
|
}
|
|
|
|
let inTransitionComponent = usesOpenClosedState !== null
|
|
if (!inTransitionComponent && open !== undefined && !rest.static) {
|
|
return (
|
|
<Transition show={open} transition={transition} unmount={rest.unmount}>
|
|
<InternalDialog ref={ref} {...rest} />
|
|
</Transition>
|
|
)
|
|
}
|
|
|
|
return <InternalDialog ref={ref} open={open} {...rest} />
|
|
}
|
|
|
|
// ---
|
|
|
|
let DEFAULT_PANEL_TAG = 'div' as const
|
|
type PanelRenderPropArg = {
|
|
open: boolean
|
|
}
|
|
|
|
export type DialogPanelProps<TTag extends ElementType = typeof DEFAULT_PANEL_TAG> = Props<
|
|
TTag,
|
|
PanelRenderPropArg,
|
|
never,
|
|
{ transition?: boolean }
|
|
>
|
|
|
|
function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
|
props: DialogPanelProps<TTag>,
|
|
ref: Ref<HTMLElement>
|
|
) {
|
|
let internalId = useId()
|
|
let { id = `headlessui-dialog-panel-${internalId}`, transition = false, ...theirProps } = props
|
|
let [{ dialogState }, state] = useDialogContext('Dialog.Panel')
|
|
let panelRef = useSyncRefs(ref, state.panelRef)
|
|
|
|
let slot = useMemo(
|
|
() => ({ open: dialogState === DialogStates.Open }) satisfies PanelRenderPropArg,
|
|
[dialogState]
|
|
)
|
|
|
|
// Prevent the click events inside the Dialog.Panel from bubbling through the React Tree which
|
|
// could submit wrapping <form> elements even if we portalled the Dialog.
|
|
let handleClick = useEvent((event: ReactMouseEvent) => {
|
|
event.stopPropagation()
|
|
})
|
|
|
|
let ourProps = {
|
|
ref: panelRef,
|
|
id,
|
|
onClick: handleClick,
|
|
}
|
|
|
|
let Wrapper = transition ? TransitionChild : Fragment
|
|
|
|
return (
|
|
<Wrapper>
|
|
{render({
|
|
ourProps,
|
|
theirProps,
|
|
slot,
|
|
defaultTag: DEFAULT_PANEL_TAG,
|
|
name: 'Dialog.Panel',
|
|
})}
|
|
</Wrapper>
|
|
)
|
|
}
|
|
|
|
// ---
|
|
|
|
let DEFAULT_BACKDROP_TAG = 'div' as const
|
|
type BackdropRenderPropArg = {
|
|
open: boolean
|
|
}
|
|
|
|
export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
|
|
TTag,
|
|
BackdropRenderPropArg,
|
|
never,
|
|
{ transition?: boolean }
|
|
>
|
|
|
|
function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
|
props: DialogBackdropProps<TTag>,
|
|
ref: Ref<HTMLElement>
|
|
) {
|
|
let { transition = false, ...theirProps } = props
|
|
let [{ dialogState }] = useDialogContext('Dialog.Backdrop')
|
|
|
|
let slot = useMemo(
|
|
() => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg,
|
|
[dialogState]
|
|
)
|
|
|
|
let ourProps = { ref }
|
|
|
|
let Wrapper = transition ? TransitionChild : Fragment
|
|
|
|
return (
|
|
<Wrapper>
|
|
{render({
|
|
ourProps,
|
|
theirProps,
|
|
slot,
|
|
defaultTag: DEFAULT_BACKDROP_TAG,
|
|
name: 'Dialog.Backdrop',
|
|
})}
|
|
</Wrapper>
|
|
)
|
|
}
|
|
|
|
// ---
|
|
|
|
let DEFAULT_TITLE_TAG = 'h2' as const
|
|
type TitleRenderPropArg = {
|
|
open: boolean
|
|
}
|
|
|
|
export type DialogTitleProps<TTag extends ElementType = typeof DEFAULT_TITLE_TAG> = Props<
|
|
TTag,
|
|
TitleRenderPropArg
|
|
>
|
|
|
|
function TitleFn<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
|
|
props: DialogTitleProps<TTag>,
|
|
ref: Ref<HTMLElement>
|
|
) {
|
|
let internalId = useId()
|
|
let { id = `headlessui-dialog-title-${internalId}`, ...theirProps } = props
|
|
let [{ dialogState, setTitleId }] = useDialogContext('Dialog.Title')
|
|
|
|
let titleRef = useSyncRefs(ref)
|
|
|
|
useEffect(() => {
|
|
setTitleId(id)
|
|
return () => setTitleId(null)
|
|
}, [id, setTitleId])
|
|
|
|
let slot = useMemo(
|
|
() => ({ open: dialogState === DialogStates.Open }) satisfies TitleRenderPropArg,
|
|
[dialogState]
|
|
)
|
|
|
|
let ourProps = { ref: titleRef, id }
|
|
|
|
return render({
|
|
ourProps,
|
|
theirProps,
|
|
slot,
|
|
defaultTag: DEFAULT_TITLE_TAG,
|
|
name: 'Dialog.Title',
|
|
})
|
|
}
|
|
|
|
// ---
|
|
|
|
export interface _internal_ComponentDialog extends HasDisplayName {
|
|
<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
|
props: DialogProps<TTag> & RefProp<typeof DialogFn>
|
|
): JSX.Element
|
|
}
|
|
|
|
export interface _internal_ComponentDialogPanel extends HasDisplayName {
|
|
<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
|
props: DialogPanelProps<TTag> & RefProp<typeof PanelFn>
|
|
): JSX.Element
|
|
}
|
|
|
|
export interface _internal_ComponentDialogBackdrop extends HasDisplayName {
|
|
<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
|
props: DialogBackdropProps<TTag> & RefProp<typeof BackdropFn>
|
|
): JSX.Element
|
|
}
|
|
|
|
export interface _internal_ComponentDialogTitle extends HasDisplayName {
|
|
<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
|
|
props: DialogTitleProps<TTag> & RefProp<typeof TitleFn>
|
|
): JSX.Element
|
|
}
|
|
|
|
export interface _internal_ComponentDialogDescription extends _internal_ComponentDescription {}
|
|
|
|
let DialogRoot = forwardRefWithAs(DialogFn) as _internal_ComponentDialog
|
|
export let DialogPanel = forwardRefWithAs(PanelFn) as _internal_ComponentDialogPanel
|
|
export let DialogBackdrop = forwardRefWithAs(BackdropFn) as _internal_ComponentDialogBackdrop
|
|
export let DialogTitle = forwardRefWithAs(TitleFn) as _internal_ComponentDialogTitle
|
|
/** @deprecated use `<Description>` instead of `<DialogDescription>` */
|
|
export let DialogDescription = Description as _internal_ComponentDialogDescription
|
|
|
|
export let Dialog = Object.assign(DialogRoot, {
|
|
/** @deprecated use `<DialogPanel>` instead of `<Dialog.Panel>` */
|
|
Panel: DialogPanel,
|
|
/** @deprecated use `<DialogTitle>` instead of `<Dialog.Title>` */
|
|
Title: DialogTitle,
|
|
/** @deprecated use `<Description>` instead of `<Dialog.Description>` */
|
|
Description: Description as _internal_ComponentDialogDescription,
|
|
})
|