diff --git a/package.json b/package.json index 26db7bd2d6..5600c4076f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ ], "dependencies": { "@types/googlemaps": "^3.39.6", - "algoliasearch-helper": "^3.3.4", + "algoliasearch-helper": "^3.4.4", "classnames": "^2.2.5", "events": "^1.1.0", "hogan.js": "^3.0.2", @@ -143,7 +143,7 @@ "bundlesize": [ { "path": "./dist/instantsearch.production.min.js", - "maxSize": "66.15 kB" + "maxSize": "66.50 kB" }, { "path": "./dist/instantsearch.development.js", diff --git a/src/components/MenuSelect/MenuSelect.tsx b/src/components/MenuSelect/MenuSelect.tsx index 21c14ac8cb..01d6fdcab7 100644 --- a/src/components/MenuSelect/MenuSelect.tsx +++ b/src/components/MenuSelect/MenuSelect.tsx @@ -4,11 +4,11 @@ import { h } from 'preact'; import cx from 'classnames'; import { find } from '../../lib/utils'; import Template from '../Template/Template'; - -type MenuSelectTemplates = { - defaultOption: string; - item: string; -}; +import { + MenuSelectCSSClasses, + MenuSelectTemplates, +} from '../../widgets/menu-select/menu-select'; +import { MenuRendererOptions } from '../../connectors/menu/connectMenu'; type MenuItem = { /** @@ -30,14 +30,9 @@ type MenuItem = { }; type Props = { - cssClasses: { - root: string; - noRefinementRoot: string; - select: string; - option: string; - }; + cssClasses: MenuSelectCSSClasses; items: MenuItem[]; - refine: (value: MenuItem['value']) => void; + refine: MenuRendererOptions['refine']; templateProps: { templates: MenuSelectTemplates; }; diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.ts similarity index 90% rename from src/connectors/menu/__tests__/connectMenu-test.js rename to src/connectors/menu/__tests__/connectMenu-test.ts index 1a7c3478d1..26319fe3c1 100644 --- a/src/connectors/menu/__tests__/connectMenu-test.js +++ b/src/connectors/menu/__tests__/connectMenu-test.ts @@ -23,6 +23,7 @@ describe('connectMenu', () => { describe('Usage', () => { it('throws without render function', () => { expect(() => { + // @ts-ignore connectMenu()({}); }).toThrowErrorMatchingInlineSnapshot(` "The render function is not valid (received type Undefined). @@ -164,7 +165,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co // test if widget is not rendered yet at this point expect(rendering).toHaveBeenCalledTimes(0); - const helper = jsHelper({}, '', config); + const helper = jsHelper(createSearchClient(), '', config); helper.search = jest.fn(); widget.init({ @@ -188,7 +189,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co ); widget.render({ - results: new SearchResults(helper.state, [{}]), + results: new SearchResults(helper.state, [createSingleSearchResponse()]), state: helper.state, helper, createURL: () => '#', @@ -214,7 +215,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); const helper = jsHelper( - {}, + createSearchClient(), '', widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) ); @@ -237,7 +238,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co expect(helper.hasRefinements('category')).toBe(true); widget.render({ - results: new SearchResults(helper.state, [{}, {}]), + results: new SearchResults(helper.state, [ + createSingleSearchResponse(), + createSingleSearchResponse(), + ]), state: helper.state, helper, createURL: () => '#', @@ -257,7 +261,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); const helper = jsHelper( - {}, + createSearchClient(), '', widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) ); @@ -281,22 +285,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widget.render({ results: new SearchResults(helper.state, [ - { + createSingleSearchResponse({ hits: [], facets: { category: { Decoration: 880, }, }, - }, - { + }), + createSingleSearchResponse({ facets: { category: { Decoration: 880, Outdoor: 47, }, }, - }, + }), ]), state: helper.state, helper, @@ -310,6 +314,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co label: 'Decoration', value: 'Decoration', count: 880, + exhaustive: true, isRefined: true, data: null, }, @@ -317,6 +322,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co label: 'Outdoor', value: 'Outdoor', count: 47, + exhaustive: true, isRefined: false, data: null, }, @@ -334,7 +340,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co // note that the helper is called with empty search parameters // which means this can only happen in a stale search situation // when this widget gets mounted - const helper = jsHelper({}, '', {}); + const helper = jsHelper(createSearchClient(), '', {}); widget.render({ results: new SearchResults(helper.state, [ @@ -376,7 +382,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); const helper = jsHelper( - {}, + createSearchClient(), '', widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) ); @@ -398,22 +404,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widget.render({ results: new SearchResults(helper.state, [ - { + createSingleSearchResponse({ hits: [], facets: { category: { Decoration: 880, }, }, - }, - { + }), + createSingleSearchResponse({ facets: { category: { Decoration: 880, Outdoor: 47, }, }, - }, + }), ]), state: helper.state, helper, @@ -435,11 +441,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co attribute: 'category', }); const helper = jsHelper( - {}, + createSearchClient(), '', - widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) + widget.getWidgetSearchParameters!(new SearchParameters(), { uiState: {} }) ); - expect(() => widget.dispose({ helper, state: helper.state })).not.toThrow(); + expect(() => + widget.dispose!({ helper, state: helper.state }) + ).not.toThrow(); }); describe('getRenderState', () => { @@ -453,7 +461,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const helper = jsHelper( createSearchClient(), 'indexName', - menu.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) + menu.getWidgetSearchParameters!(new SearchParameters(), { uiState: {} }) ); const renderState1 = menu.getRenderState( @@ -461,7 +469,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co createInitOptions({ helper }) ); - expect(renderState1.menu).toEqual({ + expect(renderState1.menu!.brand).toEqual({ items: [], createURL: expect.any(Function), refine: expect.any(Function), @@ -484,12 +492,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const helper = jsHelper( createSearchClient(), 'indexName', - menu.getWidgetSearchParameters(new SearchParameters(), { + menu.getWidgetSearchParameters!(new SearchParameters(), { uiState: {}, }) ); - menu.init(createInitOptions({ helper })); + menu.init!(createInitOptions({ helper })); expect( menu.getRenderState( @@ -499,24 +507,23 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co results: new SearchResults(helper.state, [ createSingleSearchResponse({ hits: [], - facets: { - brand: 300, - }, }), ]), }) ) ).toEqual({ menu: { - items: [], - canRefine: false, - refine: expect.any(Function), - sendEvent: expect.any(Function), - createURL: expect.any(Function), - widgetParams: { attribute: 'brand' }, - isShowingMore: false, - toggleShowMore: expect.any(Function), - canToggleShowMore: false, + brand: { + items: [], + canRefine: false, + refine: expect.any(Function), + sendEvent: expect.any(Function), + createURL: expect.any(Function), + widgetParams: { attribute: 'brand' }, + isShowingMore: false, + toggleShowMore: expect.any(Function), + canToggleShowMore: false, + }, }, }); }); @@ -533,11 +540,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const helper = jsHelper( createSearchClient(), 'indexName', - menu.getWidgetSearchParameters(new SearchParameters(), { uiState: {} }) + menu.getWidgetSearchParameters!(new SearchParameters(), { uiState: {} }) ); const renderState1 = menu.getWidgetRenderState( - {}, createInitOptions({ helper }) ); @@ -624,7 +630,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const config = widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, }); - const helper = jsHelper({}, '', config); + const helper = jsHelper(createSearchClient(), '', config); helper.search = jest.fn(); widget.init({ @@ -654,7 +660,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const config = widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, }); - const helper = jsHelper({}, '', config); + const helper = jsHelper(createSearchClient(), '', config); helper.search = jest.fn(); helper.toggleRefinement('category', 'Decoration'); @@ -667,22 +673,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widget.render({ results: new SearchResults(helper.state, [ - { + createSingleSearchResponse({ hits: [], facets: { category: { Decoration: 880, }, }, - }, - { + }), + createSingleSearchResponse({ facets: { category: { Decoration: 880, Outdoor: 47, }, }, - }, + }), ]), state: helper.state, helper, @@ -720,7 +726,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const config = widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, }); - const helper = jsHelper({}, '', config); + const helper = jsHelper(createSearchClient(), '', config); helper.search = jest.fn(); helper.toggleRefinement('category', 'Decoration'); @@ -733,21 +739,21 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widget.render({ results: new SearchResults(helper.state, [ - { + createSingleSearchResponse({ hits: [], facets: { category: { Decoration: 880, }, }, - }, - { + }), + createSingleSearchResponse({ facets: { category: { Decoration: 880, }, }, - }, + }), ]), state: helper.state, helper, @@ -763,7 +769,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co describe('getWidgetUiState', () => { test('returns the `uiState` empty', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', }); @@ -779,7 +785,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `uiState` with a refinement', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { hierarchicalFacets: [ { name: 'brand', @@ -810,7 +816,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `uiState` without namespace overridden', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { hierarchicalFacets: [ { name: 'brand', @@ -848,7 +854,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co describe('getWidgetSearchParameters', () => { test('returns the `SearchParameters` with the default value', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', }); @@ -870,7 +876,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with the default value without the previous refinement', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { hierarchicalFacets: [ { name: 'brand', @@ -903,7 +909,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with the value from `uiState`', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', }); @@ -929,7 +935,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with the value from `uiState` without the previous refinement', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { hierarchicalFacets: [ { name: 'brand', @@ -967,7 +973,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co describe('with `maxValuesPerFacet`', () => { test('returns the `SearchParameters` with default `limit`', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', }); @@ -980,7 +986,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with provided `limit`', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', limit: 5, @@ -994,7 +1000,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with default `showMoreLimit`', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', showMore: true, @@ -1008,7 +1014,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with provided `showMoreLimit`', () => { - const helper = jsHelper({}, ''); + const helper = jsHelper(createSearchClient(), ''); const widget = makeWidget({ attribute: 'brand', showMore: true, @@ -1023,7 +1029,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with the previous value if higher than `limit`/`showMoreLimit`', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { maxValuesPerFacet: 100, }); @@ -1039,7 +1045,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); test('returns the `SearchParameters` with `limit`/`showMoreLimit` if higher than previous value', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper(createSearchClient(), '', { maxValuesPerFacet: 100, }); @@ -1067,7 +1073,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const indexName = 'instant_search'; const helper = jsHelper( - {}, + createSearchClient(), indexName, widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, @@ -1100,22 +1106,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widget.render({ results: new SearchResults(helper.state, [ - { + createSingleSearchResponse({ hits: [], facets: { myFacet: { Decoration: 880, }, }, - }, - { + }), + createSingleSearchResponse({ facets: { myFacet: { Decoration: 880, Outdoor: 47, }, }, - }, + }), ]), state: helper.state, helper, @@ -1160,7 +1166,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co const indexName = 'instant_search'; const helper = jsHelper( - {}, + createSearchClient(), indexName, widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, @@ -1213,7 +1219,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); const instantSearchInstance = createInstantSearch(); const helper = jsHelper( - {}, + createSearchClient(), '', widget.getWidgetSearchParameters(new SearchParameters(), { uiState: {}, diff --git a/src/connectors/menu/connectMenu.js b/src/connectors/menu/connectMenu.ts similarity index 50% rename from src/connectors/menu/connectMenu.js rename to src/connectors/menu/connectMenu.ts index 194adccafc..0e70b569e0 100644 --- a/src/connectors/menu/connectMenu.js +++ b/src/connectors/menu/connectMenu.ts @@ -3,45 +3,106 @@ import { createDocumentationMessageGenerator, createSendEventForFacet, noop, + SendEventForFacet, } from '../../lib/utils'; +import { + Connector, + RenderOptions, + SortBy, + TransformItems, + Widget, +} from '../../types'; const withUsage = createDocumentationMessageGenerator({ name: 'menu', connector: true, }); -/** - * @typedef {Object} MenuItem - * @property {string} value The value of the menu item. - * @property {string} label Human-readable value of the menu item. - * @property {number} count Number of results matched after refinement is applied. - * @property {boolean} isRefined Indicates if the refinement is applied. - */ - -/** - * @typedef {Object} CustomMenuWidgetParams - * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping"). - * @property {number} [limit = 10] How many facets values to retrieve. - * @property {boolean} [showMore = false] Whether to display a button that expands the number of items. - * @property {number} [showMoreLimit = 20] 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. - */ - -/** - * @typedef {Object} MenuRenderingOptions - * @property {MenuItem[]} items The elements that can be refined for the current search results. - * @property {function(item.value): string} createURL Creates the URL for a single item name in the list. - * @property {function(item.value)} refine Filter the search to item value. - * @property {boolean} canRefine True if refinement can be applied. - * @property {Object} widgetParams All original `CustomMenuWidgetParams` forwarded to the `renderFn`. - * @property {boolean} isShowingMore True if the menu is displaying all the menu items. - * @property {function} toggleShowMore Toggles the number of values displayed between `limit` and `showMore.limit`. - * @property {boolean} canToggleShowMore `true` if the toggleShowMore button can be activated (enough items to display more or - * already displaying more than `limit` items) - */ +export type MenuItem = { + /** + * The value of the menu item. + */ + value: string; + /** + * Human-readable value of the menu item. + */ + label: string; + /** + * Number of results matched after refinement is applied. + */ + count: number; + /** + * Indicates if the refinement is applied. + */ + isRefined: boolean; +}; + +export type MenuConnectorParams = { + /** + * Name of the attribute for faceting (eg. "free_shipping"). + */ + attribute: string; + /** + * How many facets values to retrieve. + */ + limit?: number; + /** + * Whether to display a button that expands the number of items. + */ + showMore?: boolean; + /** + * How many facets values to retrieve when `toggleShowMore` is called, this value is meant to be greater than `limit` option. + */ + showMoreLimit?: number; + /** + * 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). + */ + sortBy?: SortBy; + /** + * Function to transform the items passed to the templates. + */ + transformItems?: TransformItems; +}; + +export type MenuRendererOptions = { + /** + * The elements that can be refined for the current search results. + */ + items: MenuItem[]; + /** + * Creates the URL for a single item name in the list. + */ + createURL(value: string): string; + /** + * Filter the search to item value. + */ + refine(value: string): void; + /** + * True if refinement can be applied. + */ + canRefine: boolean; + /** + * True if the menu is displaying all the menu items. + */ + isShowingMore: boolean; + /** + * Toggles the number of values displayed between `limit` and `showMore.limit`. + */ + toggleShowMore(): void; + /** + * `true` if the toggleShowMore button can be activated (enough items to display more or + * already displaying more than `limit` items) + */ + canToggleShowMore: boolean; + /** + * Send event to insights middleware + */ + sendEvent: SendEventForFacet; +}; + +export type MenuConnector = Connector; /** * **Menu** connector provides the logic to build a widget that will give the user the ability to choose a single value for a specific facet. The typical usage of menu is for navigation in categories. @@ -51,51 +112,14 @@ const withUsage = createDocumentationMessageGenerator({ * one that is currently selected. * * **Requirement:** the attribute passed as `attribute` must be present in "attributes for faceting" on the Algolia dashboard or configured as attributesForFaceting via a set settings call to the Algolia API. - * @type {Connector} - * @param {function(MenuRenderingOptions, boolean)} renderFn Rendering function for the custom **Menu** widget. widget. - * @param {function} unmountFn Unmount function called when the widget is disposed. - * @return {function(CustomMenuWidgetParams)} Re-usable widget factory for a custom **Menu** widget. - * @example - * // custom `renderFn` to render the custom Menu widget - * function renderFn(MenuRenderingOptions, isFirstRendering) { - * if (isFirstRendering) { - * MenuRenderingOptions.widgetParams.containerNode - * .html('