Floating Sheet
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
import { useState } from "react";
import { Button } from "@/components/react/styled/button";
import { FloatingSheet } from "@/components/react/styled/floatingSheet";
import { DemoWrapper } from "@/demos/react/demoWrapper";
export const FloatingSheetDemo = ({ code }: { code: string }) => {
const [state, setState] = useState(false);
return (
<DemoWrapper code={code}>
<Button onClick={setState.bind(null, true)}>Open Floating</Button>
<FloatingSheet
title="Floating Sheet"
subtitle="FloatingSheet Subtitle"
actions={{
accept: {
event: () => console.log("Accepted"),
label: "Accept",
},
dismiss: {
event: () => console.log("Dismissed"),
label: "Dismiss",
},
}}
state={state}
setState={setState}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias culpa
laudantium dolores rerum! Hic pariatur velit fugiat error, vitae
similique cupiditate ab, minima nesciunt minus exercitationem eaque,
accusamus laudantium magni?
</FloatingSheet>
</DemoWrapper>
);
};
Install Instructions
Verifying dependencies
Make sure the following dependencies are satisfied:
- dialog
- useTouchDialogDrag
Copy and paste the code into that file
import { lazy, Suspense } from "react";
// Sub-Component
import { Button } from "./button";
const Dialog = lazy(() =>
import("@/components/react/primitive/dialog").then((module) => ({
default: module.Dialog,
}))
);
// Types
import type { DialogProps } from "@/components/react/primitive/dialog";
// Icon
import { XMarkIcon } from "@heroicons/react/24/solid";
// CVA
import { cva } from "class-variance-authority";
// Hooks
import { useTouchDialogDrag } from "@/hooks/react/useTouchDialogDrag";
const floatingSheetClassNames = cva([
"flex-col",
"rounded-3xl",
"border",
"p-6",
"border-neutral-200",
"bg-neutral-100",
"dark:border-neutral-800",
"dark:bg-neutral-900",
"md:dark:border-neutral-700",
"md:border-neutral-300",
"opacity-100",
"motion-safe:translate-y-[calc(100%_+_theme(space.6))]",
"motion-reduce:opacity-0",
"data-[display=true]:opacity-100",
"bottom-3",
"top-auto",
"max-w-none",
"md:aspect-video",
"md:w-auto",
"md:m-0",
"md:left-[50%]",
"md:translate-x-[-50%]",
"md:max-h-[max(300px,_30vh)]",
"data-[display=true]:translate-y-0",
"left-[calc(theme(space.3)_+_env(safe-area-inset-left))]",
"right-[calc(theme(space.3)_+_env(safe-area-inset-right))]",
"w-[calc(100vw_-_theme(space.6)_-_env(safe-area-inset-left)-_env(safe-area-inset-right))]",
// ------------ PWA Safari styles ------------ //
"standalone:iphone-portrait:rounded-[3rem]",
"standalone:iphone-portrait:p-8",
"standalone:iphone-portrait:w-[calc(100vw_-_theme(space.10)_-_env(safe-area-inset-left)-_env(safe-area-inset-right))]",
"standalone:iphone-portrait:left-[calc(theme(space.5)_+_env(safe-area-inset-left))]",
"standalone:iphone-portrait:right-[calc(theme(space.5)_+_env(safe-area-inset-right))]",
"standalone:iphone-portrait:bottom-5",
"standalone:iphone-portrait:motion-safe:translate-y-[calc(100%_+_theme(space.8)]",
]);
type ActionEvent = {
event: () => void;
label: string;
};
export interface FloatingSheetProps
extends Omit<DialogProps, "className" | "ref"> {
title: string;
subtitle?: string;
actions?: {
accept?: ActionEvent;
dismiss?: ActionEvent;
};
}
/**
* Floating Sheets provide an elegant way to display information and actions
*
* @param {FloatingSheetProps} props
* @returns {JSX.Element}
*/
export const FloatingSheet = ({
children,
title,
subtitle,
actions,
setState,
...props
}: FloatingSheetProps): JSX.Element => {
const { onTouchEnd, onTouchMove, ref } = useTouchDialogDrag({
onClose: setState?.bind(null, false),
maxScroll: 300,
});
return (
<Suspense fallback={null}>
<Dialog
ref={ref}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
className={floatingSheetClassNames()}
transitionDuration={350}
setState={setState}
{...props}
>
<Button
square={true}
onClick={() => setState && setState(false)}
className="absolute top-6 right-6 rounded-[100%]"
>
<XMarkIcon className="w-4 h-4" />
</Button>
<header
data-draggable={true}
className="flex flex-col justify-center align-middle items-center"
>
<p
data-draggable={true}
className="font-semibold text-2xl p-0 mt-0 mb-2 only:mb-0 text-center"
>
{title}
</p>
{subtitle && (
<p
data-draggable={true}
className="p-0 m-0 text-center text-xs font-light opacity-75"
>
{subtitle}
</p>
)}
</header>
{children && <p className="my-4 p-0 text-sm opacity-75">{children}</p>}
{actions && (
<div data-draggable={true} className="pt-12 w-[100%] flex">
{actions.dismiss && (
<Button
className="grow"
variant="ghost"
onClick={() => {
setState && setState(false);
actions.dismiss?.event();
}}
>
{actions.dismiss.label}
</Button>
)}
{actions.accept && (
<Button
className="grow"
color="primary"
onClick={() => {
setState && setState(false);
actions.accept?.event();
}}
>
{actions.accept.label}
</Button>
)}
</div>
)}
</Dialog>
</Suspense>
);
};