Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions site/src/components/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
import { Button } from "#/components/Button/Button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "./Drawer";

const meta: Meta<typeof Drawer> = {
title: "components/Drawer",
component: Drawer,
args: {
children: (
<>
<DrawerTrigger asChild>
<Button>Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Example Drawer Title</DrawerTitle>
<DrawerDescription>Drawer description text</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button>Submit</Button>
</DrawerFooter>
</DrawerContent>
</>
),
},
};

export default meta;
type Story = StoryObj<typeof Drawer>;

export const Closed: Story = {};

export const Open: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Open Drawer" }));
// The drawer renders into a portal on `document.body`, so query the screen.
await waitFor(() =>
expect(screen.getByText("Example Drawer Title")).toBeInTheDocument(),
);
},
};

export const OpenLeft: Story = {
args: {
direction: "left",
children: (
<>
<DrawerTrigger asChild>
<Button>Open Left Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Left-side drawer</DrawerTitle>
<DrawerDescription>
Drawers can slide in from any edge via the direction prop.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</>
),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(
canvas.getByRole("button", { name: "Open Left Drawer" }),
);
await waitFor(() =>
expect(screen.getByText("Left-side drawer")).toBeInTheDocument(),
);
},
};

export const CloseWithButton: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Open Drawer" }));
await waitFor(() =>
expect(screen.getByText("Example Drawer Title")).toBeInTheDocument(),
);
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() =>
expect(
screen.queryByText("Example Drawer Title"),
).not.toBeInTheDocument(),
);
},
};
147 changes: 147 additions & 0 deletions site/src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { cva } from "class-variance-authority";
import { Dialog as DialogPrimitive } from "radix-ui";
import { createContext, useContext } from "react";
import { cn } from "#/utils/cn";

type DrawerDirection = "top" | "bottom" | "left" | "right";

const DrawerDirectionContext = createContext<DrawerDirection>("right");

type DrawerProps = React.ComponentPropsWithRef<typeof DialogPrimitive.Root> & {
/** The edge of the screen the drawer slides in from. Defaults to "right". */
direction?: DrawerDirection;
};

export const Drawer: React.FC<DrawerProps> = ({
direction = "right",
...props
}) => {
return (
<DrawerDirectionContext.Provider value={direction}>
<DialogPrimitive.Root {...props} />
</DrawerDirectionContext.Provider>
);
};

export const DrawerTrigger = DialogPrimitive.Trigger;

export const DrawerClose = DialogPrimitive.Close;

const DrawerPortal = DialogPrimitive.Portal;

const DrawerOverlay: React.FC<
React.ComponentPropsWithRef<typeof DialogPrimitive.Overlay>
> = ({ className, ...props }) => {
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-overlay",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=open]:duration-300 data-[state=closed]:duration-100",
className,
)}
{...props}
/>
);
};

const drawerContentVariants = cva(
cn(
"fixed z-50 flex h-auto flex-col bg-surface-tertiary outline-none will-change-transform",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=open]:duration-500 data-[state=closed]:duration-300",
),
{
variants: {
direction: {
top: cn(
"inset-x-0 top-0 max-h-[80vh] w-full border-b border-border",
"data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
),
bottom: cn(
"inset-x-0 bottom-0 max-h-[80vh] w-full border-t border-border",
"data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
),
left: cn(
"inset-y-0 left-0 h-full w-3/4 border-r border-border sm:max-w-sm",
"data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
),
right: cn(
"inset-y-0 right-0 h-full w-3/4 border-l border-border sm:max-w-sm",
"data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
),
},
},
defaultVariants: {
direction: "right",
},
},
);

export const DrawerContent: React.FC<
React.ComponentPropsWithRef<typeof DialogPrimitive.Content>
> = ({ className, children, ...props }) => {
const direction = useContext(DrawerDirectionContext);

return (
<DrawerPortal>
<DrawerOverlay />
<DialogPrimitive.Content
className={cn(drawerContentVariants({ direction }), className)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DrawerPortal>
);
};

export const DrawerHeader: React.FC<React.ComponentPropsWithRef<"div">> = ({
className,
...props
}) => {
return (
<div
className={cn("flex flex-col gap-0.5 p-4 md:gap-1.5", className)}
{...props}
/>
);
};

export const DrawerFooter: React.FC<React.ComponentPropsWithRef<"div">> = ({
className,
...props
}) => {
return (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
};

export const DrawerTitle: React.FC<
React.ComponentPropsWithRef<typeof DialogPrimitive.Title>
> = ({ className, ...props }) => {
return (
<DialogPrimitive.Title
className={cn(
"text-lg font-semibold leading-none tracking-tight text-content-primary",
className,
)}
{...props}
/>
);
};

export const DrawerDescription: React.FC<
React.ComponentPropsWithRef<typeof DialogPrimitive.Description>
> = ({ className, ...props }) => {
return (
<DialogPrimitive.Description
className={cn("text-sm text-content-secondary", className)}
{...props}
/>
);
};
62 changes: 62 additions & 0 deletions site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useRef, useState } from "react";
import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test";
import { JobError } from "#/api/queries/templates";
import { Button } from "#/components/Button/Button";
import {
MockProvisionerJob,
MockTemplateVersion,
Expand All @@ -13,6 +16,8 @@ const meta: Meta<typeof BuildLogsDrawer> = {
component: BuildLogsDrawer,
args: {
open: true,
onClose: fn(),
onFillVariables: fn(),
},
};

Expand All @@ -21,6 +26,63 @@ type Story = StoryObj<typeof BuildLogsDrawer>;

export const Loading: Story = {};

export const CloseWithButton: Story = {
play: async ({ args }) => {
// The drawer portals its content onto `document.body`, so query the screen.
await userEvent.click(
screen.getByRole("button", { name: "Close build logs" }),
);
await waitFor(() => expect(args.onClose).toHaveBeenCalled());
},
};

export const CloseWithEscape: Story = {
play: async ({ args }) => {
await userEvent.keyboard("{Escape}");
await waitFor(() => expect(args.onClose).toHaveBeenCalled());
},
};

// When opened from a button outside the drawer, focus must return to that
// button on close instead of falling back to the document body. The parent
// owns this via `onCloseAutoFocus`.
export const RestoresFocusToOpener: Story = {
render: () => {
const [open, setOpen] = useState(false);
const openerRef = useRef<HTMLButtonElement>(null);
return (
<>
<Button ref={openerRef} onClick={() => setOpen(true)}>
Show build logs
</Button>
<BuildLogsDrawer
open={open}
onClose={() => setOpen(false)}
onFillVariables={fn()}
onCloseAutoFocus={(event) => {
event.preventDefault();
openerRef.current?.focus();
}}
error={undefined}
templateVersion={undefined}
/>
</>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const opener = canvas.getByRole("button", { name: "Show build logs" });
await userEvent.click(opener);
await waitFor(() =>
expect(screen.getByText("Creating template...")).toBeInTheDocument(),
);
await userEvent.click(
screen.getByRole("button", { name: "Close build logs" }),
);
await waitFor(() => expect(opener).toHaveFocus());
},
};

export const MissingVariables: Story = {
args: {
templateVersion: MockTemplateVersion,
Expand Down
Loading
Loading