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..4660180fb9
--- /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 `
`
+ * @param {string} [options.cssClasses.item] CSS classes added to each ``
+ * @param {boolean} [options.autoHideContainer=false] Hide the container when no results match
+ * @return {Object}
+ */
+
+function numericSelector({
+ container,
+ operator = '=',
+ attributeName,
+ options,
+ cssClasses: userCssClasses = {},
+ autoHideContainer = false
+ }) {
+ let containerNode = utils.getContainerNode(container);
+ let usage = 'Usage: numericSelector({container, attributeName, options[, cssClasses.{root,item}, autoHideContainer]})';
+
+ let Selector = require('../../components/Selector');
+ if (autoHideContainer === true) {
+ Selector = autoHideContainerHOC(Selector);
+ }
+
+ if (!container || !options || options.length === 0 || !attributeName) {
+ throw new Error(usage);
+ }
+
+ return {
+ init: function(state, helper) {
+ const currentValue = this._getRefinedValue(helper) || options[0].value;
+ if (currentValue !== undefined) {
+ helper.addNumericRefinement(attributeName, operator, currentValue);
+ }
+ },
+
+ render: function({helper, results}) {
+ const currentValue = this._getRefinedValue(helper);
+ const hasNoResults = results.nbHits === 0;
+
+ const cssClasses = {
+ root: cx(bem(null), userCssClasses.root),
+ item: cx(bem('item'), userCssClasses.item)
+ };
+ ReactDOM.render(
+ ,
+ containerNode
+ );
+ },
+
+ _refine: function(helper, value) {
+ helper.clearRefinements(attributeName);
+ if (value !== undefined) {
+ helper.addNumericRefinement(attributeName, operator, value);
+ }
+ helper.search();
+ },
+
+ _getRefinedValue: function(helper) {
+ const refinements = helper.getRefinements(attributeName);
+ return find(refinements, {operator});
+ }
+ };
+}
+
+module.exports = numericSelector;