Skip to content

Commit

Permalink
feat(menu-select): add menu select widget (#2316)
Browse files Browse the repository at this point in the history
* feat(widgets): add menuSelect widget
* feat(dev): add example menuSelect
* doc(menu): minor fixes
* doc(menuSelect): write documentation about menuSelect widget
* feat(connectMenu): falsy values remove filter
* refactor(MenuSelect): use falsy value to remove refined value
* test(menuSelect): test widget & component
* fix(connectMenu): correctly cache returned `refine` fn
* fix(MenuSelect): templatesProps
* refactor(connectMenu): dont re-instanciate a fn at every render
* refactor(connectMenu): rename to `this._refine`
  • Loading branch information
Maxime Janton authored and Alex S committed Sep 20, 2017
1 parent 89da104 commit 680f9bd
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 3 deletions.
12 changes: 12 additions & 0 deletions dev/app/init-builtin-widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,18 @@ export default () => {
})
);
})
)
.add(
'as a Select DOM element',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.menuSelect({
container,
attributeName: 'categories',
limit: 10,
})
);
})
);

storiesOf('RangeSlider').add(
Expand Down
53 changes: 53 additions & 0 deletions src/components/MenuSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import Template from './Template';
import autoHideContainerHOC from '../decorators/autoHideContainer.js';
import headerFooterHOC from '../decorators/headerFooter.js';

class MenuSelect extends Component {
static propTypes = {
cssClasses: PropTypes.shape({
select: PropTypes.string,
option: PropTypes.string,
}),
items: PropTypes.array.isRequired,
refine: PropTypes.func.isRequired,
templateProps: PropTypes.object.isRequired,
};

handleSelectChange = ({ target: { value } }) => {
this.props.refine(value);
};

render() {
const { cssClasses, templateProps, items } = this.props;
const { value: selectedValue } = items.find(item => item.isRefined) || {
value: '',
};

return (
<select
className={cssClasses.select}
value={selectedValue}
onChange={this.handleSelectChange}
>
<option value="" className={cssClasses.option}>
<Template templateKey="seeAllOption" {...templateProps} />
</option>

{items.map(item =>
<option
key={item.value}
value={item.value}
className={cssClasses.option}
>
<Template data={item} templateKey="item" {...templateProps} />
</option>
)}
</select>
);
}
}

export default autoHideContainerHOC(headerFooterHOC(MenuSelect));
33 changes: 33 additions & 0 deletions src/components/__tests__/MenuSelect-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import MenuSelect from '../MenuSelect';
import renderer from 'react-test-renderer';

import defaultTemplates from '../../widgets/menu-select/defaultTemplates';

describe('MenuSelect', () => {
it('should render <MenuSelect /> with items', () => {
const props = {
items: [{ value: 'foo', label: 'foo' }, { value: 'bar', label: 'bar' }],
refine: () => {},
templateProps: { templates: defaultTemplates },
shouldAutoHideContainer: false,
};
const tree = renderer.create(<MenuSelect {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});

it('should render with custom css classes', () => {
const props = {
items: [{ value: 'foo', label: 'foo' }, { value: 'bar', label: 'bar' }],
refine: () => {},
templateProps: { templates: defaultTemplates },
shouldAutoHideContainer: false,
cssClasses: {
select: 'foo',
option: 'bar',
},
};
const tree = renderer.create(<MenuSelect {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});
});
123 changes: 123 additions & 0 deletions src/components/__tests__/__snapshots__/MenuSelect-test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MenuSelect should render <MenuSelect /> with items 1`] = `
<div
style={
Object {
"display": "",
}
}
>
<div
className="ais-root"
>
<div
className="ais-body"
>
<select
className={undefined}
onChange={[Function]}
value=""
>
<option
className={undefined}
value=""
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "See all",
}
}
/>
</option>
<option
className={undefined}
value="foo"
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "foo ()",
}
}
/>
</option>
<option
className={undefined}
value="bar"
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "bar ()",
}
}
/>
</option>
</select>
</div>
</div>
</div>
`;

exports[`MenuSelect should render with custom css classes 1`] = `
<div
style={
Object {
"display": "",
}
}
>
<div
className="ais-root"
>
<div
className="ais-body"
>
<select
className="foo"
onChange={[Function]}
value=""
>
<option
className="bar"
value=""
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "See all",
}
}
/>
</option>
<option
className="bar"
value="foo"
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "foo ()",
}
}
/>
</option>
<option
className="bar"
value="bar"
>
<div
dangerouslySetInnerHTML={
Object {
"__html": "bar ()",
}
}
/>
</option>
</select>
</div>
</div>
</div>
`;
17 changes: 15 additions & 2 deletions src/connectors/menu/connectMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ export default function connectMenu(renderFn) {
return this.isShowingMore ? showMoreLimit : limit;
},

refine(helper) {
return facetValue => {
const [refinedItem] = helper.getHierarchicalFacetBreadcrumb(
attributeName
);
helper
.toggleRefinement(
attributeName,
facetValue ? facetValue : refinedItem
)
.search();
};
},

getConfiguration(configuration) {
const widgetConfiguration = {
hierarchicalFacets: [
Expand All @@ -164,8 +178,7 @@ export default function connectMenu(renderFn) {
this._createURL = facetValue =>
createURL(helper.state.toggleRefinement(attributeName, facetValue));

this._refine = facetValue =>
helper.toggleRefinement(attributeName, facetValue).search();
this._refine = this.refine(helper);

renderFn(
{
Expand Down
1 change: 1 addition & 0 deletions src/widgets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ export { default as starRating } from '../widgets/star-rating/star-rating.js';
export { default as stats } from '../widgets/stats/stats.js';
export { default as toggle } from '../widgets/toggle/toggle.js';
export { default as analytics } from '../widgets/analytics/analytics.js';
export { default as menuSelect } from '../widgets/menu-select/menu-select.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`menuSelect render correctly 1`] = `
<HeaderFooter-AutoHide
canRefine={true}
cssClasses={
Object {
"footer": "ais-menu-select--footer",
"header": "ais-menu-select--header",
"option": "ais-menu-select--option",
"root": "ais-menu-select",
"select": "ais-menu-select--footer",
}
}
items={
Array [
Object {
"label": "foo",
"value": undefined,
},
Object {
"label": "bar",
"value": undefined,
},
]
}
refine={[Function]}
shouldAutoHideContainer={false}
templateProps={
Object {
"templates": Object {
"footer": "",
"header": "",
"item": "{{label}} ({{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}})",
"seeAllOption": "See all",
},
"templatesConfig": undefined,
"transformData": undefined,
"useCustomCompileOptions": Object {
"footer": false,
"header": false,
"item": false,
"seeAllOption": false,
},
}
}
/>
`;
38 changes: 38 additions & 0 deletions src/widgets/menu-select/__tests__/menu-select-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import sinon from 'sinon';

import menuSelect from '../menu-select';

describe('menuSelect', () => {
it('throws an exception when no attributeName', () => {
const container = document.createElement('div');
expect(menuSelect.bind(null, { container })).toThrow(/^Usage/);
});

it('throws an exception when no container', () => {
const attributeName = 'categories';
expect(menuSelect.bind(null, { attributeName })).toThrow(/^Usage/);
});

it('render correctly', () => {
const data = { data: [{ name: 'foo' }, { name: 'bar' }] };
const results = { getFacetValues: sinon.spy(() => data) };
const state = { toggleRefinement: sinon.spy() };
const helper = {
toggleRefinement: sinon.stub().returnsThis(),
search: sinon.spy(),
state,
};
const createURL = () => '#';
const ReactDOM = { render: sinon.spy() };
menuSelect.__Rewire__('ReactDOM', ReactDOM);
const widget = menuSelect({
container: document.createElement('div'),
attributeName: 'test',
});
const instantSearchInstance = { templatesConfig: undefined };
widget.init({ helper, createURL, instantSearchInstance });
widget.render({ results, createURL, state });
expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
menuSelect.__ResetDependency__('ReactDOM');
});
});
8 changes: 8 additions & 0 deletions src/widgets/menu-select/defaultTemplates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable max-len */
export default {
header: '',
item:
'{{label}} ({{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}})',
footer: '',
seeAllOption: 'See all',
};
Loading

0 comments on commit 680f9bd

Please sign in to comment.