diff --git a/blocks/autocomplete/README.md b/blocks/autocomplete/README.md new file mode 100644 index 0000000000000..bf3b564d3cb8e --- /dev/null +++ b/blocks/autocomplete/README.md @@ -0,0 +1,6 @@ +Autocomplete +============ + +This is an Autocomplete component for use in block UI. It is based on `Autocomplete` from `@wordpress/components` and takes the same props. In addition, it passes its autocompleters through a `blocks.Autocomplete.completers` filter to give developers an opportunity to override or extend them. + +The autocompleter interface is documented with the original `Autocomplete` component in `@wordpress/components`. diff --git a/blocks/autocomplete/index.js b/blocks/autocomplete/index.js new file mode 100644 index 0000000000000..df366d82106c2 --- /dev/null +++ b/blocks/autocomplete/index.js @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { clone } from 'lodash'; + +/** + * WordPress dependencies + */ +import { applyFilters, hasFilter } from '@wordpress/hooks'; +import { Component } from '@wordpress/element'; +import { Autocomplete as OriginalAutocomplete } from '@wordpress/components'; + +/* + * Use one array instance for fallback rather than inline array literals + * because the latter may cause rerender due to failed prop equality checks. + */ +const completersFallback = []; + +/** + * Wrap the default Autocomplete component with one that + * supports a filter hook for customizing its list of autocompleters. + * + * Since there may be many Autocomplete instances at one time, this component + * applies the filter on demand, when the component is first focused after + * receiving a new list of completers. + * + * This function is exported for unit test. + * + * @param {Function} Autocomplete Original component. + * @return {Function} Wrapped component + */ +export function withFilteredAutocompleters( Autocomplete ) { + return class FilteredAutocomplete extends Component { + constructor() { + super(); + + this.state = { completers: completersFallback }; + + this.saveParentRef = this.saveParentRef.bind( this ); + this.onFocus = this.onFocus.bind( this ); + } + + componentDidUpdate() { + const hasFocus = this.parentNode.contains( document.activeElement ); + + /* + * It's possible for props to be updated when the component has focus, + * so here, we ensure new completers are immediately applied while we + * have the focus. + * + * NOTE: This may trigger another render but only when the component has focus. + */ + if ( hasFocus && this.hasStaleCompleters() ) { + this.updateCompletersState(); + } + } + + onFocus() { + if ( this.hasStaleCompleters() ) { + this.updateCompletersState(); + } + } + + hasStaleCompleters() { + return ( + ! ( 'lastFilteredCompletersProp' in this.state ) || + this.state.lastFilteredCompletersProp !== this.props.completers + ); + } + + updateCompletersState() { + let { completers: nextCompleters } = this.props; + const lastFilteredCompletersProp = nextCompleters; + + if ( hasFilter( 'blocks.Autocomplete.completers' ) ) { + nextCompleters = applyFilters( + 'blocks.Autocomplete.completers', + // Provide copies so filters may directly modify them. + nextCompleters && nextCompleters.map( clone ) + ); + } + + this.setState( { + lastFilteredCompletersProp, + completers: nextCompleters || completersFallback, + } ); + } + + saveParentRef( parentNode ) { + this.parentNode = parentNode; + } + + render() { + const { completers } = this.state; + const autocompleteProps = { + ...this.props, + completers, + }; + + return ( +
+
+ Add text or type / to add content +
+- Write preformatted text… -+
+ Write preformatted text… ++
- Write quote… -
++ Write quote… +
+- Write quote… -
++ Write quote… +
+
- - |
-
- - |
-
- - |
-
- - |
-
+ + |
+
+ + |
+
+ + |
+
+ + |
+
- New Column -
++ New Column +
+- New Column -
++ New Column +
+- Write… -+
+ Write… ++