diff --git a/packages/react-instantsearch/src/components/List.js b/packages/react-instantsearch/src/components/List.js
index 7b715e8c1a..4f290d1fa9 100644
--- a/packages/react-instantsearch/src/components/List.js
+++ b/packages/react-instantsearch/src/components/List.js
@@ -64,6 +64,7 @@ class List extends Component {
{...this.props.cx(
'item',
item.isRefined && 'itemSelected',
+ item.noRefinement && 'itemNoRefinement',
items && 'itemParent',
items && item.isRefined && 'itemSelectedParent'
)}
diff --git a/packages/react-instantsearch/src/components/MultiRange.js b/packages/react-instantsearch/src/components/MultiRange.js
index 88b6cbd54e..ace5e6adc0 100644
--- a/packages/react-instantsearch/src/components/MultiRange.js
+++ b/packages/react-instantsearch/src/components/MultiRange.js
@@ -9,6 +9,8 @@ class MultiRange extends Component {
items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
+ isRefined: PropTypes.bool.isRequired,
+ noRefinement: PropTypes.bool.isRequired,
})).isRequired,
refine: PropTypes.func.isRequired,
transformItems: PropTypes.func,
diff --git a/packages/react-instantsearch/src/components/MultiRange.test.js b/packages/react-instantsearch/src/components/MultiRange.test.js
index 807bcc9fd9..7af5094629 100644
--- a/packages/react-instantsearch/src/components/MultiRange.test.js
+++ b/packages/react-instantsearch/src/components/MultiRange.test.js
@@ -13,10 +13,10 @@ describe('MultiRange', () => {
createURL={() => '#'}
refine={() => null}
items={[
- {label: 'label1', value: '10:', isRefined: false},
- {label: 'label2', value: '10:20', isRefined: false},
- {label: 'label3', value: '20:30', isRefined: false},
- {label: 'label4', value: '30:', isRefined: false},
+ {label: 'label1', value: '10:', isRefined: false, noRefinement: false},
+ {label: 'label2', value: '10:20', isRefined: false, noRefinement: false},
+ {label: 'label3', value: '20:30', isRefined: false, noRefinement: false},
+ {label: 'label4', value: '30:', isRefined: false, noRefinement: false},
]}
canRefine={true}
/>
@@ -30,10 +30,10 @@ describe('MultiRange', () => {
createURL={() => '#'}
refine={() => null}
items={[
- {label: 'label1', value: '10:', isRefined: false},
- {label: 'label2', value: '10:20', isRefined: true},
- {label: 'label3', value: '20:30', isRefined: false},
- {label: 'label4', value: '30:', isRefined: false},
+ {label: 'label1', value: '10:', isRefined: false, noRefinement: false},
+ {label: 'label2', value: '10:20', isRefined: true, noRefinement: false},
+ {label: 'label3', value: '20:30', isRefined: false, noRefinement: false},
+ {label: 'label4', value: '30:', isRefined: false, noRefinement: false},
]}
canRefine={true}
/>
@@ -47,10 +47,10 @@ describe('MultiRange', () => {
@@ -69,4 +69,28 @@ describe('MultiRange', () => {
wrapper.unmount();
});
+
+ it('indicate when there is no refinement', () => {
+ const refine = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ const itemWrapper = wrapper.find('.ais-MultiRange__noRefinement');
+ expect(itemWrapper.length).toBe(1);
+
+ const items = wrapper.find('.ais-MultiRange__itemNoRefinement');
+ expect(items.length).toBe(4);
+
+ wrapper.unmount();
+ });
});
diff --git a/packages/react-instantsearch/src/connectors/connectMultiRange.js b/packages/react-instantsearch/src/connectors/connectMultiRange.js
index 037e265160..688269fd82 100644
--- a/packages/react-instantsearch/src/connectors/connectMultiRange.js
+++ b/packages/react-instantsearch/src/connectors/connectMultiRange.js
@@ -38,6 +38,25 @@ function getCurrentRefinement(props, searchState) {
return '';
}
+function isRefinementsRangeIncludesInsideItemRange(stats, start, end) {
+ return stats.min > start && stats.min < end || stats.max > start && stats.max < end;
+}
+
+function isItemRangeIncludedInsideRefinementsRange(stats, start, end) {
+ return start > stats.min && start < stats.max || end > stats.min && end < stats.max;
+}
+
+function itemHasRefinement(attributeName, results, value) {
+ const stats = results.getFacetByName(attributeName) ?
+ results.getFacetStats(attributeName) : null;
+ const range = value.split(':');
+ const start = Number(range[0]) === 0 || value === '' ? Number.NEGATIVE_INFINITY : Number(range[0]);
+ const end = Number(range[1]) === 0 || value === '' ? Number.POSITIVE_INFINITY : Number(range[1]);
+ return !(Boolean(stats) &&
+ (isRefinementsRangeIncludesInsideItemRange(stats, start, end)
+ || isItemRangeIncludedInsideRefinementsRange(stats, start, end)));
+}
+
/**
* connectMultiRange connector provides the logic to build a widget that will
* give the user the ability to select a range value for a numeric attribute.
@@ -51,7 +70,7 @@ function getCurrentRefinement(props, searchState) {
* @providedPropType {function} refine - a function to select a range.
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied. follow the shape of a `string` with a pattern of `'{start}:{end}'` which corresponds to the current selected item. For instance, when the selected item is `{start: 10, end: 20}`, the searchState of the widget is `'10:20'`. When `start` isn't defined, the searchState of the widget is `':{end}'`, and the same way around when `end` isn't defined. However, when neither `start` nor `end` are defined, the searchState is an empty string.
- * @providedPropType {array.<{isRefined: boolean, label: string, value: string}>} items - the list of ranges the MultiRange can display.
+ * @providedPropType {array.<{isRefined: boolean, label: string, value: string, isRefined: boolean, noRefinement: boolean}>} items - the list of ranges the MultiRange can display.
*/
export default createConnector({
displayName: 'AlgoliaMultiRange',
@@ -67,7 +86,7 @@ export default createConnector({
transformItems: PropTypes.func,
},
- getProvidedProps(props, searchState) {
+ getProvidedProps(props, searchState, searchResults) {
const currentRefinement = getCurrentRefinement(props, searchState);
const items = props.items.map(item => {
const value = stringifyItem(item);
@@ -75,12 +94,15 @@ export default createConnector({
label: item.label,
value,
isRefined: value === currentRefinement,
+ noRefinement: searchResults && searchResults.results ?
+ itemHasRefinement(getId(props), searchResults.results, value) : false,
};
});
+
return {
items: props.transformItems ? props.transformItems(items) : items,
currentRefinement,
- canRefine: items.length > 0,
+ canRefine: !items.reduce((noRefinement, item) => noRefinement && item.noRefinement, true),
};
},
@@ -102,6 +124,7 @@ export default createConnector({
getSearchParameters(searchParameters, props, searchState) {
const {attributeName} = props;
const {start, end} = parseItem(getCurrentRefinement(props, searchState));
+ searchParameters = searchParameters.addDisjunctiveFacet(attributeName);
if (start) {
searchParameters = searchParameters.addNumericRefinement(
diff --git a/packages/react-instantsearch/src/connectors/connectMultiRange.test.js b/packages/react-instantsearch/src/connectors/connectMultiRange.test.js
index 99aff3f340..fd54d98659 100644
--- a/packages/react-instantsearch/src/connectors/connectMultiRange.test.js
+++ b/packages/react-instantsearch/src/connectors/connectMultiRange.test.js
@@ -18,14 +18,19 @@ let params;
describe('connectMultiRange', () => {
it('provides the correct props to the component', () => {
+ let results = {
+ getFacetStats: () => ({min: 0, max: 300}),
+ getFacetByName: () => true,
+ };
+
props = getProvidedProps({
items: [
{label: 'All'},
],
- }, {});
+ }, {}, {results});
expect(props).toEqual({
items: [
- {label: 'All', value: '', isRefined: true},
+ {label: 'All', value: '', isRefined: true, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
@@ -36,11 +41,11 @@ describe('connectMultiRange', () => {
{label: 'All'},
{label: 'Ok', start: 100},
],
- }, {});
+ }, {}, {results});
expect(props).toEqual({
items: [
- {label: 'All', value: '', isRefined: true},
- {label: 'Ok', value: '100:', isRefined: false},
+ {label: 'All', value: '', isRefined: true, noRefinement: false},
+ {label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
@@ -51,12 +56,11 @@ describe('connectMultiRange', () => {
{label: 'All'},
{label: 'Not ok', end: 200},
],
- canRefine: true,
- }, {});
+ }, {}, {results});
expect(props).toEqual({
items: [
- {label: 'All', value: '', isRefined: true},
- {label: 'Not ok', value: ':200', isRefined: false},
+ {label: 'All', value: '', isRefined: true, noRefinement: false},
+ {label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
@@ -69,45 +73,74 @@ describe('connectMultiRange', () => {
{label: 'Not ok', end: 200},
{label: 'Maybe ok?', start: 100, end: 200},
],
- canRefine: true,
- }, {});
+ }, {}, {results});
expect(props).toEqual({
items: [
- {label: 'All', value: '', isRefined: true},
- {label: 'Ok', value: '100:', isRefined: false},
- {label: 'Not ok', value: ':200', isRefined: false},
- {label: 'Maybe ok?', value: '100:200', isRefined: false},
+ {label: 'All', value: '', isRefined: true, noRefinement: false},
+ {label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
+ {label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
+ {label: 'Maybe ok?', value: '100:200', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
});
- props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}});
- expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
+ it('no items define', () => {
+ props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}}, {});
+ expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
- props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}});
- expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
+ props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}}, {});
+ expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
- props = getProvidedProps({attributeName: 'ok', items: [], defaultRefinement: 'wat'}, {});
- expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
+ props = getProvidedProps({attributeName: 'ok', items: [], defaultRefinement: 'wat'}, {}, {});
+ expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
+ });
- const transformItems = jest.fn(() => ['items']);
- props = getProvidedProps({
- items: [
+ it('use the transform items props if passed', () => {
+ const transformItems = jest.fn(() => ['items']);
+ props = getProvidedProps({
+ items: [
{label: 'All'},
{label: 'Ok', start: 100},
{label: 'Not ok', end: 200},
{label: 'Maybe ok?', start: 100, end: 200},
- ],
- transformItems,
- }, {});
- expect(transformItems.mock.calls[0][0]).toEqual([
- {label: 'All', value: '', isRefined: true},
- {label: 'Ok', value: '100:', isRefined: false},
- {label: 'Not ok', value: ':200', isRefined: false},
- {label: 'Maybe ok?', value: '100:200', isRefined: false},
- ]);
- expect(props.items).toEqual(['items']);
+ ],
+ transformItems,
+ }, {}, {results});
+ expect(transformItems.mock.calls[0][0]).toEqual([
+ {label: 'All', value: '', isRefined: true, noRefinement: false},
+ {label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
+ {label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
+ {label: 'Maybe ok?', value: '100:200', isRefined: false, noRefinement: false},
+ ]);
+ expect(props.items).toEqual(['items']);
+ });
+
+ it('compute the no refinement value for each item range when stats exists', () => {
+ results = {
+ getFacetStats: () => ({min: 250, max: 300}),
+ getFacetByName: () => true,
+ };
+
+ props = getProvidedProps({
+ items: [
+ {label: '1', start: 100},
+ {label: '2', start: 400},
+ {label: '3', end: 200},
+ {label: '4', start: 100, end: 200},
+ ],
+ }, {}, {results});
+ expect(props).toEqual({
+ items: [
+ {label: '1', value: '100:', isRefined: false, noRefinement: false},
+ {label: '2', value: '400:', isRefined: false, noRefinement: true},
+ {label: '3', value: ':200', isRefined: false, noRefinement: true},
+ {label: '4', value: '100:200', isRefined: false, noRefinement: true},
+ ],
+ currentRefinement: '',
+ canRefine: true,
+ });
+ });
});
it('calling refine updates the widget\'s search state', () => {
diff --git a/packages/react-instantsearch/src/widgets/MultiRange.js b/packages/react-instantsearch/src/widgets/MultiRange.js
index bfe19c907a..54d04aa617 100644
--- a/packages/react-instantsearch/src/widgets/MultiRange.js
+++ b/packages/react-instantsearch/src/widgets/MultiRange.js
@@ -17,7 +17,8 @@ import MultiRangeComponent from '../components/MultiRange.js';
* @themeKey ais-MultiRange__itemLabelSelected - The selected label item
* @themeKey ais-MultiRange__itemRadio - The radio of an item
* @themeKey ais-MultiRange__itemRadioSelected - The selected radio item
- * @themeKey ais-MultiRange__noRefinement - present when there is no refinement
+ * @themeKey ais-MultiRange__noRefinement - present when there is no refinement for all ranges
+ * @themeKey ais-MultiRange__itemNoRefinement - present when there is no refinement for one range
* @example
* import React from 'react';
*