From 5ab0dff6c2a503cc6a61d1e36a488d24aeb8d6b5 Mon Sep 17 00:00:00 2001 From: Sylvain UTARD Date: Wed, 11 Nov 2015 21:41:53 -0800 Subject: [PATCH 1/2] chore(numericSelector): initial import A new widget to build dropdown menu to refine on a numeric attribute. (This one is also mandatory for our examples). Fix #563 --- dev/app.js | 13 +++ dev/index.html | 15 ++- .../_includes/widget-jsdoc/numericSelector.md | 14 +++ docs/documentation.md | 53 +++++++++- docs/examples/airbnb/index.html | 17 +-- docs/examples/airbnb/search.js | 16 +++ docs/examples/airbnb/style.scss | 25 +++++ lib/main.js | 1 + .../__tests__/numeric-selector-test.js | 100 ++++++++++++++++++ widgets/numeric-selector/numeric-selector.js | 89 ++++++++++++++++ 10 files changed, 320 insertions(+), 23 deletions(-) create mode 100644 docs/_includes/widget-jsdoc/numericSelector.md create mode 100644 widgets/numeric-selector/__tests__/numeric-selector-test.js create mode 100644 widgets/numeric-selector/numeric-selector.js diff --git a/dev/app.js b/dev/app.js index 87f545dcfe..4a539a87c6 100644 --- a/dev/app.js +++ b/dev/app.js @@ -236,4 +236,17 @@ search.addWidget( }) ); +search.addWidget( + instantsearch.widgets.numericSelector({ + container: '#popularity-selector', + attributeName: 'popularity', + options: [ + { label: 'Select a value', value: undefined }, + { label: '1st', value: 1 }, + { label: '2nd', value: 2 }, + { label: '3rd', value: 3 } + ] + }) +); + search.start(); diff --git a/dev/index.html b/dev/index.html index 741f90c8a9..e14ae00fc1 100644 --- a/dev/index.html +++ b/dev/index.html @@ -36,10 +36,10 @@

Instant search demo using instantsearch.js
-
+
-
+
@@ -47,7 +47,15 @@

Instant search demo using instantsearch.js

-
+
+
+
+ + +
+
+
+
@@ -55,7 +63,6 @@

Instant search demo using instantsearch.js

-
diff --git a/docs/_includes/widget-jsdoc/numericSelector.md b/docs/_includes/widget-jsdoc/numericSelector.md new file mode 100644 index 0000000000..6020f753f6 --- /dev/null +++ b/docs/_includes/widget-jsdoc/numericSelector.md @@ -0,0 +1,14 @@ +| Param | Description | +| --- | --- | +| `options.container`Type: string | DOMElement | CSS Selector or DOMElement to insert the widget | +| `options.attributeName`Type: string | Name of the numeric attribute to use | +| `options.options`Type: Array | Array of objects defining the different values and labels | +| `options.options[i].value`Type: number | The numerical value to refine with | +| `options.options[i].label`Type: string | Label to display in the option | +| `options.operator`Type: string | The operator to use to refine | +| `options.cssClasses`Type: Object | CSS classes to be added | +| `options.cssClasses.root`Type: string | CSS classes added to the parent ` - - - - - - - -
-
+
+
+
Room Type
diff --git a/docs/examples/airbnb/search.js b/docs/examples/airbnb/search.js index 2f8cce49de..b594d6943a 100644 --- a/docs/examples/airbnb/search.js +++ b/docs/examples/airbnb/search.js @@ -74,4 +74,20 @@ search.addWidget( }) ); +search.addWidget( + instantsearch.widgets.numericSelector({ + container: '#guests', + attributeName: 'person_capacity', + operator: '>=', + options: [ + { label: '1 guest', value: 1 }, + { label: '2 guests', value: 2 }, + { label: '3 guests', value: 3 }, + { label: '4 guests', value: 4 }, + { label: '5 guests', value: 5 }, + { label: '6 guests', value: 6 } + ] + }) +); + search.start(); diff --git a/docs/examples/airbnb/style.scss b/docs/examples/airbnb/style.scss index d1b38c6cd3..c4992e4e9e 100644 --- a/docs/examples/airbnb/style.scss +++ b/docs/examples/airbnb/style.scss @@ -4,6 +4,7 @@ $white: #FFFFFF; $airbnb-red: #FF585B; +$airbnb-gray: #C4C4C4; $airbnb-gray-light: #DCE0E0; $airbnb-gray-lighter: #EDEFED; $airbnb-text-color: #565A5C; @@ -174,6 +175,30 @@ nav { max-width: 1280px; } +#guests { + .ais-numeric-selector { + position: relative; + appearance: none; + background: inherit; + width: 100%; + border: 1px solid $airbnb-gray; + height: 34px; + padding: 8px 10px; + &:focus { + outline: none; + } + &:after { + position: absolute; + content: '\25bc'; + color: #82888a; + top: 0; + right: 0; + width: 2em; + text-align: center; + } + } +} + #pagination { text-align: center; } diff --git a/lib/main.js b/lib/main.js index 44606d2028..e75432cf63 100644 --- a/lib/main.js +++ b/lib/main.js @@ -16,6 +16,7 @@ instantsearch.widgets = { menu: require('../widgets/menu/menu.js'), refinementList: require('../widgets/refinement-list/refinement-list.js'), numericRefinementList: require('../widgets/numeric-refinement-list/numeric-refinement-list.js'), + numericSelector: require('../widgets/numeric-selector/numeric-selector.js'), pagination: require('../widgets/pagination/pagination'), priceRanges: require('../widgets/price-ranges/price-ranges.js'), searchBox: require('../widgets/search-box/search-box'), diff --git a/widgets/numeric-selector/__tests__/numeric-selector-test.js b/widgets/numeric-selector/__tests__/numeric-selector-test.js new file mode 100644 index 0000000000..828607bf60 --- /dev/null +++ b/widgets/numeric-selector/__tests__/numeric-selector-test.js @@ -0,0 +1,100 @@ +/* eslint-env mocha */ + +import React from 'react'; +import expect from 'expect'; +import sinon from 'sinon'; +import jsdom from 'mocha-jsdom'; + +import expectJSX from 'expect-jsx'; +expect.extend(expectJSX); + +import numericSelector from '../numeric-selector'; +import Selector from '../../../components/Selector'; + +describe('numericSelector()', () => { + jsdom({useEach: true}); + + let ReactDOM; + let container; + let options; + let cssClasses; + let widget; + let props; + let helper; + let results; + let autoHideContainer; + + beforeEach(() => { + autoHideContainer = sinon.stub().returns(Selector); + ReactDOM = {render: sinon.spy()}; + + numericSelector.__Rewire__('ReactDOM', ReactDOM); + numericSelector.__Rewire__('autoHideContainerHOC', autoHideContainer); + + container = document.createElement('div'); + options = [ + {value: 1, label: 'first'}, + {value: 2, label: 'second'} + ]; + cssClasses = { + root: 'custom-root', + item: 'custom-item' + }; + widget = numericSelector({container, options, attributeName: 'aNumAttr', cssClasses}); + helper = { + addNumericRefinement: sinon.spy(), + clearRefinements: sinon.spy(), + getRefinements: sinon.stub().returns([]), + search: sinon.spy() + }; + results = { + hits: [], + nbHits: 0 + }; + }); + + it('doesn\'t configure anything', () => { + expect(widget.getConfiguration).toEqual(undefined); + }); + + it('calls twice ReactDOM.render(, container)', () => { + widget.render({helper, results, state: helper.state}); + widget.render({helper, results, state: helper.state}); + props = { + cssClasses: { + root: 'ais-numeric-selector custom-root', + item: 'ais-numeric-selector--item custom-item' + }, + currentValue: undefined, + shouldAutoHideContainer: true, + options: [ + {value: 1, label: 'first'}, + {value: 2, label: 'second'} + ], + setValue: () => {} + }; + expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice'); + expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(); + expect(ReactDOM.render.firstCall.args[1]).toEqual(container); + expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(); + expect(ReactDOM.render.secondCall.args[1]).toEqual(container); + }); + + it('sets the underlying numeric refinement', () => { + widget._refine(helper, 2); + expect(helper.addNumericRefinement.calledOnce).toBe(true, 'addNumericRefinement called once'); + expect(helper.search.calledOnce).toBe(true, 'search called once'); + }); + + it('cancles the underying numeric refinement', () => { + widget._refine(helper, undefined); + expect(helper.clearRefinements.calledOnce).toBe(true, 'clearRefinements called once'); + expect(helper.addNumericRefinement.called).toBe(false, 'addNumericRefinement never called'); + expect(helper.search.calledOnce).toBe(true, 'search called once'); + }); + + afterEach(() => { + numericSelector.__ResetDependency__('ReactDOM'); + numericSelector.__ResetDependency__('autoHideContainerHOC'); + }); +}); diff --git a/widgets/numeric-selector/numeric-selector.js b/widgets/numeric-selector/numeric-selector.js new file mode 100644 index 0000000000..31407458a1 --- /dev/null +++ b/widgets/numeric-selector/numeric-selector.js @@ -0,0 +1,89 @@ +let React = require('react'); +let ReactDOM = require('react-dom'); + +let utils = require('../../lib/utils.js'); +let cx = require('classnames'); +let find = require('lodash/collection/find'); +let autoHideContainerHOC = require('../../decorators/autoHideContainer'); + +let bem = utils.bemHelper('ais-numeric-selector'); + +/** + * Instantiate a dropdown element to choose the number of hits to display per page + * @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget + * @param {string} options.attributeName Name of the numeric attribute to use + * @param {Array} options.options Array of objects defining the different values and labels + * @param {number} options.options[i].value The numerical value to refine with + * @param {string} options.options[i].label Label to display in the option + * @param {string} [options.operator] The operator to use to refine + * @param {Object} [options.cssClasses] CSS classes to be added + * @param {string} [options.cssClasses.root] CSS classes added to the parent `