Action Sheet
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
import { useState } from "react";
import { Button } from "@/components/react/styled/button";
import { ActionSheet } from "@/components/react/styled/actionSheet";
import { DemoWrapper } from "@/demos/react/demoWrapper";
export const ActionSheetDemo = ({ code }: { code: string }) => {
const [state, setState] = useState(false);
return (
<DemoWrapper code={code}>
<Button onClick={setState.bind(null, true)}>Open Action Sheet</Button>
<ActionSheet
title="Action Sheet"
actions={[
{ action: () => {}, key: "num0", title: "first action" },
{
action: () => {},
key: "num1",
title: "second action",
type: "danger",
},
]}
mainAction={{ action: () => {}, key: "main", title: "main action" }}
state={state}
setState={setState}
/>
</DemoWrapper>
);
};
Install Instructions
Verifying dependencies
Make sure the following dependencies are satisfied:
- dialog
- useTouchDialogDrag
Copy and paste the code into that file
import {
ComponentProps,
ComponentPropsWithoutRef,
lazy,
Suspense,
} from "react";
// CVA
import { cva } from "class-variance-authority";
// TW-Merge
import { twMerge } from "tailwind-merge";
// Sub-Components
const Dialog = lazy(() =>
import("@/components/react/primitive/dialog").then((module) => ({
default: module.Dialog,
}))
);
// Hooks
import { useTouchDialogDrag } from "@/hooks/react/useTouchDialogDrag";
// Types
import type { DialogProps } from "@/components/react/primitive/dialog";
interface Action {
title: string;
key: string;
action: () => void;
closeOnAction?: boolean;
type?: "danger" | "normal";
}
interface ActionSheetProps
extends Omit<
DialogProps,
"animateOpacity" | "animateTransform" | "children" | "ref"
> {
title: string;
mainAction?: Action;
actions: Action[];
}
const Group = ({
className = "",
...props
}: ComponentPropsWithoutRef<"div">) => {
return (
<div
className={
"backdrop-blur-md md:backdrop-blur-none md:bg-transparent md:dark:bg-transparent flex w-[100%] md:gap-2 md:rounded-none grow flex-col items-center justify-center rounded-3xl overflow-hidden bg-neutral-200/50 text-center dark:bg-neutral-800/50 " +
className
}
{...props}
/>
);
};
const ActionButton = ({
className = "",
...props
}: ComponentProps<"button">) => {
return (
<button
{...props}
className={twMerge(
[
"capitalize",
"transition-colors",
"rounded-none",
"fill-stale-900",
"w-[100%]",
"md:rounded-xl",
"md:dark:bg-neutral-700",
"md:bg-neutral-200",
"md:border-t-0",
"border-t",
"border-t-neutral-700/10",
"fill-neutral-900",
"p-3",
"text-sm",
"only:border-t-0",
"data-[type=danger]:text-red-600",
"data-[type=normal]:text-sky-500",
"dark:border-t-neutral-200/10",
"dark:fill-neutral-100",
"dark:text-neutral-100",
"dark:data-[type=danger]:text-red-400",
"dark:data-[type=normal]:text-blue-400",
"md:hover:bg-neutral-300",
"md:dark:hover:bg-neutral-600",
"md:py-2",
].join(" "),
className
)}
/>
);
};
const actionSheetStyles = cva([
"not-prose",
"md:shadow-xl",
"md:border",
"md:dark:border-neutral-700",
"md:dark:bg-neutral-900",
"md:border-neutral-300",
"md:bg-neutral-100",
"md:p-4",
"md:rounded-[calc(theme(borderRadius.xl)_+_theme(space.4))]",
"bottom-[calc(calc(env(safe-area-inset-bottom)_+_theme(space.3)))]",
"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))]",
"data-[display=true]:translate-y-0",
"top-auto",
"motion-safe:translate-y-[calc(100%_+_env(safe-area-inset-bottom)_+_theme(space.3))]",
"bg-transparent",
"p-0",
"md:bottom-auto",
"md:left-[50%]",
"md:top-[50%]",
"md:right-auto",
"md:w-[min(300px,40vw)]",
"md:translate-x-[-50%]",
"md:translate-y-[-50%]",
"md:data-[display=true]:translate-y-[-50%]",
"md:data-[display=true]:blur-0",
"md:blur-md",
"md:data-[display=true]:scale-100",
"md:scale-105",
]);
/**
* Simple mobile friendly action sheet
*
* @param {ActionSheetProps} props
* @returns {JSX.Element}
*/
export const ActionSheet = ({
setState,
state,
title,
actions,
mainAction,
...props
}: ActionSheetProps): JSX.Element => {
const { onTouchEnd, onTouchMove, ref } = useTouchDialogDrag({
animateOpacity: true,
onClose: setState?.bind(null, false),
maxScroll: 300,
});
return (
<Suspense fallback={null}>
<Dialog
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
ref={ref}
state={state}
setState={setState}
className={actionSheetStyles()}
{...props}
>
<Group>
<p
data-draggable={true}
className="text-xs md:text-lg text-inherit text-neutral-900 opacity-75 dark:text-neutral-100 w-full p-3 md:pt-4 md:pb-8"
>
{title}
</p>
{actions.map(
({ action, key, title, closeOnAction = true, type = "normal" }) => (
<ActionButton
data-draggable={true}
key={key}
onClick={() => {
closeOnAction && setState && setState(false);
action();
}}
data-type={type}
>
{title}
</ActionButton>
)
)}
</Group>
{mainAction && (
<Group data-draggable={true} className="mt-4">
<ActionButton
data-draggable={true}
className="font-semibold"
onClick={() => {
mainAction.closeOnAction !== false &&
setState &&
setState(false);
mainAction.action();
}}
data-type={
mainAction.type !== undefined ? mainAction.type : "normal"
}
>
{mainAction.title}
</ActionButton>
</Group>
)}
</Dialog>
</Suspense>
);
};