@wordpress/commands 0.2.0 → 0.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/build/components/command-menu.js +47 -24
  3. package/build/components/command-menu.js.map +1 -1
  4. package/build/hooks/use-command.js +2 -1
  5. package/build/hooks/use-command.js.map +1 -1
  6. package/build/private-apis.js +4 -1
  7. package/build/private-apis.js.map +1 -1
  8. package/build/store/actions.js +33 -4
  9. package/build/store/actions.js.map +1 -1
  10. package/build/store/reducer.js +28 -2
  11. package/build/store/reducer.js.map +1 -1
  12. package/build/store/selectors.js +5 -0
  13. package/build/store/selectors.js.map +1 -1
  14. package/build-module/components/command-menu.js +47 -25
  15. package/build-module/components/command-menu.js.map +1 -1
  16. package/build-module/hooks/use-command.js +2 -1
  17. package/build-module/hooks/use-command.js.map +1 -1
  18. package/build-module/private-apis.js +3 -1
  19. package/build-module/private-apis.js.map +1 -1
  20. package/build-module/store/actions.js +29 -4
  21. package/build-module/store/actions.js.map +1 -1
  22. package/build-module/store/reducer.js +28 -2
  23. package/build-module/store/reducer.js.map +1 -1
  24. package/build-module/store/selectors.js +3 -0
  25. package/build-module/store/selectors.js.map +1 -1
  26. package/build-style/style-rtl.css +20 -76
  27. package/build-style/style.css +20 -76
  28. package/package.json +10 -10
  29. package/src/components/command-menu.js +72 -44
  30. package/src/components/style.scss +20 -78
  31. package/src/hooks/use-command.js +2 -0
  32. package/src/private-apis.js +2 -0
  33. package/src/store/actions.js +29 -5
  34. package/src/store/reducer.js +21 -0
  35. package/src/store/selectors.js +5 -0
@@ -9,11 +9,16 @@ import { Command } from 'cmdk';
9
9
  import { useSelect, useDispatch } from '@wordpress/data';
10
10
  import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
11
11
  import { __ } from '@wordpress/i18n';
12
- import { Modal, TextHighlight } from '@wordpress/components';
12
+ import {
13
+ Modal,
14
+ TextHighlight,
15
+ __experimentalHStack as HStack,
16
+ } from '@wordpress/components';
13
17
  import {
14
18
  store as keyboardShortcutsStore,
15
19
  useShortcut,
16
20
  } from '@wordpress/keyboard-shortcuts';
21
+ import { Icon } from '@wordpress/icons';
17
22
 
18
23
  /**
19
24
  * Internal dependencies
@@ -39,12 +44,18 @@ function CommandMenuLoader( { name, search, hook, setLoader, close } ) {
39
44
  value={ command.name }
40
45
  onSelect={ () => command.callback( { close } ) }
41
46
  >
42
- <span className="commands-command-menu__item">
43
- <TextHighlight
44
- text={ command.label }
45
- highlight={ search }
46
- />
47
- </span>
47
+ <HStack
48
+ alignment="left"
49
+ className="commands-command-menu__item"
50
+ >
51
+ <Icon icon={ command.icon } />
52
+ <span>
53
+ <TextHighlight
54
+ text={ command.label }
55
+ highlight={ search }
56
+ />
57
+ </span>
58
+ </HStack>
48
59
  </Command.Item>
49
60
  ) ) }
50
61
  </Command.List>
@@ -91,19 +102,25 @@ export function CommandMenuGroup( { group, search, setLoader, close } ) {
91
102
  );
92
103
 
93
104
  return (
94
- <Command.Group heading={ group }>
105
+ <Command.Group>
95
106
  { commands.map( ( command ) => (
96
107
  <Command.Item
97
108
  key={ command.name }
98
109
  value={ command.name }
99
110
  onSelect={ () => command.callback( { close } ) }
100
111
  >
101
- <span className="commands-command-menu__item">
102
- <TextHighlight
103
- text={ command.label }
104
- highlight={ search }
105
- />
106
- </span>
112
+ <HStack
113
+ alignment="left"
114
+ className="commands-command-menu__item"
115
+ >
116
+ <Icon icon={ command.icon } />
117
+ <span>
118
+ <TextHighlight
119
+ text={ command.label }
120
+ highlight={ search }
121
+ />
122
+ </span>
123
+ </HStack>
107
124
  </Command.Item>
108
125
  ) ) }
109
126
  { loaders.map( ( loader ) => (
@@ -122,14 +139,16 @@ export function CommandMenuGroup( { group, search, setLoader, close } ) {
122
139
  export function CommandMenu() {
123
140
  const { registerShortcut } = useDispatch( keyboardShortcutsStore );
124
141
  const [ search, setSearch ] = useState( '' );
125
- const [ open, setOpen ] = useState( false );
126
- const { groups } = useSelect( ( select ) => {
127
- const { getGroups } = select( commandsStore );
142
+ const { groups, isOpen } = useSelect( ( select ) => {
143
+ const { getGroups, isOpen: _isOpen } = select( commandsStore );
128
144
  return {
129
145
  groups: getGroups(),
146
+ isOpen: _isOpen(),
130
147
  };
131
148
  }, [] );
149
+ const { open, close } = useDispatch( commandsStore );
132
150
  const [ loaders, setLoaders ] = useState( {} );
151
+ const commandMenuInput = useRef();
133
152
 
134
153
  useEffect( () => {
135
154
  registerShortcut( {
@@ -147,7 +166,11 @@ export function CommandMenu() {
147
166
  'core/commands',
148
167
  ( event ) => {
149
168
  event.preventDefault();
150
- setOpen( ( prevOpen ) => ! prevOpen );
169
+ if ( isOpen ) {
170
+ close();
171
+ } else {
172
+ open();
173
+ }
151
174
  },
152
175
  {
153
176
  bindGlobal: true,
@@ -162,12 +185,19 @@ export function CommandMenu() {
162
185
  } ) ),
163
186
  []
164
187
  );
165
- const close = () => {
188
+ const closeAndReset = () => {
166
189
  setSearch( '' );
167
- setOpen( false );
190
+ close();
168
191
  };
169
192
 
170
- if ( ! open ) {
193
+ useEffect( () => {
194
+ // Focus the command menu input when mounting the modal.
195
+ if ( isOpen ) {
196
+ commandMenuInput.current.focus();
197
+ }
198
+ }, [ isOpen ] );
199
+
200
+ if ( ! isOpen ) {
171
201
  return false;
172
202
  }
173
203
  const isLoading = Object.values( loaders ).some( Boolean );
@@ -176,39 +206,37 @@ export function CommandMenu() {
176
206
  <Modal
177
207
  className="commands-command-menu"
178
208
  overlayClassName="commands-command-menu__overlay"
179
- onRequestClose={ close }
209
+ onRequestClose={ closeAndReset }
180
210
  __experimentalHideHeader
181
211
  >
182
212
  <div className="commands-command-menu__container">
183
213
  <Command label={ __( 'Global Command Menu' ) }>
184
214
  <div className="commands-command-menu__header">
185
215
  <Command.Input
186
- // The input should be focused when the modal is opened.
187
- // eslint-disable-next-line jsx-a11y/no-autofocus
188
- autoFocus
216
+ ref={ commandMenuInput }
189
217
  value={ search }
190
218
  onValueChange={ setSearch }
191
- placeholder={ __(
192
- 'Search for content and templates, or try commands like "Add…"'
193
- ) }
219
+ placeholder={ __( 'Type a command or search' ) }
194
220
  />
195
221
  </div>
196
- <Command.List>
197
- { ! isLoading && (
198
- <Command.Empty>
199
- { __( 'No results found.' ) }
200
- </Command.Empty>
201
- ) }
202
- { groups.map( ( group ) => (
203
- <CommandMenuGroup
204
- key={ group }
205
- group={ group }
206
- search={ search }
207
- setLoader={ setLoader }
208
- close={ close }
209
- />
210
- ) ) }
211
- </Command.List>
222
+ { search && (
223
+ <Command.List>
224
+ { ! isLoading && (
225
+ <Command.Empty>
226
+ { __( 'No results found.' ) }
227
+ </Command.Empty>
228
+ ) }
229
+ { groups.map( ( group ) => (
230
+ <CommandMenuGroup
231
+ key={ group }
232
+ group={ group }
233
+ search={ search }
234
+ setLoader={ setLoader }
235
+ close={ closeAndReset }
236
+ />
237
+ ) ) }
238
+ </Command.List>
239
+ ) }
212
240
  </Command>
213
241
  </div>
214
242
  </Modal>
@@ -1,8 +1,7 @@
1
1
  // dirty hack to clean up modal
2
2
  .commands-command-menu {
3
- padding: 0;
4
3
  width: 100%;
5
- max-width: 680px;
4
+ max-width: 480px;
6
5
  position: relative;
7
6
  top: 15%;
8
7
 
@@ -20,7 +19,6 @@
20
19
  .commands-command-menu__header {
21
20
  display: flex;
22
21
  align-items: center;
23
- margin: $grid-unit-15 $grid-unit-15 0 $grid-unit-15;
24
22
 
25
23
  .components-button {
26
24
  height: $grid-unit-70;
@@ -38,72 +36,47 @@
38
36
  }
39
37
 
40
38
  .commands-command-menu__container {
41
- [cmdk-linear-badge] {
42
- height: 24px;
43
- padding: 0 8px;
44
- font-size: 12px;
45
- color: $gray-800;
46
- background: $gray-300;
47
- border-radius: 4px;
48
- width: fit-content;
49
- display: flex;
50
- align-items: center;
51
- margin: $grid-unit-20 $grid-unit-20 0;
52
- }
53
-
54
- [cmdk-linear-shortcuts] {
55
- display: flex;
56
- margin-left: auto;
57
- gap: $grid-unit-10;
58
-
59
- kbd {
60
- color: $gray-800;
61
- }
62
- }
39
+ // the style here is a hack to force safari to repaint to avoid a style glitch
40
+ will-change: transform;
63
41
 
64
42
  [cmdk-input] {
65
- border: 1px solid $gray-600;
43
+ border: none;
66
44
  width: 100%;
67
- padding: $grid-unit-15 $grid-unit-20;
68
- min-height: $grid-unit-70;
45
+ padding: $grid-unit-20;
69
46
  outline: none;
70
47
  color: $gray-900;
71
48
  margin: 0;
72
- border-radius: $radius-block-ui;
49
+ font-size: 20px;
50
+ line-height: 28px;
51
+ border-bottom: 1px solid $gray-200;
52
+ border-radius: 0;
73
53
 
74
54
  &::placeholder {
75
55
  color: $gray-600;
76
56
  }
77
57
 
78
58
  &:focus {
79
- outline: 2px solid transparent;
80
- border-color: var(--wp-admin-theme-color);
81
- box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
59
+ box-shadow: none;
60
+ outline: none;
61
+ border-bottom: 1px solid $gray-200;
82
62
  }
83
63
  }
84
64
 
85
65
  [cmdk-item] {
86
- content-visibility: auto;
87
- border-radius: $radius-block-ui;
66
+ border-radius: $grid-unit-05;
88
67
  cursor: pointer;
89
- height: $grid-unit-50;
90
68
  display: flex;
91
69
  align-items: center;
92
- gap: $grid-unit-15;
93
- padding: 0 $grid-unit-20;
70
+ padding: $grid-unit;
94
71
  color: $gray-900;
95
- user-select: none;
96
- will-change: background, color;
97
- transition: all 150ms ease;
98
- transition-property: none;
99
- position: relative;
100
72
 
101
- &[aria-selected="true"] {
73
+ &[aria-selected="true"],
74
+ &:active {
102
75
  background: rgba(var(--wp-admin-theme-color--rgb), 0.04);
103
76
  color: var(--wp-admin-theme-color);
104
77
 
105
78
  svg {
106
- color: var(--wp-admin-theme-color);
79
+ fill: var(--wp-admin-theme-color);
107
80
  }
108
81
  }
109
82
 
@@ -112,46 +85,15 @@
112
85
  cursor: not-allowed;
113
86
  }
114
87
 
115
- &:active {
116
- transition-property: background;
117
- background: rgba(var(--wp-admin-theme-color--rgb), 0.08);
118
- }
119
-
120
- & + [cmdk-item] {
121
- margin-top: $grid-unit-05;
122
- }
123
-
124
- svg,
125
- .dashicon {
126
- width: $grid-unit-20;
127
- height: $grid-unit-20;
128
- color: $gray-800;
88
+ svg {
89
+ fill: $gray-600;
129
90
  }
130
91
  }
131
92
 
132
93
  [cmdk-root] > [cmdk-list] {
133
- min-height: 300px;
134
94
  max-height: 400px;
135
95
  overflow: auto;
136
- overscroll-behavior: contain;
137
- transition: 100ms ease;
138
- transition-property: height;
139
- margin: $grid-unit-15;
140
- }
141
-
142
- [cmdk-group-heading] {
143
- user-select: none;
144
- color: $gray-700;
145
- padding: $grid-unit-30 $grid-unit-20 $grid-unit-20;
146
- display: flex;
147
- align-items: center;
148
- font-size: 11px;
149
- text-transform: uppercase;
150
- font-weight: 500;
151
- position: sticky;
152
- top: 0;
153
- background: $white;
154
- z-index: 1;
96
+ padding: $grid-unit;
155
97
  }
156
98
 
157
99
  [cmdk-empty] {
@@ -26,6 +26,7 @@ export default function useCommand( command ) {
26
26
  name: command.name,
27
27
  group: command.group,
28
28
  label: command.label,
29
+ icon: command.icon,
29
30
  callback: currentCallback.current,
30
31
  } );
31
32
  return () => {
@@ -35,6 +36,7 @@ export default function useCommand( command ) {
35
36
  command.name,
36
37
  command.label,
37
38
  command.group,
39
+ command.icon,
38
40
  registerCommand,
39
41
  unregisterCommand,
40
42
  ] );
@@ -8,6 +8,7 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri
8
8
  */
9
9
  import { default as useCommand } from './hooks/use-command';
10
10
  import { default as useCommandLoader } from './hooks/use-command-loader';
11
+ import { store } from './store';
11
12
 
12
13
  export const { lock, unlock } =
13
14
  __dangerousOptInToUnstableAPIsOnlyForCoreModules(
@@ -19,4 +20,5 @@ export const privateApis = {};
19
20
  lock( privateApis, {
20
21
  useCommand,
21
22
  useCommandLoader,
23
+ store,
22
24
  } );
@@ -5,10 +5,11 @@
5
5
  *
6
6
  * @typedef {Object} WPCommandConfig
7
7
  *
8
- * @property {string} name Command name.
9
- * @property {string} label Command label.
10
- * @property {string=} group Command group.
11
- * @property {Function} callback Command callback.
8
+ * @property {string} name Command name.
9
+ * @property {string} label Command label.
10
+ * @property {string=} group Command group.
11
+ * @property {JSX.Element} icon Command icon.
12
+ * @property {Function} callback Command callback.
12
13
  */
13
14
 
14
15
  /**
@@ -32,11 +33,12 @@
32
33
  *
33
34
  * @return {Object} action.
34
35
  */
35
- export function registerCommand( { name, label, callback, group = '' } ) {
36
+ export function registerCommand( { name, label, icon, callback, group = '' } ) {
36
37
  return {
37
38
  type: 'REGISTER_COMMAND',
38
39
  name,
39
40
  label,
41
+ icon,
40
42
  callback,
41
43
  group,
42
44
  };
@@ -89,3 +91,25 @@ export function unregisterCommandLoader( name, group ) {
89
91
  group,
90
92
  };
91
93
  }
94
+
95
+ /**
96
+ * Opens the command center.
97
+ *
98
+ * @return {Object} action.
99
+ */
100
+ export function open() {
101
+ return {
102
+ type: 'OPEN',
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Closes the command center.
108
+ *
109
+ * @return {Object} action.
110
+ */
111
+ export function close() {
112
+ return {
113
+ type: 'CLOSE',
114
+ };
115
+ }
@@ -23,6 +23,7 @@ function commands( state = {}, action ) {
23
23
  label: action.label,
24
24
  group: action.group,
25
25
  callback: action.callback,
26
+ icon: action.icon,
26
27
  },
27
28
  },
28
29
  };
@@ -73,9 +74,29 @@ function commandLoaders( state = {}, action ) {
73
74
  return state;
74
75
  }
75
76
 
77
+ /**
78
+ * Reducer returning the command center open state.
79
+ *
80
+ * @param {Object} state Current state.
81
+ * @param {Object} action Dispatched action.
82
+ *
83
+ * @return {boolean} Updated state.
84
+ */
85
+ function isOpen( state = false, action ) {
86
+ switch ( action.type ) {
87
+ case 'OPEN':
88
+ return true;
89
+ case 'CLOSE':
90
+ return false;
91
+ }
92
+
93
+ return state;
94
+ }
95
+
76
96
  const reducer = combineReducers( {
77
97
  commands,
78
98
  commandLoaders,
99
+ isOpen,
79
100
  } );
80
101
 
81
102
  export default reducer;
@@ -18,6 +18,7 @@ export const getGroups = createSelector(
18
18
  ),
19
19
  ( state ) => [ state.commands, state.commandLoaders ]
20
20
  );
21
+
21
22
  export const getCommands = createSelector(
22
23
  ( state, group ) => Object.values( state.commands[ group ] ?? {} ),
23
24
  ( state, group ) => [ state.commands[ group ] ]
@@ -27,3 +28,7 @@ export const getCommandLoaders = createSelector(
27
28
  ( state, group ) => Object.values( state.commandLoaders[ group ] ?? {} ),
28
29
  ( state, group ) => [ state.commandLoaders[ group ] ]
29
30
  );
31
+
32
+ export function isOpen( state ) {
33
+ return state.isOpen;
34
+ }