Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/modal #58

Merged
merged 5 commits into from
Jan 22, 2025
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
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