Anchored Sheet

Categorystyled
ArchitectureReact Client Component
Dependencies

Usage

import { useState } from "react";

import { Button } from "@/components/react/styled/button";
import { AnchoredSheet } from "@/components/react/styled/anchoredSheet";
import { DemoWrapper } from "@/demos/react/demoWrapper";

export const AnchoredSheetDemo = ({ code }: { code: string }) => {
  const [state, setState] = useState(false);

  return (
    <DemoWrapper code={code}>
      <Button onClick={setState.bind(null, true)}>Open Anchored Sheet</Button>
      <AnchoredSheet title="Anchored Sheet" state={state} setState={setState}>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae commodi
        inventore amet nisi ab! Itaque quas numquam repellendus, quo aliquid
        nulla minus quos recusandae illo, hic distinctio nihil maxime.
        Voluptatum. Blanditiis inventore expedita et aliquam obcaecati dicta
        perspiciatis similique temporibus officia! Obcaecati minus aut
        repudiandae, quasi quaerat fugit voluptates nam odit voluptatem est et
        sit quo pariatur iste reiciendis sapiente. Eius possimus ex molestiae
        sed consequatur ut voluptates numquam earum saepe dolor eum odio nostrum
        repellendus, tempore accusantium, quod repudiandae ratione non facere?
        Ab laborum ut, praesentium accusantium qui minus.
      </AnchoredSheet>
    </DemoWrapper>
  );
};

Install Instructions

Verifying dependencies

Make sure the following dependencies are satisfied:

  • dialog
  • useTouchDialogDrag

Copy and paste the code into that file

import { Suspense, lazy } from "react";

// CVA
import { cva } from "class-variance-authority";

// Sub-Component
const Dialog = lazy(() =>
  import("@/components/react/primitive/dialog").then((module) => ({
    default: module.Dialog,
  }))
);
import { Button } from "@/components/react/styled/button";

// Types
import type { DialogProps } from "@/components/react/primitive/dialog";

// Icon
import { XMarkIcon } from "@heroicons/react/24/solid";

// Hooks
import { useTouchDialogDrag } from "@/hooks/react/useTouchDialogDrag";

export interface AnchoredSheetProps
  extends Omit<DialogProps, "className" | "ref"> {
  title: string;
}

const anchoredSheetClassNames = cva([
  "opacity-100",
  "motion-reduce:opacity-0",
  "w-[100%]",
  "top-[calc(10dvh_+_env(safe-area-inset-top)_+_env(safe-area-inset-bottom))]",
  "h-[calc(90dvh_-_env(safe-area-inset-top)_-_env(safe-area-inset-bottom))]",
  "left-0",
  "bottom-auto",
  "p-6",
  "md:p-12",
  "bg-neutral-100",
  "dark:bg-neutral-900",
  "rounded-t-3xl",
  "justify-start",
  "motion-safe:translate-y-[100%]",
  "data-[display=true]:translate-y-0",
  "md:w-auto",
  "md:h-[max(400px,_40vh)]",
  "md:rounded-3xl",
  "md:aspect-video",
  "md:top-[50%]",
  "md:left-[50%]",
  "md:bottom-auto",
  "md:m-0",
  "md:translate-y-[-50%]",
  "md:transition-all",
  "md:translate-x-[-50%]",
  "md:data-[display=false]:scale-105",
  "md:data-[display=false]:opacity-0",
  "md:data-[display=true]:opacity-1",
  "md:data-[display=true]:translate-y-[-50%]",
  "md:data-[display=true]:blur-0",
  "md:data-[display=false]:blur-sm",
  "md:duration-[var(--transition-duration)]",
  "md:border",
  "md:dark:border-neutral-700",
  "md:border-neutral-300",
  "standalone:iphone-portrait:rounded-t-[3rem]",
  "standalone:iphone-portrait:pl-[calc(theme(space.6)_-_env(safe-area-inset-left))]",
  "standalone:iphone-portrait:pr-[calc(theme(space.6)_-_env(safe-area-inset-right))]",
  "standalone:iphone-portrait:pb-[calc(theme(space.6)_-_env(safe-area-inset-bottom))]",
  "[box-shadow:_0_75vh_0_50vh_theme(colors.neutral.100)]",
  "dark:[box-shadow:_0_75vh_0_50vh_theme(colors.neutral.900)]",
  "md:[box-shadow:_none_!important]",
]);

/**
 * Anchored Sheets provide an elegant way to display information and actions
 */
export const AnchoredSheet = ({
  children,
  title,
  setState,
  ...props
}: AnchoredSheetProps): JSX.Element => {
  const { ref, onTouchEnd, onTouchMove } = useTouchDialogDrag({
    onClose: setState?.bind(null, false),
  });

  return (
    <Suspense fallback={null}>
      <Dialog
        ref={ref}
        onTouchEnd={onTouchEnd}
        onTouchMove={onTouchMove}
        className={anchoredSheetClassNames()}
        transitionDuration={350}
        setState={setState}
        {...props}
      >
        <header
          data-draggable={true}
          className="mb-6 flex justify-end md:justify-between align-middle items-center w-[100%] relative"
        >
          <p
            data-draggable={true}
            className="absolute md:static top-[50%] left-[50%] md:top-auto md:left-auto translate-x-[-50%] translate-y-[-50%] md:translate-x-0 md:translate-y-0 p-0 m-0 text-center text-xl font-semibold"
          >
            {title}
          </p>
          <Button
            square={true}
            onClick={() => setState && setState(false)}
            className="rounded-[100%]"
          >
            <XMarkIcon className="w-4 h-4" />
          </Button>
        </header>
        {children && (
          <div
            data-draggable={true}
            data-scrollable={true}
            className="max-h-[100%] w-[100%] overflow-y-auto md:grow md:flex md:justify-center md:align-bottom md:items-end"
          >
            {children}
          </div>
        )}
      </Dialog>
    </Suspense>
  );
};

Loading...