Skip to content

Commit c4df7c5

Browse files
author
vvo
committed
feat(collapsable widgets): add collapsable and collapsed option
All `header` capable widgets are now collapsable aware. New options: - collapsable: Hide the widget body and footer when clicking on header - collapsed: Initialize the widget in collapsed state This also adds: - ais-root generic css class - ais-body generic css class To be in par with already existing ais-header, ais-footer. This also adds: - ais-root__collapsable css class - ais-root__collapsed css class
1 parent e2ff425 commit c4df7c5

File tree

25 files changed

+193
-55
lines changed

25 files changed

+193
-55
lines changed

dev/app.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ search.addWidget(
122122

123123
search.addWidget(
124124
instantsearch.widgets.refinementList({
125+
collapsible: {
126+
collapsed: true
127+
},
125128
container: '#brands',
126129
attributeName: 'brand',
127130
operator: 'or',
@@ -133,7 +136,7 @@ search.addWidget(
133136
active: 'facet-active'
134137
},
135138
templates: {
136-
header: 'Brands'
139+
header: 'Brands with collapsible <span class="collapse-arrow"></span>'
137140
},
138141
showMore: {
139142
templates: {

dev/style.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,21 @@ body {
122122
.ais-show-more__active, .ais-show-more__inactive {
123123
cursor: pointer;
124124
}
125+
126+
.ais-header {
127+
position: relative;
128+
}
129+
130+
.collapse-arrow {
131+
position: absolute;
132+
right: 0;
133+
}
134+
135+
.collapse-arrow:after {
136+
content: "[-]";
137+
font-family: monospace;
138+
}
139+
140+
.ais-root__collapsed .collapse-arrow:after {
141+
content: "[+]";
142+
}

src/css/_base.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@
2929
}
3030
}
3131
}
32+
33+
.ais-root__collapsible .ais-header {
34+
cursor: pointer;
35+
}
36+
37+
.ais-root__collapsed .ais-body, .ais-root__collapsed .ais-footer {
38+
display: none;
39+
}

src/decorators/__tests__/headerFooter-test.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('headerFooter', () => {
2121
root: 'root',
2222
body: 'body'
2323
},
24+
collapsible: false,
2425
templateProps: {
2526
}
2627
};
@@ -30,8 +31,8 @@ describe('headerFooter', () => {
3031
it('should render the component in a root and body', () => {
3132
let out = render(defaultProps);
3233
expect(out).toEqualJSX(
33-
<div className="root">
34-
<div className="body">
34+
<div className="ais-root root">
35+
<div className="ais-body body">
3536
<TestComponent {...defaultProps} />
3637
</div>
3738
</div>
@@ -55,9 +56,9 @@ describe('headerFooter', () => {
5556
}
5657
};
5758
expect(out).toEqualJSX(
58-
<div className="root">
59-
<Template cssClass="ais-header" {...templateProps} />
60-
<div className="body">
59+
<div className="ais-root root">
60+
<Template cssClass="ais-header" {...templateProps} onClick={null} />
61+
<div className="ais-body body">
6162
<TestComponent {...defaultProps} />
6263
</div>
6364
</div>
@@ -81,11 +82,11 @@ describe('headerFooter', () => {
8182
}
8283
};
8384
expect(out).toEqualJSX(
84-
<div className="root">
85-
<div className="body">
85+
<div className="ais-root root">
86+
<div className="ais-body body">
8687
<TestComponent {...defaultProps} />
8788
</div>
88-
<Template cssClass="ais-footer" {...templateProps} />
89+
<Template cssClass="ais-footer" {...templateProps} onClick={null} />
8990
</div>
9091
);
9192
});

src/decorators/headerFooter.js

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,30 @@ import Template from '../components/Template.js';
99

1010
function headerFooter(ComposedComponent) {
1111
class HeaderFooter extends React.Component {
12-
componentWillMount() {
13-
// Only add header/footer if a template is defined
14-
this._header = this.getTemplate('header');
15-
this._footer = this.getTemplate('footer');
16-
this._classNames = {
17-
root: cx(this.props.cssClasses.root),
18-
body: cx(this.props.cssClasses.body)
12+
constructor(props) {
13+
super(props);
14+
this.handleHeaderClick = this.handleHeaderClick.bind(this);
15+
this.state = {
16+
collapsed: props.collapsible && props.collapsible.collapsed
1917
};
18+
19+
this._headerElement = this._getElement({
20+
type: 'header',
21+
handleClick: props.collapsible ? this.handleHeaderClick : null
22+
});
23+
24+
this._cssClasses = {
25+
root: cx('ais-root', this.props.cssClasses.root),
26+
body: cx('ais-body', this.props.cssClasses.body)
27+
};
28+
29+
this._footerElement = this._getElement({type: 'footer'});
2030
}
21-
getTemplate(type) {
31+
shouldComponentUpdate(nextProps, nextState) {
32+
return nextState.collapsed === false ||
33+
nextState !== this.state;
34+
}
35+
_getElement({type, handleClick = null}) {
2236
let templates = this.props.templateProps.templates;
2337
if (!templates || !templates[type]) {
2438
return null;
@@ -27,25 +41,54 @@ function headerFooter(ComposedComponent) {
2741
return (
2842
<Template {...this.props.templateProps}
2943
cssClass={className}
44+
onClick={handleClick}
3045
templateKey={type}
3146
transformData={null}
3247
/>
3348
);
3449
}
50+
handleHeaderClick() {
51+
this.setState({
52+
collapsed: !this.state.collapsed
53+
});
54+
}
3555
render() {
56+
let rootCssClasses = [this._cssClasses.root];
57+
58+
if (this.props.collapsible) {
59+
rootCssClasses.push('ais-root__collapsible');
60+
}
61+
62+
if (this.state.collapsed) {
63+
rootCssClasses.push('ais-root__collapsed');
64+
}
65+
66+
const cssClasses = {
67+
...this._cssClasses,
68+
root: cx(rootCssClasses)
69+
};
70+
3671
return (
37-
<div className={this._classNames.root}>
38-
{this._header}
39-
<div className={this._classNames.body}>
72+
<div className={cssClasses.root}>
73+
{this._headerElement}
74+
<div
75+
className={cssClasses.body}
76+
>
4077
<ComposedComponent {...this.props} />
4178
</div>
42-
{this._footer}
79+
{this._footerElement}
4380
</div>
4481
);
4582
}
4683
}
4784

4885
HeaderFooter.propTypes = {
86+
collapsible: React.PropTypes.oneOfType([
87+
React.PropTypes.bool,
88+
React.PropTypes.shape({
89+
collapsed: React.PropTypes.bool
90+
})
91+
]),
4992
cssClasses: React.PropTypes.shape({
5093
root: React.PropTypes.string,
5194
header: React.PropTypes.string,
@@ -56,7 +99,8 @@ function headerFooter(ComposedComponent) {
5699
};
57100

58101
HeaderFooter.defaultProps = {
59-
cssClasses: {}
102+
cssClasses: {},
103+
collapsible: false
60104
};
61105

62106
// precise displayName for ease of debugging (react dev tool, react warnings)

src/widgets/clear-all/__tests__/clear-all-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('clearAll()', () => {
5757
footer: 'ais-clear-all--footer',
5858
link: 'ais-clear-all--link'
5959
},
60+
collapsible: false,
6061
hasRefinements: false,
6162
shouldAutoHideContainer: true,
6263
templateProps: {

src/widgets/clear-all/clear-all.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,23 @@ let bem = bemHelper('ais-clear-all');
3333
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
3434
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
3535
* @param {string|string[]} [options.cssClasses.link] CSS class to add to the link element
36+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
37+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
3638
* @return {Object}
3739
*/
3840
const usage = `Usage:
3941
clearAll({
4042
container,
41-
[cssClasses.{root,header,body,footer,link}={}],
42-
[templates.{header,link,footer}={header: '', link: 'Clear all', footer: ''}],
43-
[autoHideContainer=true]
43+
[ cssClasses.{root,header,body,footer,link}={} ],
44+
[ templates.{header,link,footer}={header: '', link: 'Clear all', footer: ''} ],
45+
[ autoHideContainer=true ],
46+
[ collapsible=false ]
4447
})`;
4548
function clearAll({
4649
container,
4750
templates = defaultTemplates,
4851
cssClasses: userCssClasses = {},
52+
collapsible = false,
4953
autoHideContainer = true
5054
} = {}) {
5155
if (!container) {
@@ -79,6 +83,7 @@ function clearAll({
7983
ReactDOM.render(
8084
<ClearAll
8185
clearAll={this._clearRefinementsAndSearch}
86+
collapsible={collapsible}
8287
cssClasses={cssClasses}
8388
hasRefinements={hasRefinements}
8489
shouldAutoHideContainer={!hasRefinements}

src/widgets/current-refined-values/__tests__/current-refined-values-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ describe('currentRefinedValues()', () => {
508508
_tags: {name: '_tags'}
509509
},
510510
clearAllClick: () => {},
511+
collapsible: false,
511512
clearAllPosition: 'after',
512513
clearAllURL: '#cleared',
513514
cssClasses: {

src/widgets/current-refined-values/current-refined-values.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ let bem = bemHelper('ais-current-refined-values');
6060
* @param {string} [options.cssClasses.link] CSS classes added to the link element
6161
* @param {string} [options.cssClasses.count] CSS classes added to the count element
6262
* @param {string} [options.cssClasses.footer] CSS classes added to the footer element
63+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
64+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
6365
* @return {Object}
6466
*/
6567
const usage = `Usage:
@@ -71,14 +73,16 @@ currentRefinedValues({
7173
[ templates.{header = '', item, clearAll, footer = ''} ],
7274
[ transformData ],
7375
[ autoHideContainer = true ],
74-
[ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ]
76+
[ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ],
77+
[ collapsible=false ]
7578
})`;
7679
function currentRefinedValues({
7780
container,
7881
attributes = [],
7982
onlyListedAttributes = false,
8083
clearAll = 'before',
8184
templates = defaultTemplates,
85+
collapsible = false,
8286
transformData,
8387
autoHideContainer = true,
8488
cssClasses: userCssClasses = {}
@@ -179,6 +183,7 @@ function currentRefinedValues({
179183
clearAllURL={clearAllURL}
180184
clearRefinementClicks={clearRefinementClicks}
181185
clearRefinementURLs={clearRefinementURLs}
186+
collapsible={collapsible}
182187
cssClasses={cssClasses}
183188
refinements={refinements}
184189
shouldAutoHideContainer={shouldAutoHideContainer}

src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ describe('hierarchicalMenu()', () => {
218218
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(
219219
<RefinementList
220220
attributeNameKey="path"
221+
collapsible={false}
221222
cssClasses={cssClasses}
222223
facetValues={[{name: 'foo', url: '#'}, {name: 'bar', url: '#'}]}
223224
shouldAutoHideContainer={false}

0 commit comments

Comments
 (0)