Skip to content

Commit

Permalink
feat(collapsable widgets): add collapsable and collapsed option
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vvo committed Feb 25, 2016
1 parent e2ff425 commit c4df7c5
Show file tree
Hide file tree
Showing 25 changed files with 193 additions and 55 deletions.
5 changes: 4 additions & 1 deletion dev/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ search.addWidget(

search.addWidget(
instantsearch.widgets.refinementList({
collapsible: {
collapsed: true
},
container: '#brands',
attributeName: 'brand',
operator: 'or',
Expand All @@ -133,7 +136,7 @@ search.addWidget(
active: 'facet-active'
},
templates: {
header: 'Brands'
header: 'Brands with collapsible <span class="collapse-arrow"></span>'
},
showMore: {
templates: {
Expand Down
18 changes: 18 additions & 0 deletions dev/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,21 @@ body {
.ais-show-more__active, .ais-show-more__inactive {
cursor: pointer;
}

.ais-header {
position: relative;
}

.collapse-arrow {
position: absolute;
right: 0;
}

.collapse-arrow:after {
content: "[-]";
font-family: monospace;
}

.ais-root__collapsed .collapse-arrow:after {
content: "[+]";
}
8 changes: 8 additions & 0 deletions src/css/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@
}
}
}

.ais-root__collapsible .ais-header {
cursor: pointer;
}

.ais-root__collapsed .ais-body, .ais-root__collapsed .ais-footer {
display: none;
}
17 changes: 9 additions & 8 deletions src/decorators/__tests__/headerFooter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('headerFooter', () => {
root: 'root',
body: 'body'
},
collapsible: false,
templateProps: {
}
};
Expand All @@ -30,8 +31,8 @@ describe('headerFooter', () => {
it('should render the component in a root and body', () => {
let out = render(defaultProps);
expect(out).toEqualJSX(
<div className="root">
<div className="body">
<div className="ais-root root">
<div className="ais-body body">
<TestComponent {...defaultProps} />
</div>
</div>
Expand All @@ -55,9 +56,9 @@ describe('headerFooter', () => {
}
};
expect(out).toEqualJSX(
<div className="root">
<Template cssClass="ais-header" {...templateProps} />
<div className="body">
<div className="ais-root root">
<Template cssClass="ais-header" {...templateProps} onClick={null} />
<div className="ais-body body">
<TestComponent {...defaultProps} />
</div>
</div>
Expand All @@ -81,11 +82,11 @@ describe('headerFooter', () => {
}
};
expect(out).toEqualJSX(
<div className="root">
<div className="body">
<div className="ais-root root">
<div className="ais-body body">
<TestComponent {...defaultProps} />
</div>
<Template cssClass="ais-footer" {...templateProps} />
<Template cssClass="ais-footer" {...templateProps} onClick={null} />
</div>
);
});
Expand Down
70 changes: 57 additions & 13 deletions src/decorators/headerFooter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,30 @@ import Template from '../components/Template.js';

function headerFooter(ComposedComponent) {
class HeaderFooter extends React.Component {
componentWillMount() {
// Only add header/footer if a template is defined
this._header = this.getTemplate('header');
this._footer = this.getTemplate('footer');
this._classNames = {
root: cx(this.props.cssClasses.root),
body: cx(this.props.cssClasses.body)
constructor(props) {
super(props);
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.state = {
collapsed: props.collapsible && props.collapsible.collapsed
};

this._headerElement = this._getElement({
type: 'header',
handleClick: props.collapsible ? this.handleHeaderClick : null
});

this._cssClasses = {
root: cx('ais-root', this.props.cssClasses.root),
body: cx('ais-body', this.props.cssClasses.body)
};

this._footerElement = this._getElement({type: 'footer'});
}
getTemplate(type) {
shouldComponentUpdate(nextProps, nextState) {
return nextState.collapsed === false ||
nextState !== this.state;
}
_getElement({type, handleClick = null}) {
let templates = this.props.templateProps.templates;
if (!templates || !templates[type]) {
return null;
Expand All @@ -27,25 +41,54 @@ function headerFooter(ComposedComponent) {
return (
<Template {...this.props.templateProps}
cssClass={className}
onClick={handleClick}
templateKey={type}
transformData={null}
/>
);
}
handleHeaderClick() {
this.setState({
collapsed: !this.state.collapsed
});
}
render() {
let rootCssClasses = [this._cssClasses.root];

if (this.props.collapsible) {
rootCssClasses.push('ais-root__collapsible');
}

if (this.state.collapsed) {
rootCssClasses.push('ais-root__collapsed');
}

const cssClasses = {
...this._cssClasses,
root: cx(rootCssClasses)
};

return (
<div className={this._classNames.root}>
{this._header}
<div className={this._classNames.body}>
<div className={cssClasses.root}>
{this._headerElement}
<div
className={cssClasses.body}
>
<ComposedComponent {...this.props} />
</div>
{this._footer}
{this._footerElement}
</div>
);
}
}

HeaderFooter.propTypes = {
collapsible: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.shape({
collapsed: React.PropTypes.bool
})
]),
cssClasses: React.PropTypes.shape({
root: React.PropTypes.string,
header: React.PropTypes.string,
Expand All @@ -56,7 +99,8 @@ function headerFooter(ComposedComponent) {
};

HeaderFooter.defaultProps = {
cssClasses: {}
cssClasses: {},
collapsible: false
};

// precise displayName for ease of debugging (react dev tool, react warnings)
Expand Down
1 change: 1 addition & 0 deletions src/widgets/clear-all/__tests__/clear-all-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('clearAll()', () => {
footer: 'ais-clear-all--footer',
link: 'ais-clear-all--link'
},
collapsible: false,
hasRefinements: false,
shouldAutoHideContainer: true,
templateProps: {
Expand Down
11 changes: 8 additions & 3 deletions src/widgets/clear-all/clear-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@ let bem = bemHelper('ais-clear-all');
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
* @param {string|string[]} [options.cssClasses.link] CSS class to add to the link element
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
* @return {Object}
*/
const usage = `Usage:
clearAll({
container,
[cssClasses.{root,header,body,footer,link}={}],
[templates.{header,link,footer}={header: '', link: 'Clear all', footer: ''}],
[autoHideContainer=true]
[ cssClasses.{root,header,body,footer,link}={} ],
[ templates.{header,link,footer}={header: '', link: 'Clear all', footer: ''} ],
[ autoHideContainer=true ],
[ collapsible=false ]
})`;
function clearAll({
container,
templates = defaultTemplates,
cssClasses: userCssClasses = {},
collapsible = false,
autoHideContainer = true
} = {}) {
if (!container) {
Expand Down Expand Up @@ -79,6 +83,7 @@ function clearAll({
ReactDOM.render(
<ClearAll
clearAll={this._clearRefinementsAndSearch}
collapsible={collapsible}
cssClasses={cssClasses}
hasRefinements={hasRefinements}
shouldAutoHideContainer={!hasRefinements}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ describe('currentRefinedValues()', () => {
_tags: {name: '_tags'}
},
clearAllClick: () => {},
collapsible: false,
clearAllPosition: 'after',
clearAllURL: '#cleared',
cssClasses: {
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/current-refined-values/current-refined-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ let bem = bemHelper('ais-current-refined-values');
* @param {string} [options.cssClasses.link] CSS classes added to the link element
* @param {string} [options.cssClasses.count] CSS classes added to the count element
* @param {string} [options.cssClasses.footer] CSS classes added to the footer element
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
* @return {Object}
*/
const usage = `Usage:
Expand All @@ -71,14 +73,16 @@ currentRefinedValues({
[ templates.{header = '', item, clearAll, footer = ''} ],
[ transformData ],
[ autoHideContainer = true ],
[ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ]
[ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ],
[ collapsible=false ]
})`;
function currentRefinedValues({
container,
attributes = [],
onlyListedAttributes = false,
clearAll = 'before',
templates = defaultTemplates,
collapsible = false,
transformData,
autoHideContainer = true,
cssClasses: userCssClasses = {}
Expand Down Expand Up @@ -179,6 +183,7 @@ function currentRefinedValues({
clearAllURL={clearAllURL}
clearRefinementClicks={clearRefinementClicks}
clearRefinementURLs={clearRefinementURLs}
collapsible={collapsible}
cssClasses={cssClasses}
refinements={refinements}
shouldAutoHideContainer={shouldAutoHideContainer}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ describe('hierarchicalMenu()', () => {
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(
<RefinementList
attributeNameKey="path"
collapsible={false}
cssClasses={cssClasses}
facetValues={[{name: 'foo', url: '#'}, {name: 'bar', url: '#'}]}
shouldAutoHideContainer={false}
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/hierarchical-menu/hierarchical-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import defaultTemplates from './defaultTemplates.js';
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
* @param {string|string[]} [options.cssClasses.link] CSS class to add to each link (when using the default template)
* @param {string|string[]} [options.cssClasses.count] CSS class to add to each count element (when using the default template)
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
* @return {Object}
*/
const usage = `Usage:
Expand All @@ -51,7 +53,8 @@ hierarchicalMenu({
[ cssClasses.{root , header, body, footer, list, depth, item, active, link}={} ],
[ templates.{header, item, footer} ],
[ transformData ],
[ autoHideContainer=true ]
[ autoHideContainer=true ],
[ collapsible=false ]
})`;
function hierarchicalMenu({
container,
Expand All @@ -64,6 +67,7 @@ function hierarchicalMenu({
cssClasses: userCssClasses = {},
autoHideContainer = true,
templates = defaultTemplates,
collapsible = false,
transformData
} = {}) {
if (!container || !attributes || !attributes.length) {
Expand Down Expand Up @@ -139,6 +143,7 @@ function hierarchicalMenu({
ReactDOM.render(
<RefinementList
attributeNameKey="path"
collapsible={collapsible}
cssClasses={cssClasses}
facetValues={facetValues}
shouldAutoHideContainer={facetValues.length === 0}
Expand Down
19 changes: 12 additions & 7 deletions src/widgets/menu/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,22 @@ import defaultTemplates from './defaultTemplates.js';
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
* @param {string|string[]} [options.cssClasses.link] CSS class to add to each link (when using the default template)
* @param {string|string[]} [options.cssClasses.count] CSS class to add to each count element (when using the default template)
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
* @return {Object}
*/
const usage = `Usage:
menu({
container,
attributeName,
[sortBy],
[limit=10],
[cssClasses.{root,list,item}],
[templates.{header,item,footer}],
[transformData],
[autoHideContainer]
[showMore.{templates: {active, inactive}, limit}]
[ sortBy ],
[ limit=10 ],
[ cssClasses.{root,list,item} ],
[ templates.{header,item,footer} ],
[ transformData ],
[ autoHideContainer ],
[ showMore.{templates: {active, inactive}, limit} ],
[ collapsible=false ]
})`;
function menu({
container,
Expand All @@ -59,6 +62,7 @@ function menu({
limit = 10,
cssClasses: userCssClasses = {},
templates = defaultTemplates,
collapsible = false,
transformData,
autoHideContainer = true,
showMore = false
Expand Down Expand Up @@ -140,6 +144,7 @@ function menu({

ReactDOM.render(
<RefinementList
collapsible={collapsible}
cssClasses={cssClasses}
facetValues={facetValues}
limitMax={widgetMaxValuesPerFacet}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('numericRefinementList()', () => {
radio: 'ais-refinement-list--radio',
root: 'ais-refinement-list root cx'
},
collapsible: false,
facetValues: [
{attributeName: 'price', isRefined: true, name: 'All', url: '#'},
{attributeName: 'price', end: 4, isRefined: false, name: 'less than 4', url: '#'},
Expand Down
Loading

0 comments on commit c4df7c5

Please sign in to comment.