Skip to content

Commit

Permalink
Merge pull request #58 from getpingback/feat/modal
Browse files Browse the repository at this point in the history
Feat/modal
  • Loading branch information
roger067 authored Jan 22, 2025
2 parents 82a069e + 97ecfce commit 5251743
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 145 deletions.
80 changes: 51 additions & 29 deletions src/components/drawer/drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,84 @@
import React, { useState } from 'react';
import { Drawer } from './drawer';
import { DrawerRoot, DrawerHeader, DrawerBody, DrawerFooter } from './drawer';
import type { Meta, StoryObj } from '@storybook/react';
import { ArrowLeftIcon } from '@stash-ui/light-icons';
import { Button } from '../button/button';

const meta = {
title: 'Components/Drawer',
component: Drawer,
tags: ['autodocs']
} satisfies Meta<typeof Drawer>;
component: DrawerRoot,
tags: ['autodocs'],
parameters: {
docs: {
story: {
inline: false,
iframeHeight: 500
}
}
}
} satisfies Meta<typeof DrawerRoot>;

type Story = StoryObj<typeof meta>;

export default meta;

export const Default: Story = {
args: {
title: 'Drawer',
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.',
hasDivider: true,
open: true,
prefixIcon: <ArrowLeftIcon />,
children: (
<div className="flex flex-col gap-4">
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
</div>
),
footer: (
<div className="flex gap-4">
<Button variant="outline" className="w-full">
Cancel
</Button>
<Button variant="solid" className="w-full">
Save
</Button>
</div>
<>
<DrawerHeader
title="Drawer"
description="Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos."
prefixIcon={<ArrowLeftIcon />}
/>
<DrawerBody hasDivider>
<div className="flex flex-col gap-4">
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
<div>lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos</div>
</div>
</DrawerBody>
<DrawerFooter>
<div className="flex gap-4">
<Button variant="outline" className="w-full">
Cancel
</Button>
<Button variant="solid" className="w-full">
Save
</Button>
</div>
</DrawerFooter>
</>
)
}
};

export const Behavior: Story = {
args: {
title: 'Drawer'
children: (
<>
<DrawerHeader title="Drawer" />
<DrawerBody>
<div>Drawer Content</div>
</DrawerBody>
</>
)
},
render: ({ title }) => {
render: ({ children }) => {
const [open, setOpen] = useState(false);

return (
<>
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer title={title} open={open} onOpenChange={setOpen} />
<DrawerRoot open={open} onOpenChange={setOpen}>
{children}
</DrawerRoot>
</>
);
}
Expand Down
71 changes: 47 additions & 24 deletions src/components/drawer/drawer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,90 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { Drawer } from './drawer';
import { DrawerRoot, DrawerHeader, DrawerBody, DrawerFooter } from './drawer';
import * as React from 'react';

describe('Drawer', () => {
const defaultProps = {
title: 'Test Drawer',
open: true
};

it('should render drawer with title', () => {
render(
<Drawer {...defaultProps}>
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" />
<DrawerBody>
<div>Drawer content</div>
</DrawerBody>
</DrawerRoot>
);

expect(screen.getByText('Test Drawer')).toBeInTheDocument();
});

it('should render drawer with description', () => {
render(
<Drawer {...defaultProps} description="Test description">
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" description="Test description" />
<DrawerBody>
<div>Drawer content</div>
</DrawerBody>
</DrawerRoot>
);

expect(screen.getByText('Test description')).toBeInTheDocument();
});

it('should render drawer with preffix icon', () => {
it('should render drawer with prefix icon', () => {
const TestIcon = () => <div data-testid="test-icon">Icon</div>;

render(
<Drawer {...defaultProps} prefixIcon={<TestIcon />}>
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" prefixIcon={<TestIcon />} />
<DrawerBody>
<div>Drawer content</div>
</DrawerBody>
</DrawerRoot>
);

expect(screen.getByTestId('test-icon')).toBeInTheDocument();
});

it('should render drawer with divider when hasDivider is true', () => {
render(
<Drawer {...defaultProps} hasDivider>
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" />
<DrawerBody hasDivider>
<div>Drawer content</div>
</DrawerBody>
</DrawerRoot>
);

expect(screen.getByTestId('divider')).toBeInTheDocument();
});

it('should render drawer with footer content', () => {
render(
<Drawer {...defaultProps} footer={<div>Footer content</div>}>
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" />
<DrawerBody>
<div>Drawer content</div>
</DrawerBody>
<DrawerFooter>
<div>Footer content</div>
</DrawerFooter>
</DrawerRoot>
);

expect(screen.getByText('Footer content')).toBeInTheDocument();
});

it('should render children content', () => {
render(
<Drawer {...defaultProps}>
<div>Test children content</div>
</Drawer>
<DrawerRoot {...defaultProps}>
<DrawerHeader title="Test Drawer" />
<DrawerBody>
<div>Test children content</div>
</DrawerBody>
</DrawerRoot>
);

expect(screen.getByText('Test children content')).toBeInTheDocument();
Expand All @@ -74,9 +94,12 @@ describe('Drawer', () => {
const onOpenChange = jest.fn();

render(
<Drawer {...defaultProps} onOpenChange={onOpenChange}>
<div>Drawer content</div>
</Drawer>
<DrawerRoot {...defaultProps} onOpenChange={onOpenChange}>
<DrawerHeader title="Test Drawer" />
<DrawerBody>
<div>Drawer content</div>
</DrawerBody>
</DrawerRoot>
);

fireEvent.click(screen.getByRole('button'));
Expand Down
82 changes: 53 additions & 29 deletions src/components/drawer/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,71 @@ import * as Dialog from '@radix-ui/react-dialog';
import { TimesIcon } from '@stash-ui/light-icons';
import * as React from 'react';

interface DrawerProps extends Dialog.DialogProps {
interface DrawerRootProps extends Dialog.DialogProps {
children: React.ReactNode;
}

interface DrawerHeaderProps {
title: string;
description?: string;
prefixIcon?: React.ReactNode;
className?: string;
}

interface DrawerBodyProps {
children: React.ReactNode;
hasDivider?: boolean;
footer?: React.ReactNode;
className?: string;
}

function Drawer({ children, className, title, description, prefixIcon, hasDivider, footer, ...props }: DrawerProps) {
interface DrawerFooterProps {
children: React.ReactNode;
className?: string;
}

const DrawerRoot = ({ children, ...props }: DrawerRootProps) => {
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className=" z-50 fixed inset-0 bg-[#00000011] w-screen h-screen backdrop-blur-[1px] animate-fade-in" />
<Dialog.Content
className={cn(
'z-50 flex flex-col bg-[#FFFFFF] shadow-drawer rounded-xl w-[400px] border border-border-card fixed right-6 top-6 h-[calc(100vh-48px)] data-[state=open]:animate-drawer-slide-in data-[state=closed]:animate-drawer-slide-out',
className
)}
>
<div className="flex justify-between gap-2 p-6 pb-4">
<div className="flex gap-2">
{prefixIcon && <div className="h-fit">{prefixIcon}</div>}
<div className="flex flex-col gap-1">
<Dialog.Title className="text-lg font-bold leading-6 text-primary-foreground">{title}</Dialog.Title>
{description && (
<Dialog.Description className="text-xs font-normal text-primary-foreground opacity-65">{description}</Dialog.Description>
)}
</div>
</div>
<Dialog.Close className="h-fit">
<TimesIcon />
</Dialog.Close>
</div>
{hasDivider && <div className="h-px w-full bg-[#0000000A]" data-testid="divider" />}
<div className="flex flex-col flex-1 overflow-y-auto p-6">{children}</div>
{footer && <div className="p-6 border-t border-[#0000000A]">{footer}</div>}
<Dialog.Overlay className="z-50 fixed inset-0 bg-[#00000011] w-screen h-screen backdrop-blur-[1px] animate-fade-in" />
<Dialog.Content className="z-50 flex flex-col bg-[#FFFFFF] shadow-drawer rounded-xl w-[400px] border border-border-card fixed right-6 top-6 h-[calc(100vh-48px)] data-[state=open]:animate-drawer-slide-in data-[state=closed]:animate-drawer-slide-out">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
};

const DrawerHeader = ({ title, description, prefixIcon, className }: DrawerHeaderProps) => {
return (
<div className={cn('flex justify-between gap-2 p-6 pb-4', className)}>
<div className="flex gap-2">
{prefixIcon && <div className="h-fit">{prefixIcon}</div>}
<div className="flex flex-col gap-1">
<Dialog.Title className="text-lg font-bold leading-6 text-primary-foreground">{title}</Dialog.Title>
{description && (
<Dialog.Description className="text-xs font-normal text-primary-foreground opacity-65">{description}</Dialog.Description>
)}
</div>
</div>
<Dialog.Close className="h-fit">
<TimesIcon />
</Dialog.Close>
</div>
);
};

const DrawerBody = ({ children, hasDivider, className }: DrawerBodyProps) => {
return (
<>
{hasDivider && <div className="h-px w-full bg-[#0000000A]" data-testid="divider" />}
<div className={cn('flex flex-col flex-1 overflow-y-auto p-6', className)}>{children}</div>
</>
);
};

const DrawerFooter = ({ children, className }: DrawerFooterProps) => {
return <div className={cn('p-6 border-t border-[#0000000A]', className)}>{children}</div>;
};

export { Drawer };
export { DrawerRoot, DrawerHeader, DrawerBody, DrawerFooter };
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './command';
export * from './command-cmdk';
export * from './popover';
export * from './range-picker';
export * from './modal';
export * from './pagination';
export * from './variable-input';
export * from './radio-group';
Expand Down
1 change: 1 addition & 0 deletions src/components/modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './modal';
Loading

0 comments on commit 5251743

Please sign in to comment.