-
Notifications
You must be signed in to change notification settings - Fork 557
feat(panel): add Panel widget #3253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8e1ec86
feat(panel): add widget
francoischalifour 935082b
feat(stories): add Panel stories
francoischalifour f49257f
feat(panel): add support for rendering options
francoischalifour ddd2c71
feat(stories): update Panel stories
francoischalifour 9accd8d
fix(stories): use `canRefine` in panel
francoischalifour 0500fac
test(panel): test widget
francoischalifour 6740d50
feat(Panel): add and test component
francoischalifour 352c821
feat(panel): warn when using boolean `hidden`
francoischalifour 719ce98
refactor(panel): update ref callback
francoischalifour 90d9d11
Apply suggestions from code review
tkrugg 2a48ac5
fix(Panel): use preact-compat
samouss d35a47f
fix(panel): call widget.dispose() if exists
francoischalifour 460d035
fix(panel): call widget.render() if exists
francoischalifour e8a9cb9
refactor(panel): move renderingOptions condition outside of lifecycle
francoischalifour ac82328
fix(panel): don't display the widget name
francoischalifour b7bd36f
Merge branch 'v3' into feat/3.0-panel
francoischalifour 7ca8162
Merge branch 'v3' into feat/3.0-panel
francoischalifour 90ca56e
feat(panel): pass full options to templates
francoischalifour 0b23ca3
feat(panel): update stories
francoischalifour File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import React 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} | ||
| /> | ||
| )} | ||
|
|
||
| <div className={cssClasses.body} ref={onRef} /> | ||
|
|
||
| {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, | ||
| cssClasses: PropTypes.shape({ | ||
| root: PropTypes.string.isRequired, | ||
| noRefinementRoot: PropTypes.string.isRequired, | ||
| body: PropTypes.string.isRequired, | ||
| header: PropTypes.string.isRequired, | ||
| footer: PropTypes.string.isRequired, | ||
| }).isRequired, | ||
| templateProps: PropTypes.shape({ | ||
| templates: PropTypes.object.isRequired, | ||
| }).isRequired, | ||
| hidden: PropTypes.bool.isRequired, | ||
francoischalifour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| data: PropTypes.object.isRequired, | ||
| }; | ||
|
|
||
| export default Panel; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import React from 'react'; | ||
| import { mount } from 'enzyme'; | ||
| import Panel from '../Panel'; | ||
|
|
||
| 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', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| 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).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| test('should render component with `hidden` prop', () => { | ||
| const props = { | ||
| cssClasses: { | ||
| root: 'root', | ||
| noRefinementRoot: 'noRefinementRoot', | ||
| body: 'body', | ||
| header: 'header', | ||
| footer: 'footer', | ||
| }, | ||
| 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.props().hidden).toBe(true); | ||
| expect(wrapper).toMatchSnapshot(); | ||
| }); | ||
| }); |
55 changes: 55 additions & 0 deletions
55
src/components/Panel/__tests__/__snapshots__/Panel-test.js.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
|
||
| exports[`Panel should render component with \`hidden\` prop 1`] = ` | ||
| <div | ||
| className="root noRefinementRoot" | ||
| hidden={true} | ||
| > | ||
| <div | ||
| className="header" | ||
| dangerouslySetInnerHTML={ | ||
| Object { | ||
| "__html": "Header", | ||
| } | ||
| } | ||
| /> | ||
| <div | ||
| className="body" | ||
| /> | ||
| <div | ||
| className="footer" | ||
| dangerouslySetInnerHTML={ | ||
| Object { | ||
| "__html": "Footer", | ||
| } | ||
| } | ||
| /> | ||
| </div> | ||
| `; | ||
|
|
||
| exports[`Panel should render component with default props 1`] = ` | ||
| <div | ||
| className="root" | ||
| hidden={false} | ||
| > | ||
| <div | ||
| className="header" | ||
| dangerouslySetInnerHTML={ | ||
| Object { | ||
| "__html": "Header", | ||
| } | ||
| } | ||
| /> | ||
| <div | ||
| className="body" | ||
| /> | ||
| <div | ||
| className="footer" | ||
| dangerouslySetInnerHTML={ | ||
| Object { | ||
| "__html": "Footer", | ||
| } | ||
| } | ||
| /> | ||
| </div> | ||
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import panel from '../panel'; | ||
|
|
||
| describe('panel call', () => { | ||
| test('without arguments does not throw', () => { | ||
francoischalifour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| expect(() => panel()).not.toThrow(); | ||
| }); | ||
|
|
||
| test('with templates does not throw', () => { | ||
| expect(() => | ||
| panel({ | ||
| templates: { header: 'header' }, | ||
| }) | ||
| ).not.toThrow(); | ||
| }); | ||
|
|
||
| test('with `hidden` as function does not throw', () => { | ||
| expect(() => | ||
| panel({ | ||
| hidden: () => true, | ||
| }) | ||
| ).not.toThrow(); | ||
| }); | ||
|
|
||
| test('with `hidden` as boolean warns', () => { | ||
| const warn = jest.spyOn(global.console, 'warn'); | ||
| warn.mockImplementation(() => {}); | ||
|
|
||
| panel({ | ||
| hidden: true, | ||
| }); | ||
|
|
||
| expect(warn).toHaveBeenCalledWith( | ||
| '[InstantSearch.js]: The `hidden` option in the "panel" widget expects a function returning a boolean (received "boolean" type).' | ||
| ); | ||
|
|
||
| warn.mockRestore(); | ||
| }); | ||
|
|
||
| test('with a widget without `container` throws', () => { | ||
| const fakeWidget = () => {}; | ||
|
|
||
| expect(() => panel()(fakeWidget)({})).toThrowErrorMatchingInlineSnapshot( | ||
| `"[InstantSearch.js] The \`container\` option is required in the widget within the panel."` | ||
| ); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import React, { render, unmountComponentAtNode } from 'preact-compat'; | ||
| import cx from 'classnames'; | ||
| import { getContainerNode, prepareTemplateProps, warn } from '../../lib/utils'; | ||
| import { component } from '../../lib/suit'; | ||
| import Panel from '../../components/Panel/Panel'; | ||
|
|
||
| const suit = component('Panel'); | ||
|
|
||
| const renderer = ({ containerNode, cssClasses, templateProps }) => ({ | ||
| options, | ||
| hidden, | ||
| }) => { | ||
| let bodyRef = null; | ||
|
|
||
| render( | ||
| <Panel | ||
| cssClasses={cssClasses} | ||
| hidden={hidden} | ||
| templateProps={templateProps} | ||
| data={options} | ||
| onRef={ref => (bodyRef = ref)} | ||
| />, | ||
| containerNode | ||
| ); | ||
|
|
||
| return { bodyRef }; | ||
francoischalifour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| const usage = `Usage: | ||
| const widgetWithHeaderFooter = panel({ | ||
| [ templates.{header, footer} ], | ||
| [ hidden ], | ||
| [ cssClasses.{root, noRefinementRoot, body, header, footer} ], | ||
| })(widget); | ||
|
|
||
| const myWidget = widgetWithHeaderFooter(widgetOptions)`; | ||
|
|
||
| export default function panel({ | ||
| templates = {}, | ||
| hidden = () => false, | ||
| cssClasses: userCssClasses = {}, | ||
| } = {}) { | ||
| if (typeof hidden !== 'function') { | ||
| warn( | ||
| `The \`hidden\` option in the "panel" widget expects a function returning a boolean (received "${typeof hidden}" type).` | ||
| ); | ||
| } | ||
|
|
||
| const cssClasses = { | ||
| root: cx(suit(), userCssClasses.root), | ||
| noRefinementRoot: cx( | ||
| suit({ modifierName: 'noRefinement' }), | ||
| userCssClasses.noRefinementRoot | ||
| ), | ||
| body: cx(suit({ descendantName: 'body' }), userCssClasses.body), | ||
| header: cx(suit({ descendantName: 'header' }), userCssClasses.header), | ||
| footer: cx(suit({ descendantName: 'footer' }), userCssClasses.footer), | ||
| }; | ||
|
|
||
| return widgetFactory => (widgetOptions = {}) => { | ||
| const { container } = widgetOptions; | ||
|
|
||
| if (!container) { | ||
| throw new Error( | ||
| `[InstantSearch.js] The \`container\` option is required in the widget within the panel.` | ||
| ); | ||
| } | ||
|
|
||
| const defaultTemplates = { header: '', footer: '' }; | ||
| const templateProps = prepareTemplateProps({ defaultTemplates, templates }); | ||
|
|
||
| const renderPanel = renderer({ | ||
| containerNode: getContainerNode(container), | ||
| cssClasses, | ||
| templateProps, | ||
| }); | ||
|
|
||
| try { | ||
| const { bodyRef } = renderPanel({ | ||
francoischalifour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| options: {}, | ||
| hidden: true, | ||
| }); | ||
|
|
||
| const widget = widgetFactory({ | ||
| ...widgetOptions, | ||
| container: getContainerNode(bodyRef), | ||
| }); | ||
|
|
||
| return { | ||
| ...widget, | ||
| dispose() { | ||
| unmountComponentAtNode(getContainerNode(container)); | ||
|
|
||
| if (typeof widget.dispose === 'function') { | ||
| widget.dispose(); | ||
| } | ||
| }, | ||
| render(options) { | ||
| renderPanel({ | ||
| options, | ||
| hidden: Boolean(hidden(options)), | ||
| }); | ||
|
|
||
| if (typeof widget.render === 'function') { | ||
| widget.render(options); | ||
| } | ||
| }, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(usage); | ||
francoischalifour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* eslint-disable import/default */ | ||
|
|
||
| import { storiesOf } from 'dev-novel'; | ||
| import instantsearch from '../../../../index'; | ||
| import { wrapWithHits } from '../../utils/wrap-with-hits.js'; | ||
|
|
||
| const stories = storiesOf('Panel'); | ||
|
|
||
| export default () => { | ||
| stories.add( | ||
| 'with default', | ||
| wrapWithHits(container => { | ||
| window.search.addWidget( | ||
| instantsearch.widgets.panel({ | ||
| templates: { | ||
| header: ({ results }) => | ||
| `Header ${results ? `| ${results.nbHits} results` : ''}`, | ||
| footer: 'Footer', | ||
| }, | ||
| hidden: ({ results }) => results.nbHits === 0, | ||
| })(instantsearch.widgets.refinementList)({ | ||
| container, | ||
| attribute: 'brand', | ||
| }) | ||
| ); | ||
| }) | ||
| ); | ||
| }; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.