Skip to content

Commit 4409518

Browse files
tassoevanggazzo
authored andcommitted
feat: Accordion component (#68)
1 parent 2bdece1 commit 4409518

20 files changed

+360
-2
lines changed
Loading
Loading

packages/fuselage/.storybook/jest-results.json

+1-1
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useClassName, useToggle } from '@rocket.chat/fuselage-hooks';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
4+
5+
import { useTheme } from '../../hooks/useTheme';
6+
import { Icon } from '../Icon';
7+
import { ToggleSwitch } from '../ToggleSwitch';
8+
import { StyledAccordionItem, Bar, Title, Panel } from './styles';
9+
10+
export const Item = React.forwardRef(function Item({
11+
children,
12+
className,
13+
defaultExpanded,
14+
disabled,
15+
expanded,
16+
tabIndex = 0,
17+
title,
18+
onToggle,
19+
onToggleEnabled,
20+
...props
21+
}, ref) {
22+
const classNames = {
23+
bar: useClassName('rcx-accordion-item__bar', {}, className),
24+
title: useClassName('rcx-accordion-item__title'),
25+
toggleBtn: useClassName('rcx-accordion-item__toggle-button'),
26+
};
27+
const theme = useTheme();
28+
29+
const [internalExpanded, toggleExpanded] = useToggle(defaultExpanded);
30+
31+
const handleClick = (event) => {
32+
if (disabled) {
33+
return;
34+
}
35+
36+
if (onToggle) {
37+
return onToggle.call(event.currentTarget, event);
38+
}
39+
40+
toggleExpanded();
41+
};
42+
43+
const handleKeyDown = (event) => {
44+
if (disabled || event.currentTarget !== event.target) {
45+
return;
46+
}
47+
48+
if (event.keyCode === 32) {
49+
event.preventDefault();
50+
51+
if (event.repeat) {
52+
return;
53+
}
54+
55+
if (onToggle) {
56+
return onToggle.call(event.currentTarget, event);
57+
}
58+
59+
toggleExpanded();
60+
}
61+
};
62+
63+
const handleToggleClick = (event) => {
64+
event.stopPropagation();
65+
};
66+
67+
return <StyledAccordionItem theme={theme} {...props}>
68+
<Bar
69+
aria-checked={expanded || internalExpanded ? 'true' : 'false'}
70+
className={classNames.bar}
71+
disabled={disabled}
72+
expanded={expanded || internalExpanded}
73+
ref={ref}
74+
role='switch'
75+
tabIndex={!disabled ? tabIndex : undefined}
76+
theme={theme}
77+
onClick={handleClick}
78+
onKeyDown={handleKeyDown}
79+
>
80+
<Title className={classNames.bar} theme={theme}>{title}</Title>
81+
{(disabled || onToggleEnabled)
82+
&& <ToggleSwitch checked={!disabled} onClick={handleToggleClick} onChange={onToggleEnabled} />}
83+
<Icon name={'arrow-down'} />
84+
</Bar>
85+
<Panel className={classNames.panel} expanded={expanded || internalExpanded} theme={theme}>
86+
{children}
87+
</Panel>
88+
</StyledAccordionItem>;
89+
});
90+
91+
Item.defaultProps = {
92+
tabIndex: 0,
93+
};
94+
95+
Item.displayName = 'Accordion.Item';
96+
97+
Item.propTypes = {
98+
children: PropTypes.node,
99+
defaultExpanded: PropTypes.bool,
100+
disabled: PropTypes.bool,
101+
expanded: PropTypes.bool,
102+
tabIndex: PropTypes.number,
103+
title: PropTypes.string.isRequired,
104+
onToggle: PropTypes.func,
105+
onToggleEnabled: PropTypes.func,
106+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { action } from '@storybook/addon-actions';
2+
import { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks';
3+
4+
import { Accordion, Paragraph } from '../..';
5+
6+
<Meta title='Containers|Accordion/Accordion.Item' parameters={{ jest: ['Accordion/spec'] }} />
7+
8+
# Accordion.Item
9+
10+
A collapsible panel.
11+
12+
<Preview>
13+
<Story name='Default'>
14+
<Accordion>
15+
<Accordion.Item title='Item'>
16+
<Paragraph>Content</Paragraph>
17+
</Accordion.Item>
18+
</Accordion>
19+
</Story>
20+
</Preview>
21+
22+
<Props of={Accordion.Item} />
23+
24+
## Expanded
25+
26+
<Preview>
27+
<Story name='Expanded'>
28+
<Accordion>
29+
<Accordion.Item title='Item' defaultExpanded>
30+
<Paragraph>Content</Paragraph>
31+
</Accordion.Item>
32+
</Accordion>
33+
</Story>
34+
</Preview>
35+
36+
## With togglable state
37+
38+
### Enabled
39+
40+
<Preview>
41+
<Story name='Enabled'>
42+
<Accordion>
43+
<Accordion.Item title='Item' onToggleEnabled={action('toggleEnabled')}>
44+
<Paragraph>Content</Paragraph>
45+
</Accordion.Item>
46+
</Accordion>
47+
</Story>
48+
</Preview>
49+
50+
### Disabled
51+
52+
<Preview>
53+
<Story name='Disabled'>
54+
<Accordion>
55+
<Accordion.Item title='Item' disabled onToggleEnabled={action('toggleEnabled')}>
56+
<Paragraph>Content</Paragraph>
57+
</Accordion.Item>
58+
</Accordion>
59+
</Story>
60+
</Preview>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useClassName } from '@rocket.chat/fuselage-hooks';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
4+
5+
import { useTheme } from '../../hooks/useTheme';
6+
import { Item } from './Item';
7+
import { StyledAccordion } from './styles';
8+
9+
export const Accordion = React.forwardRef(function Accordion({
10+
className,
11+
...props
12+
}, ref) {
13+
const compoundClassName = useClassName('rcx-accordion', {}, className);
14+
const theme = useTheme();
15+
return <StyledAccordion className={compoundClassName} ref={ref} theme={theme} {...props} />;
16+
});
17+
18+
Accordion.displayName = 'Accordion';
19+
20+
Accordion.propTypes = {
21+
children: PropTypes.node.isRequired,
22+
};
23+
24+
Accordion.Item = Item;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
4+
import { Accordion } from '../..';
5+
6+
it('renders without crashing', () => {
7+
const div = document.createElement('div');
8+
ReactDOM.render(<Accordion>
9+
<Accordion.Item title='' />
10+
</Accordion>, div);
11+
ReactDOM.unmountComponentAtNode(div);
12+
});
13+
14+
describe('Accordion.Item', () => {
15+
it('renders without crashing', () => {
16+
const div = document.createElement('div');
17+
ReactDOM.render(<Accordion.Item title='' />, div);
18+
ReactDOM.unmountComponentAtNode(div);
19+
});
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks';
2+
import LinkTo from '@storybook/addon-links/react';
3+
4+
import { Accordion, Paragraph } from '../..';
5+
6+
<Meta title='Containers|Accordion' parameters={{ jest: ['Accordion/spec'] }} />
7+
8+
# Accordion
9+
10+
An <LinkTo kind='Containers|Accordion' story='Default Story'>`Accordion`</LinkTo> allows users to toggle the display of
11+
sections of content.
12+
13+
<Preview>
14+
<Story name='Default'>
15+
<Accordion>
16+
<Accordion.Item title='Item #1' defaultExpanded>
17+
<Paragraph>Content #1</Paragraph>
18+
</Accordion.Item>
19+
<Accordion.Item title='Item #2'>
20+
<Paragraph>Content #2</Paragraph>
21+
</Accordion.Item>
22+
<Accordion.Item title='Item #3'>
23+
<Paragraph>Content #3</Paragraph>
24+
</Accordion.Item>
25+
</Accordion>
26+
</Story>
27+
</Preview>
28+
29+
<Props of={Accordion} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import colors from '@rocket.chat/fuselage-tokens/colors';
2+
import styled, { css } from 'styled-components';
3+
4+
import box from '../../styles/box';
5+
import { clickable } from '../../styles/utilities/interactivity';
6+
import { truncate, subtitleBold, subtitle } from '../../styles/utilities/typography';
7+
import { StyledIcon } from '../Icon/styles';
8+
import { toRem } from '../../styles/helpers';
9+
10+
export const StyledAccordion = styled.div`
11+
${ box }
12+
13+
display: flex;
14+
flex-flow: column nowrap;
15+
16+
border-bottom-width: ${ ({ theme }) => theme.borders.width.x2 };
17+
border-bottom-color: ${ colors.dark300 };
18+
`;
19+
20+
export const StyledAccordionItem = styled.div`
21+
${ box }
22+
23+
display: flex;
24+
flex-flow: column nowrap;
25+
`;
26+
27+
export const Bar = styled.div`
28+
${ box }
29+
30+
display: flex;
31+
flex-flow: row nowrap;
32+
33+
border-width: ${ ({ theme }) => theme.borders.width.x2 };
34+
border-color: ${ colors.dark300 } transparent transparent;
35+
${ ({ theme }) => css`
36+
padding:
37+
calc(${ theme.spaces.x32 } - ${ theme.borders.width.x2 })
38+
calc(${ theme.spaces.x8 } - ${ theme.borders.width.x2 });
39+
` }
40+
41+
text-align: left;
42+
43+
${ ({ disabled }) => !disabled && css`
44+
${ clickable }
45+
` }
46+
47+
& > .rcx-toggle-switch {
48+
margin: 0 24px;
49+
}
50+
51+
& > ${ StyledIcon } {
52+
font-size: ${ ({ theme }) => theme.sizes.x24 };
53+
54+
${ ({ expanded }) => expanded && css`
55+
transform: rotate(-180deg);
56+
` }
57+
}
58+
59+
&.hover,
60+
&:hover {
61+
background-color: ${ colors.dark100 };
62+
}
63+
64+
&.focus,
65+
&:focus {
66+
border-color: ${ colors.blue500 };
67+
box-shadow: 0 0 0 ${ toRem(6) } ${ colors.blue100 };
68+
}
69+
70+
${ ({ disabled, theme }) => disabled && css`
71+
color: ${ theme.textColors.disabled };
72+
background-color: ${ colors.dark100 };
73+
` }
74+
`;
75+
76+
export const Title = styled.h2`
77+
${ box }
78+
79+
flex: 1 1 0;
80+
81+
${ ({ disabled, theme }) => disabled && css`
82+
color: ${ theme.textColors.disabled };
83+
` }
84+
85+
${ ({ theme }) => subtitle(theme) }
86+
${ ({ theme }) => subtitleBold(theme) }
87+
${ truncate }
88+
`;
89+
90+
export const Panel = styled.div`
91+
${ box }
92+
93+
overflow: hidden;
94+
95+
height: 0;
96+
${ ({ theme }) => css`
97+
padding: 0 ${ theme.spaces.x8 };
98+
` }
99+
100+
${ ({ expanded, theme }) => expanded && css`
101+
height: auto;
102+
padding: ${ theme.spaces.x32 } ${ theme.spaces.x8 };
103+
` }
104+
`;

packages/fuselage/src/components/ToggleSwitch/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const ToggleSwitch = React.forwardRef(function ToggleSwitch({
99
hidden,
1010
invisible,
1111
style,
12+
onClick,
1213
...props
1314
}, ref) {
1415
const classNames = {
@@ -17,7 +18,7 @@ export const ToggleSwitch = React.forwardRef(function ToggleSwitch({
1718
fake: useClassName('rcx-toggle-switch__fake'),
1819
};
1920

20-
return <Label className={classNames.container} hidden={hidden} invisible={invisible} style={style}>
21+
return <Label className={classNames.container} hidden={hidden} invisible={invisible} style={style} onClick={onClick}>
2122
<Box className={classNames.input} is='input' ref={ref} type='checkbox' {...props} />
2223
<Box aria-hidden='true' className={classNames.fake} is='i' />
2324
</Label>;

packages/fuselage/src/components/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './Accordion';
12
export * from './Box';
23
export * from './Button';
34
export * from './ButtonGroup';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { css } from 'styled-components';
2+
3+
export const clickable = css`
4+
cursor: pointer;
5+
6+
outline: 0;
7+
8+
*:disabled &,
9+
&:disabled,
10+
&.disabled {
11+
cursor: not-allowed;
12+
}
13+
`;

0 commit comments

Comments
 (0)