From 036fe52ffdf8e4448aeec9d20602539a788613e1 Mon Sep 17 00:00:00 2001 From: Oliver Joseph Ash Date: Mon, 22 Feb 2021 10:45:08 +0000 Subject: [PATCH 1/2] Init --- README.md | 34 +++++++ src/Autosuggest.js | 16 +++ src/Autowhatever.js | 75 +++++++------- .../AutosuggestApp.js | 97 +++++++++++++++++++ .../AutosuggestApp.test.js | 39 ++++++++ 5 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 test/render-section-container/AutosuggestApp.js create mode 100644 test/render-section-container/AutosuggestApp.test.js diff --git a/README.md b/README.md index aa6b5e0e..3668def3 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ class Example extends React.Component { | [`getSectionSuggestions`](#get-section-suggestions-prop) | Function | ✓
when `multiSection={true}` | Implement it to teach Autosuggest where to find the suggestions for every section. | | [`renderInputComponent`](#render-input-component-prop) | Function | | Use it only if you need to customize the rendering of the input. | | [`renderSuggestionsContainer`](#render-suggestions-container-prop) | Function | | Use it if you want to customize things inside the suggestions container beyond rendering the suggestions themselves. | +| [`renderSectionContainer`](#render-sections-container-prop) | Function | | Use it if you want to customize things inside the sections container beyond rendering the sections themselves. | | [`theme`](#theme-prop) | Object | | Use your imagination to style the Autosuggest. | | [`id`](#id-prop) | String | | Use it only if you have multiple Autosuggest components on a page. | @@ -602,6 +603,39 @@ function renderSuggestionsContainer({ containerProps, children }) { } ``` + + +#### renderSectionContainer (optional) + +You shouldn't specify `renderSectionContainer` unless you want to customize the content or behaviour of the sections container beyond rendering the sections themselves. For example, you might want to add a custom text before/after the sections list. + +The signature is: + +```js +function renderSectionContainer({ containerProps, children, query }) +``` + +where: + +- `containerProps` - props that you MUST pass to the topmost element that is returned from `renderSectionContainer`. +- `children` - the sections themselves. It's up to you where to render them. +- `query` - Same as `query` in [`renderSuggestion`](#render-suggestion-prop). + +For example: + +```js +function renderSectionContainer({ containerProps, children, query }) { + return ( +
+ {children} +
+ Press Enter to search {query} +
+
+ ); +} +``` + #### theme (optional) diff --git a/src/Autosuggest.js b/src/Autosuggest.js index 6f3269da..a339866b 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -9,6 +9,9 @@ const defaultShouldRenderSuggestions = (value) => value.trim().length > 0; const defaultRenderSuggestionsContainer = ({ containerProps, children }) => (
{children}
); +const defaultRenderSectionContainer = ({ containerProps, children }) => ( +
{children}
+); const REASON_SUGGESTIONS_REVEALED = 'suggestions-revealed'; const REASON_SUGGESTIONS_UPDATED = 'suggestions-updated'; @@ -47,6 +50,7 @@ export default class Autosuggest extends Component { onSuggestionHighlighted: PropTypes.func, renderInputComponent: PropTypes.func, renderSuggestionsContainer: PropTypes.func, + renderSectionContainer: PropTypes.func, getSuggestionValue: PropTypes.func.isRequired, renderSuggestion: PropTypes.func.isRequired, inputProps: (props, propName) => { @@ -100,6 +104,7 @@ export default class Autosuggest extends Component { static defaultProps = { renderSuggestionsContainer: defaultRenderSuggestionsContainer, + renderSectionContainer: defaultRenderSectionContainer, shouldRenderSuggestions: defaultShouldRenderSuggestions, alwaysRenderSuggestions: false, multiSection: false, @@ -545,6 +550,16 @@ export default class Autosuggest extends Component { }); }; + renderSectionContainer = ({ containerProps, children }) => { + const { renderSectionContainer } = this.props; + + return renderSectionContainer({ + containerProps, + children, + query: this.getQuery(), + }); + }; + render() { const { suggestions, @@ -803,6 +818,7 @@ export default class Autosuggest extends Component { items={items} renderInputComponent={renderInputComponent} renderItemsContainer={this.renderSuggestionsContainer} + renderSectionContainer={this.renderSectionContainer} renderItem={renderSuggestion} renderItemData={renderSuggestionData} renderSectionTitle={renderSectionTitle} diff --git a/src/Autowhatever.js b/src/Autowhatever.js index fe2ee8e7..f16c7113 100644 --- a/src/Autowhatever.js +++ b/src/Autowhatever.js @@ -10,6 +10,9 @@ const defaultRenderInputComponent = (props) => ; const defaultRenderItemsContainer = ({ containerProps, children }) => (
{children}
); +const defaultRenderSectionContainer = ({ containerProps, children }) => ( +
{children}
+); const defaultTheme = { container: 'react-autowhatever__container', containerOpen: 'react-autowhatever__container--open', @@ -33,6 +36,7 @@ export default class Autowhatever extends Component { multiSection: PropTypes.bool, // Indicates whether a multi section layout should be rendered. renderInputComponent: PropTypes.func, // When specified, it is used to render the input element. renderItemsContainer: PropTypes.func, // Renders the items container. + renderSectionContainer: PropTypes.func, // Renders the section container. items: PropTypes.array.isRequired, // Array of items or sections to render. renderItem: PropTypes.func, // This function renders a single item. renderItemData: PropTypes.object, // Arbitrary data that will be passed to renderItem() @@ -59,6 +63,7 @@ export default class Autowhatever extends Component { multiSection: false, renderInputComponent: defaultRenderInputComponent, renderItemsContainer: defaultRenderItemsContainer, + renderSectionContainer: defaultRenderSectionContainer, renderItem: () => { throw new Error('`renderItem` must be provided'); }, @@ -191,6 +196,7 @@ export default class Autowhatever extends Component { items, renderItem, renderItemData, + renderSectionContainer, renderSectionTitle, highlightedSectionIndex, highlightedItemIndex, @@ -203,41 +209,40 @@ export default class Autowhatever extends Component { const isFirstSection = sectionIndex === 0; // `key` is provided by theme() - /* eslint-disable react/jsx-key */ - return ( -
- - -
- ); - /* eslint-enable react/jsx-key */ + return renderSectionContainer({ + containerProps: theme( + `${sectionKeyPrefix}container`, + 'sectionContainer', + isFirstSection && 'sectionContainerFirst' + ), + children: ( + + + + + ), + }); }); } diff --git a/test/render-section-container/AutosuggestApp.js b/test/render-section-container/AutosuggestApp.js new file mode 100644 index 00000000..8780ca01 --- /dev/null +++ b/test/render-section-container/AutosuggestApp.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import sinon from 'sinon'; +import Autosuggest from '../../src/Autosuggest'; +import languages from '../plain-list/languages'; +import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js'; + +const getMatchingLanguages = value => { + const escapedValue = escapeRegexCharacters(value.trim()); + const regex = new RegExp('^' + escapedValue, 'i'); + + return languages.filter(language => regex.test(language.name)); +}; + +let app = null; + +const onChange = (event, { newValue }) => { + app.setState({ + value: newValue + }); +}; + +const onSuggestionsFetchRequested = ({ value }) => { + app.setState({ + suggestions: [{ languages: getMatchingLanguages(value) }] + }); +}; + +const onSuggestionsClearRequested = () => { + app.setState({ + suggestions: [] + }); +}; + +const getSuggestionValue = suggestion => suggestion.name; + +const renderSuggestion = suggestion => suggestion.name; + +export const renderSectionContainer = sinon.spy( + ({ containerProps, children, query }) => ( +
+ {children} +
+ Press Enter to search {query} +
+
+ ) +); + +const renderSectionTitle = () => null; + +const getSectionSuggestions = section => section.languages; + +export default class AutosuggestApp extends Component { + constructor() { + super(); + + app = this; + + this.state = { + value: '', + suggestions: [] + }; + } + + storeAutosuggestReference = autosuggest => { + if (autosuggest !== null) { + this.input = autosuggest.input; + } + }; + + alwaysRenderSuggestions = () => true; + + render() { + const { value, suggestions } = this.state; + const inputProps = { + value, + onChange + }; + + return ( + + ); + } +} diff --git a/test/render-section-container/AutosuggestApp.test.js b/test/render-section-container/AutosuggestApp.test.js new file mode 100644 index 00000000..f51be81b --- /dev/null +++ b/test/render-section-container/AutosuggestApp.test.js @@ -0,0 +1,39 @@ +import sinon from 'sinon'; +import React from 'react'; +import TestUtils from 'react-dom/test-utils'; +import { expect } from 'chai'; +import { + init, + childrenMatcher, + getInnerHTML, + getElementWithClass, + setInputValue +} from '../helpers'; +import AutosuggestApp, { renderSectionContainer } from './AutosuggestApp'; + +describe('Autosuggest with renderSectionContainer', () => { + beforeEach(() => { + init(TestUtils.renderIntoDocument()); + renderSectionContainer.resetHistory(); + setInputValue('c '); + }); + + it('should render whatever renderSectionContainer returns', () => { + expect(getElementWithClass('my-section-container-footer')).not.to.equal( + null + ); + expect(getInnerHTML(getElementWithClass('my-query'))).to.equal('c'); + }); + + it('should call renderSectionContainer once with the right parameters', () => { + expect(renderSectionContainer).to.have.been.calledOnce; + expect(renderSectionContainer).to.be.calledWith({ + containerProps: sinon.match({ + key: sinon.match.string, + className: sinon.match.string, + }), + children: childrenMatcher, + query: 'c' + }); + }); +}); From 5daff2d9f53321a04bcab1306b4b612c476a2e74 Mon Sep 17 00:00:00 2001 From: Oliver Joseph Ash Date: Mon, 22 Feb 2021 11:43:48 +0000 Subject: [PATCH 2/2] Pass in section --- src/Autosuggest.js | 3 ++- src/Autowhatever.js | 1 + test/render-section-container/AutosuggestApp.js | 14 ++++++-------- .../AutosuggestApp.test.js | 11 +++++++++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Autosuggest.js b/src/Autosuggest.js index a339866b..29260655 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -550,13 +550,14 @@ export default class Autosuggest extends Component { }); }; - renderSectionContainer = ({ containerProps, children }) => { + renderSectionContainer = ({ containerProps, children, section }) => { const { renderSectionContainer } = this.props; return renderSectionContainer({ containerProps, children, query: this.getQuery(), + section, }); }; diff --git a/src/Autowhatever.js b/src/Autowhatever.js index f16c7113..4bf6b9a4 100644 --- a/src/Autowhatever.js +++ b/src/Autowhatever.js @@ -210,6 +210,7 @@ export default class Autowhatever extends Component { // `key` is provided by theme() return renderSectionContainer({ + section, containerProps: theme( `${sectionKeyPrefix}container`, 'sectionContainer', diff --git a/test/render-section-container/AutosuggestApp.js b/test/render-section-container/AutosuggestApp.js index 8780ca01..e9e3f2b1 100644 --- a/test/render-section-container/AutosuggestApp.js +++ b/test/render-section-container/AutosuggestApp.js @@ -21,7 +21,7 @@ const onChange = (event, { newValue }) => { const onSuggestionsFetchRequested = ({ value }) => { app.setState({ - suggestions: [{ languages: getMatchingLanguages(value) }] + suggestions: [{ title: 'languages', languages: getMatchingLanguages(value) }] }); }; @@ -36,14 +36,12 @@ const getSuggestionValue = suggestion => suggestion.name; const renderSuggestion = suggestion => suggestion.name; export const renderSectionContainer = sinon.spy( - ({ containerProps, children, query }) => ( -
- {children} -
- Press Enter to search {query} -
+ ({ containerProps, children, query, section }) =>
+
+ Showing results for {query} in {section.title}
- ) + {children} +
); const renderSectionTitle = () => null; diff --git a/test/render-section-container/AutosuggestApp.test.js b/test/render-section-container/AutosuggestApp.test.js index f51be81b..08c3314f 100644 --- a/test/render-section-container/AutosuggestApp.test.js +++ b/test/render-section-container/AutosuggestApp.test.js @@ -19,7 +19,7 @@ describe('Autosuggest with renderSectionContainer', () => { }); it('should render whatever renderSectionContainer returns', () => { - expect(getElementWithClass('my-section-container-footer')).not.to.equal( + expect(getElementWithClass('my-section-container-header')).not.to.equal( null ); expect(getInnerHTML(getElementWithClass('my-query'))).to.equal('c'); @@ -33,7 +33,14 @@ describe('Autosuggest with renderSectionContainer', () => { className: sinon.match.string, }), children: childrenMatcher, - query: 'c' + query: 'c', + section: sinon.match({ + title: sinon.match.string, + languages: sinon.match.every(sinon.match({ + name: sinon.match.string, + year: sinon.match.number + })) + }) }); }); });