brunner56's picture
implement app
0bfe2e3
'use client';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { cva, VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { CloseButton } from '../button';
import { cn, ComponentAnatomy, defineStyleAnatomy } from '../core/styling';
import { useState } from 'react';
function useDrawerBodyBehavior(id: string, open: boolean | undefined) {
const [openDrawers, setOpenDrawers] = useState<string[]>([]);
React.useEffect(() => {
const body = document.querySelector('body');
if (!body) return;
if (open) {
setOpenDrawers((prev) => [...prev, id]);
} else {
setOpenDrawers((prev) => {
let next = prev.filter((i) => i !== id);
return next;
});
}
return () => {
setOpenDrawers((prev) => prev.filter((i) => i !== id));
};
}, [open]);
}
/* -------------------------------------------------------------------------------------------------
* Anatomy
* -----------------------------------------------------------------------------------------------*/
export const DrawerAnatomy = defineStyleAnatomy({
overlay: cva([
'UI-Drawer__overlay',
'fixed inset-0 z-[50] bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
// "transition-opacity duration-300",
]),
content: cva(
[
'UI-Drawer__content',
'fixed z-50 w-full gap-4 bg-[--background] p-6 shadow-lg overflow-y-auto',
'transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-500 data-[state=open]:duration-500',
'focus:outline-none focus-visible:outline-none',
process.env.NEXT_PUBLIC_PLATFORM === 'desktop' && 'select-none',
],
{
variants: {
side: {
mangaReader:
'w-full inset-x-0 top-0 border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
top: 'w-full lg:w-[calc(100%_-_20px)] inset-x-0 top-0 border data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'w-full lg:w-[calc(100%_-_20px)] inset-x-0 bottom-0 border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full lg:h-[calc(100%_-_20px)] border data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left',
right:
'inset-y-0 right-0 h-full lg:h-[calc(100%_-_20px)] border data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
},
size: { sm: null, md: null, lg: null, xl: null, full: null },
},
defaultVariants: {
side: 'right',
size: 'md',
},
compoundVariants: [
{ size: 'sm', side: 'left', className: 'sm:max-w-sm' },
{ size: 'sm', side: 'right', className: 'sm:max-w-sm' },
{ size: 'md', side: 'left', className: 'sm:max-w-md' },
{ size: 'md', side: 'right', className: 'sm:max-w-md' },
{ size: 'lg', side: 'left', className: 'sm:max-w-2xl' },
{ size: 'lg', side: 'right', className: 'sm:max-w-2xl' },
{ size: 'xl', side: 'left', className: 'sm:max-w-5xl' },
{ size: 'xl', side: 'right', className: 'sm:max-w-5xl' },
/**/
{ size: 'full', side: 'top', className: 'h-dvh' },
{ size: 'full', side: 'bottom', className: 'h-dvh' },
],
}
),
close: cva(['UI-Drawer__close', 'absolute right-4 top-4']),
header: cva([
'UI-Drawer__header',
'flex flex-col space-y-1.5 text-center sm:text-left',
]),
footer: cva([
'UI-Drawer__footer',
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
]),
title: cva([
'UI-Drawer__title',
'text-xl font-semibold leading-none tracking-tight',
]),
description: cva(['UI-Drawer__description', 'text-sm text-[--muted]']),
});
/* -------------------------------------------------------------------------------------------------
* Drawer
* -----------------------------------------------------------------------------------------------*/
export type DrawerProps = Omit<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>,
'modal'
> &
Pick<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
| 'onOpenAutoFocus'
| 'onCloseAutoFocus'
| 'onEscapeKeyDown'
| 'onPointerDownCapture'
| 'onInteractOutside'
> &
VariantProps<typeof DrawerAnatomy.content> &
ComponentAnatomy<typeof DrawerAnatomy> & {
/**
* Interaction with outside elements will be enabled and other elements will be visible to screen readers.
*/
allowOutsideInteraction?: boolean;
/**
* The button that opens the modal
*/
trigger?: React.ReactElement;
/**
* Title of the modal
*/
title?: React.ReactNode;
/**
* An optional accessible description to be announced when the dialog is opened.
*/
description?: React.ReactNode;
/**
* Footer of the modal
*/
footer?: React.ReactNode;
/**
* Optional replacement for the default close button
*/
closeButton?: React.ReactElement;
/**
* Whether to hide the close button
*/
hideCloseButton?: boolean;
/**
* Portal container
*/
portalContainer?: HTMLElement;
mangaReader?: boolean;
};
export function Drawer(props: DrawerProps) {
const {
allowOutsideInteraction = false,
trigger,
title,
footer,
description,
children,
closeButton,
overlayClass,
contentClass,
closeClass,
headerClass,
footerClass,
titleClass,
descriptionClass,
hideCloseButton,
side = 'right',
size,
open,
// Content
onOpenAutoFocus,
onCloseAutoFocus,
onEscapeKeyDown,
onPointerDownCapture,
onInteractOutside,
portalContainer,
mangaReader,
...rest
} = props;
const id = React.useId();
useDrawerBodyBehavior(id, open);
return (
<DialogPrimitive.Root
modal={!allowOutsideInteraction}
open={open}
{...rest}
>
{trigger && (
<DialogPrimitive.Trigger asChild>{trigger}</DialogPrimitive.Trigger>
)}
<DialogPrimitive.Portal container={portalContainer}>
<DialogPrimitive.Overlay
className={cn(DrawerAnatomy.overlay(), overlayClass)}
/>
<DialogPrimitive.Content
className={cn(
DrawerAnatomy.content({
size,
side: mangaReader ? 'mangaReader' : side,
}),
// process.env.NEXT_PUBLIC_PLATFORM === "desktop" && "pt-12",
!mangaReader && 'lg:m-[10px] rounded-[--radius]',
contentClass
)}
style={{
marginTop:
process.env.NEXT_PUBLIC_PLATFORM === 'desktop' && !mangaReader
? '30px'
: undefined,
height:
process.env.NEXT_PUBLIC_PLATFORM === 'desktop' &&
!mangaReader &&
(side === 'left' || side === 'right')
? 'calc(100dvh - 50px)'
: undefined,
}}
onOpenAutoFocus={onOpenAutoFocus}
onCloseAutoFocus={onCloseAutoFocus}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownCapture={onPointerDownCapture}
onInteractOutside={onInteractOutside}
tabIndex={-1}
>
{!title && !description ? (
<VisuallyHidden>
<DialogPrimitive.Title>Drawer</DialogPrimitive.Title>
</VisuallyHidden>
) : (
<div className={cn(DrawerAnatomy.header(), headerClass)}>
<DialogPrimitive.Title
className={cn(
DrawerAnatomy.title(),
process.env.NEXT_PUBLIC_PLATFORM === 'desktop' && 'relative',
titleClass
)}
>
{title}
</DialogPrimitive.Title>
{description && (
<DialogPrimitive.Description
className={cn(DrawerAnatomy.description(), descriptionClass)}
>
{description}
</DialogPrimitive.Description>
)}
</div>
)}
{children}
{footer && (
<div className={cn(DrawerAnatomy.footer(), footerClass)}>
{footer}
</div>
)}
{!hideCloseButton && (
<DialogPrimitive.Close
className={cn(
DrawerAnatomy.close(),
// process.env.NEXT_PUBLIC_PLATFORM === "desktop" && "!top-10 !right-4",
closeClass
)}
asChild
>
{closeButton ? closeButton : <CloseButton />}
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
}
Drawer.displayName = 'Drawer';