From 41b0c96f3d215d24eb1fb17109f911c11a71f434 Mon Sep 17 00:00:00 2001 From: Shahmir Ejaz Date: Mon, 19 Aug 2024 16:39:22 +0100 Subject: [PATCH] feat(react-instantsearch): add `Carousel` layout component (#6321) --- bundlesize.config.json | 2 +- examples/react/getting-started/index.html | 9 - examples/react/getting-started/package.json | 1 + examples/react/getting-started/products.html | 9 - examples/react/getting-started/src/App.css | 67 +++- examples/react/getting-started/src/App.tsx | 24 +- .../react/getting-started/src/Product.tsx | 21 +- .../components/FrequentlyBoughtTogether.tsx | 1 - .../src/components/LookingSimilar.tsx | 1 - .../src/components/RelatedProducts.tsx | 1 - .../src/components/TrendingItems.tsx | 1 - .../src/components/recommend-shared/List.tsx | 7 +- .../src/types/Recommend.ts | 3 - .../src/components/Carousel.tsx | 36 +++ .../components/__tests__/Carousel.test.tsx | 299 ++++++++++++++++++ .../src/components/index.ts | 1 + packages/react-instantsearch/src/index.ts | 1 + .../src/widgets/FrequentlyBoughtTogether.tsx | 15 + .../src/widgets/LookingSimilar.tsx | 15 + .../src/widgets/RelatedProducts.tsx | 15 + .../src/widgets/TrendingItems.tsx | 15 + .../FrequentlyBoughtTogether.test.tsx | 271 ++++++++++++++++ .../widgets/__tests__/LookingSimilar.test.tsx | 271 ++++++++++++++++ .../__tests__/RelatedProducts.test.tsx | 274 ++++++++++++++++ .../widgets/__tests__/TrendingItems.test.tsx | 265 ++++++++++++++++ 25 files changed, 1570 insertions(+), 55 deletions(-) create mode 100644 packages/react-instantsearch/src/components/Carousel.tsx create mode 100644 packages/react-instantsearch/src/components/__tests__/Carousel.test.tsx create mode 100644 packages/react-instantsearch/src/components/index.ts diff --git a/bundlesize.config.json b/bundlesize.config.json index 7579a0f21f..6808fe065f 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -22,7 +22,7 @@ }, { "path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js", - "maxSize": "64 kB" + "maxSize": "64.5 kB" }, { "path": "packages/vue-instantsearch/vue2/umd/index.js", diff --git a/examples/react/getting-started/index.html b/examples/react/getting-started/index.html index 0c360b898a..2bd6fd1f0e 100644 --- a/examples/react/getting-started/index.html +++ b/examples/react/getting-started/index.html @@ -9,15 +9,6 @@ - - - React InstantSearch — Getting started diff --git a/examples/react/getting-started/package.json b/examples/react/getting-started/package.json index 33aba4557e..a57010b330 100644 --- a/examples/react/getting-started/package.json +++ b/examples/react/getting-started/package.json @@ -9,6 +9,7 @@ "dependencies": { "algoliasearch": "4.23.2", "instantsearch.js": "4.73.4", + "instantsearch.css": "8.4.0", "react": "18.2.0", "react-dom": "18.2.0", "react-instantsearch": "7.12.4" diff --git a/examples/react/getting-started/products.html b/examples/react/getting-started/products.html index 048d3823c4..31d0653055 100644 --- a/examples/react/getting-started/products.html +++ b/examples/react/getting-started/products.html @@ -9,15 +9,6 @@ - - - React InstantSearch — Getting started diff --git a/examples/react/getting-started/src/App.css b/examples/react/getting-started/src/App.css index 3dc8bd058a..003ae882ba 100644 --- a/examples/react/getting-started/src/App.css +++ b/examples/react/getting-started/src/App.css @@ -87,16 +87,22 @@ em { flex-shrink: 0; } -.ais-TrendingItems-list, -.ais-RelatedProducts-list { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1rem; -} - .ais-TrendingItems-item, .ais-RelatedProducts-item { + background: none !important; + align-items: stretch !important; + padding: 0 !important; + box-shadow: none !important; +} + +.ais-TrendingItems-item > div, +.ais-RelatedProducts-item > div { align-items: start; + background: #fff; + align-items: center; + padding: 1.5rem; + display: flex; + box-shadow: 0 0 0 1px #23263b0d, 0 1px 3px #23263b26; } .ais-TrendingItems-item img, @@ -113,3 +119,50 @@ em { height: 100%; justify-content: space-between; } + +.ais-TrendingItems-item h2, +.ais-RelatedProducts-item h2 { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.2; +} + +.ais-Carousel-item { + padding: 0.5rem !important; +} + +.ais-Carousel-list { + margin: -0.5rem !important; + grid-auto-columns: calc(22% - 0.5rem) !important; +} + +.ais-Carousel::before, +.ais-Carousel::after { + position: absolute; + top: 0; + bottom: 0; + width: 0.5rem; + display: block; + background: rgb(255, 255, 255); + content: ''; +} + +.ais-Carousel::before { + left: -0.5rem; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 1) 0%, + rgba(255, 255, 255, 0) 100% + ); +} + +.ais-Carousel::after { + right: -0.5rem; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 1) 100% + ); +} diff --git a/examples/react/getting-started/src/App.tsx b/examples/react/getting-started/src/App.tsx index 2d55e4b9cb..88cb053cd3 100644 --- a/examples/react/getting-started/src/App.tsx +++ b/examples/react/getting-started/src/App.tsx @@ -10,11 +10,13 @@ import { RefinementList, SearchBox, TrendingItems, + Carousel, } from 'react-instantsearch'; import { Panel } from './Panel'; import './App.css'; +import 'instantsearch.css/themes/satellite.css'; const searchClient = algoliasearch( 'latency', @@ -58,7 +60,11 @@ export function App() {
- +
@@ -92,12 +98,14 @@ function HitComponent({ hit }: { hit: HitType }) { function ItemComponent({ item }: { item: Hit }) { return ( - +
+ +
); } diff --git a/examples/react/getting-started/src/Product.tsx b/examples/react/getting-started/src/Product.tsx index b75eea1e07..9ef899eef5 100644 --- a/examples/react/getting-started/src/Product.tsx +++ b/examples/react/getting-started/src/Product.tsx @@ -6,9 +6,11 @@ import { Hits, InstantSearch, RelatedProducts, + Carousel, } from 'react-instantsearch'; import './App.css'; +import 'instantsearch.css/themes/satellite.css'; const searchClient = algoliasearch( 'latency', @@ -48,7 +50,8 @@ export function Product({ pid }: { pid: string }) { itemComponent={ItemComponent} emptyComponent={() => <>} objectIDs={[pid]} - limit={4} + limit={6} + layoutComponent={Carousel} /> @@ -76,12 +79,14 @@ function HitComponent({ hit }: { hit: HitType }) { function ItemComponent({ item }: { item: HitType }) { return ( - +
+ +
); } diff --git a/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx b/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx index 6c26bdbac1..f2cd13179f 100644 --- a/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx +++ b/packages/instantsearch-ui-components/src/components/FrequentlyBoughtTogether.tsx @@ -93,7 +93,6 @@ export function createFrequentlyBoughtTogetherComponent({ ( - userProps: RecommendLayoutProps< - TItem, - RecommendTranslations, - Partial - > + userProps: RecommendLayoutProps> ) { const { classNames = {}, diff --git a/packages/instantsearch-ui-components/src/types/Recommend.ts b/packages/instantsearch-ui-components/src/types/Recommend.ts index b38b4a0def..28a0e07c32 100644 --- a/packages/instantsearch-ui-components/src/types/Recommend.ts +++ b/packages/instantsearch-ui-components/src/types/Recommend.ts @@ -40,7 +40,6 @@ export type RecommendTranslations = { export type RecommendLayoutProps< TItem extends RecordWithObjectID, - TTranslations extends Record, TClassNames extends Record > = { classNames: TClassNames; @@ -51,7 +50,6 @@ export type RecommendLayoutProps< TComponentProps ) => JSX.Element; items: TItem[]; - translations: TTranslations; sendEvent: SendEventForHits; }; @@ -75,7 +73,6 @@ export type RecommendComponentProps< layout?: ( props: RecommendLayoutProps< RecordWithObjectID, - Required, Record > & TComponentProps diff --git a/packages/react-instantsearch/src/components/Carousel.tsx b/packages/react-instantsearch/src/components/Carousel.tsx new file mode 100644 index 0000000000..eff4979ca7 --- /dev/null +++ b/packages/react-instantsearch/src/components/Carousel.tsx @@ -0,0 +1,36 @@ +import { + createCarouselComponent, + generateCarouselId, +} from 'instantsearch-ui-components'; +import React, { createElement, Fragment, useRef } from 'react'; + +import type { + CarouselProps as CarouselUiProps, + Pragma, +} from 'instantsearch-ui-components'; + +const CarouselUiComponent = createCarouselComponent({ + createElement: createElement as Pragma, + Fragment, +}); + +export type CarouselProps> = Omit< + CarouselUiProps, + 'listRef' | 'nextButtonRef' | 'previousButtonRef' | 'carouselIdRef' +>; + +export function Carousel>( + props: CarouselProps +) { + const carouselRefs: Pick< + CarouselUiProps, + 'listRef' | 'nextButtonRef' | 'previousButtonRef' | 'carouselIdRef' + > = { + listRef: useRef(null), + nextButtonRef: useRef(null), + previousButtonRef: useRef(null), + carouselIdRef: useRef(generateCarouselId()), + }; + + return ; +} diff --git a/packages/react-instantsearch/src/components/__tests__/Carousel.test.tsx b/packages/react-instantsearch/src/components/__tests__/Carousel.test.tsx new file mode 100644 index 0000000000..a20b7129d4 --- /dev/null +++ b/packages/react-instantsearch/src/components/__tests__/Carousel.test.tsx @@ -0,0 +1,299 @@ +/** + * @jest-environment jsdom + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { Carousel } from '../Carousel'; + +describe('Carousel', () => { + test('renders with default props', () => { + const { container } = render( +

{item.objectID}

} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); + }); + + test('adds custom class names', () => { + const { container } = render( +

{item.objectID}

} + classNames={{ + root: 'ROOT', + list: 'LIST', + item: 'ITEM', + navigation: 'NAVIGATION', + navigationPrevious: 'NAVIGATION_PREVIOUS', + navigationNext: 'NAVIGATION_NEXT', + }} + /> + ); + + expect(container.querySelector('.ais-Carousel')).toHaveClass('ROOT'); + expect(container.querySelector('.ais-Carousel-list')).toHaveClass('LIST'); + expect(container.querySelector('.ais-Carousel-item')).toHaveClass('ITEM'); + expect(container.querySelector('.ais-Carousel-navigation')).toHaveClass( + 'NAVIGATION' + ); + expect( + container.querySelector('.ais-Carousel-navigation--previous') + ).toHaveClass('NAVIGATION_PREVIOUS'); + expect( + container.querySelector('.ais-Carousel-navigation--next') + ).toHaveClass('NAVIGATION_NEXT'); + }); + + test('adds custom icon components', () => { + const { container } = render( +

{item.objectID}

} + previousIconComponent={() =>

Previous

} + nextIconComponent={() =>

Next

} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); + }); + + test('adds custom translations', () => { + const { container } = render( +

{item.objectID}

} + translations={{ + nextButtonLabel: 'Next button label', + nextButtonTitle: 'Next button title', + previousButtonLabel: 'Previous button label', + previousButtonTitle: 'Previous button title', + listLabel: 'List label', + }} + /> + ); + + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); + }); +}); diff --git a/packages/react-instantsearch/src/components/index.ts b/packages/react-instantsearch/src/components/index.ts new file mode 100644 index 0000000000..c0ab19964d --- /dev/null +++ b/packages/react-instantsearch/src/components/index.ts @@ -0,0 +1 @@ +export * from './Carousel'; diff --git a/packages/react-instantsearch/src/index.ts b/packages/react-instantsearch/src/index.ts index 4cf3bff55c..f3a146c20b 100644 --- a/packages/react-instantsearch/src/index.ts +++ b/packages/react-instantsearch/src/index.ts @@ -1,2 +1,3 @@ export * from 'react-instantsearch-core'; export * from './widgets'; +export * from './components'; diff --git a/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx b/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx index 07b13c6623..8d35263b55 100644 --- a/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx +++ b/packages/react-instantsearch/src/widgets/FrequentlyBoughtTogether.tsx @@ -18,6 +18,7 @@ type UiProps = Pick< | 'itemComponent' | 'headerComponent' | 'emptyComponent' + | 'layout' | 'status' | 'sendEvent' >; @@ -30,6 +31,7 @@ export type FrequentlyBoughtTogetherProps = Omit< itemComponent?: FrequentlyBoughtTogetherPropsUiComponentProps['itemComponent']; headerComponent?: FrequentlyBoughtTogetherPropsUiComponentProps['headerComponent']; emptyComponent?: FrequentlyBoughtTogetherPropsUiComponentProps['emptyComponent']; + layoutComponent?: FrequentlyBoughtTogetherPropsUiComponentProps['layout']; }; const FrequentlyBoughtTogetherUiComponent = @@ -48,6 +50,7 @@ export function FrequentlyBoughtTogether({ itemComponent, headerComponent, emptyComponent, + layoutComponent, ...props }: FrequentlyBoughtTogetherProps) { const { status } = useInstantSearch(); @@ -63,11 +66,23 @@ export function FrequentlyBoughtTogether({ { $$widgetType: 'ais.frequentlyBoughtTogether' } ); + const layout: typeof layoutComponent = layoutComponent + ? (layoutProps) => + layoutComponent({ + ...layoutProps, + classNames: { + list: layoutProps.classNames.list, + item: layoutProps.classNames.item, + }, + }) + : undefined; + const uiProps: UiProps = { items, itemComponent, headerComponent, emptyComponent, + layout, status, sendEvent: () => {}, }; diff --git a/packages/react-instantsearch/src/widgets/LookingSimilar.tsx b/packages/react-instantsearch/src/widgets/LookingSimilar.tsx index aea86ec091..7655331b24 100644 --- a/packages/react-instantsearch/src/widgets/LookingSimilar.tsx +++ b/packages/react-instantsearch/src/widgets/LookingSimilar.tsx @@ -15,6 +15,7 @@ type UiProps = Pick< | 'itemComponent' | 'headerComponent' | 'emptyComponent' + | 'layout' | 'status' | 'sendEvent' >; @@ -27,6 +28,7 @@ export type LookingSimilarProps = Omit< itemComponent?: LookingSimilarPropsUiComponentProps['itemComponent']; headerComponent?: LookingSimilarPropsUiComponentProps['headerComponent']; emptyComponent?: LookingSimilarPropsUiComponentProps['emptyComponent']; + layoutComponent?: LookingSimilarPropsUiComponentProps['layout']; }; const LookingSimilarUiComponent = createLookingSimilarComponent({ @@ -45,6 +47,7 @@ export function LookingSimilar({ itemComponent, headerComponent, emptyComponent, + layoutComponent, ...props }: LookingSimilarProps) { const { status } = useInstantSearch(); @@ -61,11 +64,23 @@ export function LookingSimilar({ { $$widgetType: 'ais.lookingSimilar' } ); + const layout: typeof layoutComponent = layoutComponent + ? (layoutProps) => + layoutComponent({ + ...layoutProps, + classNames: { + list: layoutProps.classNames.list, + item: layoutProps.classNames.item, + }, + }) + : undefined; + const uiProps: UiProps = { items, itemComponent, headerComponent, emptyComponent, + layout, status, sendEvent: () => {}, }; diff --git a/packages/react-instantsearch/src/widgets/RelatedProducts.tsx b/packages/react-instantsearch/src/widgets/RelatedProducts.tsx index 89087fb0de..0c8cc33c04 100644 --- a/packages/react-instantsearch/src/widgets/RelatedProducts.tsx +++ b/packages/react-instantsearch/src/widgets/RelatedProducts.tsx @@ -15,6 +15,7 @@ type UiProps = Pick< | 'itemComponent' | 'headerComponent' | 'emptyComponent' + | 'layout' | 'status' | 'sendEvent' >; @@ -27,6 +28,7 @@ export type RelatedProductsProps = Omit< itemComponent?: RelatedProductsUiComponentProps['itemComponent']; headerComponent?: RelatedProductsUiComponentProps['headerComponent']; emptyComponent?: RelatedProductsUiComponentProps['emptyComponent']; + layoutComponent?: RelatedProductsUiComponentProps['layout']; }; const RelatedProductsUiComponent = createRelatedProductsComponent({ @@ -45,6 +47,7 @@ export function RelatedProducts({ itemComponent, headerComponent, emptyComponent, + layoutComponent, ...props }: RelatedProductsProps) { const { status } = useInstantSearch(); @@ -61,11 +64,23 @@ export function RelatedProducts({ { $$widgetType: 'ais.relatedProducts' } ); + const layout: typeof layoutComponent = layoutComponent + ? (layoutProps) => + layoutComponent({ + ...layoutProps, + classNames: { + list: layoutProps.classNames.list, + item: layoutProps.classNames.item, + }, + }) + : undefined; + const uiProps: UiProps = { items: items as Array>, itemComponent, headerComponent, emptyComponent, + layout, status, sendEvent: () => {}, }; diff --git a/packages/react-instantsearch/src/widgets/TrendingItems.tsx b/packages/react-instantsearch/src/widgets/TrendingItems.tsx index 1196c46fa8..1f245a336e 100644 --- a/packages/react-instantsearch/src/widgets/TrendingItems.tsx +++ b/packages/react-instantsearch/src/widgets/TrendingItems.tsx @@ -15,6 +15,7 @@ type UiProps = Pick< | 'itemComponent' | 'headerComponent' | 'emptyComponent' + | 'layout' | 'status' | 'sendEvent' >; @@ -27,6 +28,7 @@ export type TrendingItemsProps = Omit< itemComponent?: TrendingItemsUiComponentProps['itemComponent']; headerComponent?: TrendingItemsUiComponentProps['headerComponent']; emptyComponent?: TrendingItemsUiComponentProps['emptyComponent']; + layoutComponent?: TrendingItemsUiComponentProps['layout']; }; const TrendingItemsUiComponent = createTrendingItemsComponent({ @@ -46,6 +48,7 @@ export function TrendingItems({ itemComponent, headerComponent, emptyComponent, + layoutComponent, ...props }: TrendingItemsProps) { const facetParameters = @@ -65,11 +68,23 @@ export function TrendingItems({ { $$widgetType: 'ais.trendingItems' } ); + const layout: typeof layoutComponent = layoutComponent + ? (layoutProps) => + layoutComponent({ + ...layoutProps, + classNames: { + list: layoutProps.classNames.list, + item: layoutProps.classNames.item, + }, + }) + : undefined; + const uiProps: UiProps = { items: items as Array>, itemComponent, headerComponent, emptyComponent, + layout, status, sendEvent: () => {}, }; diff --git a/packages/react-instantsearch/src/widgets/__tests__/FrequentlyBoughtTogether.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/FrequentlyBoughtTogether.test.tsx index ab7c4f1fbb..a338839ff6 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/FrequentlyBoughtTogether.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/FrequentlyBoughtTogether.test.tsx @@ -5,8 +5,10 @@ import { createRecommendSearchClient } from '@instantsearch/mocks/fixtures'; import { InstantSearchTestWrapper } from '@instantsearch/testutils'; import { render, waitFor } from '@testing-library/react'; +import { cx } from 'instantsearch-ui-components'; import React from 'react'; +import { Carousel } from '../../components/Carousel'; import { FrequentlyBoughtTogether } from '../FrequentlyBoughtTogether'; describe('FrequentlyBoughtTogether', () => { @@ -81,4 +83,273 @@ describe('FrequentlyBoughtTogether', () => { expect(root).toHaveClass('MyFrequentlyBoughtTogether', 'ROOT'); expect(root).toHaveAttribute('aria-hidden', 'true'); }); + + test('renders custom layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + + ( +
    + {items.map((item) => ( +
  • +

    {item.objectID}

    +
  • + ))} +
+ )} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-FrequentlyBoughtTogether')) + .toMatchInlineSnapshot(` +
+

+ Frequently bought together +

+
    +
  • +

    + 1 +

    +
  • +
  • +

    + 2 +

    +
  • +
+
+ `); + }); + }); + + test('renders Carousel as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + +

{item.objectID}

} + layoutComponent={Carousel} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-FrequentlyBoughtTogether')) + .toMatchInlineSnapshot(` +
+

+ Frequently bought together +

+ +
+ `); + }); + }); + + test('renders Carousel with custom props as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + + const { container } = render( + +

{item.objectID}

} + layoutComponent={(props) => { + return ( +

Previous

} + nextIconComponent={() =>

Next

} + classNames={{ + root: 'ROOT', + list: cx('LIST', props.classNames.list), + item: cx('ITEM', props.classNames.item), + navigation: 'NAVIGATION', + navigationNext: 'NAVIGATION_NEXT', + navigationPrevious: 'NAVIGATION_PREVIOUS', + }} + translations={{ + nextButtonLabel: 'NEXT_BUTTON_LABEL', + nextButtonTitle: 'NEXT_BUTTON_TITLE', + previousButtonLabel: 'PREVIOUS_BUTTON_LABEL', + previousButtonTitle: 'PREVIOUS_BUTTON_TITLE', + listLabel: 'LIST_LABEL', + }} + /> + ); + }} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-FrequentlyBoughtTogether')) + .toMatchInlineSnapshot(` +
+

+ Frequently bought together +

+ +
+ `); + }); + }); }); diff --git a/packages/react-instantsearch/src/widgets/__tests__/LookingSimilar.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/LookingSimilar.test.tsx index 69ed826d3d..ccfe1c617e 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/LookingSimilar.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/LookingSimilar.test.tsx @@ -5,8 +5,10 @@ import { createRecommendSearchClient } from '@instantsearch/mocks/fixtures'; import { InstantSearchTestWrapper } from '@instantsearch/testutils'; import { render, waitFor } from '@testing-library/react'; +import { cx } from 'instantsearch-ui-components'; import React from 'react'; +import { Carousel } from '../../components/Carousel'; import { LookingSimilar } from '../LookingSimilar'; describe('LookingSimilar', () => { @@ -78,4 +80,273 @@ describe('LookingSimilar', () => { expect(root).toHaveClass('MyLookingSimilar', 'ROOT'); expect(root).toHaveAttribute('aria-hidden', 'true'); }); + + test('renders custom layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + + ( +
    + {items.map((item) => ( +
  • +

    {item.objectID}

    +
  • + ))} +
+ )} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-LookingSimilar')) + .toMatchInlineSnapshot(` +
+

+ Looking similar +

+
    +
  • +

    + 1 +

    +
  • +
  • +

    + 2 +

    +
  • +
+
+ `); + }); + }); + + test('renders Carousel as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + +

{item.objectID}

} + layoutComponent={Carousel} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-LookingSimilar')) + .toMatchInlineSnapshot(` +
+

+ Looking similar +

+ +
+ `); + }); + }); + + test('renders Carousel with custom props as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + + const { container } = render( + +

{item.objectID}

} + layoutComponent={(props) => { + return ( +

Previous

} + nextIconComponent={() =>

Next

} + classNames={{ + root: 'ROOT', + list: cx('LIST', props.classNames.list), + item: cx('ITEM', props.classNames.item), + navigation: 'NAVIGATION', + navigationNext: 'NAVIGATION_NEXT', + navigationPrevious: 'NAVIGATION_PREVIOUS', + }} + translations={{ + nextButtonLabel: 'NEXT_BUTTON_LABEL', + nextButtonTitle: 'NEXT_BUTTON_TITLE', + previousButtonLabel: 'PREVIOUS_BUTTON_LABEL', + previousButtonTitle: 'PREVIOUS_BUTTON_TITLE', + listLabel: 'LIST_LABEL', + }} + /> + ); + }} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-LookingSimilar')) + .toMatchInlineSnapshot(` +
+

+ Looking similar +

+ +
+ `); + }); + }); }); diff --git a/packages/react-instantsearch/src/widgets/__tests__/RelatedProducts.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/RelatedProducts.test.tsx index 5985893861..5662be440e 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/RelatedProducts.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/RelatedProducts.test.tsx @@ -5,8 +5,10 @@ import { createRecommendSearchClient } from '@instantsearch/mocks/fixtures'; import { InstantSearchTestWrapper } from '@instantsearch/testutils'; import { render, waitFor } from '@testing-library/react'; +import { cx } from 'instantsearch-ui-components'; import React from 'react'; +import { Carousel } from '../../components/Carousel'; import { RelatedProducts } from '../RelatedProducts'; describe('RelatedProducts', () => { @@ -81,4 +83,276 @@ describe('RelatedProducts', () => { expect(root).toHaveClass('MyRelatedProducts', 'ROOT'); expect(root).toHaveAttribute('aria-hidden', 'true'); }); + + test('renders with a custom layout', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + + const { container } = render( + + { + return ( +
    + {items.map((item) => ( +
  • +

    {item.objectID}

    +
  • + ))} +
+ ); + }} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-RelatedProducts')) + .toMatchInlineSnapshot(` +
+

+ Related products +

+
    +
  • +

    + 1 +

    +
  • +
  • +

    + 2 +

    +
  • +
+
+ `); + }); + }); + + test('renders Carousel as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + +

{item.objectID}

} + layoutComponent={Carousel} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-RelatedProducts')) + .toMatchInlineSnapshot(` +
+

+ Related products +

+ +
+ `); + }); + }); + + test('renders Carousel with custom props as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + + const { container } = render( + +

{item.objectID}

} + layoutComponent={(props) => { + return ( +

Previous

} + nextIconComponent={() =>

Next

} + classNames={{ + root: 'ROOT', + list: cx('LIST', props.classNames.list), + item: cx('ITEM', props.classNames.item), + navigation: 'NAVIGATION', + navigationNext: 'NAVIGATION_NEXT', + navigationPrevious: 'NAVIGATION_PREVIOUS', + }} + translations={{ + nextButtonLabel: 'NEXT_BUTTON_LABEL', + nextButtonTitle: 'NEXT_BUTTON_TITLE', + previousButtonLabel: 'PREVIOUS_BUTTON_LABEL', + previousButtonTitle: 'PREVIOUS_BUTTON_TITLE', + listLabel: 'LIST_LABEL', + }} + /> + ); + }} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-RelatedProducts')) + .toMatchInlineSnapshot(` +
+

+ Related products +

+ +
+ `); + }); + }); }); diff --git a/packages/react-instantsearch/src/widgets/__tests__/TrendingItems.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/TrendingItems.test.tsx index b101ec024a..ce6cb1c808 100644 --- a/packages/react-instantsearch/src/widgets/__tests__/TrendingItems.test.tsx +++ b/packages/react-instantsearch/src/widgets/__tests__/TrendingItems.test.tsx @@ -5,8 +5,10 @@ import { createRecommendSearchClient } from '@instantsearch/mocks/fixtures'; import { InstantSearchTestWrapper } from '@instantsearch/testutils'; import { render, waitFor } from '@testing-library/react'; +import { cx } from 'instantsearch-ui-components'; import React from 'react'; +import { Carousel } from '../../components/Carousel'; import { TrendingItems } from '../TrendingItems'; describe('TrendingItems', () => { @@ -77,4 +79,267 @@ describe('TrendingItems', () => { expect(root).toHaveClass('MyTrendingItems', 'ROOT'); expect(root).toHaveAttribute('aria-hidden', 'true'); }); + + test('renders custom layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + + ( +
    + {items.map((item) => ( +
  • +

    {item.objectID}

    +
  • + ))} +
+ )} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-TrendingItems')) + .toMatchInlineSnapshot(` +
+

+ Trending items +

+
    +
  • +

    + 1 +

    +
  • +
  • +

    + 2 +

    +
  • +
+
+ `); + }); + }); + + test('renders Carousel as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + +

{item.objectID}

} + layoutComponent={Carousel} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-TrendingItems')) + .toMatchInlineSnapshot(` +
+

+ Trending items +

+ +
+ `); + }); + }); + + test('renders Carousel with custom props as a layout component', async () => { + const client = createRecommendSearchClient({ + minimal: true, + }); + const { container } = render( + +

{item.objectID}

} + layoutComponent={(props) => ( +

Previous

} + nextIconComponent={() =>

Next

} + classNames={{ + root: 'ROOT', + list: cx('LIST', props.classNames.list), + item: cx('ITEM', props.classNames.item), + navigation: 'NAVIGATION', + navigationNext: 'NAVIGATION_NEXT', + navigationPrevious: 'NAVIGATION_PREVIOUS', + }} + translations={{ + nextButtonLabel: 'NEXT_BUTTON_LABEL', + nextButtonTitle: 'NEXT_BUTTON_TITLE', + previousButtonLabel: 'PREVIOUS_BUTTON_LABEL', + previousButtonTitle: 'PREVIOUS_BUTTON_TITLE', + listLabel: 'LIST_LABEL', + }} + /> + )} + /> +
+ ); + + await waitFor(() => { + expect(client.getRecommendations).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(container.querySelector('.ais-TrendingItems')) + .toMatchInlineSnapshot(` +
+

+ Trending items +

+ +
+ `); + }); + }); });