diff --git a/bundlesize.config.json b/bundlesize.config.json index 42649581f8..1be8588259 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -10,7 +10,7 @@ }, { "path": "./packages/instantsearch.js/dist/instantsearch.production.min.js", - "maxSize": "84.50 kB" + "maxSize": "85.25 kB" }, { "path": "./packages/instantsearch.js/dist/instantsearch.development.js", @@ -18,11 +18,11 @@ }, { "path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js", - "maxSize": "52 kB" + "maxSize": "52.25 kB" }, { "path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js", - "maxSize": "66 kB" + "maxSize": "66.75 kB" }, { "path": "packages/vue-instantsearch/vue2/umd/index.js", diff --git a/packages/instantsearch-ui-components/src/components/Carousel.tsx b/packages/instantsearch-ui-components/src/components/Carousel.tsx index 7916447a37..0f263b738f 100644 --- a/packages/instantsearch-ui-components/src/components/Carousel.tsx +++ b/packages/instantsearch-ui-components/src/components/Carousel.tsx @@ -1,4 +1,5 @@ /** @jsx createElement */ + import { cx } from '../lib'; import { createDefaultItemComponent } from './recommend-shared'; diff --git a/packages/instantsearch-ui-components/src/components/TrendingFacets.tsx b/packages/instantsearch-ui-components/src/components/TrendingFacets.tsx new file mode 100644 index 0000000000..c06294ff1f --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/TrendingFacets.tsx @@ -0,0 +1,107 @@ +/** @jsx createElement */ + +import { cx } from '../lib'; + +import { + createDefaultEmptyComponent, + createDefaultHeaderComponent, + createListComponent, +} from './recommend-shared'; + +import type { + ComponentProps, + RecommendClassNames, + RecommendComponentProps, + RecommendTranslations, + Renderer, + TrendingFacetHit, +} from '../types'; + +export type TrendingFacetsProps< + TComponentProps extends Record = Record +> = ComponentProps<'div'> & + Omit< + RecommendComponentProps, + 'itemComponent' + > & { + itemComponent: RecommendComponentProps< + TrendingFacetHit, + TComponentProps + >['itemComponent']; + }; + +export function createTrendingFacetsComponent({ + createElement, + Fragment, +}: Renderer) { + return function TrendingFacets(userProps: TrendingFacetsProps) { + const { + classNames = {}, + emptyComponent: EmptyComponent = createDefaultEmptyComponent({ + createElement, + Fragment, + }), + headerComponent: HeaderComponent = createDefaultHeaderComponent({ + createElement, + Fragment, + }), + // Fallback to a no-op component if no itemComponent is provided + // This is to ensure that the component can render when the layout + // the user provided does not need an itemComponent, but still allows + // us to later create a default itemComponent that has an action. + itemComponent: ItemComponent = () => null as any, + layout: Layout = createListComponent({ createElement, Fragment }), + items, + status, + translations: userTranslations, + sendEvent, + ...props + } = userProps; + + const translations: Required = { + title: 'Trending facets', + sliderLabel: 'Trending facets', + ...userTranslations, + }; + + const cssClasses: RecommendClassNames = { + root: cx('ais-TrendingFacets', classNames.root), + emptyRoot: cx( + 'ais-TrendingFacets', + classNames.root, + 'ais-TrendingFacets--empty', + classNames.emptyRoot, + props.className + ), + title: cx('ais-TrendingFacets-title', classNames.title), + container: cx('ais-TrendingFacets-container', classNames.container), + list: cx('ais-TrendingFacets-list', classNames.list), + item: cx('ais-TrendingFacets-item', classNames.item), + }; + + if (items.length === 0 && status === 'idle') { + return ( +
+ +
+ ); + } + + return ( +
+ + + null as any)} + items={items} + sendEvent={sendEvent} + /> +
+ ); + }; +} diff --git a/packages/instantsearch-ui-components/src/components/__tests__/TrendingFacets.test.tsx b/packages/instantsearch-ui-components/src/components/__tests__/TrendingFacets.test.tsx new file mode 100644 index 0000000000..94cbb0bc55 --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/__tests__/TrendingFacets.test.tsx @@ -0,0 +1,396 @@ +/** + * @jest-environment jsdom + */ +/** @jsx createElement */ +import { render } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import { createElement, Fragment } from 'preact'; + +import { createTrendingFacetsComponent } from '../TrendingFacets'; + +import type { TrendingFacetsProps } from '../TrendingFacets'; + +const TrendingFacets = createTrendingFacetsComponent({ + createElement, + Fragment, +}); + +const ItemComponent: TrendingFacetsProps['itemComponent'] = ({ item }) => ( +
{item.objectID}
+); + +describe('TrendingFacets', () => { + test('renders items with default layout and header', () => { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+
+

+ Trending facets +

+
+
    +
  1. +
    + category:electronics +
    +
  2. +
  3. +
    + category:books +
    +
  4. +
+
+
+
+ `); + }); + + test('renders default empty component', () => { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+
+ No results +
+
+ `); + }); + + test('renders custom header', () => { + const { container } = render( + ( +
My custom header
+ )} + itemComponent={ItemComponent} + sendEvent={jest.fn()} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+
+
+ My custom header +
+
+
    +
  1. +
    + 1:1 +
    +
  2. +
+
+
+
+ `); + }); + + test('renders custom layout', () => { + const { container } = render( + ( +
+
    + {props.items.map((item) => ( +
  1. + +
  2. + ))} +
+
+ )} + sendEvent={jest.fn()} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+
+

+ Trending facets +

+
+
    +
  1. +
    + 1:1 +
    +
  2. +
+
+
+
+ `); + }); + + test('renders custom empty component', () => { + const { container } = render( + My custom empty component} + itemComponent={ItemComponent} + sendEvent={jest.fn()} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+
+ My custom empty component +
+
+ `); + }); + + test('sends a `click` event when clicking on an item', () => { + const sendEvent = jest.fn(); + const items = [ + { objectID: '1:1', _score: 1, attribute: '1', value: '1', __position: 1 }, + ]; + + const { container } = render( + + ); + + userEvent.click(container.querySelectorAll('.ais-TrendingFacets-item')[0]!); + + expect(sendEvent).toHaveBeenCalledTimes(1); + expect(sendEvent).toHaveBeenNthCalledWith( + 1, + 'click:internal', + items[0], + 'Item Clicked' + ); + }); + + test('accepts custom title translation', () => { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+
+

+ My custom title +

+
+
    +
  1. +
    + 1:1 +
    +
  2. +
+
+
+
+ `); + }); + + test('forwards `div` props to the root element', () => { + const { container } = render( +