diff --git a/dev/app/builtin/stories/menu.stories.js b/dev/app/builtin/stories/menu.stories.js index 700917fbfa..1d5ed0dbb3 100644 --- a/dev/app/builtin/stories/menu.stories.js +++ b/dev/app/builtin/stories/menu.stories.js @@ -14,7 +14,7 @@ export default () => { window.search.addWidget( instantsearch.widgets.menu({ container, - attributeName: 'categories', + attribute: 'categories', }) ); }) @@ -25,7 +25,7 @@ export default () => { window.search.addWidget( instantsearch.widgets.menu({ container, - attributeName: 'categories', + attribute: 'categories', transformItems: items => items.map(item => ({ ...item, @@ -36,35 +36,33 @@ export default () => { }) ) .add( - 'with show more and header', + 'with show more', wrapWithHits(container => { window.search.addWidget( instantsearch.widgets.menu({ container, - attributeName: 'categories', + attribute: 'categories', limit: 3, - showMore: { - templates: { - active: '', - inactive: '', - }, - limit: 10, - }, - templates: { - header: 'Categories (menu widget)', - }, + showMore: true, + showMoreLimit: 10, }) ); }) ) .add( - 'as a Select DOM element', + 'with show more and templates', wrapWithHits(container => { window.search.addWidget( - instantsearch.widgets.menuSelect({ + instantsearch.widgets.menu({ container, - attributeName: 'categories', - limit: 10, + attribute: 'categories', + limit: 3, + showMore: true, + showMoreLimit: 10, + templates: { + showMoreActive: 'Show way less', + showMoreInactive: 'Show way more', + }, }) ); }) diff --git a/dev/app/jquery/stories/menu.stories.js b/dev/app/jquery/stories/menu.stories.js index 4e73b49be9..48e373f86c 100644 --- a/dev/app/jquery/stories/menu.stories.js +++ b/dev/app/jquery/stories/menu.stories.js @@ -12,7 +12,7 @@ export default () => { window.search.addWidget( widgets.menu({ containerNode, - attributeName: 'categories', + attribute: 'categories', limit: 3, }) ); @@ -24,7 +24,7 @@ export default () => { window.search.addWidget( widgets.showMoreMenu({ containerNode, - attributeName: 'categories', + attribute: 'categories', limit: 3, showMoreLimit: 10, }) diff --git a/docgen/src/guides/v3-migration.md b/docgen/src/guides/v3-migration.md index f627b433f4..eb765c9815 100644 --- a/docgen/src/guides/v3-migration.md +++ b/docgen/src/guides/v3-migration.md @@ -278,6 +278,81 @@ With the redo button: ``` +### Menu + +#### Options + +| Before | After | +| ----------------------------- | ---------------------------- | +| `attributeName` | `attribute` | +| `showMore.limit` | `showMoreLimit` | +| `showMore.templates.active` | `templates.showMoreActive` | +| `showMore.templates.inactive` | `templates.showMoreInactive` | + +- `showMore` is now a boolean option (`showMore.templates` are now in `templates`) +- `sortBy` defaults to `['isRefined', 'name:asc']` + +#### CSS classes + +| Before | After | +| ------------------------ | ----------------------------- | +| `ais-menu` | `ais-Menu` | +| `ais-menu--list` | `ais-Menu-list` | +| `ais-menu--item` | `ais-Menu-item` | +| `ais-menu--item__active` | `ais-Menu-item--selected` | +| `ais-menu--link` | `ais-Menu-link` | +| | `ais-Menu-label` | +| `ais-menu--count` | `ais-Menu-count` | +| | `ais-Menu-noResults` | +| | `ais-Menu-showMore` | +| | `ais-Menu-showMore--disabled` | + +#### Markup + +##### Default + +```html +
+ + +
+``` + +##### Show more disabled + +```html +
+ + +
+``` + ### MenuSelect #### Options diff --git a/src/connectors/menu/connectMenu.js b/src/connectors/menu/connectMenu.js index 36922cab59..df0d7861cb 100644 --- a/src/connectors/menu/connectMenu.js +++ b/src/connectors/menu/connectMenu.js @@ -17,8 +17,8 @@ search.addWidget( customMenu({ attribute, [ limit ], - [ showMoreLimit ] - [ sortBy = ['name:asc'] ] + [ showMoreLimit ], + [ sortBy = ['isRefined', 'name:asc'] ], [ transformItems ] }) ); @@ -37,8 +37,8 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v * @typedef {Object} CustomMenuWidgetOptions * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping"). * @property {number} [limit = 10] How many facets values to retrieve. - * @property {number} [showMoreLimit = undefined] How many facets values to retrieve when `toggleShowMore` is called, this value is meant to be greater than `limit` option. - * @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. + * @property {number} [showMoreLimit = 10] How many facets values to retrieve when `toggleShowMore` is called, this value is meant to be greater than `limit` option. + * @property {string[]|function} [sortBy = ['isRefined', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. * * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates. @@ -113,8 +113,8 @@ export default function connectMenu(renderFn, unmountFn) { const { attribute, limit = 10, - sortBy = ['name:asc'], - showMoreLimit, + sortBy = ['isRefined', 'name:asc'], + showMoreLimit = limit, transformItems = items => items, } = widgetParams; diff --git a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap index 5137cde5cb..8be2964019 100644 --- a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap +++ b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap @@ -3,19 +3,19 @@ exports[`menu render renders transformed items 1`] = ` {{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}", + "item": "{{label}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}", + "showMoreActive": "Show less", + "showMoreInactive": "Show more", }, "templatesConfig": undefined, "transformData": undefined, "useCustomCompileOptions": Object { - "footer": false, - "header": false, "item": false, + "showMoreActive": false, + "showMoreInactive": false, }, } } @@ -62,19 +61,19 @@ exports[`menu render renders transformed items 1`] = ` exports[`menu render snapshot 1`] = ` {{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}", + "item": "{{label}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}", + "showMoreActive": "Show less", + "showMoreInactive": "Show more", }, "templatesConfig": undefined, "transformData": undefined, "useCustomCompileOptions": Object { - "footer": false, - "header": false, "item": false, + "showMoreActive": false, + "showMoreInactive": false, }, } } diff --git a/src/widgets/menu/__tests__/menu-test.js b/src/widgets/menu/__tests__/menu-test.js index 17e78aeac8..716df658de 100644 --- a/src/widgets/menu/__tests__/menu-test.js +++ b/src/widgets/menu/__tests__/menu-test.js @@ -1,14 +1,37 @@ import menu from '../menu'; describe('menu', () => { - it('throws an exception when no attributeName', () => { + it('throws an exception when no attribute', () => { const container = document.createElement('div'); expect(menu.bind(null, { container })).toThrow(/^Usage/); }); it('throws an exception when no container', () => { - const attributeName = ''; - expect(menu.bind(null, { attributeName })).toThrow(/^Usage/); + expect(menu.bind(null, { attribute: '' })).toThrow(/^Usage/); + }); + + it('throws an exception when showMoreLimit without showMore option', () => { + const container = document.createElement('div'); + expect( + menu.bind(null, { attribute: 'attribute', container, showMoreLimit: 10 }) + ).toThrowErrorMatchingInlineSnapshot( + `"\`showMoreLimit\` must be used with \`showMore\` set to \`true\`."` + ); + }); + + it('throws an exception when showMoreLimit is lower than limit', () => { + const container = document.createElement('div'); + expect( + menu.bind(null, { + attribute: 'attribute', + container, + limit: 20, + showMore: true, + showMoreLimit: 10, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"\`showMoreLimit\` should be greater than \`limit\`."` + ); }); describe('render', () => { @@ -35,7 +58,7 @@ describe('menu', () => { it('snapshot', () => { const widget = menu({ container: document.createElement('div'), - attributeName: 'test', + attribute: 'test', }); widget.init({ @@ -51,7 +74,7 @@ describe('menu', () => { it('renders transformed items', () => { const widget = menu({ container: document.createElement('div'), - attributeName: 'test', + attribute: 'test', transformItems: items => items.map(item => ({ ...item, transformed: true })), }); diff --git a/src/widgets/menu/defaultTemplates.js b/src/widgets/menu/defaultTemplates.js index 64def3bcd6..e93aae4ed3 100644 --- a/src/widgets/menu/defaultTemplates.js +++ b/src/widgets/menu/defaultTemplates.js @@ -1,7 +1,10 @@ /* eslint-disable max-len */ export default { - header: '', item: - '{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}', - footer: '', + '' + + '{{label}}' + + '{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}' + + '', + showMoreActive: 'Show less', + showMoreInactive: 'Show more', }; diff --git a/src/widgets/menu/menu.js b/src/widgets/menu/menu.js index 62adcaca31..45cdb6b491 100644 --- a/src/widgets/menu/menu.js +++ b/src/widgets/menu/menu.js @@ -1,35 +1,24 @@ import React, { render, unmountComponentAtNode } from 'preact-compat'; import cx from 'classnames'; - -import defaultTemplates from './defaultTemplates.js'; -import getShowMoreConfig from '../../lib/show-more/getShowMoreConfig.js'; -import connectMenu from '../../connectors/menu/connectMenu.js'; import RefinementList from '../../components/RefinementList/RefinementList.js'; +import connectMenu from '../../connectors/menu/connectMenu.js'; +import defaultTemplates from './defaultTemplates.js'; +import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js'; +import { component } from '../../lib/suit'; -import { - bemHelper, - prepareTemplateProps, - getContainerNode, - prefixKeys, -} from '../../lib/utils.js'; - -const bem = bemHelper('ais-menu'); +const suit = component('Menu'); const renderer = ({ containerNode, cssClasses, - collapsible, - autoHideContainer, renderState, templates, - transformData, - showMoreConfig, + showMore, }) => ( { refine, items, createURL, - canRefine, instantSearchInstance, isShowingMore, toggleShowMore, @@ -39,7 +28,6 @@ const renderer = ({ ) => { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ - transformData, defaultTemplates, templatesConfig: instantSearchInstance.templatesConfig, templates, @@ -51,16 +39,13 @@ const renderer = ({ ...facetValue, url: createURL(facetValue.name), })); - const shouldAutoHideContainer = autoHideContainer && !canRefine; render( than the limit in the main configuration'); // eslint-disable-line + if (!showMore && showMoreLimit) { + throw new Error( + '`showMoreLimit` must be used with `showMore` set to `true`.' + ); } - const containerNode = getContainerNode(container); + if (showMore && showMoreLimit < limit) { + throw new Error('`showMoreLimit` should be greater than `limit`.'); + } - const showMoreLimit = (showMoreConfig && showMoreConfig.limit) || undefined; - const showMoreTemplates = - showMoreConfig && prefixKeys('show-more-', showMoreConfig.templates); - const allTemplates = showMoreTemplates - ? { ...templates, ...showMoreTemplates } - : templates; + const containerNode = getContainerNode(container); const cssClasses = { - root: cx(bem(null), userCssClasses.root), - header: cx(bem('header'), userCssClasses.header), - body: cx(bem('body'), userCssClasses.body), - footer: cx(bem('footer'), userCssClasses.footer), - list: cx(bem('list'), userCssClasses.list), - item: cx(bem('item'), userCssClasses.item), - active: cx(bem('item', 'active'), userCssClasses.active), - link: cx(bem('link'), userCssClasses.link), - count: cx(bem('count'), userCssClasses.count), + root: cx(suit(), userCssClasses.root), + noRefinementRoot: cx( + suit({ modifierName: 'noRefinement' }), + userCssClasses.noRefinementRoot + ), + list: cx(suit({ descendantName: 'list' }), userCssClasses.list), + item: cx(suit({ descendantName: 'item' }), userCssClasses.item), + selectedItem: cx( + suit({ descendantName: 'item', modifierName: 'selected' }), + userCssClasses.selectedItem + ), + link: cx(suit({ descendantName: 'link' }), userCssClasses.link), + label: cx(suit({ descendantName: 'label' }), userCssClasses.label), + count: cx(suit({ descendantName: 'count' }), userCssClasses.count), + showMore: cx(suit({ descendantName: 'showMore' }), userCssClasses.showMore), + disabledShowMore: cx( + suit({ descendantName: 'showMore', modifierName: 'disabled' }), + userCssClasses.disabledShowMore + ), }; const specializedRenderer = renderer({ containerNode, cssClasses, - collapsible, - autoHideContainer, renderState: {}, - templates: allTemplates, - transformData, - showMoreConfig, + templates, + showMore, }); try { @@ -224,13 +189,13 @@ export default function menu({ unmountComponentAtNode(containerNode) ); return makeWidget({ - attribute: attributeName, + attribute, limit, sortBy, showMoreLimit, transformItems, }); - } catch (e) { + } catch (error) { throw new Error(usage); } }