From 78dde89b370476502ab69c664084c29001b50be5 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Mon, 28 Sep 2020 07:14:07 -0600 Subject: [PATCH] Add post author selector enhanced with ComboboxControl (#23237) Co-authored-by: epiqueras Co-authored-by: Enrique Piqueras Co-authored-by: Noah Allen Co-authored-by: Riad Benguella Co-authored-by: jasmussen --- .../block-library/src/post-author/edit.js | 4 +- .../components/src/combobox-control/index.js | 28 +--- .../src/combobox-control/stories/index.js | 2 +- .../src/combobox-control/style.scss | 6 +- packages/core-data/src/selectors.js | 3 + .../src/components/post-author/check.js | 4 +- .../src/components/post-author/index.js | 147 +++++++++++------- .../src/components/post-author/test/index.js | 64 -------- 8 files changed, 114 insertions(+), 144 deletions(-) delete mode 100644 packages/editor/src/components/post-author/test/index.js diff --git a/packages/block-library/src/post-author/edit.js b/packages/block-library/src/post-author/edit.js index 3a159fbf76be3e..cedbede9f5de64 100644 --- a/packages/block-library/src/post-author/edit.js +++ b/packages/block-library/src/post-author/edit.js @@ -23,7 +23,7 @@ function PostAuthorEdit( { isSelected, context, attributes, setAttributes } ) { const { authorId, authorDetails, authors } = useSelect( ( select ) => { - const { getEditedEntityRecord, getUser, getAuthors } = select( + const { getEditedEntityRecord, getUser, getUsers } = select( 'core' ); const _authorId = getEditedEntityRecord( @@ -35,7 +35,7 @@ function PostAuthorEdit( { isSelected, context, attributes, setAttributes } ) { return { authorId: _authorId, authorDetails: _authorId ? getUser( _authorId ) : null, - authors: getAuthors(), + authors: getUsers( { who: 'authors' } ), }; }, [ postType, postId ] diff --git a/packages/components/src/combobox-control/index.js b/packages/components/src/combobox-control/index.js index e3ee035f86fdfe..fcc8d332e41bd0 100644 --- a/packages/components/src/combobox-control/index.js +++ b/packages/components/src/combobox-control/index.js @@ -19,7 +19,7 @@ function ComboboxControl( { label, options, onChange, - onInputChange: onInputChangeProp = () => {}, + onFilterValueChange, hideLabelFromVision, help, messages = { @@ -31,11 +31,10 @@ function ComboboxControl( { const [ isExpanded, setIsExpanded ] = useState( false ); const [ inputValue, setInputValue ] = useState( '' ); const inputContainer = useRef(); + const currentOption = options.find( ( option ) => option.value === value ); + const currentLabel = currentOption?.label ?? ''; const matchingSuggestions = useMemo( () => { - if ( ! inputValue || inputValue.length === 0 ) { - return options.filter( ( option ) => option.value !== value ); - } const startsWithMatch = []; const containsMatch = []; const match = inputValue.toLocaleLowerCase(); @@ -55,7 +54,7 @@ function ComboboxControl( { onChange( newSelectedSuggestion.value ); speak( messages.selected, 'assertive' ); setSelectedSuggestion( newSelectedSuggestion ); - setInputValue( selectedSuggestion.label ); + setInputValue( '' ); setIsExpanded( false ); }; @@ -109,33 +108,20 @@ function ComboboxControl( { // TODO: TokenInput should preferably forward ref inputContainer.current.input.focus(); setIsExpanded( true ); + onFilterValueChange( '' ); }; const onBlur = () => { - const currentOption = options.find( - ( option ) => option.value === value - ); - setInputValue( currentOption?.label ?? '' ); setIsExpanded( false ); }; const onInputChange = ( event ) => { const text = event.value; setInputValue( text ); - onInputChangeProp( text ); + onFilterValueChange( text ); setIsExpanded( true ); }; - // Reset the value on change - useEffect( () => { - if ( matchingSuggestions.indexOf( selectedSuggestion ) === -1 ) { - setSelectedSuggestion( null ); - } - if ( ! inputValue || matchingSuggestions.length === 0 ) { - onChange( null ); - } - }, [ matchingSuggestions, inputValue, value ] ); - // Announcements useEffect( () => { const hasMatchingSuggestions = matchingSuggestions.length > 0; @@ -179,7 +165,7 @@ function ComboboxControl( { className="components-combobox-control__input" instanceId={ instanceId } ref={ inputContainer } - value={ inputValue } + value={ isExpanded ? inputValue : currentLabel } onBlur={ onBlur } isExpanded={ isExpanded } selectedSuggestionIndex={ matchingSuggestions.indexOf( diff --git a/packages/components/src/combobox-control/stories/index.js b/packages/components/src/combobox-control/stories/index.js index dbe474fb2b6d01..a7f250e38573e1 100644 --- a/packages/components/src/combobox-control/stories/index.js +++ b/packages/components/src/combobox-control/stories/index.js @@ -276,7 +276,7 @@ function ComboboxControlWithState() { onChange={ setValue } label="Select a country" options={ filteredOptions } - onInputChange={ ( filter ) => + onFilterValueChange={ ( filter ) => setFilteredOptions( countries .filter( ( country ) => diff --git a/packages/components/src/combobox-control/style.scss b/packages/components/src/combobox-control/style.scss index 7015bca4e3a097..2dbfcb3b70c35b 100644 --- a/packages/components/src/combobox-control/style.scss +++ b/packages/components/src/combobox-control/style.scss @@ -2,10 +2,14 @@ width: 100%; } -.components-combobox-control__input { +input.components-combobox-control__input[type="text"] { width: 100%; border: none; box-shadow: none; + padding: 2px; + margin: 0; + line-height: inherit; + min-height: auto; &:focus { outline: none; diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 8eaedd8dbf1da1..cd56f12bfc0526 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -44,6 +44,9 @@ export const isRequestingEmbedPreview = createRegistrySelector( * @return {Array} Authors list. */ export function getAuthors( state ) { + deprecated( "select( 'core' ).getAuthors()", { + alternative: "select( 'core' ).getUsers({ who: 'authors' })", + } ); return getUserQueryResults( state, 'authors' ); } diff --git a/packages/editor/src/components/post-author/check.js b/packages/editor/src/components/post-author/check.js index 45475476545a04..8ee69ff4826c99 100644 --- a/packages/editor/src/components/post-author/check.js +++ b/packages/editor/src/components/post-author/check.js @@ -19,7 +19,7 @@ export function PostAuthorCheck( { authors, children, } ) { - if ( ! hasAssignAuthorAction || authors.length < 2 ) { + if ( ! hasAssignAuthorAction || ! authors || authors.length < 2 ) { return null; } @@ -40,7 +40,7 @@ export default compose( [ false ), postType: select( 'core/editor' ).getCurrentPostType(), - authors: select( 'core' ).getAuthors(), + authors: select( 'core' ).getUsers( { who: 'authors' } ), }; } ), withInstanceId, diff --git a/packages/editor/src/components/post-author/index.js b/packages/editor/src/components/post-author/index.js index b99f3f80ce4510..d97a6223fd232f 100644 --- a/packages/editor/src/components/post-author/index.js +++ b/packages/editor/src/components/post-author/index.js @@ -1,71 +1,112 @@ +/** + * External dependencies + */ +import { debounce } from 'lodash'; + /** * WordPress dependencies */ +import { useState, useMemo, useEffect } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { withInstanceId, compose } from '@wordpress/compose'; -import { Component } from '@wordpress/element'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; +import { ComboboxControl } from '@wordpress/components'; /** * Internal dependencies */ import PostAuthorCheck from './check'; -export class PostAuthor extends Component { - constructor() { - super( ...arguments ); +function PostAuthor() { + const [ fieldValue, setFieldValue ] = useState(); - this.setAuthorId = this.setAuthorId.bind( this ); - } + const { authorId, isLoading, authors, postAuthor } = useSelect( + ( select ) => { + const { getUser, getUsers, isResolving } = select( 'core' ); + const { getEditedPostAttribute } = select( 'core/editor' ); + const author = getUser( getEditedPostAttribute( 'author' ) ); + const query = + ! fieldValue || '' === fieldValue ? {} : { search: fieldValue }; + return { + authorId: getEditedPostAttribute( 'author' ), + postAuthor: author, + authors: getUsers( { who: 'authors', ...query } ), + isLoading: isResolving( 'core', 'getUsers', [ + { search: fieldValue, who: 'authors' }, + ] ), + }; + }, + [ fieldValue ] + ); + const { editPost } = useDispatch( 'core/editor' ); - setAuthorId( event ) { - const { onUpdateAuthor } = this.props; - const { value } = event.target; - onUpdateAuthor( Number( value ) ); - } + const authorOptions = useMemo( () => { + const fetchedAuthors = ( authors ?? [] ).map( ( author ) => { + return { + value: author.id, + label: author.name, + }; + } ); + + // Ensure the current author is included in the dropdown list. + const foundAuthor = fetchedAuthors.findIndex( + ( { value } ) => postAuthor?.id === value + ); - render() { - const { postAuthor, instanceId, authors } = this.props; - const selectId = 'post-author-selector-' + instanceId; + if ( foundAuthor < 0 && postAuthor ) { + return [ + { value: postAuthor.id, label: postAuthor.name }, + ...fetchedAuthors, + ]; + } - // Disable reason: A select with an onchange throws a warning + return fetchedAuthors; + }, [ authors, postAuthor ] ); - /* eslint-disable jsx-a11y/no-onchange */ - return ( - - - - - ); - /* eslint-enable jsx-a11y/no-onchange */ + // Initializes the post author properly + // Also ensures external changes are reflected. + useEffect( () => { + if ( postAuthor ) { + setFieldValue( postAuthor.name ); + } + }, [ postAuthor ] ); + + /** + * Handle author selection. + * + * @param {number} postAuthorId The selected Author. + */ + const handleSelect = ( postAuthorId ) => { + if ( ! postAuthorId ) { + return; + } + editPost( { author: postAuthorId } ); + }; + + /** + * Handle user input. + * + * @param {string} inputValue The current value of the input field. + */ + const handleKeydown = ( inputValue ) => { + setFieldValue( inputValue ); + }; + + if ( ! postAuthor ) { + return null; } + + return ( + + + + ); } -export default compose( [ - withSelect( ( select ) => { - return { - postAuthor: select( 'core/editor' ).getEditedPostAttribute( - 'author' - ), - authors: select( 'core' ).getAuthors(), - }; - } ), - withDispatch( ( dispatch ) => ( { - onUpdateAuthor( author ) { - dispatch( 'core/editor' ).editPost( { author } ); - }, - } ) ), - withInstanceId, -] )( PostAuthor ); +export default PostAuthor; diff --git a/packages/editor/src/components/post-author/test/index.js b/packages/editor/src/components/post-author/test/index.js deleted file mode 100644 index 368be4fda7d26c..00000000000000 --- a/packages/editor/src/components/post-author/test/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { PostAuthor } from '../'; - -describe( 'PostAuthor', () => { - const authors = [ - { - id: 1, - name: 'admin', - capabilities: { - level_1: true, - }, - }, - { - id: 2, - name: 'subscriber', - capabilities: { - level_0: true, - }, - }, - { - id: 3, - name: 'andrew', - capabilities: { - level_1: true, - }, - }, - ]; - - const user = { - data: { - capabilities: { - publish_posts: true, - }, - }, - }; - - describe( '#render()', () => { - it( 'should update author', () => { - const onUpdateAuthor = jest.fn(); - const wrapper = shallow( - - ); - - wrapper.find( 'select' ).simulate( 'change', { - target: { - value: '3', - }, - } ); - - expect( onUpdateAuthor ).toHaveBeenCalledWith( 3 ); - } ); - } ); -} );