-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component): add AccordionPanel component (#933)
* feat(component): add AccordionGroup component * feat(component): add PR feedback * feat(component): add styled wrapper for panel component * feat(component): move logic to hook * feat(component): more PR feedback * feat(component): remove commented code * feat(component): export hook logic and add PR feedback * --wip-- [skip ci] * --wip-- [skip ci] * feat(component): refactor hook * feat(component): fix testing variables Co-authored-by: Chancellor Clark <chancellor.clark@bigcommerce.com>
- Loading branch information
1 parent
088a169
commit e22ffa2
Showing
14 changed files
with
540 additions
and
0 deletions.
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
packages/big-design/src/components/AccordionPanel/Accordion/Accordion.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { ExpandMoreIcon } from '@bigcommerce/big-design-icons'; | ||
import React, { memo } from 'react'; | ||
|
||
import { useUniqueId } from '../../../hooks'; | ||
|
||
import { StyledAccordionButton, StyledAccordionContent } from './styled'; | ||
|
||
export interface AccordionProps { | ||
children?: React.ReactNode; | ||
defaultExpanded?: boolean; | ||
header: string; | ||
iconLeft?: React.ReactNode; | ||
isExpanded: boolean; | ||
onClick: React.MouseEventHandler<HTMLButtonElement>; | ||
} | ||
|
||
export const Accordion: React.FC<AccordionProps> = memo( | ||
({ children, header, iconLeft, isExpanded, onClick }) => { | ||
const accordionId = useUniqueId('accordion'); | ||
const accordionItemId = useUniqueId('accordion-item'); | ||
|
||
return ( | ||
<> | ||
<StyledAccordionButton | ||
aria-controls={accordionItemId} | ||
aria-expanded={isExpanded} | ||
iconLeft={iconLeft} | ||
iconRight={<ExpandMoreIcon className="collapse-icon" color="secondary70" />} | ||
id={accordionId} | ||
isExpanded={isExpanded} | ||
onClick={onClick} | ||
variant="subtle" | ||
> | ||
{header} | ||
</StyledAccordionButton> | ||
<StyledAccordionContent | ||
aria-labelledby={accordionId} | ||
display={isExpanded ? 'block' : 'none'} | ||
hidden={!isExpanded} | ||
iconLeft={iconLeft} | ||
id={accordionItemId} | ||
role="region" | ||
> | ||
{children} | ||
</StyledAccordionContent> | ||
</> | ||
); | ||
}, | ||
); |
4 changes: 4 additions & 0 deletions
4
packages/big-design/src/components/AccordionPanel/Accordion/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { AccordionProps as _AccordionProps } from './Accordion'; | ||
|
||
export { Accordion } from './Accordion'; | ||
export type AccordionProps = _AccordionProps; |
58 changes: 58 additions & 0 deletions
58
packages/big-design/src/components/AccordionPanel/Accordion/styled.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { theme as defaultTheme, remCalc } from '@bigcommerce/big-design-theme'; | ||
import styled, { css } from 'styled-components'; | ||
|
||
import { withTransition } from '../../../mixins/transitions'; | ||
import { Box } from '../../Box'; | ||
import { StyleableButton } from '../../Button/private'; | ||
|
||
interface StyledAccordionButtonProps { | ||
isExpanded: boolean; | ||
} | ||
|
||
interface StyledAccordionContentProps { | ||
iconLeft: React.ReactNode; | ||
} | ||
|
||
export const StyledAccordionButton = styled(StyleableButton)<StyledAccordionButtonProps>` | ||
border-top: ${({ theme }) => theme.border.box}; | ||
border-radius: 0; | ||
padding: ${({ theme }) => theme.spacing.xLarge}; | ||
text-align: left; | ||
width: 100%; | ||
span { | ||
width: 100%; | ||
color: ${({ theme }) => theme.colors.secondary70}; | ||
grid-template-columns: ${({ iconLeft, theme }) => | ||
iconLeft | ||
? `${theme.spacing.xLarge} 1fr ${theme.spacing.medium}` | ||
: `1fr ${theme.spacing.medium}`}; | ||
} | ||
${({ isExpanded }) => | ||
isExpanded && | ||
css` | ||
border-bottom: ${({ theme }) => theme.border.box}; | ||
`} | ||
&:focus { | ||
z-index: ${({ theme }) => theme.zIndex.fixed}; | ||
} | ||
.collapse-icon { | ||
${withTransition(['transform'])} | ||
${({ isExpanded }) => | ||
isExpanded && | ||
css` | ||
transform: rotate(-180deg); | ||
`} | ||
} | ||
`; | ||
|
||
export const StyledAccordionContent = styled(Box)<StyledAccordionContentProps>` | ||
padding: ${({ theme }) => theme.spacing.xLarge}}; | ||
padding-left: ${({ iconLeft, theme }) => (iconLeft ? remCalc(60) : `${theme.spacing.xLarge}`)}; | ||
`; | ||
|
||
StyledAccordionButton.defaultProps = { theme: defaultTheme }; | ||
StyledAccordionContent.defaultProps = { theme: defaultTheme }; |
23 changes: 23 additions & 0 deletions
23
packages/big-design/src/components/AccordionPanel/AccordionPanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React, { memo } from 'react'; | ||
|
||
import { Panel } from '../Panel'; | ||
|
||
import { Accordion, AccordionProps } from './Accordion'; | ||
import { StyledAccordionPanelWrapper } from './styled'; | ||
|
||
export interface AccordionPanelProps { | ||
header: string; | ||
panels: AccordionProps[]; | ||
} | ||
|
||
export const AccordionPanel: React.FC<AccordionPanelProps> = memo(({ header, panels }) => { | ||
return ( | ||
<Panel header={header}> | ||
<StyledAccordionPanelWrapper> | ||
{panels.map((panel, index) => ( | ||
<Accordion {...panel} key={index} /> | ||
))} | ||
</StyledAccordionPanelWrapper> | ||
</Panel> | ||
); | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/big-design/src/components/AccordionPanel/__snapshots__/spec.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`it renders accordion panel header 1`] = ` | ||
<h2 | ||
class="styled__StyledH2-sc-tqnj75-2 styled__StyledH2-sc-1h6ef3q-1 kEGvNd fjdBDH" | ||
> | ||
Accordion Panel Header | ||
</h2> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { AccordionPanelProps as _AccordionPanelProps } from './AccordionPanel'; | ||
|
||
export { AccordionPanel } from './AccordionPanel'; | ||
export { useAccordionPanel } from './useAccordionPanel'; | ||
export type AccordionPanelProps = _AccordionPanelProps; |
135 changes: 135 additions & 0 deletions
135
packages/big-design/src/components/AccordionPanel/spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { ErrorIcon } from '@bigcommerce/big-design-icons'; | ||
import userEvent from '@testing-library/user-event'; | ||
import React, { useState } from 'react'; | ||
|
||
import { render, screen } from '@test/utils'; | ||
|
||
import { Text } from '../Typography'; | ||
|
||
import { AccordionProps } from './Accordion'; | ||
|
||
import { AccordionPanel, useAccordionPanel } from '.'; | ||
|
||
const TestComponent: React.FC = () => { | ||
const { panels } = useAccordionPanel([ | ||
{ | ||
header: 'Panel Header', | ||
children: <Text>This is a child component</Text>, | ||
}, | ||
]); | ||
|
||
return <AccordionPanel header="Accordion Panel Header" panels={panels} />; | ||
}; | ||
|
||
const TestComponentWithoutHook: React.FC = () => { | ||
const [isExpanded, setIsExpanded] = useState(false); | ||
|
||
const testPanels: AccordionProps[] = [ | ||
{ | ||
header: 'Panel Header', | ||
isExpanded, | ||
children: <Text>This is a child component</Text>, | ||
onClick: () => setIsExpanded(!isExpanded), | ||
}, | ||
]; | ||
|
||
return <AccordionPanel header="Accordion Panel Header" panels={testPanels} />; | ||
}; | ||
|
||
const TestComponentWithDefault: React.FC = () => { | ||
const { panels } = useAccordionPanel([ | ||
{ | ||
defaultExpanded: true, | ||
header: 'Panel Header', | ||
iconLeft: <ErrorIcon />, | ||
children: <Text>This is a child component</Text>, | ||
}, | ||
]); | ||
|
||
return <AccordionPanel header="Accordion Panel Header" panels={panels} />; | ||
}; | ||
|
||
test('it renders accordion panel header', async () => { | ||
render(<TestComponent />); | ||
|
||
const header = await screen.findByRole('heading', { name: /Accordion Panel Header/i }); | ||
|
||
expect(header).toMatchSnapshot(); | ||
expect(header).toBeVisible(); | ||
}); | ||
|
||
test('it renders accordion panel children', async () => { | ||
render(<TestComponent />); | ||
|
||
const child = await screen.findByText('Panel Header'); | ||
|
||
expect(child).toBeVisible(); | ||
}); | ||
|
||
test('accordion renders header', async () => { | ||
render(<TestComponent />); | ||
|
||
const header = await screen.findByText('Panel Header'); | ||
|
||
expect(header).toBeVisible(); | ||
}); | ||
|
||
test('accordion collapses and expands on click', async () => { | ||
render(<TestComponent />); | ||
|
||
const buttonPanel = await screen.findByRole('button'); | ||
|
||
await userEvent.click(buttonPanel); | ||
|
||
const panelChild = await screen.findByRole('region'); | ||
|
||
expect(panelChild).toBeVisible(); | ||
|
||
await userEvent.click(buttonPanel); | ||
|
||
expect(panelChild).not.toBeVisible(); | ||
}); | ||
|
||
test('accordion renders children', async () => { | ||
render(<TestComponent />); | ||
|
||
const buttonPanel = await screen.findByRole('button'); | ||
|
||
await userEvent.click(buttonPanel); | ||
|
||
const child = await screen.findByRole('region'); | ||
|
||
expect(child).toBeVisible(); | ||
}); | ||
|
||
test('if defaultExpanded is set to true, accordion panel is expanded', async () => { | ||
render(<TestComponentWithDefault />); | ||
|
||
const panelChild = await screen.findByRole('region'); | ||
|
||
expect(panelChild).toBeVisible(); | ||
}); | ||
|
||
test('it renders icon if iconLeft prop is defined', async () => { | ||
render(<TestComponentWithDefault />); | ||
|
||
const icons = (await screen.findByRole('button')).querySelectorAll('svg'); | ||
|
||
const icon = icons[0]; | ||
|
||
expect(icon).toBeVisible(); | ||
}); | ||
|
||
test('it renders header and children accordion elements without hook', async () => { | ||
render(<TestComponentWithoutHook />); | ||
|
||
const header = await screen.findByText('Panel Header'); | ||
const buttonPanel = await screen.findByRole('button'); | ||
|
||
await userEvent.click(buttonPanel); | ||
|
||
const child = await screen.findByRole('region'); | ||
|
||
expect(child).toBeVisible(); | ||
expect(header).toBeVisible(); | ||
}); |
20 changes: 20 additions & 0 deletions
20
packages/big-design/src/components/AccordionPanel/styled.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; | ||
import styled from 'styled-components'; | ||
|
||
import { Flex } from '../Flex'; | ||
|
||
export const StyledAccordionPanelWrapper = styled(Flex)` | ||
flex-direction: column; | ||
margin-bottom: -${({ theme }) => theme.spacing.medium}; | ||
margin-left: -${({ theme }) => theme.spacing.medium}; | ||
margin-right: -${({ theme }) => theme.spacing.medium}; | ||
${({ theme }) => theme.breakpoints.tablet} { | ||
margin-bottom: -${({ theme }) => theme.spacing.xLarge}; | ||
margin-left: -${({ theme }) => theme.spacing.xLarge}; | ||
margin-right: -${({ theme }) => theme.spacing.xLarge}; | ||
} | ||
`; | ||
|
||
StyledAccordionPanelWrapper.defaultProps = { theme: defaultTheme }; |
25 changes: 25 additions & 0 deletions
25
packages/big-design/src/components/AccordionPanel/useAccordionPanel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { useState } from 'react'; | ||
|
||
import { AccordionProps } from './Accordion'; | ||
|
||
type InitialPanels = Array<Omit<AccordionProps, 'isExpanded' | 'onClick'>>; | ||
|
||
export const useAccordionPanel = (initialPanels: InitialPanels) => { | ||
const [expandedPanels, setExpandedPanels] = useState(() => { | ||
return initialPanels.map(({ defaultExpanded }) => defaultExpanded ?? false); | ||
}); | ||
|
||
const handleOnClick = (panelIndex: number) => () => { | ||
setExpandedPanels( | ||
expandedPanels.map((isExpanded, index) => (index === panelIndex ? !isExpanded : isExpanded)), | ||
); | ||
}; | ||
|
||
return { | ||
panels: expandedPanels.map<AccordionProps>((isExpanded, index) => ({ | ||
...initialPanels[index], | ||
isExpanded, | ||
onClick: handleOnClick(index), | ||
})), | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './AccordionPanel'; | ||
export * from './Alert'; | ||
export * from './Badge'; | ||
export * from './Box'; | ||
|
Oops, something went wrong.