Skip to content

Commit 439b67c

Browse files
pgr1David Barrera
and
David Barrera
authored
feat: added Accordion component (#172)
* feat: add Accordion component and basic documentation * feat: added test * chore: remove default export and add PropsWithChildren * chore: update api table * chore: chheck place holder to not be in document * chore: added onExpand and onCollapse * chore: solve lint issues * chore: solve lint issues * chore: solve lint issues Co-authored-by: David Barrera <[email protected]>
1 parent 14cf21b commit 439b67c

13 files changed

+534
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import * as React from 'react';
4+
import { Accordion } from './Accordion';
5+
6+
describe('Accordion', () => {
7+
it('allows rendering react nodes as children', () => {
8+
render(
9+
<Accordion defaultExpanded>
10+
<p>paragraph</p>
11+
</Accordion>
12+
);
13+
expect(screen.getByText('paragraph')).toBeInTheDocument();
14+
});
15+
16+
it('render default variant closed', () => {
17+
render(
18+
<Accordion heading="Some heading" info="some info" description="some description" buttonLabel="button">
19+
<p>Place holder</p>
20+
</Accordion>
21+
);
22+
expect(screen.getByText('Some heading')).toBeInTheDocument();
23+
expect(screen.getByText('some info')).toBeInTheDocument();
24+
expect(screen.getByText('some description')).toBeInTheDocument();
25+
expect(screen.getByText('button')).toBeInTheDocument();
26+
expect(screen.queryByText('Place holder')).not.toBeInTheDocument();
27+
});
28+
29+
it('render default variant open', () => {
30+
render(
31+
<Accordion heading="Some heading" info="some info" description="some description" buttonLabel="button">
32+
<p>Place holder</p>
33+
</Accordion>
34+
);
35+
userEvent.click(screen.getByText('Some heading'));
36+
expect(screen.getByText('Place holder')).toBeInTheDocument();
37+
expect(screen.getByText('some description')).toBeInTheDocument();
38+
expect(screen.getByText('button')).toBeInTheDocument();
39+
userEvent.click(screen.getByText('button'));
40+
expect(screen.queryByText('Place holder')).toBeFalsy();
41+
});
42+
43+
it('render compact variant correct', () => {
44+
render(
45+
<Accordion
46+
heading="Some heading"
47+
info="some info"
48+
description="some description"
49+
buttonLabel="button"
50+
variant="compact"
51+
>
52+
<p>Place holder</p>
53+
</Accordion>
54+
);
55+
expect(screen.getByText('Some heading')).toBeInTheDocument();
56+
expect(screen.getByText('some description')).toBeInTheDocument();
57+
expect(screen.queryByText('some info')).toBeFalsy();
58+
expect(screen.queryByText('paragraph')).toBeFalsy();
59+
});
60+
61+
it('render compact variant open', () => {
62+
render(
63+
<Accordion heading="Some heading" description="some description" variant="compact">
64+
<p>Place holder</p>
65+
</Accordion>
66+
);
67+
userEvent.click(screen.getByText('Some heading'));
68+
expect(screen.getByText('Place holder')).toBeInTheDocument();
69+
expect(screen.getByText('some description')).toBeInTheDocument();
70+
userEvent.click(screen.getByText('Some heading'));
71+
expect(screen.queryByText('Place holder')).toBeFalsy();
72+
});
73+
});
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { ReactNode } from 'react';
2+
import styled from 'styled-components';
3+
4+
import { Colors } from '../../essentials';
5+
import { Box } from '../Box/Box';
6+
import { Compact } from './components/Compact';
7+
import { DefaultPanel } from './components/Default';
8+
import { AccordionProps } from './types';
9+
10+
const HorizontalDivider = styled(Box)`
11+
border: 0;
12+
border-top: solid 0.0625rem ${Colors.AUTHENTIC_BLUE_200};
13+
`;
14+
15+
const HorizontalDividerTop = HorizontalDivider;
16+
17+
const HorizontalDividerBottom = styled(HorizontalDivider)`
18+
display: none;
19+
`;
20+
21+
const RenderedSection = styled(Box)`
22+
:last-child ${HorizontalDividerBottom} {
23+
display: inherit;
24+
}
25+
`;
26+
27+
const Accordion = ({
28+
heading,
29+
description,
30+
info,
31+
buttonLabel,
32+
variant,
33+
defaultExpanded,
34+
children,
35+
onExpand = () => undefined,
36+
onCollapse = () => undefined
37+
}: AccordionProps) => (
38+
<RenderedSection role="group">
39+
<HorizontalDividerTop />
40+
{variant === 'compact' ? (
41+
<Compact
42+
heading={heading}
43+
description={description}
44+
defaultExpanded={defaultExpanded}
45+
onExpand={onExpand}
46+
onCollapse={onCollapse}
47+
>
48+
{children}
49+
</Compact>
50+
) : (
51+
<DefaultPanel
52+
heading={heading}
53+
description={description}
54+
buttonLabel={buttonLabel}
55+
info={info}
56+
defaultExpanded={defaultExpanded}
57+
onExpand={onExpand}
58+
onCollapse={onCollapse}
59+
>
60+
{children}
61+
</DefaultPanel>
62+
)}
63+
<HorizontalDividerBottom />
64+
</RenderedSection>
65+
);
66+
67+
export { Accordion, AccordionProps };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components';
2+
import { ChevronDownIcon } from '../../../icons';
3+
import { Colors } from '../../../essentials';
4+
5+
export const ChevronDown = styled(ChevronDownIcon)`
6+
color: ${props => (props.color ? props.color : Colors.AUTHENTIC_BLUE_900)};
7+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components';
2+
import { ChevronUpIcon } from '../../../icons';
3+
import { Colors } from '../../../essentials';
4+
5+
export const ChevronUp = styled(ChevronUpIcon)`
6+
color: ${props => (props.color ? props.color : Colors.AUTHENTIC_BLUE_900)};
7+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
import styled from 'styled-components';
3+
4+
import { Colors } from '../../../essentials';
5+
import { Box } from '../../Box/Box';
6+
import { Headline } from '../../Headline/Headline';
7+
import { Header } from './Header';
8+
import { ChevronUp } from './ChevronUp';
9+
import { ChevronDown } from './ChevronDown';
10+
import { Description } from './Description';
11+
import { AccordionProps } from '../types';
12+
13+
type Props = Pick<
14+
AccordionProps,
15+
'heading' | 'description' | 'defaultExpanded' | 'children' | 'onExpand' | 'onCollapse'
16+
>;
17+
18+
const StyleHeadline = styled(Headline)``;
19+
20+
const PanelHeader = styled(Header)`
21+
&:hover ${StyleHeadline} {
22+
color: ${Colors.ACTION_BLUE_1000};
23+
}
24+
25+
&:hover ${ChevronDown} {
26+
color: ${Colors.ACTION_BLUE_1000};
27+
}
28+
29+
&:hover ${ChevronUp} {
30+
color: ${Colors.ACTION_BLUE_1000};
31+
}
32+
`;
33+
34+
const PanelIcon = ({ isOpen }: { isOpen: boolean }) => (isOpen ? <ChevronUp /> : <ChevronDown />);
35+
36+
export const Compact = ({ heading, description, defaultExpanded = false, children, onExpand, onCollapse }: Props) => {
37+
const [isOpen, setIsOpen] = React.useState<boolean>(defaultExpanded);
38+
39+
return (
40+
<>
41+
<PanelHeader
42+
onClick={() => {
43+
if (isOpen) {
44+
onExpand();
45+
} else {
46+
onCollapse();
47+
}
48+
setIsOpen(!isOpen);
49+
}}
50+
>
51+
<Box display="flex" flexDirection="column" maxWidth="33%">
52+
<Headline as="h4" mr="3">
53+
{heading}
54+
</Headline>
55+
{isOpen && <Description mt="1" description={description} />}
56+
</Box>
57+
{!isOpen && <Description mt="1" description={description} />}
58+
<Box ml="3">
59+
<PanelIcon isOpen={isOpen} />
60+
</Box>
61+
</PanelHeader>
62+
{isOpen && (
63+
<Box mx="2" mb="5">
64+
{children}
65+
</Box>
66+
)}
67+
</>
68+
);
69+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { useState, PropsWithChildren } from 'react';
2+
import styled from 'styled-components';
3+
4+
import { Colors } from '../../../essentials';
5+
import { Text } from '../../Text/Text';
6+
import { Box } from '../../Box/Box';
7+
import { Headline } from '../../Headline/Headline';
8+
import { Header } from './Header';
9+
import { ChevronUp } from './ChevronUp';
10+
import { ChevronDown } from './ChevronDown';
11+
import { Description } from './Description';
12+
import { AccordionProps } from '../types';
13+
14+
const ButtonLabel = styled(Text).attrs({ as: 'p' })`
15+
color: ${Colors.ACTION_BLUE_900};
16+
`;
17+
18+
const PanelHeader = styled(Header)`
19+
&:hover {
20+
background-color: ${Colors.ACTION_BLUE_50};
21+
}
22+
23+
&:hover ${ButtonLabel} {
24+
color: ${Colors.ACTION_BLUE_1000};
25+
}
26+
27+
&:hover ${ChevronDown} {
28+
color: ${Colors.ACTION_BLUE_1000};
29+
}
30+
`;
31+
32+
const CardHeader = styled(Header).attrs({ p: '3' })`
33+
background-color: ${Colors.AUTHENTIC_BLUE_50};
34+
border-radius: 0.3125rem 0.3125rem 0 0;
35+
36+
&:hover {
37+
background-color: ${Colors.ACTION_BLUE_50};
38+
}
39+
40+
&:hover ${ButtonLabel} {
41+
color: ${Colors.ACTION_BLUE_1000};
42+
}
43+
44+
&:hover ${ChevronUp} {
45+
color: ${Colors.ACTION_BLUE_1000};
46+
}
47+
`;
48+
49+
const PanelBody = styled(Box).attrs({ my: '3' })`
50+
border: solid 0.0625rem ${Colors.AUTHENTIC_BLUE_200};
51+
border-radius: 0.3125rem;
52+
`;
53+
54+
const PanelIcon = ({ isOpen }: { isOpen: boolean }) =>
55+
isOpen ? <ChevronUp color={Colors.ACTION_BLUE_900} /> : <ChevronDown color={Colors.ACTION_BLUE_900} />;
56+
57+
export const DefaultPanel = ({
58+
heading,
59+
description,
60+
info,
61+
buttonLabel,
62+
defaultExpanded = false,
63+
children,
64+
onExpand,
65+
onCollapse
66+
}: PropsWithChildren<AccordionProps>) => {
67+
const [isOpen, setIsOpen] = useState<boolean>(defaultExpanded);
68+
69+
return (
70+
<>
71+
{isOpen ? (
72+
<PanelBody>
73+
<CardHeader
74+
onClick={() => {
75+
setIsOpen(!isOpen);
76+
onCollapse();
77+
}}
78+
>
79+
<Box display="flex" flexDirection="column" maxWidth="33%">
80+
<Headline as="h4" mr="3">
81+
{heading}
82+
</Headline>
83+
<Description mt="1" description={description} />
84+
</Box>
85+
<Box ml="3" display="flex" flexDirection="row">
86+
<ButtonLabel>{buttonLabel}</ButtonLabel>
87+
<PanelIcon isOpen={isOpen} />
88+
</Box>
89+
</CardHeader>
90+
<Box m="3">{children}</Box>
91+
</PanelBody>
92+
) : (
93+
<PanelHeader
94+
onClick={() => {
95+
setIsOpen(!isOpen);
96+
onExpand();
97+
}}
98+
>
99+
<Headline as="h4" mr="3">
100+
{heading}
101+
</Headline>
102+
<Box>
103+
<Description description={description} />
104+
<Text as="p" style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
105+
{info}
106+
</Text>
107+
</Box>
108+
<Box ml="3" display="flex" flexDirection="row">
109+
<ButtonLabel>{buttonLabel}</ButtonLabel>
110+
<PanelIcon isOpen={isOpen} />
111+
</Box>
112+
</PanelHeader>
113+
)}
114+
</>
115+
);
116+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import { MarginProps } from 'styled-system';
3+
import { Text } from '../../Text/Text';
4+
5+
interface Props extends MarginProps {
6+
description?: string;
7+
}
8+
9+
export const Description = ({ description, ...rest }: Props) => (
10+
<Text
11+
as="p"
12+
fontSize="small"
13+
weak
14+
style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}
15+
{...rest}
16+
>
17+
{description}
18+
</Text>
19+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import styled from 'styled-components';
2+
3+
import { Colors } from '../../../essentials';
4+
import { Box } from '../../Box/Box';
5+
6+
export const Header = styled(Box).attrs({ p: '2', color: Colors.AUTHENTIC_BLUE_900 })`
7+
display: flex;
8+
flex-direction: row;
9+
justify-content: space-between;
10+
11+
cursor: pointer;
12+
min-height: 2.5rem;
13+
`;

0 commit comments

Comments
 (0)