Skip to content

Commit

Permalink
Break component down to trigger and content
Browse files Browse the repository at this point in the history
  • Loading branch information
RulaKhaled committed May 29, 2024
1 parent a7fdd9f commit d706154
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 65 deletions.
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"dependencies": {
"@hookform/resolvers": "^2.8.5",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3",
Expand Down
37 changes: 32 additions & 5 deletions src/ui/ExpandableSection/ExpandableSection.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React, { ReactNode, useState } from 'react'

import { ExpandableSection } from 'ui/ExpandableSection'

interface TestExpandableSectionProps {
title: string
children: ReactNode
}

const TestExpandableSection: React.FC<TestExpandableSectionProps> = ({
title,
children,
}) => {
const [isExpanded, setIsExpanded] = useState(false)

return (
<ExpandableSection>
<ExpandableSection.Trigger
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
>
{title}
</ExpandableSection.Trigger>
<ExpandableSection.Content>{children}</ExpandableSection.Content>
</ExpandableSection>
)
}

describe('ExpandableSection', () => {
it('renders the title correctly', () => {
const title = 'Test Title'
render(
<ExpandableSection title={title}>
<TestExpandableSection title={title}>
<div>Content</div>
</ExpandableSection>
</TestExpandableSection>
)
const titleElement = screen.getByText(title)
expect(titleElement).toBeInTheDocument()
})

it('renders the children correctly after expanding', async () => {
render(
<ExpandableSection title="Test Title">
<TestExpandableSection title="Test Title">
<div>Test Content</div>
</ExpandableSection>
</TestExpandableSection>
)

const button = screen.getByRole('button')
Expand All @@ -31,7 +56,9 @@ describe('ExpandableSection', () => {

it('collapses the children after clicking the button twice', async () => {
render(
<ExpandableSection title="Test Title">Test Content</ExpandableSection>
<TestExpandableSection title="Test Title">
Test Content
</TestExpandableSection>
)

const button = screen.getByRole('button')
Expand Down
80 changes: 45 additions & 35 deletions src/ui/ExpandableSection/ExpandableSection.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,60 @@
import { Meta, StoryObj } from '@storybook/react'
import React, { useState } from 'react'

import { ExpandableSection } from './ExpandableSection'

type ExpandableSectionStory = {
title: string
children: React.ReactNode
}

const meta: Meta<ExpandableSectionStory> = {
const meta: Meta<typeof ExpandableSection> = {
title: 'Components/ExpandableSection',
component: ExpandableSection,
argTypes: {
title: {
description: 'Title of the expandable section',
control: 'text',
},
children: {
description: 'Content of the expandable section',
control: 'text',
},
},
}
export default meta

type Story = StoryObj<ExpandableSectionStory>
type Story = StoryObj<typeof ExpandableSection>

const DefaultStory: React.FC = () => {
const [isExpanded, setIsExpanded] = useState(false)
return (
<ExpandableSection>
<ExpandableSection.Trigger
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
>
Expandable Section
</ExpandableSection.Trigger>
<ExpandableSection.Content>
This is the content of the expandable section.
</ExpandableSection.Content>
</ExpandableSection>
)
}

const WithHtmlContentStory: React.FC = () => {
const [isExpanded, setIsExpanded] = useState(false)
return (
<ExpandableSection>
<ExpandableSection.Trigger
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
>
Expandable Section with HTML
</ExpandableSection.Trigger>
<ExpandableSection.Content>
<div>
<p>This is the content of the expandable section.</p>
<p>
It can contain HTML elements like <strong>bold text</strong> and{' '}
<em>italic text</em>.
</p>
</div>
</ExpandableSection.Content>
</ExpandableSection>
)
}

export const Default: Story = {
args: {
title: 'Expandable Section',
children: 'This is the content of the expandable section.',
},
render: (args) => <ExpandableSection {...args} />,
render: () => <DefaultStory />,
}

export const WithHtmlContent: Story = {
args: {
title: 'Expandable Section with HTML',
children: (
<div>
<p>This is the content of the expandable section.</p>
<p>
It can contain HTML elements like <strong>bold text</strong> and{' '}
<em>italic text</em>.
</p>
</div>
),
},
render: (args) => <ExpandableSection {...args} />,
render: () => <WithHtmlContentStory />,
}
85 changes: 60 additions & 25 deletions src/ui/ExpandableSection/ExpandableSection.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
import * as Collapsible from '@radix-ui/react-collapsible'
import React, { ReactNode } from 'react'
import cs from 'classnames'
import React, { forwardRef, ReactNode } from 'react'

import Icon from 'ui/Icon'

interface ExpandableSectionProps {
title: string
const ExpandableSectionRoot = forwardRef<
React.ElementRef<typeof Collapsible.Root>,
React.ComponentPropsWithoutRef<typeof Collapsible.Root>
>(({ children, className, ...props }, ref) => (
<Collapsible.Root
className={cs('my-2 border border-gray-200', className)}
{...props}
ref={ref}
>
{children}
</Collapsible.Root>
))

ExpandableSectionRoot.displayName = 'ExpandableSectionRoot'

interface ExpandableSectionTriggerProps
extends React.ComponentPropsWithoutRef<typeof Collapsible.Trigger> {
isExpanded: boolean
children: ReactNode
}

export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
title,
children,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false)

return (
<div className="my-2 border border-gray-200">
<Collapsible.Root open={isExpanded} onOpenChange={setIsExpanded}>
<Collapsible.Trigger asChild>
<button className="flex w-full items-center justify-between p-4 text-left font-semibold hover:bg-gray-100">
<span>{title}</span>
<Icon name={isExpanded ? 'chevronUp' : 'chevronDown'} size="sm" />
</button>
</Collapsible.Trigger>
<Collapsible.Content className="border-t border-gray-200 p-4">
{children}
</Collapsible.Content>
</Collapsible.Root>
</div>
)
}
const ExpandableSectionTrigger = forwardRef<
React.ElementRef<typeof Collapsible.Trigger>,
ExpandableSectionTriggerProps
>(({ isExpanded, className, children, ...props }, ref) => (
<Collapsible.Trigger asChild>
<button
className={cs(
'flex w-full items-center justify-between p-4 text-left font-semibold hover:bg-gray-100',
className
)}
{...props}
ref={ref}
>
<span>{children}</span>
<Icon name={isExpanded ? 'chevronUp' : 'chevronDown'} size="sm" />
</button>
</Collapsible.Trigger>
))

ExpandableSectionTrigger.displayName = 'ExpandableSectionTrigger'

const ExpandableSectionContent = forwardRef<
React.ElementRef<typeof Collapsible.Content>,
React.ComponentPropsWithoutRef<typeof Collapsible.Content>
>(({ children, className, ...props }, ref) => (
<Collapsible.Content
className={cs('border-t border-gray-200 p-4', className)}
{...props}
ref={ref}
>
{children}
</Collapsible.Content>
))

ExpandableSectionContent.displayName = 'ExpandableSectionContent'

export const ExpandableSection = Object.assign(ExpandableSectionRoot, {
Trigger: ExpandableSectionTrigger,
Content: ExpandableSectionContent,
})

0 comments on commit d706154

Please sign in to comment.