Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "117.25 kB"
"maxSize": "117.50 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
Expand All @@ -22,7 +22,7 @@
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "96.75 kB"
"maxSize": "97 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
14 changes: 8 additions & 6 deletions packages/instantsearch-ui-components/src/components/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import type {
SendEventForHits,
} from '../types';

export type HeaderComponentProps = {
canScrollLeft: boolean;
canScrollRight: boolean;
scrollLeft: () => void;
scrollRight: () => void;
};

export type CarouselProps<
TObject,
TComponentProps extends Record<string, unknown> = Record<string, unknown>
Expand All @@ -31,12 +38,7 @@ export type CarouselProps<
) => JSX.Element;
previousIconComponent?: () => JSX.Element;
nextIconComponent?: () => JSX.Element;
headerComponent?: (props: {
canScrollLeft: boolean;
canScrollRight: boolean;
scrollLeft: () => void;
scrollRight: () => void;
}) => JSX.Element;
headerComponent?: (props: HeaderComponentProps) => JSX.Element;
showNavigation?: boolean;
classNames?: Partial<CarouselClassNames>;
translations?: Partial<CarouselTranslations>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/** @jsx createElement */

import { createButtonComponent } from '../../Button';
import { createCarouselComponent, generateCarouselId } from '../../Carousel';
import { ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '../icons';

import type { RecordWithObjectID, Renderer } from '../../../types';
import type {
CarouselProps,
HeaderComponentProps as CarouselHeaderComponentProps,
} from '../../Carousel';
import type { ClientSideToolComponentProps } from '../types';
import type { SearchParameters } from 'algoliasearch-helper';

type SearchToolInput = {
query: string;
number_of_results?: number;
facet_filters?: string[][];
};

type HeaderProps = {
showViewAll: boolean;
canScrollLeft: boolean;
canScrollRight: boolean;
scrollLeft: () => void;
scrollRight: () => void;
nbHits?: number;
input?: SearchToolInput;
hitsPerPage?: number;
applyFilters: ClientSideToolComponentProps['applyFilters'];
getSearchPageURL?: (params: SearchParameters) => string;
onClose: () => void;
};

export type SearchIndexToolProps<THit extends RecordWithObjectID> = {
useMemo: <TType>(factory: () => TType, inputs: readonly unknown[]) => TType;
useRef: <TType>(initialValue: TType) => { current: TType };
useState: <TType>(
initialState: TType
) => [TType, (newState: TType) => unknown];
getSearchPageURL?: (params: SearchParameters) => string;
toolProps: ClientSideToolComponentProps;
itemComponent?: CarouselProps<THit>['itemComponent'];
headerComponent?: (props: HeaderProps) => JSX.Element;
headerProps: Pick<HeaderProps, 'showViewAll'>;
};

function createHeaderComponent({ createElement }: Renderer) {
const Button = createButtonComponent({ createElement });

return function HeaderComponent({
showViewAll,
canScrollLeft,
canScrollRight,
scrollLeft,
scrollRight,
nbHits,
input,
hitsPerPage,
applyFilters,
getSearchPageURL,
onClose,
}: HeaderProps) {
if ((hitsPerPage ?? 0) < 1) {
return null;
}

return (
<div className="ais-ChatToolSearchIndexCarouselHeader">
<div className="ais-ChatToolSearchIndexCarouselHeaderResults">
{nbHits && (
<div className="ais-ChatToolSearchIndexCarouselHeaderCount">
{hitsPerPage ?? 0} of {nbHits.toLocaleString()} result
{nbHits > 1 ? 's' : ''}
</div>
)}
{showViewAll && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (!input || !applyFilters) return;

const params = applyFilters({
query: input.query,
facetFilters: input.facet_filters,
});

if (
getSearchPageURL &&
new URL(getSearchPageURL(params)).pathname !==
window.location.pathname
) {
window.location.href = getSearchPageURL(params);
}

onClose();
}}
className="ais-ChatToolSearchIndexCarouselHeaderViewAll"
>
View all
<ArrowRightIcon createElement={createElement} />
</Button>
)}
</div>

{(hitsPerPage ?? 0) > 2 && (
<div className="ais-ChatToolSearchIndexCarouselHeaderScrollButtons">
<Button
variant="outline"
size="sm"
iconOnly
onClick={scrollLeft}
disabled={!canScrollLeft}
className="ais-ChatToolSearchIndexCarouselHeaderScrollButton"
>
<ChevronLeftIcon createElement={createElement} />
</Button>
<Button
variant="outline"
size="sm"
iconOnly
onClick={scrollRight}
disabled={!canScrollRight}
className="ais-ChatToolSearchIndexCarouselHeaderScrollButton"
>
<ChevronRightIcon createElement={createElement} />
</Button>
</div>
)}
</div>
);
};
}

export function createSearchIndexToolComponent<
TObject extends RecordWithObjectID
>({ createElement, Fragment }: Renderer) {
const DefaultHeader = createHeaderComponent({ createElement, Fragment });
const Carousel = createCarouselComponent({ createElement, Fragment });

return function SearchIndexTool(userProps: SearchIndexToolProps<TObject>) {
const {
useMemo,
useRef,
useState,
itemComponent: ItemComponent,
headerComponent: HeaderComponent,
getSearchPageURL,
toolProps: { message, applyFilters, onClose },
headerProps: { showViewAll },
} = userProps;

const input = message?.input as
| {
query: string;
number_of_results?: number;
}
| undefined;

const output = message?.output as
| {
hits?: Array<RecordWithObjectID<TObject>>;
nbHits?: number;
}
| undefined;

const items = output?.hits || [];

const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);

const carouselRefs: Pick<
CarouselProps<TObject>,
| 'listRef'
| 'nextButtonRef'
| 'previousButtonRef'
| 'carouselIdRef'
| 'canScrollLeft'
| 'canScrollRight'
| 'setCanScrollLeft'
| 'setCanScrollRight'
> = {
listRef: useRef(null),
nextButtonRef: useRef(null),
previousButtonRef: useRef(null),
carouselIdRef: useRef(generateCarouselId()),
canScrollLeft,
canScrollRight,
setCanScrollLeft,
setCanScrollRight,
};

const MemoedHeaderComponent = useMemo(() => {
if (HeaderComponent) {
return (props: CarouselHeaderComponentProps) => (
<HeaderComponent
showViewAll={showViewAll}
nbHits={output?.nbHits}
input={input}
hitsPerPage={items.length}
applyFilters={applyFilters}
getSearchPageURL={getSearchPageURL}
onClose={onClose}
{...props}
/>
);
}

return (props: CarouselHeaderComponentProps) => (
<DefaultHeader
showViewAll={showViewAll}
nbHits={output?.nbHits}
input={input}
hitsPerPage={items.length}
applyFilters={applyFilters}
getSearchPageURL={getSearchPageURL}
onClose={onClose}
{...props}
/>
);
}, [
showViewAll,
HeaderComponent,
output?.nbHits,
input,
items.length,
applyFilters,
getSearchPageURL,
onClose,
]);

return (
<Carousel
{...carouselRefs}
items={items}
itemComponent={ItemComponent}
headerComponent={MemoedHeaderComponent}
showNavigation={false}
sendEvent={() => {}}
/>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './chat/ChatMessageError';
export * from './chat/ChatPrompt';
export * from './chat/ChatPromptSuggestions';
export * from './chat/ChatToggleButton';
export * from './chat/tools/SearchIndexTool';
export * from './chat/icons';
export * from './chat/types';
export * from './FrequentlyBoughtTogether';
Expand Down
Loading