feat(v5): Add new footerAction prop (#2952)
* feat(v5): Add new footerAction prop * Resolve PR comments
This commit is contained in:
parent
4e44b558e1
commit
b28bc84904
20 changed files with 428 additions and 99 deletions
14
.changeset/sixty-lights-try.md
Normal file
14
.changeset/sixty-lights-try.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
"@docsearch/react": patch
|
||||
"@docsearch/css": patch
|
||||
"@docsearch/js": patch
|
||||
---
|
||||
|
||||
feat(v5): add customizable footer action
|
||||
|
||||
- New `footerAction` prop renders a custom action in the modal footer,
|
||||
before the Algolia logo, inside `.DocSearch-Footer-Action`
|
||||
- `@docsearch/js` supports `footerAction` via template patterns (html helper,
|
||||
JSX, or function-based)
|
||||
- Fixes typing differences between `@docsearch/react` and `@docsearch/js`
|
||||
- Restyle the footer with a `.DocSearch-Footer-Actions` wrapper
|
||||
22
bun.lock
22
bun.lock
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
"adapters/docusaurus-theme-search-algolia": {
|
||||
"name": "@docsearch/docusaurus-adapter",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-core": "1.19.2",
|
||||
"@docsearch/core": "workspace:*",
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
},
|
||||
"packages/docsearch-core": {
|
||||
"name": "@docsearch/core",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
},
|
||||
"packages/docsearch-css": {
|
||||
"name": "@docsearch/css",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"devDependencies": {
|
||||
"browserslist": "4.28.2",
|
||||
"lightningcss": "1.32.0",
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
},
|
||||
"packages/docsearch-js": {
|
||||
"name": "@docsearch/js",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@docsearch/core": "4.6.0",
|
||||
"@docsearch/react": "4.6.0",
|
||||
|
|
@ -160,7 +160,7 @@
|
|||
},
|
||||
"packages/docsearch-modal": {
|
||||
"name": "@docsearch/modal",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@docsearch/core": "4.6.0",
|
||||
"@docsearch/react": "4.6.0",
|
||||
|
|
@ -185,13 +185,13 @@
|
|||
},
|
||||
"packages/docsearch-react": {
|
||||
"name": "@docsearch/react",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^2.0.30",
|
||||
"@algolia/autocomplete-core": "1.19.2",
|
||||
"@base-ui/react": "^1.5.0",
|
||||
"@docsearch/core": "5.0.0-beta.1",
|
||||
"@docsearch/css": "5.0.0-beta.1",
|
||||
"@docsearch/core": "5.0.0-beta.2",
|
||||
"@docsearch/css": "5.0.0-beta.2",
|
||||
"ai": "^5.0.30",
|
||||
"algoliasearch": "^5.28.0",
|
||||
"marked": "^16.3.0",
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@algolia/autocomplete-core": "1.19.2",
|
||||
"@docsearch/core": "5.0.0-beta.1",
|
||||
"@docsearch/core": "5.0.0-beta.2",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
"preact": "11.0.0-beta.0",
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
},
|
||||
"packages/docsearch-sidepanel": {
|
||||
"name": "@docsearch/sidepanel",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@docsearch/core": "4.6.0",
|
||||
"@docsearch/css": "4.6.0",
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
},
|
||||
"packages/docsearch-sidepanel-js": {
|
||||
"name": "@docsearch/sidepanel-js",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-beta.2",
|
||||
"dependencies": {
|
||||
"@docsearch/core": "4.6.0",
|
||||
"@docsearch/react": "4.6.0",
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/css": "5.0.0-beta.2",
|
||||
"@docsearch/js": "5.0.0-beta.2",
|
||||
"@docsearch/sidepanel-js": "5.0.0-beta.2"
|
||||
"@docsearch/css": "workspace:*",
|
||||
"@docsearch/js": "workspace:*",
|
||||
"@docsearch/sidepanel-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.7"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import docsearch, {
|
||||
type DocSearchInstance,
|
||||
type TemplateHelpers,
|
||||
} from '@docsearch/js';
|
||||
import docsearch, { type DocSearchInstance } from '@docsearch/js';
|
||||
import sidepanel, { type SidepanelInstance } from '@docsearch/sidepanel-js';
|
||||
|
||||
import './app.css';
|
||||
|
|
@ -91,10 +88,7 @@ docsearchInstance = docsearch({
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('[demo-js] docsearch onClose()');
|
||||
},
|
||||
resultsFooterComponent: ({ state }, helpers?: TemplateHelpers) => {
|
||||
const { html } = helpers || {};
|
||||
if (!html) return null;
|
||||
|
||||
resultsFooterComponent: ({ state }, { html }) => {
|
||||
return html`
|
||||
<div class="DocSearch-HitsFooter">
|
||||
<a href="https://docsearch.algolia.com/apply" target="_blank">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ const customTools: ToolCalls = {
|
|||
},
|
||||
};
|
||||
|
||||
const FooterAction = (): JSX.Element => {
|
||||
return <span>Bonjour</span>;
|
||||
};
|
||||
|
||||
export default function BasicAskAI({
|
||||
theme,
|
||||
}: {
|
||||
|
|
@ -50,6 +54,7 @@ export default function BasicAskAI({
|
|||
translations={{ button: { buttonText: 'Search with Ask AI' } }}
|
||||
theme={theme}
|
||||
resultBadgeKey="type"
|
||||
footerAction={<FooterAction />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1034,7 +1034,6 @@
|
|||
border-block-start: 1px solid var(--docsearch-subtle-color);
|
||||
border-radius: 0 0 var(--docsearch-modal-radius) var(--docsearch-modal-radius);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-shrink: 0;
|
||||
height: var(--docsearch-footer-height);
|
||||
justify-content: space-between;
|
||||
|
|
@ -1091,6 +1090,23 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.DocSearch-Footer-Actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (width > 768px) {
|
||||
justify-content: unset;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:not(:has(.DocSearch-Footer-Action)) .DocSearch-Logo {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide element accessibly, so that it is still accessible to
|
||||
assistive tech users */
|
||||
.DocSearch-VisuallyHiddenForAccessibility {
|
||||
|
|
|
|||
103
packages/docsearch-js/src/__tests__/createDocSearch.test.tsx
Normal file
103
packages/docsearch-js/src/__tests__/createDocSearch.test.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { createElement, type JSX } from 'preact';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { createDocSearch, type DocSearchInstance } from '../createDocSearch';
|
||||
import type { FooterActionFn as KeywordFooterActionFn } from '../docsearch';
|
||||
import type { FooterActionFn as AskAiFooterActionFn } from '../index';
|
||||
|
||||
interface TestComponentProps {
|
||||
footerAction?: JSX.Element | null;
|
||||
}
|
||||
|
||||
function TestComponent({ footerAction }: TestComponentProps): JSX.Element {
|
||||
return createElement('div', null, footerAction);
|
||||
}
|
||||
|
||||
const askAiFooterAction: AskAiFooterActionFn = (_props, { html }) =>
|
||||
html`<button type="button">Ask AI footer action</button>`;
|
||||
|
||||
const keywordFooterAction: KeywordFooterActionFn = (_props, { html }) =>
|
||||
html`<button type="button">Keyword footer action</button>`;
|
||||
|
||||
describe('createDocSearch', () => {
|
||||
let instance: DocSearchInstance | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
instance?.destroy();
|
||||
instance = undefined;
|
||||
});
|
||||
|
||||
it('adapts footerAction templates for the rendered component', () => {
|
||||
const container = document.createElement('div');
|
||||
const footerAction = (): JSX.Element =>
|
||||
createElement('button', { type: 'button' }, 'Footer action');
|
||||
const docsearch = createDocSearch<TestComponentProps>(
|
||||
TestComponent,
|
||||
'test'
|
||||
);
|
||||
|
||||
instance = docsearch({ container, footerAction });
|
||||
|
||||
expect(container.textContent).toContain('Footer action');
|
||||
});
|
||||
|
||||
it('provides the html helper to footerAction templates', () => {
|
||||
const container = document.createElement('div');
|
||||
const docsearch = createDocSearch<TestComponentProps>(
|
||||
TestComponent,
|
||||
'test'
|
||||
);
|
||||
|
||||
instance = docsearch({ container, footerAction: askAiFooterAction });
|
||||
|
||||
expect(container.textContent).toContain('Ask AI footer action');
|
||||
});
|
||||
|
||||
it('accepts footerAction templates from the keyword-only entry point', () => {
|
||||
const container = document.createElement('div');
|
||||
const docsearch = createDocSearch<TestComponentProps>(
|
||||
TestComponent,
|
||||
'test'
|
||||
);
|
||||
|
||||
instance = docsearch({ container, footerAction: keywordFooterAction });
|
||||
|
||||
expect(container.textContent).toContain('Keyword footer action');
|
||||
});
|
||||
|
||||
it('renders string and component footerAction template returns', () => {
|
||||
const container = document.createElement('div');
|
||||
const docsearch = createDocSearch<TestComponentProps>(
|
||||
TestComponent,
|
||||
'test'
|
||||
);
|
||||
|
||||
instance = docsearch({
|
||||
container,
|
||||
footerAction: () => () =>
|
||||
createElement('button', { type: 'button' }, 'Component footer action'),
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain('Component footer action');
|
||||
|
||||
instance.destroy();
|
||||
instance = docsearch({
|
||||
container,
|
||||
footerAction: () => 'Text footer action',
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain('Text footer action');
|
||||
});
|
||||
|
||||
it('supports footerAction templates that return null', () => {
|
||||
const container = document.createElement('div');
|
||||
const docsearch = createDocSearch<TestComponentProps>(
|
||||
TestComponent,
|
||||
'test'
|
||||
);
|
||||
|
||||
instance = docsearch({ container, footerAction: () => null });
|
||||
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
import type { DocSearchRef, InitialAskAiMessage } from '@docsearch/core';
|
||||
import type {
|
||||
ResultsFooterComponentProps,
|
||||
HitComponentProps,
|
||||
} from '@docsearch/react';
|
||||
import htm from 'htm';
|
||||
import type { ComponentType, JSX, Attributes } from 'preact';
|
||||
import {
|
||||
|
|
@ -25,10 +29,64 @@ export interface DocSearchCallbacks {
|
|||
interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
|
||||
}
|
||||
|
||||
export type TemplateHelpers = { html: typeof html };
|
||||
|
||||
// Defines the public facing interface for each "template" function
|
||||
type TemplateFnReturnType = JSX.Element | string | (() => JSX.Element) | null;
|
||||
|
||||
export type HitComponentFn = (
|
||||
props: HitComponentProps,
|
||||
helpers: TemplateHelpers
|
||||
) => TemplateFnReturnType;
|
||||
|
||||
export type ResultsFooterComponentFn = (
|
||||
props: ResultsFooterComponentProps,
|
||||
helpers: TemplateHelpers
|
||||
) => TemplateFnReturnType;
|
||||
|
||||
export type FooterActionFn = (
|
||||
props: never,
|
||||
helpers: TemplateHelpers
|
||||
) => TemplateFnReturnType;
|
||||
|
||||
export type DocSearchProps<TProps> = DocSearchCallbacks &
|
||||
Omit<TProps, 'onSidepanelClose' | 'onSidepanelOpen'> & {
|
||||
Omit<
|
||||
TProps,
|
||||
| 'onSidepanelClose'
|
||||
| 'onSidepanelOpen'
|
||||
| 'hitComponent'
|
||||
| 'resultsFooterComponent'
|
||||
| 'footerAction'
|
||||
> & {
|
||||
container: HTMLElement | string;
|
||||
environment?: typeof window;
|
||||
/**
|
||||
* Custom component to render an individual hit. Supports template patterns:
|
||||
*
|
||||
* - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
|
||||
* - JSX templates: (props) => <div>...</div>
|
||||
* - Function-based templates: (props) => string | JSX.Element | Function.
|
||||
*/
|
||||
hitComponent?: HitComponentFn;
|
||||
/**
|
||||
* Custom component rendered at the bottom of the results panel. Supports
|
||||
* template patterns:
|
||||
*
|
||||
* - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
|
||||
* - JSX templates: (props) => <div>...</div>
|
||||
* - Function-based templates: (props) => string | JSX.Element | Function.
|
||||
*/
|
||||
resultsFooterComponent?: ResultsFooterComponentFn;
|
||||
/**
|
||||
* A custom action that can be rendered in the Modal's footer before the
|
||||
* Algolia logo. The component will be rendered as a child of `<div
|
||||
* className="DocSearch-Footer-Action" />`. Supports template patterns:
|
||||
*
|
||||
* - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
|
||||
* - JSX templates: (props) => <div>...</div>
|
||||
* - Function-based templates: (props) => string | JSX.Element | Function.
|
||||
*/
|
||||
footerAction?: FooterActionFn;
|
||||
};
|
||||
|
||||
function getHTMLElement(
|
||||
|
|
@ -51,14 +109,12 @@ const html = htm.bind(createElement) as unknown as (
|
|||
...values: unknown[]
|
||||
) => JSX.Element;
|
||||
|
||||
export type TemplateHelpers = Record<string, unknown> & { html: typeof html };
|
||||
|
||||
function createTemplateFunction<
|
||||
P extends Record<string, unknown>,
|
||||
R = JSX.Element | string | (() => JSX.Element),
|
||||
P = Record<string, unknown>,
|
||||
R = TemplateFnReturnType,
|
||||
>(
|
||||
original: ((props: P, helpers?: TemplateHelpers) => R) | undefined
|
||||
): ((props: P) => JSX.Element) | undefined {
|
||||
original: ((props: P, helpers: TemplateHelpers) => R) | undefined
|
||||
): ((props: P) => JSX.Element | null) | undefined {
|
||||
if (!original) return undefined;
|
||||
|
||||
return (props: P) => {
|
||||
|
|
@ -73,15 +129,10 @@ function createTemplateFunction<
|
|||
}
|
||||
|
||||
interface ComponentProps {
|
||||
hitComponent?: (
|
||||
props: Record<string, unknown>,
|
||||
helpers?: TemplateHelpers
|
||||
) => JSX.Element;
|
||||
resultsFooterComponent?: (
|
||||
props: Record<string, unknown>,
|
||||
helpers?: TemplateHelpers
|
||||
) => JSX.Element | null;
|
||||
hitComponent?: HitComponentFn;
|
||||
resultsFooterComponent?: ResultsFooterComponentFn;
|
||||
transformSearchClient?: (searchClient: unknown) => unknown;
|
||||
footerAction?: FooterActionFn;
|
||||
}
|
||||
|
||||
export function createDocSearch<TComponentProps, TInputProps = TComponentProps>(
|
||||
|
|
@ -96,6 +147,7 @@ export function createDocSearch<TComponentProps, TInputProps = TComponentProps>(
|
|||
transformSearchClient,
|
||||
hitComponent,
|
||||
resultsFooterComponent,
|
||||
footerAction,
|
||||
...rest
|
||||
} = input;
|
||||
const containerElement = getHTMLElement(
|
||||
|
|
@ -105,11 +157,19 @@ export function createDocSearch<TComponentProps, TInputProps = TComponentProps>(
|
|||
const ref = createRef<DocSearchRef>();
|
||||
let isReady = false;
|
||||
|
||||
const FooterAction = createTemplateFunction(footerAction);
|
||||
|
||||
const props: TComponentProps = {
|
||||
...rest,
|
||||
ref,
|
||||
hitComponent: createTemplateFunction(hitComponent),
|
||||
resultsFooterComponent: createTemplateFunction(resultsFooterComponent),
|
||||
hitComponent: createTemplateFunction<HitComponentProps>(hitComponent),
|
||||
resultsFooterComponent:
|
||||
createTemplateFunction<ResultsFooterComponentProps>(
|
||||
resultsFooterComponent
|
||||
),
|
||||
footerAction: FooterAction
|
||||
? createElement(FooterAction, null)
|
||||
: undefined,
|
||||
transformSearchClient: (searchClient: unknown): unknown => {
|
||||
if (
|
||||
typeof searchClient === 'object' &&
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
export type {
|
||||
HitComponentFn,
|
||||
ResultsFooterComponentFn,
|
||||
FooterActionFn,
|
||||
TemplateHelpers,
|
||||
} from './createDocSearch';
|
||||
export { docsearch as default } from './docsearchComponent';
|
||||
export type {
|
||||
DocSearchCallbacks,
|
||||
DocSearchInstance,
|
||||
DocSearchProps,
|
||||
TemplateHelpers,
|
||||
} from './docsearchComponent';
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@ import {
|
|||
type DocSearchProps as CreateDocSearchProps,
|
||||
} from './createDocSearch';
|
||||
|
||||
export type {
|
||||
DocSearchCallbacks,
|
||||
DocSearchInstance,
|
||||
TemplateHelpers,
|
||||
} from './createDocSearch';
|
||||
export type { DocSearchCallbacks, DocSearchInstance } from './createDocSearch';
|
||||
export type DocSearchAIProps = CreateDocSearchProps<DocSearchComponentProps>;
|
||||
|
||||
export const docsearchAi: (allProps: DocSearchAIProps) => DocSearchInstance =
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import {
|
|||
type DocSearchProps as CreateDocSearchProps,
|
||||
} from './createDocSearch';
|
||||
|
||||
export type {
|
||||
DocSearchCallbacks,
|
||||
DocSearchInstance,
|
||||
TemplateHelpers,
|
||||
} from './createDocSearch';
|
||||
export type { DocSearchCallbacks, DocSearchInstance } from './createDocSearch';
|
||||
export type DocSearchProps = CreateDocSearchProps<DocSearchComponentProps>;
|
||||
|
||||
export const docsearch: (allProps: DocSearchProps) => DocSearchInstance =
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
export type {
|
||||
HitComponentFn,
|
||||
ResultsFooterComponentFn,
|
||||
FooterActionFn,
|
||||
TemplateHelpers,
|
||||
} from './createDocSearch';
|
||||
export { docsearchAi as default } from './docsearchAi';
|
||||
export type {
|
||||
DocSearchAIProps as DocSearchProps,
|
||||
DocSearchInstance,
|
||||
DocSearchCallbacks,
|
||||
TemplateHelpers,
|
||||
} from './docsearchAi';
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ export interface DocSearchFacet {
|
|||
label?: string;
|
||||
}
|
||||
|
||||
export interface HitComponentProps {
|
||||
hit: InternalDocSearchHit | StoredDocSearchHit;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ResultsFooterComponentProps {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
}
|
||||
|
||||
export interface DocSearchProps {
|
||||
/** Algolia application id used by the search client. */
|
||||
appId: string;
|
||||
|
|
@ -69,38 +78,18 @@ export interface DocSearchProps {
|
|||
maxResultsPerGroup?: number;
|
||||
/** Hook to post-process hits before rendering. */
|
||||
transformItems?: (items: DocSearchHit[]) => DocSearchHit[];
|
||||
/**
|
||||
* Custom component to render an individual hit. Supports template patterns:
|
||||
*
|
||||
* - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
|
||||
* - JSX templates: (props) => <div>...</div>
|
||||
* - Function-based templates: (props) => string | JSX.Element | Function.
|
||||
*/
|
||||
hitComponent?: (
|
||||
props: {
|
||||
hit: InternalDocSearchHit | StoredDocSearchHit;
|
||||
children: React.ReactNode;
|
||||
},
|
||||
helpers?: {
|
||||
html: (template: TemplateStringsArray, ...values: any[]) => any;
|
||||
}
|
||||
) => JSX.Element;
|
||||
/**
|
||||
* Custom component rendered at the bottom of the results panel. Supports
|
||||
* template patterns:
|
||||
*
|
||||
* - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
|
||||
* - JSX templates: (props) => <div>...</div>
|
||||
* - Function-based templates: (props) => string | JSX.Element | Function.
|
||||
*/
|
||||
/** Custom component to render an individual hit. */
|
||||
hitComponent?: (props: HitComponentProps) => JSX.Element;
|
||||
/** Custom component rendered at the bottom of the results panel. */
|
||||
resultsFooterComponent?: (
|
||||
props: {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
},
|
||||
helpers?: {
|
||||
html: (template: TemplateStringsArray, ...values: any[]) => any;
|
||||
}
|
||||
props: ResultsFooterComponentProps
|
||||
) => JSX.Element | null;
|
||||
/**
|
||||
* A custom action that can be rendered in the Modal's footer before the
|
||||
* Algolia logo. The component will be rendered as a child of `<div
|
||||
* className="DocSearch-Footer-Action" />`.
|
||||
*/
|
||||
footerAction?: React.ReactNode;
|
||||
/** Hook to wrap or modify the algolia search client. */
|
||||
transformSearchClient?: (
|
||||
searchClient: DocSearchTransformClient
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export function DocSearchAskAiModal({
|
|||
indices,
|
||||
facets,
|
||||
isHybridModeSupported = false,
|
||||
footerAction,
|
||||
...props
|
||||
}: DocSearchAskAiModalProps): JSX.Element {
|
||||
const {
|
||||
|
|
@ -645,6 +646,7 @@ export function DocSearchAskAiModal({
|
|||
<Footer
|
||||
translations={footerTranslations}
|
||||
isAskAiActive={isAskAiActive}
|
||||
footerAction={footerAction}
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function DocSearchModal({
|
|||
recentSearchesWithFavoritesLimit = 4,
|
||||
indices,
|
||||
facets,
|
||||
footerAction,
|
||||
...props
|
||||
}: DocSearchModalProps): JSX.Element {
|
||||
const {
|
||||
|
|
@ -296,7 +297,9 @@ export function DocSearchModal({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
footer={<Footer translations={footerTranslations} />}
|
||||
footer={
|
||||
<Footer translations={footerTranslations} footerAction={footerAction} />
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type FooterTranslations = Partial<{
|
|||
type FooterProps = Partial<{
|
||||
translations: FooterTranslations;
|
||||
isAskAiActive: boolean;
|
||||
footerAction: React.ReactNode;
|
||||
}>;
|
||||
|
||||
interface CommandIconProps {
|
||||
|
|
@ -50,6 +51,7 @@ function CommandIcon(props: CommandIconProps): JSX.Element {
|
|||
export function Footer({
|
||||
translations = {},
|
||||
isAskAiActive = false,
|
||||
footerAction,
|
||||
}: FooterProps): JSX.Element {
|
||||
const {
|
||||
selectText = 'Select',
|
||||
|
|
@ -64,11 +66,13 @@ export function Footer({
|
|||
poweredByText = 'Powered by',
|
||||
} = translations;
|
||||
|
||||
const hasFooterAction =
|
||||
footerAction !== null &&
|
||||
footerAction !== undefined &&
|
||||
typeof footerAction !== 'boolean';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="DocSearch-Logo">
|
||||
<AlgoliaLogo translations={{ poweredByText }} />
|
||||
</div>
|
||||
<ul className="DocSearch-Commands">
|
||||
<li>
|
||||
<kbd className="DocSearch-Commands-Key">
|
||||
|
|
@ -105,6 +109,14 @@ export function Footer({
|
|||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="DocSearch-Footer-Actions">
|
||||
{hasFooterAction && (
|
||||
<div className="DocSearch-Footer-Action">{footerAction}</div>
|
||||
)}
|
||||
<div className="DocSearch-Logo">
|
||||
<AlgoliaLogo translations={{ poweredByText }} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ function DocSearchAI(props: Partial<DocSearchAIProps>): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
function FooterAction(): JSX.Element {
|
||||
return <button type="button">Footer action</button>;
|
||||
}
|
||||
|
||||
function EmptyFooterAction(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// mock empty response
|
||||
function noResultSearch(_queries: any, _requestOptions?: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -96,6 +104,80 @@ describe('api', () => {
|
|||
expect(document.querySelector(docSearchSelector)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('footerAction', () => {
|
||||
it('renders the action in the standard modal', async () => {
|
||||
render(<DocSearch footerAction={<FooterAction />} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'Footer action' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render an empty action', async () => {
|
||||
render(<DocSearch footerAction={<EmptyFooterAction />} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Footer action' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render an action wrapper when no action is provided', async () => {
|
||||
render(<DocSearch />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(
|
||||
document.querySelector('.DocSearch-Footer-Action')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render an action wrapper for a boolean action', async () => {
|
||||
render(<DocSearch footerAction={false} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(
|
||||
document.querySelector('.DocSearch-Footer-Action')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders numeric footer actions', async () => {
|
||||
render(<DocSearch footerAction={0} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(document.querySelector('.DocSearch-Footer-Action')).toHaveTextContent(
|
||||
'0'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the action in the Ask AI modal', async () => {
|
||||
render(<DocSearchAI footerAction={<FooterAction />} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(await screen.findByText('Search'));
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'Footer action' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('translations', () => {
|
||||
it('overrides the default DocSearchButton text', () => {
|
||||
render(
|
||||
|
|
|
|||
|
|
@ -176,16 +176,22 @@ Function that transforms hits before grouping and rendering. Defaults to the ide
|
|||
|
||||
### `hitComponent`
|
||||
|
||||
> `type: template function` | **optional**
|
||||
> `type: ({ hit, children }, { html }) => JSX.Element` | **optional**
|
||||
|
||||
Template for a result link. Defaults to the built-in hit template. See [Templates](#templates).
|
||||
|
||||
### `resultsFooterComponent`
|
||||
|
||||
> `type: template function` | **optional**
|
||||
> `type: ({ state }, { html }) => JSX.Element | null` | **optional**
|
||||
|
||||
Template below the result collections. There's no default. See [Templates](#templates).
|
||||
|
||||
### `footerAction`
|
||||
|
||||
> `type: (_, { html }) => JSX.Element | null` | **optional**
|
||||
|
||||
Optional function to render a custom action in the Modal's footer. It will be rendered in line with the Algolia "powered by" logo. See [Templates](#templates).
|
||||
|
||||
### `transformSearchClient`
|
||||
|
||||
> `type: (client) => client` | **optional**
|
||||
|
|
@ -290,16 +296,26 @@ Set `translations.modal.resultsScreen.resultBadgeLabelText` to describe the badg
|
|||
|
||||
### Templates
|
||||
|
||||
`hitComponent` receives `{ hit, children }`. `resultsFooterComponent` receives `{ state }`. JavaScript templates can return a Preact element, a string, or a component function. The optional second argument provides an `html` tagged-template helper.
|
||||
- `hitComponent` receives `{ hit, children }`.
|
||||
- `resultsFooterComponent` receives `{ state }`.
|
||||
- `footerAction` currently does not receive any extra props.
|
||||
|
||||
JavaScript templates can return a Preact element, a string, or a component function. The optional second argument provides an `html` tagged-template helper.
|
||||
|
||||
```js title="docsearch-options.js"
|
||||
hitComponent({ hit, children }, { html }) {
|
||||
return html`<a href=${hit.url} data-result-type=${hit.type}>${children}</a>`;
|
||||
}
|
||||
docsearch({
|
||||
hitComponent({ hit, children }, { html }) {
|
||||
return html`<a href=${hit.url} data-result-type=${hit.type}>${children}</a>`;
|
||||
},
|
||||
resultsFooterComponent({ state }, { html }) {
|
||||
return html`<p>No results found for query: ${state.query}</p>`;
|
||||
},
|
||||
footerAction(_, { html }) {
|
||||
return html`<a href="https://algolia.com">Our other project</a>`;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
String returns render as text, not HTML.
|
||||
|
||||
## Ask AI options
|
||||
|
||||
Ask AI is available from the default `@docsearch/js` entry. Its `askAi` option is required by that entry's `DocSearchProps` type.
|
||||
|
|
|
|||
|
|
@ -121,16 +121,22 @@ Receives the modal portal. Defaults to `document.body`.
|
|||
|
||||
#### `hitComponent`
|
||||
|
||||
> `type: (props, helpers?) => JSX.Element` | **optional**
|
||||
> `type: (props) => JSX.Element` | **optional**
|
||||
|
||||
Renders an individual result. Defaults to the built-in hit component.
|
||||
|
||||
#### `resultsFooterComponent`
|
||||
|
||||
> `type: (props, helpers?) => JSX.Element | null` | **optional**
|
||||
> `type: (props) => JSX.Element | null` | **optional**
|
||||
|
||||
Renders content below the results. Defaults to `null`.
|
||||
|
||||
#### `footerAction`
|
||||
|
||||
> `type: React.ReactNode` | **optional**
|
||||
|
||||
Renders a custom element in the modal footer, before the "Powered by Algolia" logo. Passing `null` renders nothing.
|
||||
|
||||
#### `disableUserPersonalization`
|
||||
|
||||
> `type: boolean` | **optional**
|
||||
|
|
|
|||
|
|
@ -95,12 +95,12 @@ Transforms hits before DocSearch groups and renders them. Defaults to the identi
|
|||
|
||||
### `hitComponent`
|
||||
|
||||
> `type: React.ComponentType<HitProps>` | **optional**
|
||||
> `type: ({ hit, children }: HitComponentProps) => JSX.Element` | **optional**
|
||||
|
||||
Renders one result link. The default component renders the standard result content.
|
||||
|
||||
```tsx title="Hit.tsx"
|
||||
function Hit({ hit, children }: HitProps): JSX.Element {
|
||||
function Hit({ hit, children }: HitComponentProps): JSX.Element {
|
||||
return (
|
||||
<a href={hit.url} data-result-type={hit.type}>
|
||||
{children}
|
||||
|
|
@ -113,10 +113,35 @@ Preserve `children` to retain the default hit content. `hit` is an `InternalDocS
|
|||
|
||||
### `resultsFooterComponent`
|
||||
|
||||
> `type: React.ComponentType<ResultsFooterProps>` | **optional**
|
||||
> `type: ({ state }: ResultsFooterComponentProps) => JSX.Element | null` | **optional**
|
||||
|
||||
Renders below result collections. It receives the current Autocomplete state. By default, DocSearch doesn't render a footer.
|
||||
|
||||
### `footerAction`
|
||||
|
||||
> `type: React.ReactNode` | **optional**
|
||||
|
||||
Renders a custom element in the modal footer, before the "Powered by Algolia" logo. Passing `null` renders nothing.
|
||||
|
||||
```tsx title="Search.tsx"
|
||||
function FooterAction(): JSX.Element {
|
||||
return (
|
||||
<a href="https://algolia.com">
|
||||
Our other project
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function Search() {
|
||||
return (
|
||||
<DocSearch
|
||||
// ...
|
||||
footerAction={<FooterAction />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### `transformSearchClient`
|
||||
|
||||
> `type: (client: SearchClient) => SearchClient` | **optional**
|
||||
|
|
|
|||
Loading…
Reference in a new issue