@wordpress/commands 1.42.0 → 1.43.1-next.v.202604091042.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/build/components/command-menu.cjs +156 -154
- package/build/components/command-menu.cjs.map +3 -3
- package/build/components/use-recent-commands.cjs +125 -0
- package/build/components/use-recent-commands.cjs.map +7 -0
- package/build/store/index.cjs +2 -0
- package/build/store/index.cjs.map +2 -2
- package/build/store/private-actions.cjs +11 -2
- package/build/store/private-actions.cjs.map +2 -2
- package/build/store/private-selectors.cjs +32 -0
- package/build/store/private-selectors.cjs.map +7 -0
- package/build/store/reducer.cjs +12 -1
- package/build/store/reducer.cjs.map +2 -2
- package/build-module/components/command-menu.mjs +160 -154
- package/build-module/components/command-menu.mjs.map +2 -2
- package/build-module/components/use-recent-commands.mjs +104 -0
- package/build-module/components/use-recent-commands.mjs.map +7 -0
- package/build-module/store/index.mjs +2 -0
- package/build-module/store/index.mjs.map +2 -2
- package/build-module/store/private-actions.mjs +9 -1
- package/build-module/store/private-actions.mjs.map +2 -2
- package/build-module/store/private-selectors.mjs +8 -0
- package/build-module/store/private-selectors.mjs.map +7 -0
- package/build-module/store/reducer.mjs +12 -1
- package/build-module/store/reducer.mjs.map +2 -2
- package/build-style/style-rtl.css +25 -21
- package/build-style/style.css +25 -21
- package/package.json +12 -11
- package/src/components/command-menu.js +179 -161
- package/src/components/style.scss +32 -29
- package/src/components/use-recent-commands.js +131 -0
- package/src/store/index.js +2 -0
- package/src/store/private-actions.js +16 -0
- package/src/store/private-selectors.js +10 -0
- package/src/store/reducer.js +13 -0
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
useState,
|
|
13
13
|
useEffect,
|
|
14
14
|
useRef,
|
|
15
|
-
useCallback,
|
|
16
|
-
useMemo,
|
|
17
15
|
isValidElement,
|
|
18
16
|
Component,
|
|
19
17
|
} from '@wordpress/element';
|
|
@@ -35,9 +33,16 @@ import { Icon, search as inputIcon, arrowRight } from '@wordpress/icons';
|
|
|
35
33
|
*/
|
|
36
34
|
import { store as commandsStore } from '../store';
|
|
37
35
|
import { unlock } from '../lock-unlock';
|
|
36
|
+
import {
|
|
37
|
+
recordUsage,
|
|
38
|
+
useLoaderCollector,
|
|
39
|
+
useRecentCommands,
|
|
40
|
+
} from './use-recent-commands';
|
|
38
41
|
|
|
39
42
|
const { withIgnoreIMEEvents } = unlock( componentsPrivateApis );
|
|
40
43
|
|
|
44
|
+
// Namespaces item ids to avoid collisions with other elements on the page.
|
|
45
|
+
const ITEM_ID_PREFIX = 'command-palette-item-';
|
|
41
46
|
const inputLabel = __( 'Search commands and settings' );
|
|
42
47
|
|
|
43
48
|
/**
|
|
@@ -80,18 +85,62 @@ export function isValidIcon( icon ) {
|
|
|
80
85
|
);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
function CommandItem( { command, search, category, valuePrefix } ) {
|
|
89
|
+
const { close } = useDispatch( commandsStore );
|
|
90
|
+
const commandCategory = category ?? command.category;
|
|
91
|
+
const label = command.searchLabel ?? command.label;
|
|
92
|
+
const value = valuePrefix ? `${ valuePrefix }${ command.name }` : label;
|
|
93
|
+
return (
|
|
94
|
+
<Command.Item
|
|
95
|
+
key={ command.name }
|
|
96
|
+
id={ `${ ITEM_ID_PREFIX }${ value.toLowerCase() }` }
|
|
97
|
+
value={ value }
|
|
98
|
+
keywords={
|
|
99
|
+
valuePrefix
|
|
100
|
+
? [ ...( command.keywords ?? [] ), label ]
|
|
101
|
+
: command.keywords
|
|
102
|
+
}
|
|
103
|
+
onSelect={ () => {
|
|
104
|
+
recordUsage( command.name );
|
|
105
|
+
command.callback( { close } );
|
|
106
|
+
} }
|
|
107
|
+
>
|
|
108
|
+
<HStack
|
|
109
|
+
alignment="left"
|
|
110
|
+
className={ clsx( 'commands-command-menu__item', {
|
|
111
|
+
'has-icon':
|
|
112
|
+
CATEGORY_ICONS[ commandCategory ] || command.icon,
|
|
113
|
+
} ) }
|
|
114
|
+
>
|
|
115
|
+
{ CATEGORY_ICONS[ commandCategory ] ? (
|
|
116
|
+
<Icon icon={ CATEGORY_ICONS[ commandCategory ] } />
|
|
117
|
+
) : (
|
|
118
|
+
isValidIcon( command.icon ) && (
|
|
119
|
+
<Icon icon={ command.icon } />
|
|
120
|
+
)
|
|
121
|
+
) }
|
|
122
|
+
<span className="commands-command-menu__item-label">
|
|
123
|
+
<TextHighlight
|
|
124
|
+
text={ command.label }
|
|
125
|
+
highlight={ search }
|
|
126
|
+
/>
|
|
127
|
+
</span>
|
|
128
|
+
{ CATEGORY_LABELS[ commandCategory ] && (
|
|
129
|
+
<span className="commands-command-menu__item-category">
|
|
130
|
+
{ CATEGORY_LABELS[ commandCategory ] }
|
|
131
|
+
</span>
|
|
132
|
+
) }
|
|
133
|
+
</HStack>
|
|
134
|
+
</Command.Item>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function CommandMenuLoader( { name, search, hook, category, valuePrefix } ) {
|
|
139
|
+
const { setLoaderLoading } = unlock( useDispatch( commandsStore ) );
|
|
140
|
+
const { isLoading: loading, commands = [] } = hook( { search } ) ?? {};
|
|
92
141
|
useEffect( () => {
|
|
93
|
-
|
|
94
|
-
}, [
|
|
142
|
+
setLoaderLoading( name, loading );
|
|
143
|
+
}, [ setLoaderLoading, name, loading ] );
|
|
95
144
|
|
|
96
145
|
if ( ! commands.length ) {
|
|
97
146
|
return null;
|
|
@@ -99,64 +148,24 @@ function CommandMenuLoader( {
|
|
|
99
148
|
|
|
100
149
|
return (
|
|
101
150
|
<>
|
|
102
|
-
{ commands.map( ( command ) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
>
|
|
112
|
-
<HStack
|
|
113
|
-
alignment="left"
|
|
114
|
-
className={ clsx( 'commands-command-menu__item', {
|
|
115
|
-
'has-icon':
|
|
116
|
-
CATEGORY_ICONS[ commandCategory ] ||
|
|
117
|
-
command.icon,
|
|
118
|
-
} ) }
|
|
119
|
-
>
|
|
120
|
-
{ CATEGORY_ICONS[ commandCategory ] && (
|
|
121
|
-
<Icon
|
|
122
|
-
icon={ CATEGORY_ICONS[ commandCategory ] }
|
|
123
|
-
/>
|
|
124
|
-
) }
|
|
125
|
-
{ ! CATEGORY_ICONS[ commandCategory ] &&
|
|
126
|
-
isValidIcon( command.icon ) && (
|
|
127
|
-
<Icon icon={ command.icon } />
|
|
128
|
-
) }
|
|
129
|
-
<span className="commands-command-menu__item-label">
|
|
130
|
-
<TextHighlight
|
|
131
|
-
text={ command.label }
|
|
132
|
-
highlight={ search }
|
|
133
|
-
/>
|
|
134
|
-
</span>
|
|
135
|
-
{ CATEGORY_LABELS[ commandCategory ] && (
|
|
136
|
-
<span className="commands-command-menu__item-category">
|
|
137
|
-
{ CATEGORY_LABELS[ commandCategory ] }
|
|
138
|
-
</span>
|
|
139
|
-
) }
|
|
140
|
-
</HStack>
|
|
141
|
-
</Command.Item>
|
|
142
|
-
);
|
|
143
|
-
} ) }
|
|
151
|
+
{ commands.map( ( command ) => (
|
|
152
|
+
<CommandItem
|
|
153
|
+
key={ command.name }
|
|
154
|
+
command={ command }
|
|
155
|
+
search={ search }
|
|
156
|
+
category={ command.category ?? category }
|
|
157
|
+
valuePrefix={ valuePrefix }
|
|
158
|
+
/>
|
|
159
|
+
) ) }
|
|
144
160
|
</>
|
|
145
161
|
);
|
|
146
162
|
}
|
|
147
163
|
|
|
148
|
-
|
|
149
|
-
hook,
|
|
150
|
-
search,
|
|
151
|
-
setLoader,
|
|
152
|
-
close,
|
|
153
|
-
category,
|
|
154
|
-
} ) {
|
|
164
|
+
function CommandMenuLoaderWrapper( { hook, ...props } ) {
|
|
155
165
|
// The "hook" prop is actually a custom React hook
|
|
156
166
|
// so to avoid breaking the rules of hooks
|
|
157
167
|
// the CommandMenuLoaderWrapper component need to be
|
|
158
|
-
// remounted on each hook prop change
|
|
159
|
-
// We use the key state to make sure we do that properly.
|
|
168
|
+
// remounted on each hook prop change.
|
|
160
169
|
const currentLoaderRef = useRef( hook );
|
|
161
170
|
const [ key, setKey ] = useState( 0 );
|
|
162
171
|
useEffect( () => {
|
|
@@ -170,96 +179,123 @@ export function CommandMenuLoaderWrapper( {
|
|
|
170
179
|
<CommandMenuLoader
|
|
171
180
|
key={ key }
|
|
172
181
|
hook={ currentLoaderRef.current }
|
|
173
|
-
|
|
174
|
-
setLoader={ setLoader }
|
|
175
|
-
close={ close }
|
|
176
|
-
category={ category }
|
|
182
|
+
{ ...props }
|
|
177
183
|
/>
|
|
178
184
|
);
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
function CommandList( { search, commands, loaders, valuePrefix } ) {
|
|
188
|
+
return (
|
|
189
|
+
<>
|
|
190
|
+
{ commands.map( ( command ) => (
|
|
191
|
+
<CommandItem
|
|
192
|
+
key={ command.name }
|
|
193
|
+
command={ command }
|
|
194
|
+
search={ search }
|
|
195
|
+
valuePrefix={ valuePrefix }
|
|
196
|
+
/>
|
|
197
|
+
) ) }
|
|
198
|
+
{ loaders.map( ( loader ) => (
|
|
199
|
+
<CommandMenuLoaderWrapper
|
|
200
|
+
key={ loader.name }
|
|
201
|
+
name={ loader.name }
|
|
202
|
+
search={ search }
|
|
203
|
+
hook={ loader.hook }
|
|
204
|
+
category={ loader.category }
|
|
205
|
+
valuePrefix={ valuePrefix }
|
|
206
|
+
/>
|
|
207
|
+
) ) }
|
|
208
|
+
</>
|
|
191
209
|
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function RecentLoaderRunner( { hook, name, filterNames, onResolved } ) {
|
|
213
|
+
useLoaderCollector( hook, name, filterNames, onResolved );
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function RecentGroup() {
|
|
218
|
+
const { commands, loaders, recentSet, onResolved } = useRecentCommands();
|
|
192
219
|
|
|
193
220
|
if ( ! commands.length && ! loaders.length ) {
|
|
194
221
|
return null;
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
return (
|
|
198
|
-
<Command.Group>
|
|
199
|
-
{ commands.map( ( command ) => (
|
|
200
|
-
<Command.Item
|
|
201
|
-
key={ command.name }
|
|
202
|
-
value={ command.searchLabel ?? command.label }
|
|
203
|
-
keywords={ command.keywords }
|
|
204
|
-
onSelect={ () => command.callback( { close } ) }
|
|
205
|
-
id={ command.name }
|
|
206
|
-
>
|
|
207
|
-
<HStack
|
|
208
|
-
alignment="left"
|
|
209
|
-
className={ clsx( 'commands-command-menu__item', {
|
|
210
|
-
'has-icon':
|
|
211
|
-
CATEGORY_ICONS[ command.category ] ||
|
|
212
|
-
command.icon,
|
|
213
|
-
} ) }
|
|
214
|
-
>
|
|
215
|
-
{ CATEGORY_ICONS[ command.category ] ? (
|
|
216
|
-
<Icon icon={ CATEGORY_ICONS[ command.category ] } />
|
|
217
|
-
) : (
|
|
218
|
-
command.icon && <Icon icon={ command.icon } />
|
|
219
|
-
) }
|
|
220
|
-
<span>
|
|
221
|
-
<TextHighlight
|
|
222
|
-
text={ command.label }
|
|
223
|
-
highlight={ search }
|
|
224
|
-
/>
|
|
225
|
-
</span>
|
|
226
|
-
{ CATEGORY_LABELS[ command.category ] && (
|
|
227
|
-
<span className="commands-command-menu__item-category">
|
|
228
|
-
{ CATEGORY_LABELS[ command.category ] }
|
|
229
|
-
</span>
|
|
230
|
-
) }
|
|
231
|
-
</HStack>
|
|
232
|
-
</Command.Item>
|
|
233
|
-
) ) }
|
|
225
|
+
<Command.Group heading={ __( 'Recent' ) }>
|
|
234
226
|
{ loaders.map( ( loader ) => (
|
|
235
|
-
<
|
|
227
|
+
<RecentLoaderRunner
|
|
236
228
|
key={ loader.name }
|
|
229
|
+
name={ loader.name }
|
|
237
230
|
hook={ loader.hook }
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
filterNames={ recentSet }
|
|
232
|
+
onResolved={ onResolved }
|
|
233
|
+
/>
|
|
234
|
+
) ) }
|
|
235
|
+
{ commands.map( ( command ) => (
|
|
236
|
+
<CommandItem
|
|
237
|
+
key={ command.name }
|
|
238
|
+
command={ command }
|
|
239
|
+
search=""
|
|
240
|
+
valuePrefix="recent-"
|
|
242
241
|
/>
|
|
243
242
|
) ) }
|
|
244
243
|
</Command.Group>
|
|
245
244
|
);
|
|
246
245
|
}
|
|
247
246
|
|
|
248
|
-
function
|
|
247
|
+
function SuggestionsGroup() {
|
|
248
|
+
const { commands, loaders } = useSelect( ( select ) => {
|
|
249
|
+
const { getCommands, getCommandLoaders } = select( commandsStore );
|
|
250
|
+
return {
|
|
251
|
+
commands: getCommands( true ),
|
|
252
|
+
loaders: getCommandLoaders( true ),
|
|
253
|
+
};
|
|
254
|
+
}, [] );
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Command.Group heading={ __( 'Suggestions' ) }>
|
|
258
|
+
<CommandList search="" commands={ commands } loaders={ loaders } />
|
|
259
|
+
</Command.Group>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function ResultsGroup( { search } ) {
|
|
264
|
+
const { commands, contextualCommands, loaders, contextualLoaders } =
|
|
265
|
+
useSelect( ( select ) => {
|
|
266
|
+
const { getCommands, getCommandLoaders } = select( commandsStore );
|
|
267
|
+
return {
|
|
268
|
+
commands: getCommands( false ),
|
|
269
|
+
contextualCommands: getCommands( true ),
|
|
270
|
+
loaders: getCommandLoaders( false ),
|
|
271
|
+
contextualLoaders: getCommandLoaders( true ),
|
|
272
|
+
};
|
|
273
|
+
}, [] );
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<Command.Group heading={ __( 'Results' ) }>
|
|
277
|
+
<CommandList
|
|
278
|
+
search={ search }
|
|
279
|
+
commands={ commands }
|
|
280
|
+
loaders={ loaders }
|
|
281
|
+
/>
|
|
282
|
+
<CommandList
|
|
283
|
+
search={ search }
|
|
284
|
+
commands={ contextualCommands }
|
|
285
|
+
loaders={ contextualLoaders }
|
|
286
|
+
/>
|
|
287
|
+
</Command.Group>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function CommandInput( { search, setSearch } ) {
|
|
249
292
|
const commandMenuInput = useRef();
|
|
250
293
|
const _value = useCommandState( ( state ) => state.value );
|
|
251
|
-
const selectedItemId =
|
|
252
|
-
const item = document.querySelector(
|
|
253
|
-
`[cmdk-item=""][data-value="${ _value }"]`
|
|
254
|
-
);
|
|
255
|
-
return item?.getAttribute( 'id' );
|
|
256
|
-
}, [ _value ] );
|
|
294
|
+
const selectedItemId = _value ? `${ ITEM_ID_PREFIX }${ _value }` : null;
|
|
257
295
|
useEffect( () => {
|
|
258
296
|
// Focus the command palette input when mounting the modal.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
}, [ isOpen ] );
|
|
297
|
+
commandMenuInput.current.focus();
|
|
298
|
+
}, [] );
|
|
263
299
|
return (
|
|
264
300
|
<Command.Input
|
|
265
301
|
ref={ commandMenuInput }
|
|
@@ -277,12 +313,14 @@ function CommandInput( { isOpen, search, setSearch } ) {
|
|
|
277
313
|
export function CommandMenu() {
|
|
278
314
|
const { registerShortcut } = useDispatch( keyboardShortcutsStore );
|
|
279
315
|
const [ search, setSearch ] = useState( '' );
|
|
280
|
-
const isOpen = useSelect(
|
|
281
|
-
( select ) =>
|
|
316
|
+
const { isOpen: paletteIsOpen, loadersLoading } = useSelect(
|
|
317
|
+
( select ) => ( {
|
|
318
|
+
isOpen: select( commandsStore ).isOpen(),
|
|
319
|
+
loadersLoading: unlock( select( commandsStore ) ).isLoading(),
|
|
320
|
+
} ),
|
|
282
321
|
[]
|
|
283
322
|
);
|
|
284
323
|
const { open, close } = useDispatch( commandsStore );
|
|
285
|
-
const [ loaders, setLoaders ] = useState( {} );
|
|
286
324
|
|
|
287
325
|
useEffect( () => {
|
|
288
326
|
registerShortcut( {
|
|
@@ -306,7 +344,7 @@ export function CommandMenu() {
|
|
|
306
344
|
}
|
|
307
345
|
|
|
308
346
|
event.preventDefault();
|
|
309
|
-
if (
|
|
347
|
+
if ( paletteIsOpen ) {
|
|
310
348
|
close();
|
|
311
349
|
} else {
|
|
312
350
|
open();
|
|
@@ -317,35 +355,26 @@ export function CommandMenu() {
|
|
|
317
355
|
}
|
|
318
356
|
);
|
|
319
357
|
|
|
320
|
-
const setLoader = useCallback(
|
|
321
|
-
( name, value ) =>
|
|
322
|
-
setLoaders( ( current ) => ( {
|
|
323
|
-
...current,
|
|
324
|
-
[ name ]: value,
|
|
325
|
-
} ) ),
|
|
326
|
-
[]
|
|
327
|
-
);
|
|
328
358
|
const closeAndReset = () => {
|
|
329
359
|
setSearch( '' );
|
|
330
360
|
close();
|
|
331
361
|
};
|
|
332
362
|
|
|
333
|
-
if ( !
|
|
363
|
+
if ( ! paletteIsOpen ) {
|
|
334
364
|
return false;
|
|
335
365
|
}
|
|
336
366
|
|
|
337
|
-
const isLoading = Object.values( loaders ).some( Boolean );
|
|
338
|
-
|
|
339
367
|
return (
|
|
340
368
|
<Modal
|
|
341
369
|
className="commands-command-menu"
|
|
342
370
|
overlayClassName="commands-command-menu__overlay"
|
|
343
371
|
onRequestClose={ closeAndReset }
|
|
344
372
|
__experimentalHideHeader
|
|
373
|
+
size="medium"
|
|
345
374
|
contentLabel={ __( 'Command palette' ) }
|
|
346
375
|
>
|
|
347
376
|
<div className="commands-command-menu__container">
|
|
348
|
-
<Command label={ inputLabel }>
|
|
377
|
+
<Command label={ inputLabel } loop>
|
|
349
378
|
<div className="commands-command-menu__header">
|
|
350
379
|
<Icon
|
|
351
380
|
className="commands-command-menu__header-search-icon"
|
|
@@ -354,28 +383,17 @@ export function CommandMenu() {
|
|
|
354
383
|
<CommandInput
|
|
355
384
|
search={ search }
|
|
356
385
|
setSearch={ setSearch }
|
|
357
|
-
isOpen={ isOpen }
|
|
358
386
|
/>
|
|
359
387
|
</div>
|
|
360
388
|
<Command.List label={ __( 'Command suggestions' ) }>
|
|
361
|
-
{ search && !
|
|
389
|
+
{ search && ! loadersLoading && (
|
|
362
390
|
<Command.Empty>
|
|
363
391
|
{ __( 'No results found.' ) }
|
|
364
392
|
</Command.Empty>
|
|
365
393
|
) }
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
close={ closeAndReset }
|
|
370
|
-
isContextual
|
|
371
|
-
/>
|
|
372
|
-
{ search && (
|
|
373
|
-
<CommandMenuGroup
|
|
374
|
-
search={ search }
|
|
375
|
-
setLoader={ setLoader }
|
|
376
|
-
close={ closeAndReset }
|
|
377
|
-
/>
|
|
378
|
-
) }
|
|
394
|
+
{ ! search && <RecentGroup /> }
|
|
395
|
+
{ ! search && <SuggestionsGroup /> }
|
|
396
|
+
{ search && <ResultsGroup search={ search } /> }
|
|
379
397
|
</Command.List>
|
|
380
398
|
</Command>
|
|
381
399
|
</div>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
@use "@wordpress/base-styles/variables" as *;
|
|
3
3
|
@use "@wordpress/base-styles/colors" as *;
|
|
4
4
|
|
|
5
|
+
$palette-max-height: 376px;
|
|
6
|
+
$palette-header-height: 48px;
|
|
7
|
+
|
|
5
8
|
// Here we extend the modal styles to be tighter, and to the center. Because the palette uses the modal as a container.
|
|
6
9
|
.commands-command-menu {
|
|
7
|
-
border-radius: $grid-unit-05;
|
|
8
|
-
width: calc(100% - #{$grid-unit-40});
|
|
9
10
|
margin: auto;
|
|
10
|
-
max-width: 400px;
|
|
11
11
|
position: relative;
|
|
12
12
|
top: calc(5% + #{$header-height});
|
|
13
13
|
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
[cmdk-input] {
|
|
62
62
|
border: none;
|
|
63
63
|
width: 100%;
|
|
64
|
-
padding: $grid-unit-
|
|
64
|
+
padding: $grid-unit-15 $grid-unit-05;
|
|
65
65
|
outline: none;
|
|
66
66
|
color: $gray-900;
|
|
67
67
|
margin: 0;
|
|
@@ -80,23 +80,13 @@
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
[cmdk-item] {
|
|
83
|
-
border-radius: $radius-small;
|
|
84
83
|
cursor: pointer;
|
|
85
84
|
display: flex;
|
|
86
85
|
align-items: center;
|
|
86
|
+
padding: $grid-unit-05 0;
|
|
87
87
|
color: $gray-900;
|
|
88
88
|
font-size: $default-font-size;
|
|
89
89
|
|
|
90
|
-
&[aria-selected="true"],
|
|
91
|
-
&:active {
|
|
92
|
-
background: var(--wp-admin-theme-color);
|
|
93
|
-
color: $white;
|
|
94
|
-
|
|
95
|
-
svg {
|
|
96
|
-
fill: $white;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
90
|
&[aria-disabled="true"] {
|
|
101
91
|
color: $gray-600;
|
|
102
92
|
cursor: not-allowed;
|
|
@@ -107,19 +97,34 @@
|
|
|
107
97
|
}
|
|
108
98
|
|
|
109
99
|
> div {
|
|
110
|
-
|
|
100
|
+
border-radius: $radius-small;
|
|
101
|
+
min-height: $grid-unit-40;
|
|
111
102
|
padding: $grid-unit-05;
|
|
112
103
|
padding-left: $grid-unit-50; // Account for commands without icons.
|
|
113
104
|
padding-right: $grid-unit-20; // Accounts for the command category.
|
|
114
105
|
}
|
|
115
106
|
|
|
107
|
+
&[aria-selected="true"] > div,
|
|
108
|
+
&:active > div {
|
|
109
|
+
background: var(--wp-admin-theme-color);
|
|
110
|
+
color: $white;
|
|
111
|
+
|
|
112
|
+
svg {
|
|
113
|
+
fill: $white;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
116
117
|
> .has-icon {
|
|
117
118
|
padding-left: $grid-unit;
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
[cmdk-group]:has([cmdk-group-items]:empty) {
|
|
123
|
+
display: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
[cmdk-root] > [cmdk-list] {
|
|
122
|
-
max-height: $palette-max-height
|
|
127
|
+
max-height: min($palette-max-height - $palette-header-height, calc(70vh - $palette-header-height));
|
|
123
128
|
overflow: auto;
|
|
124
129
|
scroll-padding-top: $grid-unit-10;
|
|
125
130
|
scroll-padding-bottom: $grid-unit-10;
|
|
@@ -130,20 +135,18 @@
|
|
|
130
135
|
border-top: $border-width solid $gray-300;
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
padding-top: $grid-unit-10;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Ensures there is always padding bottom on the last group, when there are commands.
|
|
139
|
-
&
|
|
140
|
-
[cmdk-list-sizer] > [cmdk-group]:last-child
|
|
141
|
-
[cmdk-group-items]:not(:empty) {
|
|
142
|
-
padding-bottom: $grid-unit-10;
|
|
138
|
+
& [cmdk-list-sizer] > [cmdk-group] > [cmdk-group-items]:not(:empty) {
|
|
139
|
+
padding: 0 $grid-unit-10 $grid-unit-05;
|
|
143
140
|
}
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
[cmdk-group-heading] {
|
|
143
|
+
margin-top: $grid-unit-10;
|
|
144
|
+
padding: $grid-unit-10 $grid-unit-20;
|
|
145
|
+
line-height: $grid-unit-20;
|
|
146
|
+
font-size: $font-size-x-small;
|
|
147
|
+
font-weight: $font-weight-medium;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
color: $gray-900;
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
useSelect,
|
|
6
|
+
useDispatch,
|
|
7
|
+
select as globalSelect,
|
|
8
|
+
dispatch,
|
|
9
|
+
} from '@wordpress/data';
|
|
10
|
+
import { store as preferencesStore } from '@wordpress/preferences';
|
|
11
|
+
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Internal dependencies
|
|
15
|
+
*/
|
|
16
|
+
import { store as commandsStore } from '../store';
|
|
17
|
+
import { unlock } from '../lock-unlock';
|
|
18
|
+
|
|
19
|
+
const MAX_RECENTLY_SAVED = 30;
|
|
20
|
+
const MAX_RECENTLY_DISPLAYED = 5;
|
|
21
|
+
const EMPTY_ARRAY = [];
|
|
22
|
+
const EMPTY_SET = new Set();
|
|
23
|
+
|
|
24
|
+
export function recordUsage( name ) {
|
|
25
|
+
const current =
|
|
26
|
+
globalSelect( preferencesStore ).get(
|
|
27
|
+
'core/commands',
|
|
28
|
+
'recentlyUsed'
|
|
29
|
+
) ?? [];
|
|
30
|
+
const next = [ name, ...current.filter( ( n ) => n !== name ) ].slice(
|
|
31
|
+
0,
|
|
32
|
+
MAX_RECENTLY_SAVED
|
|
33
|
+
);
|
|
34
|
+
dispatch( preferencesStore ).set( 'core/commands', 'recentlyUsed', next );
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useLoaderCollector( hook, name, filterNames, onResolved ) {
|
|
38
|
+
const { setLoaderLoading } = unlock( useDispatch( commandsStore ) );
|
|
39
|
+
const { isLoading: loading, commands = [] } = hook( { search: '' } ) ?? {};
|
|
40
|
+
|
|
41
|
+
useEffect( () => {
|
|
42
|
+
setLoaderLoading( name, loading );
|
|
43
|
+
}, [ setLoaderLoading, name, loading ] );
|
|
44
|
+
|
|
45
|
+
const filtered = filterNames
|
|
46
|
+
? commands.filter( ( c ) => filterNames.has( c.name ) )
|
|
47
|
+
: commands;
|
|
48
|
+
|
|
49
|
+
useEffect( () => {
|
|
50
|
+
onResolved( name, filtered );
|
|
51
|
+
}, [ onResolved, name, filtered ] );
|
|
52
|
+
|
|
53
|
+
// Clear this loader's entries when it unmounts.
|
|
54
|
+
useEffect( () => {
|
|
55
|
+
return () => onResolved( name, [] );
|
|
56
|
+
}, [ onResolved, name ] );
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useRecentCommands() {
|
|
60
|
+
const {
|
|
61
|
+
contextualCommands,
|
|
62
|
+
staticCommands,
|
|
63
|
+
contextualLoaders,
|
|
64
|
+
staticLoaders,
|
|
65
|
+
recentlyUsedNames = EMPTY_ARRAY,
|
|
66
|
+
} = useSelect( ( select ) => {
|
|
67
|
+
const { getCommands, getCommandLoaders } = select( commandsStore );
|
|
68
|
+
return {
|
|
69
|
+
contextualCommands: getCommands( true ),
|
|
70
|
+
staticCommands: getCommands( false ),
|
|
71
|
+
contextualLoaders: getCommandLoaders( true ),
|
|
72
|
+
staticLoaders: getCommandLoaders( false ),
|
|
73
|
+
recentlyUsedNames: select( preferencesStore ).get(
|
|
74
|
+
'core/commands',
|
|
75
|
+
'recentlyUsed'
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
}, [] );
|
|
79
|
+
|
|
80
|
+
const [ resolvedMap, setResolvedMap ] = useState( () => new Map() );
|
|
81
|
+
|
|
82
|
+
const onResolved = useCallback( ( loaderName, cmds ) => {
|
|
83
|
+
setResolvedMap( ( prev ) => {
|
|
84
|
+
const prevCmds = prev.get( loaderName );
|
|
85
|
+
if (
|
|
86
|
+
prevCmds &&
|
|
87
|
+
prevCmds.length === cmds.length &&
|
|
88
|
+
prevCmds.every( ( c, i ) => c.name === cmds[ i ].name )
|
|
89
|
+
) {
|
|
90
|
+
return prev;
|
|
91
|
+
}
|
|
92
|
+
const next = new Map( prev );
|
|
93
|
+
next.set( loaderName, cmds );
|
|
94
|
+
return next;
|
|
95
|
+
} );
|
|
96
|
+
}, [] );
|
|
97
|
+
|
|
98
|
+
const { recentNames, recentSet } = useMemo( () => {
|
|
99
|
+
const names = recentlyUsedNames.slice( 0, MAX_RECENTLY_DISPLAYED );
|
|
100
|
+
return { recentNames: names, recentSet: new Set( names ) };
|
|
101
|
+
}, [ recentlyUsedNames ] );
|
|
102
|
+
|
|
103
|
+
if ( ! recentlyUsedNames.length ) {
|
|
104
|
+
return {
|
|
105
|
+
commands: [],
|
|
106
|
+
loaders: [],
|
|
107
|
+
recentSet: EMPTY_SET,
|
|
108
|
+
onResolved,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const allStaticCommands = [ ...contextualCommands, ...staticCommands ];
|
|
113
|
+
const loaders = [ ...contextualLoaders, ...staticLoaders ];
|
|
114
|
+
|
|
115
|
+
// Merge static commands with loader-resolved commands.
|
|
116
|
+
const allByName = new Map();
|
|
117
|
+
allStaticCommands.forEach( ( c ) => allByName.set( c.name, c ) );
|
|
118
|
+
for ( const cmds of resolvedMap.values() ) {
|
|
119
|
+
cmds.forEach( ( c ) => {
|
|
120
|
+
if ( ! allByName.has( c.name ) ) {
|
|
121
|
+
allByName.set( c.name, c );
|
|
122
|
+
}
|
|
123
|
+
} );
|
|
124
|
+
}
|
|
125
|
+
// Return in recency order.
|
|
126
|
+
const commands = recentNames
|
|
127
|
+
.map( ( n ) => allByName.get( n ) )
|
|
128
|
+
.filter( Boolean );
|
|
129
|
+
|
|
130
|
+
return { commands, loaders, recentSet, onResolved };
|
|
131
|
+
}
|