diff --git a/src/components/PoweredBy/PoweredBy.tsx b/src/components/PoweredBy/PoweredBy.tsx index 4bcecf57cc..51c2ffdfb4 100644 --- a/src/components/PoweredBy/PoweredBy.tsx +++ b/src/components/PoweredBy/PoweredBy.tsx @@ -1,20 +1,19 @@ /** @jsx h */ import { h } from 'preact'; +import { PoweredByCSSClasses } from '../../widgets/powered-by/powered-by'; -type CSSClasses = { - root: string; - link: string; - logo: string; +export type PoweredByComponentCSSClasses = { + [TClassName in keyof PoweredByCSSClasses]: string; }; -type Props = { +export type PoweredByProps = { url: string; theme: string; - cssClasses: CSSClasses; + cssClasses: PoweredByComponentCSSClasses; }; -const PoweredBy = ({ url, theme, cssClasses }: Props) => ( +const PoweredBy = ({ url, theme, cssClasses }: PoweredByProps) => (
{ - const module = jest.requireActual('preact'); - - module.render = jest.fn(); - - return module; -}); - -describe('poweredBy call', () => { - it('throws an exception when no container', () => { - expect(poweredBy).toThrowErrorMatchingInlineSnapshot(` -"The \`container\` option is required. - -See documentation: https://www.algolia.com/doc/api-reference/widgets/powered-by/js/" -`); - }); -}); - -describe('poweredBy', () => { - let widget; - let container; - - beforeEach(() => { - render.mockClear(); - - container = document.createElement('div'); - widget = poweredBy({ - container, - cssClasses: { - root: 'root', - link: 'link', - logo: 'logo', - }, - }); - - widget.init({}); - }); - - it('renders only once at init', () => { - widget.render({}); - widget.render({}); - - const [firstRender] = render.mock.calls; - - expect(render).toHaveBeenCalledTimes(1); - expect(firstRender[0].props).toMatchSnapshot(); - expect(firstRender[1]).toEqual(container); - }); -}); diff --git a/src/widgets/powered-by/__tests__/powered-by-test.ts b/src/widgets/powered-by/__tests__/powered-by-test.ts new file mode 100644 index 0000000000..49ea5dfc3a --- /dev/null +++ b/src/widgets/powered-by/__tests__/powered-by-test.ts @@ -0,0 +1,74 @@ +import algoliasearchHelper, { AlgoliaSearchHelper } from 'algoliasearch-helper'; +import { render as preactRender, VNode } from 'preact'; +import { createSearchClient } from '../../../../test/mock/createSearchClient'; +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/mock/createWidget'; +import { castToJestMock } from '../../../../test/utils/castToJestMock'; +import { PoweredByProps } from '../../../components/PoweredBy/PoweredBy'; +import poweredBy from '../powered-by'; + +const render = castToJestMock(preactRender); +jest.mock('preact', () => { + const module = jest.requireActual('preact'); + + module.render = jest.fn(); + + return module; +}); + +describe('poweredBy call', () => { + it('throws an exception when no container', () => { + expect(poweredBy).toThrowErrorMatchingInlineSnapshot(` +"The \`container\` option is required. + +See documentation: https://www.algolia.com/doc/api-reference/widgets/powered-by/js/" +`); + }); +}); + +describe('poweredBy', () => { + let widget: ReturnType; + let container: HTMLElement; + let helper: AlgoliaSearchHelper; + + beforeEach(() => { + render.mockClear(); + + container = document.createElement('div'); + widget = poweredBy({ + container, + cssClasses: { + root: 'root', + link: 'link', + logo: 'logo', + }, + }); + + helper = algoliasearchHelper(createSearchClient(), '', {}); + + widget.init!(createInitOptions({ helper })); + }); + + it('renders only once at init', () => { + widget.render!(createRenderOptions({ helper })); + widget.render!(createRenderOptions({ helper })); + + const firstRender = render.mock.calls[0][0] as VNode; + const firstContainer = render.mock.calls[0][1]; + + expect(render).toHaveBeenCalledTimes(1); + expect(firstRender.props).toEqual({ + cssClasses: { + link: 'ais-PoweredBy-link link', + logo: 'ais-PoweredBy-logo logo', + root: 'ais-PoweredBy ais-PoweredBy--light root', + }, + theme: 'light', + url: + 'https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby', + }); + expect(firstContainer).toEqual(container); + }); +}); diff --git a/src/widgets/powered-by/powered-by.js b/src/widgets/powered-by/powered-by.tsx similarity index 52% rename from src/widgets/powered-by/powered-by.js rename to src/widgets/powered-by/powered-by.tsx index 7b9e187964..88623eb5dd 100644 --- a/src/widgets/powered-by/powered-by.js +++ b/src/widgets/powered-by/powered-by.tsx @@ -3,22 +3,30 @@ import { h, render } from 'preact'; import cx from 'classnames'; import PoweredBy from '../../components/PoweredBy/PoweredBy'; -import connectPoweredBy from '../../connectors/powered-by/connectPoweredBy'; +import connectPoweredBy, { + PoweredByConnectorParams, + PoweredByRenderState, + PoweredByWidgetDescription, +} from '../../connectors/powered-by/connectPoweredBy'; import { getContainerNode, createDocumentationMessageGenerator, } from '../../lib/utils'; import { component } from '../../lib/suit'; +import { Renderer, WidgetFactory } from '../../types'; const suit = component('PoweredBy'); const withUsage = createDocumentationMessageGenerator({ name: 'powered-by' }); -const renderer = ({ containerNode, cssClasses }) => ( +const renderer = ({ + containerNode, + cssClasses, +}): Renderer> => ( { url, widgetParams }, isFirstRendering ) => { if (isFirstRendering) { - const { theme } = widgetParams; + const { theme = 'light' } = widgetParams; render( , @@ -29,36 +37,48 @@ const renderer = ({ containerNode, cssClasses }) => ( } }; -/** - * @typedef {Object} PoweredByWidgetCssClasses - * @property {string|string[]} [root] CSS classes added to the root element of the widget. - * @property {string|string[]} [link] CSS class to add to the link. - * @property {string|string[]} [logo] CSS class to add to the SVG logo. - */ - -/** - * @typedef {Object} PoweredByWidgetParams - * @property {string|HTMLElement} container Place where to insert the widget in your webpage. - * @property {string} [theme] The theme of the logo ("light" or "dark"). - * @property {PoweredByWidgetCssClasses} [cssClasses] CSS classes to add. - */ - -/** - * The `poweredBy` widget is used to display the logo to redirect to Algolia. - * @type {WidgetFactory} - * @devNovel PoweredBy - * @category metadata - * @param {PoweredByWidgetParams} widgetParams PoweredBy widget options. Some keys are mandatory: `container`, - * @return {Widget} A new poweredBy widget instance - * @example - * search.addWidgets([ - * instantsearch.widgets.poweredBy({ - * container: '#poweredBy-container', - * theme: 'dark', - * }) - * ]); - */ -export default function poweredBy(widgetParams) { +export type PoweredByCSSClasses = { + /** + * CSS class to add to the wrapping element. + */ + root: string | string[]; + + /** + * CSS class to add to the link. + */ + link: string | string[]; + + /** + * CSS class to add to the SVG logo. + */ + logo: string | string[]; +}; + +export type PoweredByWidgetParams = { + /** + * CSS Selector or HTMLElement to insert the widget. + */ + container: string | HTMLElement; + + /** + * The theme of the logo. + * @default 'light' + */ + theme?: 'light' | 'dark'; + + /** + * CSS classes to add. + */ + cssClasses?: Partial; +}; + +export type PoweredByWidget = WidgetFactory< + PoweredByWidgetDescription & { $$widgetType: 'ais.poweredBy' }, + PoweredByConnectorParams, + PoweredByWidgetParams +>; + +const poweredBy: PoweredByWidget = function poweredBy(widgetParams) { const { container, cssClasses: userCssClasses = {}, theme = 'light' } = widgetParams || {}; @@ -91,4 +111,6 @@ export default function poweredBy(widgetParams) { ...makeWidget({ theme }), $$widgetType: 'ais.poweredBy', }; -} +}; + +export default poweredBy;