From f98fe4c98d833778f375038e350f53c3fa4c3692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20Bu=C4=9Fra=20Yi=C4=9Fiter?= Date: Thu, 29 Feb 2024 21:35:47 +0300 Subject: [PATCH] Add controlled collapsible component (#83) * Add controlled collapsible component * Fix stories * Update component directory * Update collapsible component * Refactor Collapsible component to handle open state more efficiently --- .../Collapsible/Collapsible.stories.tsx | 27 ++++++++++++ .../Collapsible/Collapsible.test.tsx | 25 +++++++++++ src/components/Collapsible/Collapsible.tsx | 41 ++++++++++++++----- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/components/Collapsible/Collapsible.stories.tsx b/src/components/Collapsible/Collapsible.stories.tsx index cde2a33f0..efd698a3d 100644 --- a/src/components/Collapsible/Collapsible.stories.tsx +++ b/src/components/Collapsible/Collapsible.stories.tsx @@ -1,4 +1,5 @@ import { StoryObj, Meta } from '@storybook/react' +import { useEffect, useState } from 'react' import { Text } from '~/components/Text' @@ -11,6 +12,28 @@ export default { type Story = StoryObj +const CollapsibleStory = () => { + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + setIsOpen(true) + }, []) + + return ( + setIsOpen(open)} + > + {[1, 2, 3, 4, 5].map(x => ( + + Item {x} + + ))} + + ) +} + export const Default: Story = { args: { label: 'My Heading', @@ -21,3 +44,7 @@ export const Default: Story = { )), }, } + +export const Controlled: Story = { + render: () => , +} diff --git a/src/components/Collapsible/Collapsible.test.tsx b/src/components/Collapsible/Collapsible.test.tsx index f162577a6..3c8a956a8 100644 --- a/src/components/Collapsible/Collapsible.test.tsx +++ b/src/components/Collapsible/Collapsible.test.tsx @@ -1,7 +1,22 @@ import { cleanup, render, screen, fireEvent } from '@testing-library/react' +import { useState } from 'react' import { Collapsible } from './Collapsible' +const TestComponent = () => { + const [isOpen, setIsOpen] = useState(false) + + return ( + setIsOpen(open)} + label="Hello" + > + World + + ) +} + describe('', () => { afterEach(cleanup) @@ -15,6 +30,16 @@ describe('', () => { expect(screen.getByText(/World/)).toBeInTheDocument() }) + it('controlled', () => { + render() + 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( diff --git a/src/components/Collapsible/Collapsible.tsx b/src/components/Collapsible/Collapsible.tsx index 5ad5b286c..ac28dbc5d 100644 --- a/src/components/Collapsible/Collapsible.tsx +++ b/src/components/Collapsible/Collapsible.tsx @@ -18,17 +18,36 @@ type CollapsibleProps = BoxProps & } export const Collapsible = (props: CollapsibleProps) => { - const { className, children, defaultOpen, onOpenChange, label, ...rest } = - props + const { + className, + children, + defaultOpen, + open, + onOpenChange, + label, + ...rest + } = props + const [expanded, toggleExpanded] = useState(defaultOpen) + const isOpen = open ?? expanded + + const handleSetExpanded = (isExpanded: boolean) => { + if (open !== undefined) { + return + } + + toggleExpanded(isExpanded) + } + + const handleOpenChange = (isOpen: boolean) => { + handleSetExpanded(isOpen) - const handleOpenChange = (open: boolean) => { - toggleExpanded(open) - onOpenChange?.(open) + onOpenChange?.(isOpen) } return ( { { position="absolute" right="0" marginRight="4" - initial={{ rotate: defaultOpen ? 180 : 0 }} - animate={{ rotate: expanded ? 180 : 0 }} + initial={{ rotate: isOpen ? 180 : 0 }} + animate={{ rotate: isOpen ? 180 : 0 }} transition={{ ease: 'linear', duration: 0.1 }} > - {expanded && ( + {isOpen && (