Skip to content

Commit

Permalink
Add controlled collapsible component
Browse files Browse the repository at this point in the history
  • Loading branch information
yigiterdev committed Feb 29, 2024
1 parent c8caa57 commit bcfcf6c
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react'
import { useEffect, useState } from 'react'

import { Text } from '~/components/Text'

import { ControlledCollapsible } from './ControlledCollapsible'

export default {
title: 'Components/Collapsible',
component: ControlledCollapsible,
} as Meta<typeof ControlledCollapsible>

type Story = StoryObj<typeof ControlledCollapsible>

const CollapsibleStory = () => {
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
setIsOpen(true)
}, [])

return (
<ControlledCollapsible
open={isOpen}
label="My Heading"
onOpenChange={open => setIsOpen(open)}
>
{[1, 2, 3, 4, 5].map(x => (
<Text variant="normal" as="p" color="text80" key={x}>
Item {x}
</Text>
))}
</ControlledCollapsible>
)
}

export const Default: Story = {
render: () => <CollapsibleStory />,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { cleanup, render, screen, fireEvent } from '@testing-library/react'
import { useState } from 'react'

import { ControlledCollapsible } from './ControlledCollapsible'

const TestComponent = () => {
const [isOpen, setIsOpen] = useState(false)

return (
<ControlledCollapsible
open={isOpen}
onOpenChange={open => setIsOpen(open)}
label="Hello"
>
World
</ControlledCollapsible>
)
}

describe('<Collapsible />', () => {
afterEach(cleanup)

it('renders', () => {
render(<TestComponent />)
expect(screen.getByText(/Hello/)).toBeInTheDocument()
expect(screen.queryByText(/World/)).toBeNull()

fireEvent.click(screen.getByRole('button'))

expect(screen.getByText(/World/)).toBeInTheDocument()
})

it('with default open', () => {
render(
<ControlledCollapsible label="Hello" open={true}>
World
</ControlledCollapsible>
)
expect(screen.getByText(/World/)).toBeInTheDocument()
})
})
73 changes: 73 additions & 0 deletions src/components/ControlledCollapsible/ControlledCollapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
import { clsx } from 'clsx'
import { AnimatePresence, motion } from 'framer-motion'
import { ReactNode } from 'react'

import { Text } from '~/components/Text'
import { ChevronDownIcon } from '~/icons'

import { Box, BoxProps } from '../Box'
import * as styles from '../Collapsible/styles.css'

type ControlledCollapsibleProps = BoxProps &
Omit<CollapsiblePrimitive.CollapsibleProps, 'open' | 'defaultOpen'> & {
open: boolean
label: ReactNode
}

export const ControlledCollapsible = (props: ControlledCollapsibleProps) => {
const { className, children, open, onOpenChange, label, ...rest } = props

return (
<CollapsiblePrimitive.Root open={open} onOpenChange={onOpenChange} asChild>
<Box
as={motion.div}
className={clsx(className, styles.root)}
initial={{ height: open ? 'auto' : styles.COLLAPSED_HEIGHT }}
animate={{ height: open ? 'auto' : styles.COLLAPSED_HEIGHT }}
transition={{ ease: 'easeOut', duration: 0.3 }}
borderRadius="md"
background="backgroundSecondary"
position="relative"
overflow="hidden"
width="full"
{...rest}
>
<CollapsiblePrimitive.Trigger className={styles.trigger}>
<Text as="div" variant="normal" fontWeight="bold" color="text80">
{label}
</Text>
<Box
as={motion.div}
position="absolute"
right="0"
marginRight="4"
initial={{ rotate: open ? 180 : 0 }}
animate={{ rotate: open ? 180 : 0 }}
transition={{ ease: 'linear', duration: 0.1 }}
>
<ChevronDownIcon className={styles.icon} color="text50" />
</Box>
</CollapsiblePrimitive.Trigger>
<AnimatePresence>
{open && (
<CollapsiblePrimitive.Content
className={styles.content}
asChild
forceMount
>
<motion.div
initial={{ opacity: open ? 1 : 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: 'easeOut', duration: 0.3 }}
>
{children}
</motion.div>
</CollapsiblePrimitive.Content>
)}
</AnimatePresence>
</Box>
</CollapsiblePrimitive.Root>
)
}
1 change: 1 addition & 0 deletions src/components/ControlledCollapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ControlledCollapsible } from './ControlledCollapsible'

0 comments on commit bcfcf6c

Please sign in to comment.