Files
simple-react-app-kit/packages/overlays/src/drawer.tsx
T

127 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-09-28 23:13:44 +08:00
import { useTranslate } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import {
createDrawerHandle,
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "@workspace/ui/components/drawer"
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 { DrawerOptions } from "./types"
type DrawerRequestOptions = Omit<DrawerOptions<unknown>, "scope">
const drawerChannel = Symbol("overlay.drawer")
function getDrawerController(scope: OverlayScope) {
return scope.getChannel(drawerChannel, () => {
const handle = createDrawerHandle<OverlayRequest<DrawerRequestOptions>>()
return new OverlayController<DrawerRequestOptions, typeof handle>({
handle,
isDismissible: (options) => options.dismissible !== false,
scope,
})
})
}
export function openDrawer<Result = void>(options: DrawerOptions<Result>) {
const { scope = globalOverlayScope, ...drawerOptions } = options
return getDrawerController(scope).open<Result | undefined>(
drawerOptions as DrawerRequestOptions,
undefined
)
}
export interface DrawerHostProps {
scope?: OverlayScope
}
export function DrawerHost({ scope }: DrawerHostProps) {
const controller = useOverlayController(scope, getDrawerController)
const activeOptions = controller.getActive()?.options
return (
<Drawer<OverlayRequest<DrawerRequestOptions>>
disablePointerDismissal={activeOptions?.dismissible === false}
handle={controller.handle}
modal={activeOptions?.modal}
showSwipeHandle={activeOptions?.showSwipeHandle}
snapPoints={activeOptions?.snapPoints}
swipeDirection={activeOptions?.swipeDirection}
onOpenChange={(open, details) => {
if (!open) controller.handleCloseRequest(details)
}}
onOpenChangeComplete={(open) => {
if (!open) controller.handleCloseComplete()
}}
>
{({ payload }) =>
payload ? (
<DrawerContentView
key={payload.id}
options={payload.options}
onDismiss={controller.dismiss}
onResolve={(value) => controller.resolve(value)}
/>
) : null
}
</Drawer>
)
}
function DrawerContentView({
onDismiss,
onResolve,
options,
}: {
onDismiss: () => void
onResolve: (value: unknown) => void
options: DrawerRequestOptions
}) {
const t = useTranslate()
const dismissible = options.dismissible !== false
return (
<DrawerContent className={options.className}>
<DrawerHeader>
<DrawerTitle>{options.title}</DrawerTitle>
{options.description != null && (
<DrawerDescription>{options.description}</DrawerDescription>
)}
</DrawerHeader>
<div className="min-h-0 flex-1 overflow-auto p-4">
{renderOverlayContent(options.content, {
close: onResolve,
dismiss: onDismiss,
})}
</div>
{dismissible && (
<DrawerClose
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" />
</DrawerClose>
)}
</DrawerContent>
)
}
export type { DrawerOptions } from "./types"