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 (
-
-
-
-
{item.name}
-
- See product
-
+
);
}
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 (
-
-
-
-
{item.name}
-
- See product
-
+
);
}
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(`
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+
+ 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(`
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+
+ 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(`
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+});
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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
+
+ 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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
});
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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
+
+ 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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
});
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(`
+
+ `);
+ });
+ });
+
+ 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(`
+
+ `);
+ });
+ });
+
+ 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(`
+
+ `);
+ });
+ });
});
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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
+
+ 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
+
+
+
+
+ -
+
+ 1
+
+
+ -
+
+ 2
+
+
+
+
+
+
+ `);
+ });
+ });
});