Skip to content

Commit 31eac90

Browse files
mareklibrajeff-phillips-18
authored andcommitted
feat(ExpandCollapse): introduce ExpandCollapse component (#613)
Implementation for the Expand Collapse Section is provided. The component can be farther used for the "More Information" use-case, where additional text/components are hidden unless the user clicks on a link. Such a per-request rendered content is used for texts exceeding scope of a tooltip or when the user is expected to copy and paste part of the information.
1 parent 25ca229 commit 31eac90

File tree

11 files changed

+400
-0
lines changed

11 files changed

+400
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.expand-collapse-pf {
2+
.expand-collapse-pf-link-container {
3+
display: flex;
4+
}
5+
6+
.btn.btn-link {
7+
&:focus {
8+
outline: none;
9+
}
10+
.fa,
11+
.pficon {
12+
color: initial;
13+
font-size: 14px;
14+
margin-right: 5px;
15+
width: 10px;
16+
}
17+
}
18+
19+
.expand-collapse-pf-separator {
20+
flex: 1;
21+
height: 1px;
22+
margin-top: 12px;
23+
&.bordered {
24+
border-top: 1px solid @color-pf-black-300;
25+
}
26+
}
27+
}

packages/patternfly-3/patternfly-react/less/patternfly-react.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
@import 'verticalnavdivider';
1414
@import 'treeview';
1515
@import 'pagination';
16+
@import 'expand-collapse';
17+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.expand-collapse-pf {
2+
.expand-collapse-pf-link-container {
3+
display: flex;
4+
}
5+
6+
.btn.btn-link {
7+
&:focus {
8+
outline: none;
9+
}
10+
.fa,
11+
.pficon {
12+
color: initial;
13+
font-size: 14px;
14+
margin-right: 5px;
15+
width: 10px;
16+
}
17+
}
18+
19+
.expand-collapse-pf-separator {
20+
flex: 1;
21+
height: 1px;
22+
margin-top: 12px;
23+
&.bordered {
24+
border-top: 1px solid $color-pf-black-300;
25+
}
26+
}
27+
}

packages/patternfly-3/patternfly-react/sass/patternfly-react/_patternfly-react.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
@import 'verticalnavdivider';
1414
@import 'treeview';
1515
@import 'pagination';
16+
@import 'expand-collapse';
17+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import classNames from 'classnames';
4+
5+
import { Button } from '../Button';
6+
import { Icon } from '../Icon';
7+
8+
import { ALIGN_LEFT, ALIGN_CENTER, ALIGN_TYPES } from './constants';
9+
10+
class ExpandCollapse extends React.Component {
11+
state = { expanded: false, mirroredExpanded: false };
12+
13+
static getDerivedStateFromProps(nextProps, prevState) {
14+
if (prevState.mirroredExpanded !== nextProps.expanded) {
15+
return {
16+
expanded: nextProps.expanded,
17+
mirroredExpanded: nextProps.expanded
18+
};
19+
}
20+
return null;
21+
}
22+
23+
onClick = () => {
24+
this.setState(prevState => ({ expanded: !prevState.expanded }));
25+
};
26+
27+
render() {
28+
const { children, textCollapsed, textExpanded, align, className, bordered } = this.props;
29+
const { expanded } = this.state;
30+
31+
const separatorClass = classNames('expand-collapse-pf-separator', { bordered });
32+
33+
return (
34+
<div className={classNames('expand-collapse-pf', className)}>
35+
<div className="expand-collapse-pf-link-container">
36+
{align === ALIGN_CENTER && <span className={separatorClass} />}
37+
<Button bsStyle="link" onClick={this.onClick}>
38+
<Icon type="fa" name={expanded ? 'angle-down' : 'angle-right'} />
39+
{expanded ? textExpanded : textCollapsed}
40+
</Button>
41+
<span className={separatorClass} />
42+
</div>
43+
{this.state.expanded && children}
44+
</div>
45+
);
46+
}
47+
}
48+
49+
ExpandCollapse.propTypes = {
50+
children: PropTypes.any.isRequired,
51+
/** Top-level custom class */
52+
className: PropTypes.string,
53+
/** Text for the link in collapsed state */
54+
textCollapsed: PropTypes.string,
55+
/** Text for the link in expanded state */
56+
textExpanded: PropTypes.string,
57+
/** Align the link to the left or center. Default: left. */
58+
align: PropTypes.oneOf(ALIGN_TYPES),
59+
/** Flag to show a separation border line */
60+
bordered: PropTypes.bool,
61+
/** Flag to control expansion state */
62+
expanded: PropTypes.bool // eslint-disable-line react/no-unused-prop-types
63+
};
64+
65+
ExpandCollapse.defaultProps = {
66+
className: '',
67+
textCollapsed: 'Show Advanced Options',
68+
textExpanded: 'Hide Advanced Options',
69+
align: ALIGN_LEFT,
70+
bordered: true,
71+
expanded: false
72+
};
73+
74+
export default ExpandCollapse;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import { withInfo } from '@storybook/addon-info';
4+
import { defaultTemplate } from 'storybook/decorators/storyTemplates';
5+
import { storybookPackageName, STORYBOOK_CATEGORY, DOCUMENTATION_URL } from 'storybook/constants/siteConstants';
6+
import { name } from '../../../package.json';
7+
import { boolean, select, text, withKnobs } from '@storybook/addon-knobs';
8+
9+
import { ExpandCollapse } from './index';
10+
import { ALIGN_TYPES } from './constants';
11+
12+
const stories = storiesOf(
13+
`${storybookPackageName(name)}/${STORYBOOK_CATEGORY.FORMS_AND_CONTROLS}/Expand Collapse`,
14+
module
15+
);
16+
stories.addDecorator(withKnobs);
17+
stories.addDecorator(
18+
defaultTemplate({
19+
title: 'Expand Collapse',
20+
documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_FORMS}expand-collapse-section/`
21+
})
22+
);
23+
24+
stories.add(
25+
'ExpandCollapse',
26+
withInfo(`This is the ExpandCollapse component.`)(() => (
27+
<div style={{ width: '600px', border: '1px solid lightgray' }}>
28+
<ExpandCollapse
29+
align={select('align', ALIGN_TYPES)}
30+
bordered={boolean('bordered', true)}
31+
textExpanded={text('textExpanded', 'Hide Advanced Options')}
32+
textCollapsed={text('textCollapsed', 'Show Advanced Options')}
33+
expanded={boolean('expanded', false)}
34+
>
35+
<p>Well done! The component takes 100% width by default and aligns the link to the left or center.</p>
36+
<p>And other text comes here.</p>
37+
</ExpandCollapse>
38+
</div>
39+
))
40+
);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react';
2+
import { mount, render } from 'enzyme';
3+
4+
import ExpandCollapse from './ExpandCollapse';
5+
import { ALIGN_LEFT, ALIGN_CENTER } from './constants';
6+
7+
test('ExpandCollapse with content', () => {
8+
const view = mount(
9+
<ExpandCollapse>
10+
<div id="content">My text</div>
11+
</ExpandCollapse>
12+
);
13+
14+
expect(view.find('#content')).toHaveLength(0);
15+
expect(view.find('span.fa-angle-right')).toHaveLength(1);
16+
expect(view.find('span.fa-angle-down')).toHaveLength(0);
17+
expect(view.find('.btn-link').text()).toEqual('Show Advanced Options');
18+
19+
const button = view.find('.btn-link');
20+
expect(button).toHaveLength(1);
21+
button.simulate('click');
22+
expect(view.find('#content')).toHaveLength(1);
23+
expect(view.find('span.fa-angle-right')).toHaveLength(0);
24+
expect(view.find('span.fa-angle-down')).toHaveLength(1);
25+
expect(view.find('.btn-link').text()).toEqual('Hide Advanced Options');
26+
button.simulate('click');
27+
expect(view.find('#content')).toHaveLength(0);
28+
expect(view.find('span.fa-angle-right')).toHaveLength(1);
29+
expect(view.find('span.fa-angle-down')).toHaveLength(0);
30+
expect(view.find('.btn-link').text()).toEqual('Show Advanced Options');
31+
});
32+
33+
test('localized ExpandCollapse', () => {
34+
const view = mount(
35+
<ExpandCollapse textCollapsed="Click to expand" textExpanded="Click to collapse">
36+
<div id="content">My text</div>
37+
</ExpandCollapse>
38+
);
39+
expect(view).toMatchSnapshot();
40+
view.find('.btn-link').simulate('click');
41+
expect(view).toMatchSnapshot();
42+
});
43+
44+
test('aligned ExpandCollapse', () => {
45+
const def = render(
46+
<ExpandCollapse>
47+
<div id="content">My text</div>
48+
</ExpandCollapse>
49+
);
50+
expect(def.find('.expand-collapse-pf-separator')).toHaveLength(1);
51+
52+
const left = render(
53+
<ExpandCollapse align={ALIGN_LEFT}>
54+
<div id="content">My text</div>
55+
</ExpandCollapse>
56+
);
57+
expect(left.find('.expand-collapse-pf-separator')).toHaveLength(1);
58+
59+
const center = render(
60+
<ExpandCollapse align={ALIGN_CENTER}>
61+
<div id="content">My text</div>
62+
</ExpandCollapse>
63+
);
64+
expect(center.find('.expand-collapse-pf-separator')).toHaveLength(2);
65+
});
66+
67+
test('ExpandCollapse with separator', () => {
68+
const def = render(
69+
<ExpandCollapse>
70+
<div id="content">My text</div>
71+
</ExpandCollapse>
72+
);
73+
expect(def.find('.expand-collapse-pf-separator.bordered')).toHaveLength(1);
74+
75+
const noSep = render(
76+
<ExpandCollapse bordered={false}>
77+
<div id="content">My text</div>
78+
</ExpandCollapse>
79+
);
80+
expect(noSep.find('.expand-collapse-pf-separator')).toHaveLength(1);
81+
expect(noSep.find('.expand-collapse-pf-separator.bordered')).toHaveLength(0);
82+
83+
const center = render(
84+
<ExpandCollapse align={ALIGN_CENTER}>
85+
<div id="content">My text</div>
86+
</ExpandCollapse>
87+
);
88+
expect(center.find('.expand-collapse-pf-separator.bordered')).toHaveLength(2);
89+
90+
const centerNoSep = render(
91+
<ExpandCollapse align={ALIGN_CENTER} bordered={false}>
92+
<div id="content">My text</div>
93+
</ExpandCollapse>
94+
);
95+
expect(centerNoSep.find('.expand-collapse-pf-separator')).toHaveLength(2);
96+
expect(centerNoSep.find('.expand-collapse-pf-separator.bordered')).toHaveLength(0);
97+
});
98+
99+
test('ExpandCollapse with explicit override prop', () => {
100+
const view = mount(
101+
<ExpandCollapse expanded>
102+
<div id="content">My text</div>
103+
</ExpandCollapse>
104+
);
105+
106+
expect(view.find('#content')).toHaveLength(1);
107+
expect(view.find('span.fa-angle-right')).toHaveLength(0);
108+
expect(view.find('span.fa-angle-down')).toHaveLength(1);
109+
expect(view.find('.btn-link').text()).toEqual('Hide Advanced Options');
110+
});

0 commit comments

Comments
 (0)