Skip to content

Commit

Permalink
fix(panel): append panel body as a child element (#3561)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Mar 12, 2019
1 parent 3dcced9 commit 3de59a3
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 102 deletions.
75 changes: 42 additions & 33 deletions src/components/Panel/Panel.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
import React from 'preact-compat';
import React, { Component } from 'preact-compat';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Template from '../Template/Template';

const Panel = ({ cssClasses, hidden, templateProps, data, onRef }) => (
<div
className={cx(cssClasses.root, {
[cssClasses.noRefinementRoot]: hidden,
})}
hidden={hidden}
>
{templateProps.templates.header && (
<Template
{...templateProps}
templateKey="header"
rootProps={{
className: cssClasses.header,
}}
data={data}
/>
)}
class Panel extends Component {
componentDidMount() {
this.bodyRef.appendChild(this.props.bodyElement);
}

<div className={cssClasses.body} ref={onRef} />
render() {
const { cssClasses, hidden, templateProps, data } = this.props;

{templateProps.templates.footer && (
<Template
{...templateProps}
templateKey="footer"
rootProps={{
className: cssClasses.footer,
}}
data={data}
/>
)}
</div>
);
return (
<div
className={cx(cssClasses.root, {
[cssClasses.noRefinementRoot]: hidden,
})}
hidden={hidden}
>
{templateProps.templates.header && (
<Template
{...templateProps}
templateKey="header"
rootProps={{
className: cssClasses.header,
}}
data={data}
/>
)}

<div className={cssClasses.body} ref={node => (this.bodyRef = node)} />

{templateProps.templates.footer && (
<Template
{...templateProps}
templateKey="footer"
rootProps={{
className: cssClasses.footer,
}}
data={data}
/>
)}
</div>
);
}
}

Panel.propTypes = {
// Prop to get the panel body reference to insert the widget
onRef: PropTypes.func,
bodyElement: PropTypes.instanceOf(Element).isRequired,
cssClasses: PropTypes.shape({
root: PropTypes.string.isRequired,
noRefinementRoot: PropTypes.string.isRequired,
Expand Down
78 changes: 37 additions & 41 deletions src/components/Panel/__tests__/Panel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,60 @@ import React from 'react';
import { mount } from 'enzyme';
import Panel from '../Panel';

const cssClasses = {
root: 'root',
noRefinementRoot: 'noRefinementRoot',
body: 'body',
header: 'header',
footer: 'footer',
};

const getDefaultProps = () => ({
bodyElement: document.createElement('div'),
cssClasses,
hidden: false,
data: {},
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
},
},
});

describe('Panel', () => {
test('should render component with default props', () => {
const props = {
cssClasses: {
root: 'root',
noRefinementRoot: 'noRefinementRoot',
body: 'body',
header: 'header',
footer: 'footer',
},
hidden: false,
data: {},
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
},
},
...getDefaultProps(),
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find('.root')).toHaveLength(1);
expect(wrapper.find('.noRefinementRoot')).toHaveLength(0);
expect(wrapper.find('.body')).toHaveLength(1);
expect(wrapper.find('.header')).toHaveLength(1);
expect(wrapper.find('.footer')).toHaveLength(1);
expect(wrapper.find('.header').text()).toBe('Header');
expect(wrapper.find('.footer').text()).toBe('Footer');
expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(
false
);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).text()).toBe('Header');
expect(wrapper.find(`.${cssClasses.footer}`).text()).toBe('Footer');
expect(wrapper).toMatchSnapshot();
});

test('should render component with `hidden` prop', () => {
const props = {
cssClasses: {
root: 'root',
noRefinementRoot: 'noRefinementRoot',
body: 'body',
header: 'header',
footer: 'footer',
},
...getDefaultProps(),
hidden: true,
data: {},
templateProps: {
templates: {
header: 'Header',
footer: 'Footer',
},
},
};

const wrapper = mount(<Panel {...props} />);

expect(wrapper.find('.root')).toHaveLength(1);
expect(wrapper.find('.noRefinementRoot')).toHaveLength(1);
expect(wrapper.find('.body')).toHaveLength(1);
expect(wrapper.find('.header')).toHaveLength(1);
expect(wrapper.find('.footer')).toHaveLength(1);
expect(wrapper.find(`.${cssClasses.root}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.noRefinementRoot}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.body}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.header}`).exists()).toBe(true);
expect(wrapper.find(`.${cssClasses.footer}`).exists()).toBe(true);
expect(wrapper.props().hidden).toBe(true);
expect(wrapper).toMatchSnapshot();
});
Expand Down
58 changes: 50 additions & 8 deletions src/widgets/panel/__tests__/panel-test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { render, unmountComponentAtNode } from 'preact-compat';
import panel from '../panel';

jest.mock('preact-compat', () => {
const module = require.requireActual('preact-compat');

module.render = jest.fn();
module.unmountComponentAtNode = jest.fn();

return module;
});

describe('Usage', () => {
test('without arguments does not throw', () => {
expect(() => panel()).not.toThrow();
expect(() => {
panel();
}).not.toThrow();
});

test('with templates does not throw', () => {
expect(() =>
expect(() => {
panel({
templates: { header: 'header' },
})
).not.toThrow();
});
}).not.toThrow();
});

test('with `hidden` as function does not throw', () => {
expect(() =>
expect(() => {
panel({
hidden: () => true,
})
).not.toThrow();
});
}).not.toThrow();
});

test('with `hidden` as boolean warns', () => {
Expand All @@ -34,10 +46,40 @@ describe('Usage', () => {
test('with a widget without `container` throws', () => {
const fakeWidget = () => {};

expect(() => panel()(fakeWidget)({})).toThrowErrorMatchingInlineSnapshot(`
expect(() => {
panel()(fakeWidget)({});
}).toThrowErrorMatchingInlineSnapshot(`
"The \`container\` option is required in the widget within the panel.
See documentation: https://www.algolia.com/doc/api-reference/widgets/panel/js/"
`);
});
});

describe('Lifecycle', () => {
beforeEach(() => {
render.mockClear();
unmountComponentAtNode.mockClear();
});

test('calls the inner widget lifecycle', () => {
const widget = {
init: jest.fn(),
render: jest.fn(),
dispose: jest.fn(),
};
const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

widgetWithPanel.init({});
widgetWithPanel.render({});
widgetWithPanel.dispose({});

expect(widget.init).toHaveBeenCalledTimes(1);
expect(widget.render).toHaveBeenCalledTimes(1);
expect(widget.dispose).toHaveBeenCalledTimes(1);
});
});
22 changes: 11 additions & 11 deletions src/widgets/panel/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,22 @@ import Panel from '../../components/Panel/Panel';
const withUsage = createDocumentationMessageGenerator({ name: 'panel' });
const suit = component('Panel');

const renderer = ({ containerNode, cssClasses, templateProps }) => ({
options,
hidden,
}) => {
let bodyRef = null;

const renderer = ({
containerNode,
bodyContainerNode,
cssClasses,
templateProps,
}) => ({ options, hidden }) => {
render(
<Panel
cssClasses={cssClasses}
hidden={hidden}
templateProps={templateProps}
data={options}
onRef={ref => (bodyRef = ref)}
bodyElement={bodyContainerNode}
/>,
containerNode
);

return { bodyRef };
};

/**
Expand Down Expand Up @@ -85,6 +83,7 @@ export default function panel({
`The \`hidden\` option in the "panel" widget expects a function returning a boolean (received "${typeof hidden}" type).`
);

const bodyContainerNode = document.createElement('div');
const cssClasses = {
root: cx(suit(), userCssClasses.root),
noRefinementRoot: cx(
Expand Down Expand Up @@ -112,18 +111,19 @@ export default function panel({

const renderPanel = renderer({
containerNode: getContainerNode(container),
bodyContainerNode,
cssClasses,
templateProps,
});

const { bodyRef } = renderPanel({
renderPanel({
options: {},
hidden: true,
});

const widget = widgetFactory({
...widgetOptions,
container: getContainerNode(bodyRef),
container: bodyContainerNode,
});

return {
Expand Down
21 changes: 12 additions & 9 deletions stories/panel.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,40 @@ storiesOf('Panel', module)
})
)
.add(
'with ratingMenu',
'with range input',
withHits(({ search, container, instantsearch }) => {
search.addWidget(
instantsearch.widgets.panel({
templates: {
header: ({ results }) =>
`Header ${results ? `| ${results.nbHits} results` : ''}`,
header: 'Price',
footer: 'Footer',
},
hidden: ({ results }) => results.nbHits === 0,
})(instantsearch.widgets.ratingMenu)({
})(instantsearch.widgets.rangeInput)({
container,
attribute: 'price',
})
);
})
)
.add(
'with menu',
'with range slider',
withHits(({ search, container, instantsearch }) => {
search.addWidget(
instantsearch.widgets.panel({
templates: {
header: ({ results }) =>
`Header ${results ? `| ${results.nbHits} results` : ''}`,
header: 'Price',
footer: 'Footer',
},
hidden: ({ results }) => results.nbHits === 0,
})(instantsearch.widgets.menu)({
})(instantsearch.widgets.rangeSlider)({
container,
attribute: 'brand',
attribute: 'price',
tooltips: {
format(rawValue) {
return `$${Math.round(rawValue).toLocaleString()}`;
},
},
})
);
})
Expand Down

0 comments on commit 3de59a3

Please sign in to comment.