Skip to content

Commit

Permalink
feat(component): add AccordionPanel component (#933)
Browse files Browse the repository at this point in the history
* 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
bc-athorne and chanceaclark authored Aug 17, 2022
1 parent 088a169 commit e22ffa2
Show file tree
Hide file tree
Showing 14 changed files with 540 additions and 0 deletions.
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>
</>
);
},
);
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;
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 };
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>
);
});
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>
`;
5 changes: 5 additions & 0 deletions packages/big-design/src/components/AccordionPanel/index.ts
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 packages/big-design/src/components/AccordionPanel/spec.tsx
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 packages/big-design/src/components/AccordionPanel/styled.tsx
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 };
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),
})),
};
};
1 change: 1 addition & 0 deletions packages/big-design/src/components/index.ts
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';
Expand Down
Loading

0 comments on commit e22ffa2

Please sign in to comment.