diff --git a/.gitignore b/.gitignore index eeec3cb1e8..001eb1d62f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ scripts/*/CHANGELOG.md # private files .env .idea/ +.vscode/ # Caches .eslintcache diff --git a/babel.config.js b/babel.config.js index 6b85cafb23..f33b615d1f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -79,6 +79,9 @@ module.exports = (api) => { // false positive (babel doesn't know types) // this is actually only called on arrays 'String.prototype.includes', + + // false positive (spread) + 'Object.getOwnPropertyDescriptors', ]; if (defaultShouldInject && !exclude.includes(name)) { throw new Error( diff --git a/bundlesize.config.json b/bundlesize.config.json index b866303f84..2a66d24374 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -26,7 +26,7 @@ }, { "path": "packages/vue-instantsearch/vue2/umd/index.js", - "maxSize": "66.75 kB" + "maxSize": "67 kB" }, { "path": "packages/vue-instantsearch/vue3/umd/index.js", diff --git a/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx b/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx new file mode 100644 index 0000000000..e69b813f52 --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx @@ -0,0 +1,85 @@ +/** @jsx createElement */ + +import { cx } from '../lib'; + +import { createDefaultHeaderComponent } from './recommend-shared/DefaultHeader'; +import { createListViewComponent } from './recommend-shared/ListView'; + +import type { + RecommendTranslations, + Renderer, + ComponentProps, + RecommendComponentProps, +} from '../types'; + +export type FrequentlyBoughtTogetherProps< + TObject, + TComponentProps extends Record = Record +> = ComponentProps<'div'> & RecommendComponentProps; + +export function createFrequentlyBoughtTogetherComponent({ + createElement, + Fragment, +}: Renderer) { + return function FrequentlyBoughtTogether( + userProps: FrequentlyBoughtTogetherProps + ) { + const { + classNames = {}, + fallbackComponent: FallbackComponent = () => null, + headerComponent: HeaderComponent = createDefaultHeaderComponent({ + createElement, + Fragment, + }), + itemComponent: ItemComponent, + view: View = createListViewComponent({ createElement, Fragment }), + items, + status, + translations: userTranslations, + sendEvent, + ...props + } = userProps; + + const translations: Required = { + title: 'Frequently bought together', + sliderLabel: 'Frequently bought together products', + ...userTranslations, + }; + + if (items.length === 0 && status === 'idle') { + return ; + } + + return ( +
+ + + +
+ ); + }; +} diff --git a/packages/instantsearch-ui-components/src/components/Hits.tsx b/packages/instantsearch-ui-components/src/components/Hits.tsx index 44fc478f88..ae65344b10 100644 --- a/packages/instantsearch-ui-components/src/components/Hits.tsx +++ b/packages/instantsearch-ui-components/src/components/Hits.tsx @@ -1,13 +1,12 @@ /** @jsx createElement */ import { cx } from '../lib'; -import type { ComponentProps, Renderer } from '../types'; +import type { ComponentProps, Renderer, SendEventForHits } from '../types'; // Should be imported from a shared package in the future type Hit = Record & { objectID: string; }; -type SendEventForHits = (...props: unknown[]) => void; type Banner = { image: { urls: Array<{ diff --git a/packages/instantsearch-ui-components/src/components/__tests__/FrequentlyBoughtTogether.test.tsx b/packages/instantsearch-ui-components/src/components/__tests__/FrequentlyBoughtTogether.test.tsx new file mode 100644 index 0000000000..aad0a052b4 --- /dev/null +++ b/packages/instantsearch-ui-components/src/components/__tests__/FrequentlyBoughtTogether.test.tsx @@ -0,0 +1,340 @@ +/** + * @jest-environment jsdom + */ +/** @jsx createElement */ +import { render } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import { createElement, Fragment } from 'preact'; + +import { createFrequentlyBoughtTogetherComponent } from '../FrequentlyBoughtTogether'; + +import type { RecordWithObjectID } from '../../types'; +import type { FrequentlyBoughtTogetherProps } from '../FrequentlyBoughtTogether'; + +const FrequentlyBoughtTogether = createFrequentlyBoughtTogetherComponent({ + createElement, + Fragment, +}); + +const ItemComponent: FrequentlyBoughtTogetherProps['itemComponent'] = + ({ item, ...itemProps }) => ( +
  • +
    {item.objectID}
    +
  • + ); + +describe('FrequentlyBoughtTogether', () => { + test('renders items with default view and header', () => { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
    +
    +

    + Frequently bought together +

    +
    +
      +
    1. +
      + 1 +
      +
    2. +
    3. +
      + 2 +
      +
    4. +
    +
    +
    +
    + `); + }); + + test('renders default fallback', () => { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(`
    `); + }); + + test('renders custom header', () => { + const { container } = render( + ( +
    My custom header
    + )} + itemComponent={ItemComponent} + sendEvent={jest.fn()} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
    +
    +
    + My custom header +
    +
    +
      +
    1. +
      + 1 +
      +
    2. +
    +
    +
    +
    + `); + }); + + test('renders custom view', () => { + const { container } = render( + ( +
    +
      + {props.items.map((item) => ( +
    1. + +
    2. + ))} +
    +
    + )} + sendEvent={jest.fn()} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
    +
    +

    + Frequently bought together +

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

    + My custom title +

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