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;