Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Commit

Permalink
feat(MenuSelect): add component and connector
Browse files Browse the repository at this point in the history
* feat(components): add <MenuSelect />

* feat(widgets): add MenuSelect

* feat(dom): export MenuSelect widget

* fix(MenuSelect): use another value for "See all" option

* doc(storybook): add MenuSelect stories

* test(MenuSelect): update snapshot

* doc(MenuSelect): remove className root not existing

* style(MenuSelect): apply select theme

* test(MenuSelect): remove un-used import

* doc(MenuSelect): re-write classes description
  • Loading branch information
Maxime Janton authored and mthuret committed Oct 23, 2017
1 parent 502627b commit cc6e0d7
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/react-instantsearch-theme-algolia/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ $sffv-searchbox-config: (
'styles/HitsPerPage',
'styles/InfiniteHits',
'styles/Menu',
'styles/MenuSelect',
'styles/MultiRange',
'styles/Pagination',
'styles/PoweredBy',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ais-MenuSelect__select {
@include select();
}
1 change: 1 addition & 0 deletions packages/react-instantsearch/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { default as Hits } from './src/widgets/Hits.js';
export { default as HitsPerPage } from './src/widgets/HitsPerPage.js';
export { default as InfiniteHits } from './src/widgets/InfiniteHits.js';
export { default as Menu } from './src/widgets/Menu.js';
export { default as MenuSelect } from './src/widgets/MenuSelect.js';
export { default as MultiRange } from './src/widgets/MultiRange.js';
export { default as Pagination } from './src/widgets/Pagination.js';
export { default as PoweredBy } from './src/widgets/PoweredBy.js';
Expand Down
73 changes: 73 additions & 0 deletions packages/react-instantsearch/src/components/MenuSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { find } from 'lodash';

import classNames from './classNames.js';
import translatable from '../core/translatable';

const cx = classNames('MenuSelect');

class MenuSelect extends Component {
static propTypes = {
canRefine: PropTypes.bool.isRequired,
refine: PropTypes.func.isRequired,
translate: PropTypes.func.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
isRefined: PropTypes.bool.isRequired,
})
),
};

static contextTypes = {
canRefine: PropTypes.func,
};

get selectedValue() {
const { value } = find(this.props.items, { isRefined: true }) || {
value: 'ais__see__all__option',
};
return value;
}

componentWillMount() {
if (this.context.canRefine) this.context.canRefine(this.props.canRefine);
}

componentWillReceiveProps(props) {
if (this.context.canRefine) this.context.canRefine(props.canRefine);
}

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

render() {
const { items, translate } = this.props;

return (
<select
value={this.selectedValue}
onChange={this.handleSelectChange}
{...cx('select')}
>
<option value="ais__see__all__option" {...cx('option')}>
{translate('seeAllOption')}
</option>

{items.map(item => (
<option key={item.value} value={item.value} {...cx('option')}>
{item.label} ({item.count})
</option>
))}
</select>
);
}
}

export default translatable({
seeAllOption: 'See all',
})(MenuSelect);
79 changes: 79 additions & 0 deletions packages/react-instantsearch/src/components/MenuSelect.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-env jest, jasmine */

import React from 'react';
import renderer from 'react-test-renderer';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

import MenuSelect from './MenuSelect';

describe('MenuSelect', () => {
it('default menu select', () => {
const tree = renderer
.create(
<MenuSelect
refine={() => {}}
items={[
{ label: 'white', value: 'white', count: 10, isRefined: false },
{ label: 'black', value: 'black', count: 20, isRefined: false },
{ label: 'blue', value: 'blue', count: 30, isRefined: false },
{ label: 'green', value: 'green', count: 30, isRefined: false },
{ label: 'red', value: 'red', count: 30, isRefined: false },
]}
canRefine={true}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

it('applies translations', () => {
const tree = renderer
.create(
<MenuSelect
refine={() => {}}
items={[
{ label: 'white', value: 'white', count: 10, isRefined: false },
{ label: 'black', value: 'black', count: 20, isRefined: false },
{ label: 'blue', value: 'blue', count: 30, isRefined: false },
{ label: 'green', value: 'green', count: 30, isRefined: false },
{ label: 'red', value: 'red', count: 30, isRefined: false },
]}
translations={{
seeAllOption: 'Everything',
}}
canRefine={true}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

it('refines its value on change', () => {
const refine = jest.fn();
const wrapper = mount(
<MenuSelect
refine={refine}
items={[
{ label: 'white', value: 'white', count: 10, isRefined: false },
{ label: 'black', value: 'black', count: 20, isRefined: false },
{ label: 'blue', value: 'blue', count: 30, isRefined: false },
]}
canRefine={true}
/>
);

const items = wrapper.find('.ais-MenuSelect__option');
expect(items.length).toBe(4); // +1 from "see all option"

wrapper
.find('.ais-MenuSelect__select')
.simulate('change', { target: { value: 'blue' } });

expect(refine).toHaveBeenCalledTimes(1);
expect(refine).toHaveBeenCalledWith('blue');

wrapper.unmount();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MenuSelect applies translations 1`] = `
<select
className="ais-MenuSelect__select"
onChange={[Function]}
value="ais__see__all__option"
>
<option
className="ais-MenuSelect__option"
value="ais__see__all__option"
>
Everything
</option>
<option
className="ais-MenuSelect__option"
value="white"
>
white
(
10
)
</option>
<option
className="ais-MenuSelect__option"
value="black"
>
black
(
20
)
</option>
<option
className="ais-MenuSelect__option"
value="blue"
>
blue
(
30
)
</option>
<option
className="ais-MenuSelect__option"
value="green"
>
green
(
30
)
</option>
<option
className="ais-MenuSelect__option"
value="red"
>
red
(
30
)
</option>
</select>
`;

exports[`MenuSelect default menu select 1`] = `
<select
className="ais-MenuSelect__select"
onChange={[Function]}
value="ais__see__all__option"
>
<option
className="ais-MenuSelect__option"
value="ais__see__all__option"
>
See all
</option>
<option
className="ais-MenuSelect__option"
value="white"
>
white
(
10
)
</option>
<option
className="ais-MenuSelect__option"
value="black"
>
black
(
20
)
</option>
<option
className="ais-MenuSelect__option"
value="blue"
>
blue
(
30
)
</option>
<option
className="ais-MenuSelect__option"
value="green"
>
green
(
30
)
</option>
<option
className="ais-MenuSelect__option"
value="red"
>
red
(
30
)
</option>
</select>
`;
35 changes: 35 additions & 0 deletions packages/react-instantsearch/src/widgets/MenuSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import connectMenu from '../connectors/connectMenu.js';
import MenuSelectComponent from '../components/MenuSelect.js';

/**
* The MenuSelect component displays a select that lets the user choose a single value for a specific attribute.
* @name MenuSelect
* @kind widget
* @requirements The attribute passed to the `attributeName` prop must be present in "attributes for faceting"
* on the Algolia dashboard or configured as `attributesForFaceting` via a set settings call to the Algolia API.
* @propType {string} attributeName - the name of the attribute in the record
* @propType {string} [defaultRefinement] - the value of the item selected by default
* @propType {function} [transformItems] - Function to modify the items being displayed, e.g. for filtering or sorting them. Takes an items as parameter and expects it back in return.
* @themeKey ais-MenuSelect__select - the <select> DOM element.
* @themeKey ais-MenuSelect__option - the <option> DOM element for a single item
* @translationkey seeAllOption - The label of the option to select to remove the refinement
* @example
* import React from 'react';
*
* import { MenuSelect, InstantSearch } from 'react-instantsearch/dom';
*
* export default function App() {
* return (
* <InstantSearch
* appId="latency"
* apiKey="6be0576ff61c053d5f9a3225e2a90f76"
* indexName="ikea"
* >
* <MenuSelect
* attributeName="category"
* />
* </InstantSearch>
* );
* }
*/
export default connectMenu(MenuSelectComponent);
65 changes: 65 additions & 0 deletions stories/MenuSelect.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { orderBy } from 'lodash';
import { storiesOf } from '@storybook/react';
import { withKnobs, text } from '@storybook/addon-knobs';

import { WrapWithHits } from './util';
import {
MenuSelect,
Panel,
SearchBox,
} from '../packages/react-instantsearch/dom';

const stories = storiesOf('MenuSelect', module);

stories.addDecorator(withKnobs);

stories
.add('default', () => (
<WrapWithHits hasPlayground={true} linkedStoryGroup="MenuSelect">
<MenuSelect attributeName="category" />
</WrapWithHits>
))
.add('with default selected item', () => (
<WrapWithHits hasPlayground={true} linkedStoryGroup="MenuSelect">
<MenuSelect attributeName="category" defaultRefinement="Eating" />
</WrapWithHits>
))
.add('with the sort strategy changed', () => (
<WrapWithHits hasPlayground={true} linkedStoryGroup="MenuSelect">
<MenuSelect
attributeName="category"
transformItems={items =>
orderBy(items, ['label', 'count'], ['asc', 'desc'])}
/>
</WrapWithHits>
))
.add('with panel', () => (
<WrapWithHits hasPlayground={true} linkedStoryGroup="MenuSelect">
<Panel title="Category">
<MenuSelect attributeName="category" />
</Panel>
</WrapWithHits>
))
.add('with panel but no available refinement', () => (
<WrapWithHits
searchBox={false}
hasPlayground={true}
linkedStoryGroup="MenuSelect"
>
<Panel title="Category">
<MenuSelect attributeName="category" />
<div style={{ display: 'none' }}>
<SearchBox defaultRefinement="dkjsakdjskajdksjakdjaskj" />
</div>
</Panel>
</WrapWithHits>
))
.add('playground', () => (
<WrapWithHits linkedStoryGroup="MenuSelect">
<MenuSelect
attributeName="category"
defaultRefinement={text('defaultSelectedItem', 'Bathroom')}
/>
</WrapWithHits>
));

0 comments on commit cc6e0d7

Please sign in to comment.