138 lines
3.5 KiB
TypeScript
138 lines
3.5 KiB
TypeScript
import * as React from "react";
|
|
import { X } from "lucide-react";
|
|
|
|
export interface SheetProps {
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
export interface SheetTriggerProps {
|
|
children?: React.ReactNode;
|
|
asChild?: boolean;
|
|
}
|
|
|
|
export interface SheetContentProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export interface SheetHeaderProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export interface SheetTitleProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export interface SheetDescriptionProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
const SheetContext = React.createContext<{
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
}>({});
|
|
|
|
export function Sheet({ open, onOpenChange, children }: SheetProps): React.JSX.Element {
|
|
const contextValue: { open?: boolean; onOpenChange?: (open: boolean) => void } = {};
|
|
|
|
if (open !== undefined) {
|
|
contextValue.open = open;
|
|
}
|
|
|
|
if (onOpenChange !== undefined) {
|
|
contextValue.onOpenChange = onOpenChange;
|
|
}
|
|
|
|
return <SheetContext.Provider value={contextValue}>{children}</SheetContext.Provider>;
|
|
}
|
|
|
|
export function SheetTrigger({ children, asChild }: SheetTriggerProps): React.JSX.Element {
|
|
const { onOpenChange } = React.useContext(SheetContext);
|
|
|
|
if (asChild && React.isValidElement(children)) {
|
|
return React.cloneElement(children, {
|
|
onClick: () => onOpenChange?.(true),
|
|
} as React.HTMLAttributes<HTMLElement>);
|
|
}
|
|
|
|
return (
|
|
<button type="button" onClick={() => onOpenChange?.(true)}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function SheetContent({
|
|
children,
|
|
className = "",
|
|
}: SheetContentProps): React.JSX.Element | null {
|
|
const { open, onOpenChange } = React.useContext(SheetContext);
|
|
|
|
React.useEffect(() => {
|
|
if (!open) {
|
|
return;
|
|
}
|
|
|
|
const onKeyDown = (event: KeyboardEvent): void => {
|
|
if (event.key === "Escape") {
|
|
onOpenChange?.(false);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
return (): void => {
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [onOpenChange, open]);
|
|
|
|
if (!open) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50">
|
|
<button
|
|
type="button"
|
|
aria-label="Close sheet"
|
|
className="absolute inset-0 h-full w-full bg-black/50"
|
|
onClick={() => onOpenChange?.(false)}
|
|
/>
|
|
|
|
<div
|
|
className={`absolute inset-y-0 right-0 z-10 flex h-full w-full max-w-3xl flex-col border-l border-border bg-background p-6 shadow-xl ${className}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenChange?.(false)}
|
|
className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Close</span>
|
|
</button>
|
|
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SheetHeader({ children, className = "" }: SheetHeaderProps): React.JSX.Element {
|
|
return <div className={`space-y-1 pr-8 ${className}`}>{children}</div>;
|
|
}
|
|
|
|
export function SheetTitle({ children, className = "" }: SheetTitleProps): React.JSX.Element {
|
|
return <h2 className={`text-lg font-semibold ${className}`}>{children}</h2>;
|
|
}
|
|
|
|
export function SheetDescription({
|
|
children,
|
|
className = "",
|
|
}: SheetDescriptionProps): React.JSX.Element {
|
|
return <p className={`text-sm text-muted-foreground ${className}`}>{children}</p>;
|
|
}
|