feat(overlays): add managed overlay package
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useTranslate } from "@workspace/i18n"
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
import {
|
||||
createDialogHandle,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@workspace/ui/components/dialog"
|
||||
import { XIcon } from "@workspace/ui/components/icon"
|
||||
|
||||
import { OverlayController, type OverlayRequest } from "./internal/controller"
|
||||
import { renderOverlayContent } from "./internal/render-content"
|
||||
import { useOverlayController } from "./internal/use-controller"
|
||||
import { overlayMessages } from "./messages"
|
||||
import { globalOverlayScope, type OverlayScope } from "./scope"
|
||||
import type { ModalOptions } from "./types"
|
||||
|
||||
type ModalRequestOptions = Omit<ModalOptions<unknown>, "scope">
|
||||
const modalChannel = Symbol("overlay.modal")
|
||||
|
||||
function getModalController(scope: OverlayScope) {
|
||||
return scope.getChannel(modalChannel, () => {
|
||||
const handle = createDialogHandle<OverlayRequest<ModalRequestOptions>>()
|
||||
return new OverlayController<ModalRequestOptions, typeof handle>({
|
||||
handle,
|
||||
isDismissible: (options) => options.dismissible !== false,
|
||||
scope,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function openModal<Result = void>(options: ModalOptions<Result>) {
|
||||
const { scope = globalOverlayScope, ...modalOptions } = options
|
||||
return getModalController(scope).open<Result | undefined>(
|
||||
modalOptions as ModalRequestOptions,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
export interface ModalHostProps {
|
||||
scope?: OverlayScope
|
||||
}
|
||||
|
||||
export function ModalHost({ scope }: ModalHostProps) {
|
||||
const controller = useOverlayController(scope, getModalController)
|
||||
const activeOptions = controller.getActive()?.options
|
||||
|
||||
return (
|
||||
<Dialog<OverlayRequest<ModalRequestOptions>>
|
||||
disablePointerDismissal={activeOptions?.dismissible === false}
|
||||
handle={controller.handle}
|
||||
modal={activeOptions?.modal}
|
||||
onOpenChange={(open, details) => {
|
||||
if (!open) controller.handleCloseRequest(details)
|
||||
}}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open) controller.handleCloseComplete()
|
||||
}}
|
||||
>
|
||||
{({ payload }) =>
|
||||
payload ? (
|
||||
<ModalContent
|
||||
key={payload.id}
|
||||
options={payload.options}
|
||||
onDismiss={controller.dismiss}
|
||||
onResolve={(value) => controller.resolve(value)}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function ModalContent({
|
||||
onDismiss,
|
||||
onResolve,
|
||||
options,
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
onResolve: (value: unknown) => void
|
||||
options: ModalRequestOptions
|
||||
}) {
|
||||
const t = useTranslate()
|
||||
const dismissible = options.dismissible !== false
|
||||
|
||||
return (
|
||||
<DialogContent className={options.className} showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{options.title}</DialogTitle>
|
||||
{options.description != null && (
|
||||
<DialogDescription>{options.description}</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
{renderOverlayContent(options.content, {
|
||||
close: onResolve,
|
||||
dismiss: onDismiss,
|
||||
})}
|
||||
{dismissible && (
|
||||
<DialogClose
|
||||
aria-label={t(overlayMessages.close)}
|
||||
onClick={onDismiss}
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="absolute end-4 top-4"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
</DialogClose>
|
||||
)}
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
export type { ModalOptions } from "./types"
|
||||
Reference in New Issue
Block a user