Skip to content

Commit

Permalink
feat: add accordion ui
Browse files Browse the repository at this point in the history
  • Loading branch information
siloneco committed Nov 10, 2023
1 parent 44b2d03 commit 2323d5d
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@commitlint/config-conventional": "^17.7.0",
"@markuplint/jsx-parser": "^3.11.0",
"@markuplint/react-spec": "^3.12.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
Expand Down
60 changes: 60 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions src/components/ui/Accordion/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Accordion,
AccordionItem,
AccordionContent,
AccordionTrigger,
} from '.';

import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof Accordion> = {
component: Accordion,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof Accordion>;

export const Default: Story = {
render: () => (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Accordion Trigger</AccordionTrigger>
<AccordionContent>Accordion content</AccordionContent>
</AccordionItem>
</Accordion>
),
};

export const MultipleItems: Story = {
render: () => (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Accordion Trigger 1</AccordionTrigger>
<AccordionContent>Accordion content 1</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Accordion Trigger 2</AccordionTrigger>
<AccordionContent>Accordion content 2</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Accordion Trigger 3</AccordionTrigger>
<AccordionContent>Accordion content 3</AccordionContent>
</AccordionItem>
</Accordion>
),
};

export const LongContent: Story = {
render: () => (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Accordion Trigger</AccordionTrigger>
<AccordionContent>
This is a very long accordion content for checking the animation
</AccordionContent>
</AccordionItem>
</Accordion>
),
};
72 changes: 72 additions & 0 deletions src/components/ui/Accordion/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { render } from '@testing-library/react';

import '@testing-library/jest-dom';
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '.';

describe('ui/Accordion', () => {
it('renders trigger text correctly', () => {
const triggerText = 'Trigger';
const screen = render(
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>{triggerText}</AccordionTrigger>
<AccordionContent>Content</AccordionContent>
</AccordionItem>
</Accordion>
);

expect(screen.getByText(triggerText)).toBeInTheDocument();
});

it('renders content text correctly', () => {
const contentText = 'Content';
const screen = render(
<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>Trigger</AccordionTrigger>
<AccordionContent>{contentText}</AccordionContent>
</AccordionItem>
</Accordion>
);

expect(screen.getByText(contentText)).toBeInTheDocument();
});

it('accordion is closed default', () => {
const contentText = 'Content';
const screen = render(
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Trigger</AccordionTrigger>
<AccordionContent>{contentText}</AccordionContent>
</AccordionItem>
</Accordion>
);

expect(() => screen.getByText(contentText)).toThrow();
});

it('renders components with correct classes', () => {
const triggerText = 'Trigger';
const contentText = 'Content';

const screen = render(
<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>{triggerText}</AccordionTrigger>
<AccordionContent>{contentText}</AccordionContent>
</AccordionItem>
</Accordion>
);

expect(screen.getByText(triggerText)).toHaveClass(
'flex flex-1 items-center justify-between py-4 font-medium transition-all'
);
expect(screen.getByText(contentText)).toHaveClass('pb-4 pt-0');
});
});
61 changes: 61 additions & 0 deletions src/components/ui/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { forwardRef } from 'react';

import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';

import { cn } from '@/libs/utils';

const Accordion = AccordionPrimitive.Root;

const AccordionItem = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b', className)}
{...props}
/>
));
AccordionItem.displayName = 'AccordionItem';

const AccordionTrigger = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

const AccordionContent = forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

0 comments on commit 2323d5d

Please sign in to comment.