diff --git a/src/components/Panel/Panel.tsx b/src/components/Panel/Panel.tsx index 44173267de..f2cdcfd074 100644 --- a/src/components/Panel/Panel.tsx +++ b/src/components/Panel/Panel.tsx @@ -6,13 +6,10 @@ import cx from 'classnames'; import Template from '../Template/Template'; import type { PanelCSSClasses, + PanelSharedOptions, PanelTemplates, } from '../../widgets/panel/panel'; -import type { - ComponentCSSClasses, - RenderOptions, - UnknownWidgetFactory, -} from '../../types'; +import type { ComponentCSSClasses, UnknownWidgetFactory } from '../../types'; export type PanelComponentCSSClasses = ComponentCSSClasses< // `collapseIcon` is only used in the default templates of the widget @@ -26,7 +23,7 @@ export type PanelProps = { hidden: boolean; collapsible: boolean; isCollapsed: boolean; - data: RenderOptions | Record; + data: PanelSharedOptions; cssClasses: PanelComponentCSSClasses; templates: PanelComponentTemplates; bodyElement: HTMLElement; diff --git a/src/widgets/panel/__tests__/panel-test.ts b/src/widgets/panel/__tests__/panel-test.ts index b16faa4f92..1a866fb8a9 100644 --- a/src/widgets/panel/__tests__/panel-test.ts +++ b/src/widgets/panel/__tests__/panel-test.ts @@ -114,10 +114,12 @@ describe('Templates', () => { test('with default templates', () => { const widgetWithPanel = panel()(widgetFactory); - widgetWithPanel({ + const widget = widgetWithPanel({ container: document.createElement('div'), }); + widget.init(createInitOptions()); + const firstRender = render.mock.calls[0][0] as VNode< PanelProps >; @@ -137,10 +139,12 @@ describe('Templates', () => { }, })(widgetFactory); - widgetWithPanel({ + const widget = widgetWithPanel({ container: document.createElement('div'), }); + widget.init(createInitOptions()); + const firstRender = render.mock.calls[0][0] as VNode< PanelProps >; @@ -156,10 +160,12 @@ describe('Templates', () => { }, })(widgetFactory); - widgetWithPanel({ + const widget = widgetWithPanel({ container: document.createElement('div'), }); + widget.init(createInitOptions()); + const firstRender = render.mock.calls[0][0] as VNode< PanelProps >; @@ -175,10 +181,12 @@ describe('Templates', () => { }, })(widgetFactory); - widgetWithPanel({ + const widget = widgetWithPanel({ container: document.createElement('div'), }); + widget.init(createInitOptions()); + const firstRender = render.mock.calls[0][0] as VNode< PanelProps >; @@ -206,32 +214,235 @@ describe('Lifecycle', () => { container: document.createElement('div'), }); - widgetWithPanel.init!(createInitOptions()); - widgetWithPanel.render!(createRenderOptions()); - widgetWithPanel.dispose!(createDisposeOptions()); + widgetWithPanel.init(createInitOptions()); + widgetWithPanel.render(createRenderOptions()); + widgetWithPanel.dispose(createDisposeOptions()); expect(widget.init).toHaveBeenCalledTimes(1); expect(widget.render).toHaveBeenCalledTimes(1); expect(widget.dispose).toHaveBeenCalledTimes(1); }); - test('returns the `state` from the widget dispose function', () => { - const nextSearchParameters = new algoliasearchHelper.SearchParameters({ - facets: ['brands'], + describe('init', () => { + test("calls the wrapped widget's init", () => { + const widget = { + $$type: 'mock.widget', + init: jest.fn(), + }; + const widgetFactory = () => widget; + + const widgetWithPanel = panel()(widgetFactory)({ + container: document.createElement('div'), + }); + + const initOptions = createInitOptions(); + + widgetWithPanel.init(initOptions); + + expect(widget.init).toHaveBeenCalledTimes(1); + expect(widget.init).toHaveBeenCalledWith(initOptions); }); - const widget = { - $$type: 'mock.widget', - init: jest.fn(), - dispose: jest.fn(() => nextSearchParameters), - }; - const widgetFactory = () => widget; - const widgetWithPanel = panel()(widgetFactory)({ - container: document.createElement('div'), + test('does not call hidden and collapsed yet', () => { + const renderState = { + widgetParams: {}, + swag: true, + }; + + const widget = { + $$type: 'mock.widget', + render: jest.fn(), + getWidgetRenderState() { + return renderState; + }, + }; + + const widgetFactory = () => widget; + + const hiddenFn = jest.fn(); + const collapsedFn = jest.fn(); + + const widgetWithPanel = panel({ + hidden: hiddenFn, + collapsed: collapsedFn, + })(widgetFactory)({ + container: document.createElement('div'), + }); + + const initOptions = createInitOptions(); + + widgetWithPanel.init(initOptions); + + expect(hiddenFn).toHaveBeenCalledTimes(0); + expect(collapsedFn).toHaveBeenCalledTimes(0); + }); + + test('renders with render state', () => { + const renderState = { + widgetParams: {}, + swag: true, + }; + + const widget = { + $$type: 'mock.widget', + render: jest.fn(), + getWidgetRenderState() { + return renderState; + }, + }; + + const widgetFactory = () => widget; + + const widgetWithPanel = panel()(widgetFactory)({ + container: document.createElement('div'), + }); + + const initOptions = createInitOptions(); + + widgetWithPanel.init(initOptions); + + const firstRender = render.mock.calls[0][0] as VNode< + PanelProps + >; + + expect(firstRender.props).toEqual( + expect.objectContaining({ + hidden: true, + collapsible: false, + isCollapsed: false, + data: { + ...renderState, + ...initOptions, + }, + }) + ); + }); + }); + + describe('render', () => { + test("calls the wrapped widget's render", () => { + const widget = { + $$type: 'mock.widget', + render: jest.fn(), + }; + const widgetFactory = () => widget; + + const widgetWithPanel = panel()(widgetFactory)({ + container: document.createElement('div'), + }); + + const renderOptions = createRenderOptions(); + + widgetWithPanel.render(renderOptions); + + expect(widget.render).toHaveBeenCalledTimes(1); + expect(widget.render).toHaveBeenCalledWith(renderOptions); + }); + + test("calls hidden and collapsed with the wrapped widget's render state", () => { + const renderState = { + widgetParams: {}, + swag: true, + }; + + const widget = { + $$type: 'mock.widget', + render: jest.fn(), + getWidgetRenderState() { + return renderState; + }, + }; + + const widgetFactory = () => widget; + + const hiddenFn = jest.fn(); + const collapsedFn = jest.fn(); + + const widgetWithPanel = panel({ + hidden: hiddenFn, + collapsed: collapsedFn, + })(widgetFactory)({ + container: document.createElement('div'), + }); + + const renderOptions = createRenderOptions(); + + widgetWithPanel.render(renderOptions); + + expect(hiddenFn).toHaveBeenCalledTimes(1); + expect(hiddenFn).toHaveBeenCalledWith({ + ...renderState, + ...renderOptions, + }); + + expect(collapsedFn).toHaveBeenCalledTimes(1); + expect(collapsedFn).toHaveBeenCalledWith({ + ...renderState, + ...renderOptions, + }); + }); + + test('renders with render state', () => { + const renderState = { + widgetParams: {}, + swag: true, + }; + + const widget = { + $$type: 'mock.widget', + render: jest.fn(), + getWidgetRenderState() { + return renderState; + }, + }; + + const widgetFactory = () => widget; + + const widgetWithPanel = panel()(widgetFactory)({ + container: document.createElement('div'), + }); + + const renderOptions = createRenderOptions(); + + widgetWithPanel.render(renderOptions); + + const firstRender = render.mock.calls[0][0] as VNode< + PanelProps + >; + + expect(firstRender.props).toEqual( + expect.objectContaining({ + hidden: false, + collapsible: false, + isCollapsed: false, + data: { + ...renderState, + ...renderOptions, + }, + }) + ); }); + }); - const nextState = widgetWithPanel.dispose!(createDisposeOptions({})); + describe('dispose', () => { + test("returns the state from the widget's dispose function", () => { + const nextSearchParameters = new algoliasearchHelper.SearchParameters({ + facets: ['brands'], + }); + const widget = { + $$type: 'mock.widget', + init: jest.fn(), + dispose: jest.fn(() => nextSearchParameters), + }; + const widgetFactory = () => widget; + + const widgetWithPanel = panel()(widgetFactory)({ + container: document.createElement('div'), + }); + + const nextState = widgetWithPanel.dispose(createDisposeOptions()); - expect(nextState).toEqual(nextSearchParameters); + expect(nextState).toEqual(nextSearchParameters); + }); }); }); diff --git a/src/widgets/panel/panel.tsx b/src/widgets/panel/panel.tsx index 221a7d5464..8f2bf0bee3 100644 --- a/src/widgets/panel/panel.tsx +++ b/src/widgets/panel/panel.tsx @@ -11,7 +11,13 @@ import { import { component } from '../../lib/suit'; import type { PanelComponentCSSClasses } from '../../components/Panel/Panel'; import Panel from '../../components/Panel/Panel'; -import type { Template, RenderOptions, WidgetFactory } from '../../types'; +import type { + Template, + RenderOptions, + WidgetFactory, + InitOptions, + Widget, +} from '../../types'; export type PanelCSSClasses = Partial<{ /** @@ -100,6 +106,12 @@ type GetWidgetRenderState = export type PanelRenderOptions = RenderOptions & GetWidgetRenderState; +export type PanelSharedOptions = ( + | InitOptions + | RenderOptions +) & + GetWidgetRenderState; + export type PanelWidgetParams = { /** * A function that is called on each render to determine if the @@ -145,7 +157,7 @@ const renderer = collapsible, collapsed, }: { - options: RenderOptions | Record; + options: PanelSharedOptions; hidden: boolean; collapsible: boolean; collapsed: boolean; @@ -164,13 +176,19 @@ const renderer = ); }; +type AugmentedWidget< + TWidgetFactory extends AnyWidgetFactory, + TOverriddenKeys extends keyof Widget = 'init' | 'render' | 'dispose' +> = Omit, TOverriddenKeys> & + Pick, TOverriddenKeys>; + export type PanelWidget = ( panelWidgetParams?: PanelWidgetParams ) => ( widgetFactory: TWidgetFactory ) => ( widgetParams: Parameters[0] -) => ReturnType; +) => AugmentedWidget; /** * The panel widget wraps other widgets in a consistent panel design. @@ -265,32 +283,37 @@ const panel: PanelWidget = (panelWidgetParams) => { }, }); - renderPanel({ - options: {}, - hidden: true, - collapsible, - collapsed: false, - }); - const widget = widgetFactory({ ...widgetParams, container: bodyContainerNode, }); // TypeScript somehow loses track of the ...widget type, since it's - // not directly returned. Eventually the "as ReturnType" + // not directly returned. Eventually the "as AugmentedWidget" // will not be needed anymore. // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...widget, - dispose(...args) { - render(null, containerNode); + init(...args) { + const [renderOptions] = args; - if (typeof widget.dispose === 'function') { - return widget.dispose.call(this, ...args); - } + const options = { + ...(widget.getWidgetRenderState + ? widget.getWidgetRenderState(renderOptions) + : {}), + ...renderOptions, + }; - return undefined; + renderPanel({ + options, + hidden: true, + collapsible, + collapsed: false, + }); + + if (typeof widget.init === 'function') { + widget.init.call(this, ...args); + } }, render(...args) { const [renderOptions] = args; @@ -313,7 +336,16 @@ const panel: PanelWidget = (panelWidgetParams) => { widget.render.call(this, ...args); } }, - } as ReturnType; + dispose(...args) { + render(null, containerNode); + + if (typeof widget.dispose === 'function') { + return widget.dispose.call(this, ...args); + } + + return undefined; + }, + } as AugmentedWidget; }; };