import type { JSX } from 'solid-js'; import { createComponent, createEffect, createUniqueId, mergeProps } from 'solid-js'; import { omitProps } from 'solid-use/props'; import type { DisclosureStateControlledOptions, DisclosureStateRenderProps, DisclosureStateUncontrolledOptions, } from '../../states/create-disclosure-state'; import { DisclosureStateProvider, createDisclosureState, } from '../../states/create-disclosure-state'; import createDynamic from '../../utils/create-dynamic'; import type { UnmountableProps } from '../../utils/create-unmountable'; import { createUnmountable } from '../../utils/create-unmountable'; import type { DynamicProps, HeadlessProps, ValidConstructor } from '../../utils/dynamic-prop'; import { createARIADisabledState, createDisabledState, createExpandedState, } from '../../utils/state-props'; import type { Prettify } from '../../utils/types'; import useFocusStartPoint from '../../utils/use-focus-start-point'; import { DialogContext } from './DialogContext'; import { DIALOG_TAG } from './tags'; export type DialogControlledBaseProps = Prettify< DisclosureStateControlledOptions & DisclosureStateRenderProps & UnmountableProps >; export type DialogControlledProps = HeadlessProps< T, DialogControlledBaseProps >; export type DialogUncontrolledBaseProps = Prettify< DisclosureStateUncontrolledOptions & DisclosureStateRenderProps & UnmountableProps >; export type DialogUncontrolledProps = HeadlessProps< T, DialogUncontrolledBaseProps >; export type DialogProps = | DialogControlledProps | DialogUncontrolledProps; function isDialogUncontrolled( props: DialogProps, ): props is DialogUncontrolledProps { return 'defaultOpen' in props; } /** * A modal dialog. While it is open the panel traps `Tab`, Escape * closes it, and focus returns to whatever opened it. Rendering it into a * portal is up to you. * * Renders a `
` by default. * * @see {@link https://github.com/lxsmnsyc/terracotta/blob/main/docs/components/dialog.md} */ export function Dialog(props: DialogProps): JSX.Element { const ownerID = createUniqueId(); const panelID = createUniqueId(); const titleID = createUniqueId(); const descriptionID = createUniqueId(); const fsp = useFocusStartPoint(); const state = createDisclosureState(props); createEffect(() => { if (state.isOpen()) { fsp.save(); } else { fsp.load(); } }); return createComponent(DialogContext.Provider, { value: { ownerID, panelID, titleID, descriptionID, }, get children() { return createUnmountable( props, () => state.isOpen(), () => createDynamic( () => props.as ?? ('div' as T), mergeProps( DIALOG_TAG, { id: ownerID, role: 'dialog', 'aria-modal': true, 'aria-labelledby': titleID, 'aria-describedby': descriptionID, get children() { return createComponent(DisclosureStateProvider, { state, get children() { return props.children; }, }); }, }, createDisabledState(() => state.disabled()), createARIADisabledState(() => state.disabled()), createExpandedState(() => state.isOpen()), isDialogUncontrolled(props) ? omitProps(props, [ 'as', 'children', 'defaultOpen', 'disabled', 'onChange', 'onClose', 'onOpen', 'unmount', ]) : omitProps(props, [ 'as', 'children', 'isOpen', 'disabled', 'onChange', 'onClose', 'onOpen', 'unmount', ]), ) as DynamicProps, ), ); }, }); }