Skip to content

Commit 69b4835

Browse files
authored
Merge 1f8bba9 into 4b9625f
2 parents 4b9625f + 1f8bba9 commit 69b4835

8 files changed

Lines changed: 136 additions & 128 deletions

File tree

‎packages/editor/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Enhancements
6+
7+
- `PostAuthor`: The field renders with the `SearchableSelect` component of `@wordpress/ui` instead of `ComboboxControl` and `SelectControl` ([#83323](https://github.com/WordPress/gutenberg/pull/83323)).
8+
59
## 15.1.0 (2026-09-23)
610

711
### Enhancements

‎packages/editor/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ _Returns_
978978

979979
### PostAuthor
980980

981-
Renders the component for selecting the post author.
981+
Renders the control for selecting the post author.
982982

983983
_Returns_
984984

‎packages/editor/src/components/post-author/combobox.jsx‎

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1-
import { __ } from '@wordpress/i18n';
21
import { useMemo } from '@wordpress/element';
32
import { useSelect } from '@wordpress/data';
43
import { decodeEntities } from '@wordpress/html-entities';
54
import { store as coreStore } from '@wordpress/core-data';
65
import { store as editorStore } from '../../store';
76
import { AUTHORS_QUERY, BASE_QUERY } from './constants';
87

8+
/**
9+
* Maps an author record to the `{ value, label }` shape the select works with.
10+
*
11+
* @param {Object} author The author record.
12+
*
13+
* @return {{value: string, label: string}} The select item.
14+
*/
15+
const authorToItem = ( author ) => ( {
16+
value: String( author.id ),
17+
label: decodeEntities( author.name ),
18+
} );
19+
20+
/**
21+
* Queries the authors available for the post, optionally narrowed by a search
22+
* term.
23+
*
24+
* @param {string} [search] The term to search authors by.
25+
*
26+
* @return {{items: Object[], value: ?Object, isLoading: boolean}} The select
27+
* items, the currently selected item, and whether a query is in flight.
28+
*/
929
export function useAuthorsQuery( search ) {
10-
const { authorId, authors, postAuthor, isLoading } = useSelect(
30+
const { authors, postAuthor, isLoading } = useSelect(
1131
( select ) => {
1232
const { getUser, getUsers, isResolving } = select( coreStore );
1333
const { getEditedPostAttribute } = select( editorStore );
@@ -20,7 +40,6 @@ export function useAuthorsQuery( search ) {
2040
}
2141

2242
return {
23-
authorId: _authorId,
2443
authors: getUsers( query ),
2544
postAuthor: getUser( _authorId, BASE_QUERY ),
2645
isLoading: isResolving( 'getUsers', [ query ] ),
@@ -29,38 +48,28 @@ export function useAuthorsQuery( search ) {
2948
[ search ]
3049
);
3150

32-
const authorOptions = useMemo( () => {
33-
const fetchedAuthors = ( authors ?? [] ).map( ( author ) => {
34-
return {
35-
value: author.id,
36-
label: decodeEntities( author.name ),
37-
};
38-
} );
39-
40-
// Ensure the current author is included in the dropdown list.
41-
const foundAuthor = fetchedAuthors.findIndex(
42-
( { value } ) => postAuthor?.id === value
43-
);
51+
const items = useMemo( () => {
52+
const fetchedAuthors = ( authors ?? [] ).map( authorToItem );
4453

45-
let currentAuthor = [];
46-
if ( foundAuthor < 0 && postAuthor ) {
47-
currentAuthor = [
48-
{
49-
value: postAuthor.id,
50-
label: decodeEntities( postAuthor.name ),
51-
},
52-
];
53-
} else if ( foundAuthor < 0 && ! postAuthor ) {
54-
currentAuthor = [
55-
{
56-
value: 0,
57-
label: __( '(No author)' ),
58-
},
59-
];
54+
// Ensure the current author is listed, unless these are search results,
55+
// where an author that does not match would be noise.
56+
if (
57+
! search &&
58+
postAuthor &&
59+
! fetchedAuthors.some(
60+
( { value } ) => value === String( postAuthor.id )
61+
)
62+
) {
63+
return [ authorToItem( postAuthor ), ...fetchedAuthors ];
6064
}
6165

62-
return [ ...currentAuthor, ...fetchedAuthors ];
63-
}, [ authors, postAuthor ] );
66+
return fetchedAuthors;
67+
}, [ authors, postAuthor, search ] );
68+
69+
const value = useMemo(
70+
() => ( postAuthor ? authorToItem( postAuthor ) : null ),
71+
[ postAuthor ]
72+
);
6473

65-
return { authorId, authorOptions, postAuthor, isLoading };
74+
return { items, value, isLoading };
6675
}
Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,98 @@
1-
import { useSelect } from '@wordpress/data';
2-
import { store as coreStore } from '@wordpress/core-data';
3-
import PostAuthorCombobox from './combobox';
4-
import PostAuthorSelect from './select';
5-
import { AUTHORS_QUERY } from './constants';
1+
import { __, _n, sprintf } from '@wordpress/i18n';
2+
import { useState } from '@wordpress/element';
3+
import { useDispatch } from '@wordpress/data';
4+
import { useDebounce } from '@wordpress/compose';
5+
import {
6+
// eslint-disable-next-line @wordpress/use-recommended-components -- SearchableSelect is the only searchable single-select in `@wordpress/ui`.
7+
SearchableSelect,
8+
Spinner,
9+
Stack,
10+
VisuallyHidden,
11+
} from '@wordpress/ui';
12+
import { store as editorStore } from '../../store';
13+
import { useAuthorsQuery } from './hook';
614

7-
const minimumUsersForCombobox = 25;
15+
const EMPTY_ARRAY = [];
16+
const SEARCH_DEBOUNCE_MS = 300;
17+
18+
const isSameAuthor = ( authorA, authorB ) => authorA.value === authorB.value;
819

920
/**
10-
* Renders the component for selecting the post author.
21+
* Renders the control for selecting the post author.
1122
*
1223
* @return {React.ReactNode} The rendered component.
1324
*/
14-
function PostAuthor() {
15-
const showCombobox = useSelect( ( select ) => {
16-
const authors = select( coreStore ).getUsers( AUTHORS_QUERY );
25+
function PostAuthorControl() {
26+
const [ inputValue, setInputValue ] = useState( '' );
27+
const [ search, setSearch ] = useState( '' );
28+
const debouncedSetSearch = useDebounce( setSearch, SEARCH_DEBOUNCE_MS );
29+
30+
const { editPost } = useDispatch( editorStore );
31+
const { items, value, isLoading } = useAuthorsQuery( search );
32+
33+
// Results lag the input by the debounce and the request.
34+
const isSearching = isLoading || inputValue !== search;
35+
// Nothing filters on the client, so stale results have to go.
36+
const authors = isSearching ? EMPTY_ARRAY : items;
37+
38+
function onInputValueChange( nextInputValue ) {
39+
setInputValue( nextInputValue );
1740

18-
return authors?.length >= minimumUsersForCombobox;
19-
}, [] );
41+
// Cleared by the user, or after a selection.
42+
if ( ! nextInputValue ) {
43+
debouncedSetSearch.cancel();
44+
setSearch( '' );
45+
return;
46+
}
2047

21-
if ( showCombobox ) {
22-
return <PostAuthorCombobox />;
48+
debouncedSetSearch( nextInputValue );
2349
}
24-
return <PostAuthorSelect />;
50+
51+
function onValueChange( author ) {
52+
if ( ! author ) {
53+
return;
54+
}
55+
56+
editPost( { author: Number( author.value ) } );
57+
}
58+
59+
return (
60+
<SearchableSelect
61+
aria-label={ __( 'Author' ) }
62+
// Authors are searched through the REST API.
63+
filter={ null }
64+
items={ authors }
65+
value={ value }
66+
onValueChange={ onValueChange }
67+
isItemEqualToValue={ isSameAuthor }
68+
inputValue={ inputValue }
69+
onInputValueChange={ onInputValueChange }
70+
placeholder={ __( 'Select author' ) }
71+
statusContent={
72+
isSearching ? (
73+
<Stack direction="row" gap="sm" align="center">
74+
<Spinner />
75+
{ __( 'Searching…' ) }
76+
</Stack>
77+
) : (
78+
authors.length > 0 && (
79+
<VisuallyHidden>
80+
{ sprintf(
81+
/* translators: %d: number of results. */
82+
_n(
83+
'%d result found.',
84+
'%d results found.',
85+
authors.length
86+
),
87+
authors.length
88+
) }
89+
</VisuallyHidden>
90+
)
91+
)
92+
}
93+
emptyContent={ isSearching ? null : __( 'No authors found.' ) }
94+
/>
95+
);
2596
}
2697

27-
export default PostAuthor;
98+
export default PostAuthorControl;

‎packages/editor/src/components/post-author/panel.jsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '
66
import { useSelect } from '@wordpress/data';
77
import { store as coreStore } from '@wordpress/core-data';
88
import PostAuthorCheck from './check';
9-
import PostAuthorForm from './index';
9+
import PostAuthorControl from './index';
1010
import PostPanelRow from '../post-panel-row';
1111
import { BASE_QUERY } from './constants';
1212
import { store as editorStore } from '../../store';
@@ -76,7 +76,7 @@ export function PostAuthor() {
7676
title={ __( 'Author' ) }
7777
onClose={ onClose }
7878
/>
79-
<PostAuthorForm onClose={ onClose } />
79+
<PostAuthorControl />
8080
</div>
8181
) }
8282
/>

‎packages/editor/src/components/post-author/select.jsx‎

Lines changed: 0 additions & 26 deletions
This file was deleted.

‎tools/eslint/suppressions.json‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6285,11 +6285,6 @@
62856285
"count": 1
62866286
}
62876287
},
6288-
"packages/editor/src/components/post-author/combobox.jsx": {
6289-
"react/jsx-filename-extension": {
6290-
"count": 1
6291-
}
6292-
},
62936288
"packages/editor/src/components/post-author/index.jsx": {
62946289
"react/jsx-filename-extension": {
62956290
"count": 1
@@ -6300,11 +6295,6 @@
63006295
"count": 1
63016296
}
63026297
},
6303-
"packages/editor/src/components/post-author/select.jsx": {
6304-
"react/jsx-filename-extension": {
6305-
"count": 1
6306-
}
6307-
},
63086298
"packages/editor/src/components/post-author/test/check.jsdom.test.jsx": {
63096299
"react/jsx-filename-extension": {
63106300
"count": 1

0 commit comments

Comments
 (0)