@wordpress/commands 1.39.1-next.v.202602091733.0 → 1.40.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/commands",
3
- "version": "1.39.1-next.v.202602091733.0+daa0b19c4",
3
+ "version": "1.40.0",
4
4
  "description": "Handles the commands menu.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -44,14 +44,15 @@
44
44
  "react-native": "src/index",
45
45
  "wpScript": true,
46
46
  "dependencies": {
47
- "@wordpress/base-styles": "^6.15.1-next.v.202602091733.0+daa0b19c4",
48
- "@wordpress/components": "^32.2.1-next.v.202602091733.0+daa0b19c4",
49
- "@wordpress/data": "^10.39.1-next.v.202602091733.0+daa0b19c4",
50
- "@wordpress/element": "^6.39.1-next.v.202602091733.0+daa0b19c4",
51
- "@wordpress/i18n": "^6.12.1-next.v.202602091733.0+daa0b19c4",
52
- "@wordpress/icons": "^11.6.1-next.v.202602091733.0+daa0b19c4",
53
- "@wordpress/keyboard-shortcuts": "^5.39.1-next.v.202602091733.0+daa0b19c4",
54
- "@wordpress/private-apis": "^1.39.1-next.v.202602091733.0+daa0b19c4",
47
+ "@wordpress/base-styles": "^6.16.0",
48
+ "@wordpress/components": "^32.2.0",
49
+ "@wordpress/data": "^10.40.0",
50
+ "@wordpress/element": "^6.40.0",
51
+ "@wordpress/i18n": "^6.13.0",
52
+ "@wordpress/icons": "^11.7.0",
53
+ "@wordpress/keyboard-shortcuts": "^5.40.0",
54
+ "@wordpress/private-apis": "^1.40.0",
55
+ "@wordpress/warning": "^3.40.0",
55
56
  "clsx": "^2.1.1",
56
57
  "cmdk": "^1.0.0"
57
58
  },
@@ -62,5 +63,5 @@
62
63
  "publishConfig": {
63
64
  "access": "public"
64
65
  },
65
- "gitHead": "74f59922b25e30904319373dda91bf8e81f3544e"
66
+ "gitHead": "376124aa10dbc2cc0c81c964ec00b99fcfee5382"
66
67
  }
@@ -14,6 +14,8 @@ import {
14
14
  useRef,
15
15
  useCallback,
16
16
  useMemo,
17
+ isValidElement,
18
+ Component,
17
19
  } from '@wordpress/element';
18
20
  import { __ } from '@wordpress/i18n';
19
21
  import {
@@ -26,7 +28,7 @@ import {
26
28
  store as keyboardShortcutsStore,
27
29
  useShortcut,
28
30
  } from '@wordpress/keyboard-shortcuts';
29
- import { Icon, search as inputIcon } from '@wordpress/icons';
31
+ import { Icon, search as inputIcon, arrowRight } from '@wordpress/icons';
30
32
 
31
33
  /**
32
34
  * Internal dependencies
@@ -38,7 +40,54 @@ const { withIgnoreIMEEvents } = unlock( componentsPrivateApis );
38
40
 
39
41
  const inputLabel = __( 'Search commands and settings' );
40
42
 
41
- function CommandMenuLoader( { name, search, hook, setLoader, close } ) {
43
+ /**
44
+ * Icons enforced per command category.
45
+ * Categories listed here will always use the specified icon,
46
+ * ignoring whatever icon the command itself provides.
47
+ */
48
+ const CATEGORY_ICONS = {
49
+ view: arrowRight,
50
+ };
51
+
52
+ /**
53
+ * Translatable labels for command categories.
54
+ */
55
+ const CATEGORY_LABELS = {
56
+ command: __( 'Command' ),
57
+ view: __( 'View' ),
58
+ edit: __( 'Edit' ),
59
+ action: __( 'Action' ),
60
+ workflow: __( 'Workflow' ),
61
+ };
62
+
63
+ /**
64
+ * Function that checks if the parameter is a valid icon.
65
+ * Taken from @wordpress/blocks/src/api/utils.js and copied
66
+ * in case requirements diverge and to avoid a dependency on @wordpress/blocks.
67
+ *
68
+ * @param {*} icon Parameter to be checked.
69
+ *
70
+ * @return {boolean} True if the parameter is a valid icon and false otherwise.
71
+ */
72
+
73
+ export function isValidIcon( icon ) {
74
+ return (
75
+ !! icon &&
76
+ ( typeof icon === 'string' ||
77
+ isValidElement( icon ) ||
78
+ typeof icon === 'function' ||
79
+ icon instanceof Component )
80
+ );
81
+ }
82
+
83
+ function CommandMenuLoader( {
84
+ name,
85
+ search,
86
+ hook,
87
+ setLoader,
88
+ close,
89
+ category,
90
+ } ) {
42
91
  const { isLoading, commands = [] } = hook( { search } ) ?? {};
43
92
  useEffect( () => {
44
93
  setLoader( name, isLoading );
@@ -50,35 +99,59 @@ function CommandMenuLoader( { name, search, hook, setLoader, close } ) {
50
99
 
51
100
  return (
52
101
  <>
53
- { commands.map( ( command ) => (
54
- <Command.Item
55
- key={ command.name }
56
- value={ command.searchLabel ?? command.label }
57
- keywords={ command.keywords }
58
- onSelect={ () => command.callback( { close } ) }
59
- id={ command.name }
60
- >
61
- <HStack
62
- alignment="left"
63
- className={ clsx( 'commands-command-menu__item', {
64
- 'has-icon': command.icon,
65
- } ) }
102
+ { commands.map( ( command ) => {
103
+ const commandCategory = command.category ?? category;
104
+ return (
105
+ <Command.Item
106
+ key={ command.name }
107
+ value={ command.searchLabel ?? command.label }
108
+ keywords={ command.keywords }
109
+ onSelect={ () => command.callback( { close } ) }
110
+ id={ command.name }
66
111
  >
67
- { command.icon && <Icon icon={ command.icon } /> }
68
- <span>
69
- <TextHighlight
70
- text={ command.label }
71
- highlight={ search }
72
- />
73
- </span>
74
- </HStack>
75
- </Command.Item>
76
- ) ) }
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
+ } ) }
77
144
  </>
78
145
  );
79
146
  }
80
147
 
81
- export function CommandMenuLoaderWrapper( { hook, search, setLoader, close } ) {
148
+ export function CommandMenuLoaderWrapper( {
149
+ hook,
150
+ search,
151
+ setLoader,
152
+ close,
153
+ category,
154
+ } ) {
82
155
  // The "hook" prop is actually a custom React hook
83
156
  // so to avoid breaking the rules of hooks
84
157
  // the CommandMenuLoaderWrapper component need to be
@@ -100,6 +173,7 @@ export function CommandMenuLoaderWrapper( { hook, search, setLoader, close } ) {
100
173
  search={ search }
101
174
  setLoader={ setLoader }
102
175
  close={ close }
176
+ category={ category }
103
177
  />
104
178
  );
105
179
  }
@@ -133,16 +207,27 @@ export function CommandMenuGroup( { isContextual, search, setLoader, close } ) {
133
207
  <HStack
134
208
  alignment="left"
135
209
  className={ clsx( 'commands-command-menu__item', {
136
- 'has-icon': command.icon,
210
+ 'has-icon':
211
+ CATEGORY_ICONS[ command.category ] ||
212
+ command.icon,
137
213
  } ) }
138
214
  >
139
- { command.icon && <Icon icon={ command.icon } /> }
215
+ { CATEGORY_ICONS[ command.category ] ? (
216
+ <Icon icon={ CATEGORY_ICONS[ command.category ] } />
217
+ ) : (
218
+ command.icon && <Icon icon={ command.icon } />
219
+ ) }
140
220
  <span>
141
221
  <TextHighlight
142
222
  text={ command.label }
143
223
  highlight={ search }
144
224
  />
145
225
  </span>
226
+ { CATEGORY_LABELS[ command.category ] && (
227
+ <span className="commands-command-menu__item-category">
228
+ { CATEGORY_LABELS[ command.category ] }
229
+ </span>
230
+ ) }
146
231
  </HStack>
147
232
  </Command.Item>
148
233
  ) ) }
@@ -153,6 +238,7 @@ export function CommandMenuGroup( { isContextual, search, setLoader, close } ) {
153
238
  search={ search }
154
239
  setLoader={ setLoader }
155
240
  close={ close }
241
+ category={ loader.category }
156
242
  />
157
243
  ) ) }
158
244
  </Command.Group>
@@ -212,7 +298,7 @@ export function CommandMenu() {
212
298
 
213
299
  useShortcut(
214
300
  'core/commands',
215
- /** @type {import('react').KeyboardEventHandler} */
301
+ /** @type {React.KeyboardEventHandler} */
216
302
  withIgnoreIMEEvents( ( event ) => {
217
303
  // Bails to avoid obscuring the effect of the preceding handler(s).
218
304
  if ( event.defaultPrevented ) {
@@ -18,6 +18,7 @@
18
18
  .components-modal__content {
19
19
  margin: 0;
20
20
  padding: 0;
21
+ overflow: hidden;
21
22
  }
22
23
  }
23
24
 
@@ -29,6 +30,7 @@
29
30
  .commands-command-menu__header {
30
31
  display: flex;
31
32
  align-items: center;
33
+ gap: $grid-unit-10;
32
34
  padding: 0 $grid-unit-20;
33
35
 
34
36
  .components-button {
@@ -64,7 +66,7 @@
64
66
  color: $gray-900;
65
67
  margin: 0;
66
68
  font-size: 15px;
67
- line-height: 28px;
69
+ line-height: 24px;
68
70
  border-radius: 0;
69
71
 
70
72
  &::placeholder {
@@ -108,6 +110,7 @@
108
110
  min-height: $button-size-next-default-40px;
109
111
  padding: $grid-unit-05;
110
112
  padding-left: $grid-unit-50; // Account for commands without icons.
113
+ padding-right: $grid-unit-20; // Accounts for the command category.
111
114
  }
112
115
 
113
116
  > .has-icon {
@@ -118,6 +121,19 @@
118
121
  [cmdk-root] > [cmdk-list] {
119
122
  max-height: $palette-max-height; // Specific to not have commands overflow oddly.
120
123
  overflow: auto;
124
+ scroll-padding-top: $grid-unit-10;
125
+ scroll-padding-bottom: $grid-unit-10;
126
+
127
+ // Show border when there are commands or "nothing found" message.
128
+ &:has([cmdk-group-items]:not(:empty)),
129
+ &:has([cmdk-empty]) {
130
+ border-top: $border-width solid $gray-300;
131
+ }
132
+
133
+ // Only add vertical padding when there are commands to show.
134
+ &:has([cmdk-group-items]:not(:empty)) {
135
+ padding-top: $grid-unit-10;
136
+ }
121
137
 
122
138
  // Ensures there is always padding bottom on the last group, when there are commands.
123
139
  &
@@ -149,13 +165,29 @@
149
165
  }
150
166
  }
151
167
 
152
- .commands-command-menu__item span {
168
+ .commands-command-menu__item-label {
153
169
  // Ensure commands do not run off the edge (great for post titles).
154
170
  display: inline-block;
155
171
  overflow: hidden;
156
172
  text-overflow: ellipsis;
157
173
  white-space: nowrap;
158
174
  flex: 1;
175
+ min-width: 0;
176
+ }
177
+
178
+ .commands-command-menu__item-category {
179
+ flex: 0 0 auto;
180
+ margin-left: auto;
181
+ padding-left: $grid-unit-30;
182
+ color: $gray-700;
183
+ font-size: $default-font-size;
184
+ line-height: $default-line-height;
185
+ text-align: right;
186
+
187
+ [aria-selected="true"] &,
188
+ [cmdk-item]:active & {
189
+ color: rgba($white, 0.8);
190
+ }
159
191
  }
160
192
 
161
193
  .commands-command-menu__item mark {
@@ -54,6 +54,7 @@ import { store as commandsStore } from '../store';
54
54
  * ? record.title?.rendered
55
55
  * : __( '(no title)' ),
56
56
  * icon: page,
57
+ * category: 'edit',
57
58
  * callback: ( { close } ) => {
58
59
  * const args = {
59
60
  * p: '/page',
@@ -89,6 +90,7 @@ export default function useCommandLoader( loader ) {
89
90
  name: loader.name,
90
91
  hook: loader.hook,
91
92
  context: loader.context,
93
+ category: loader.category,
92
94
  } );
93
95
  return () => {
94
96
  unregisterCommandLoader( loader.name );
@@ -97,6 +99,7 @@ export default function useCommandLoader( loader ) {
97
99
  loader.name,
98
100
  loader.hook,
99
101
  loader.context,
102
+ loader.category,
100
103
  loader.disabled,
101
104
  registerCommandLoader,
102
105
  unregisterCommandLoader,
@@ -23,6 +23,7 @@ import { store as commandsStore } from '../store';
23
23
  * name: 'myplugin/my-command-name',
24
24
  * label: __( 'Add new post' ),
25
25
  * icon: plus,
26
+ * category: 'command',
26
27
  * callback: ({ close }) => {
27
28
  * document.location.href = 'post-new.php';
28
29
  * close();
@@ -44,6 +45,7 @@ export function useCommand( command ) {
44
45
  registerCommand( {
45
46
  name: command.name,
46
47
  context: command.context,
48
+ category: command.category,
47
49
  label: command.label,
48
50
  searchLabel: command.searchLabel,
49
51
  icon: command.icon,
@@ -59,6 +61,7 @@ export function useCommand( command ) {
59
61
  command.searchLabel,
60
62
  command.icon,
61
63
  command.context,
64
+ command.category,
62
65
  command.keywords,
63
66
  command.disabled,
64
67
  registerCommand,
@@ -81,6 +84,7 @@ export function useCommand( command ) {
81
84
  * name: 'myplugin/add-post',
82
85
  * label: __( 'Add new post' ),
83
86
  * icon: plus,
87
+ * category: 'command',
84
88
  * callback: ({ close }) => {
85
89
  * document.location.href = 'post-new.php';
86
90
  * close();
@@ -90,6 +94,7 @@ export function useCommand( command ) {
90
94
  * name: 'myplugin/edit-posts',
91
95
  * label: __( 'Edit posts' ),
92
96
  * icon: pencil,
97
+ * category: 'view',
93
98
  * callback: ({ close }) => {
94
99
  * document.location.href = 'edit.php';
95
100
  * close();
@@ -124,6 +129,7 @@ export function useCommands( commands ) {
124
129
  registerCommand( {
125
130
  name: command.name,
126
131
  context: command.context,
132
+ category: command.category,
127
133
  label: command.label,
128
134
  searchLabel: command.searchLabel,
129
135
  icon: command.icon,
@@ -1,18 +1,37 @@
1
1
  /** @typedef {import('@wordpress/keycodes').WPKeycodeModifier} WPKeycodeModifier */
2
2
 
3
+ /**
4
+ * @typedef {'command'|'view'|'edit'|'workflow'|'action'} WPCommandCategory
5
+ */
6
+
7
+ /**
8
+ * Command categories allowed via registerCommand.
9
+ * The 'workflow' category is reserved for internal use
10
+ * and cannot be registered through this API.
11
+ *
12
+ * @type {Set<WPCommandCategory>}
13
+ */
14
+ const REGISTERABLE_CATEGORIES = new Set( [
15
+ 'command',
16
+ 'view',
17
+ 'edit',
18
+ 'action',
19
+ ] );
20
+
3
21
  /**
4
22
  * Configuration of a registered keyboard shortcut.
5
23
  *
6
24
  * @typedef {Object} WPCommandConfig
7
25
  *
8
- * @property {string} name Command name.
9
- * @property {string} label Command label.
10
- * @property {string=} searchLabel Command search label.
11
- * @property {string=} context Command context.
12
- * @property {JSX.Element} icon Command icon.
13
- * @property {Function} callback Command callback.
14
- * @property {boolean} disabled Whether to disable the command.
15
- * @property {string[]=} keywords Command keywords for search matching.
26
+ * @property {string} name Command name.
27
+ * @property {string} label Command label.
28
+ * @property {string=} searchLabel Command search label.
29
+ * @property {string=} context Command context.
30
+ * @property {WPCommandCategory=} category Command category.
31
+ * @property {React.JSX.Element} icon Command icon.
32
+ * @property {Function} callback Command callback.
33
+ * @property {boolean} disabled Whether to disable the command.
34
+ * @property {string[]=} keywords Command keywords for search matching.
16
35
  */
17
36
 
18
37
  /**
@@ -26,6 +45,7 @@
26
45
  *
27
46
  * @property {string} name Command loader name.
28
47
  * @property {string=} context Command loader context.
48
+ * @property {WPCommandCategory=} category Command loader category.
29
49
  * @property {WPCommandLoaderHook} hook Command loader hook.
30
50
  * @property {boolean} disabled Whether to disable the command loader.
31
51
  */
@@ -38,9 +58,17 @@
38
58
  * @return {Object} action.
39
59
  */
40
60
  export function registerCommand( config ) {
61
+ let { category } = config;
62
+
63
+ // Defaults to 'action' if no category is provided or if the category is invalid. Future versions will emit a warning.
64
+ if ( ! category || ! REGISTERABLE_CATEGORIES.has( category ) ) {
65
+ category = 'action';
66
+ }
67
+
41
68
  return {
42
69
  type: 'REGISTER_COMMAND',
43
70
  ...config,
71
+ category,
44
72
  };
45
73
  }
46
74
 
@@ -66,9 +94,17 @@ export function unregisterCommand( name ) {
66
94
  * @return {Object} action.
67
95
  */
68
96
  export function registerCommandLoader( config ) {
97
+ let { category } = config;
98
+
99
+ // Defaults to 'action' if no category is provided or if the category is invalid. Future versions will emit a warning.
100
+ if ( ! category || ! REGISTERABLE_CATEGORIES.has( category ) ) {
101
+ category = 'action';
102
+ }
103
+
69
104
  return {
70
105
  type: 'REGISTER_COMMAND_LOADER',
71
106
  ...config,
107
+ category,
72
108
  };
73
109
  }
74
110
 
@@ -21,6 +21,7 @@ function commands( state = {}, action ) {
21
21
  label: action.label,
22
22
  searchLabel: action.searchLabel,
23
23
  context: action.context,
24
+ category: action.category,
24
25
  callback: action.callback,
25
26
  icon: action.icon,
26
27
  keywords: action.keywords,
@@ -51,6 +52,7 @@ function commandLoaders( state = {}, action ) {
51
52
  [ action.name ]: {
52
53
  name: action.name,
53
54
  context: action.context,
55
+ category: action.category,
54
56
  hook: action.hook,
55
57
  },
56
58
  };