Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/reference-guides/data/data-core-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ Namespace: `core/commands`.

<!-- START TOKEN(Autogenerated selectors|../../../packages/commands/src/store/selectors.js) -->

### getCommand

Returns a specific command by name.

_Parameters_

- _state_ `Object`: State tree.
- _name_ `string`: Command name.

_Returns_

- `import('./actions').WPCommandConfig|undefined`: The requested command, if it exists.

### getCommandLoaders

Returns the registered command loaders.
Expand Down Expand Up @@ -102,6 +115,18 @@ _Returns_

- `Object`: action.

### triggerCommand

Triggers a command by name.

_Parameters_

- _name_ `string`: Command name.

_Returns_

- `Object`: action.

### unregisterCommand

Returns an action object used to unregister a command.
Expand Down
101 changes: 101 additions & 0 deletions packages/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,99 @@ The Command Palette also offers a number of [selectors and actions](https://deve
- Retrieving the registered commands and command loaders using the following selectors `getCommands` and `getCommandLoader`
- Checking if the Command Palette is open using the `isOpen` selector.
- Programmatically open or close the Command Palette using the `open` and `close` actions.
- Trigger a registered command by name using the `triggerCommand` action.

See the [Commands Data](https://developer.wordpress.org/block-editor/reference-guides/data/data-core-commands/) documentation for more information.

## Integration with Keyboard Shortcuts

Commands can be integrated with keyboard shortcuts using the `useShortcutCommand` hook. This hook binds a keyboard shortcut to a command, allowing the command to be triggered either through the Command Palette or via the keyboard shortcut.

### useShortcutCommand

The `useShortcutCommand` hook connects an existing keyboard shortcut to a command. This allows you to define your action once as a command and trigger it through a keyboard shortcut.

```js
import { useShortcutCommand } from '@wordpress/commands';

function MyComponent() {
// This connects the existing keyboard shortcut to the command.
// When the shortcut is pressed, the command will be executed.
useShortcutCommand(
'core/edit-post/toggle-fullscreen', // Keyboard shortcut name
'core/toggle-fullscreen-mode' // Command name
);

return <div>My Component</div>;
}
```

### Triggering Commands Programmatically

You can also trigger commands directly from your components or other parts of your code:

```js
import { useDispatch } from '@wordpress/data';
import { store as commandsStore } from '@wordpress/commands';

function MyComponent() {
const { triggerCommand } = useDispatch( commandsStore );

const handleClick = () => {
// This will execute the registered command with the given name.
triggerCommand( 'core/toggle-fullscreen-mode' );
};

return <button onClick={ handleClick }>Toggle Fullscreen</button>;
}
```

### Complete Integration Example

Here's a complete example showing how to register both a command and a keyboard shortcut, then connect them:

```js
import { useCommand, useShortcutCommand } from '@wordpress/commands';
import { __ } from '@wordpress/i18n';
import { fullscreen } from '@wordpress/icons';
import { useDispatch } from '@wordpress/data';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { useEffect } from '@wordpress/element';

function MyComponent() {
// 1. Register the command.
useCommand( {
name: 'app/toggle-feature',
label: __( 'Toggle Feature' ),
icon: fullscreen,
callback: ( { close } ) => {
// Feature toggle implementation here
console.log( 'Feature toggled!' );
close();
},
} );

// 2. Register the keyboard shortcut.
const { registerShortcut } = useDispatch( keyboardShortcutsStore );
useEffect( () => {
registerShortcut( {
name: 'app/toggle-feature-shortcut',
category: 'global',
description: __( 'Toggle a feature on or off' ),
keyCombination: {
modifier: 'ctrl+alt',
character: 't',
},
} );
}, [] );

// 3. Connect the shortcut to the command.
useShortcutCommand( 'app/toggle-feature-shortcut', 'app/toggle-feature' );

return <div>Unified Command Example</div>;
}
```

## Installation

Install the module
Expand Down Expand Up @@ -179,6 +269,17 @@ _Parameters_

- _loader_ `import('../store/actions').WPCommandLoaderConfig`: command loader config.

### useShortcutCommand

Hook that binds a keyboard shortcut to a command.

_Parameters_

- _shortcutName_ `string`: The name of the keyboard shortcut.
- _commandName_ `string`: The name of the command to trigger.
- _options_ `Object`: Shortcut options.
- _options.isDisabled_ `boolean`: Whether to disable the shortcut.

<!-- END TOKEN(Autogenerated API docs) -->

## Contributing to this package
Expand Down
59 changes: 59 additions & 0 deletions packages/commands/src/hooks/use-shortcut-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useShortcut } from '@wordpress/keyboard-shortcuts';

/**
* Internal dependencies
*/
import { store as commandsStore } from '../store';

/**
* Hook that binds a keyboard shortcut to a command.
*
* @param {string} shortcutName The name of the keyboard shortcut.
* @param {string} commandName The name of the command to trigger.
* @param {Object} options Shortcut options.
* @param {boolean} options.isDisabled Whether to disable the shortcut.
*/
export default function useShortcutCommand(
shortcutName,
commandName,
{ isDisabled = false } = {}
) {
const command = useSelect(
( select ) => {
const allCommands = select( commandsStore ).getCommands();
return allCommands.find( ( cmd ) => cmd.name === commandName );
},
[ commandName ]
);

const { triggerCommand, close: closeCommandPalette } =
useDispatch( commandsStore );

useShortcut(
shortcutName,
( event ) => {
event.preventDefault();

// If we have the command and it has a callback, execute it directly
// This is a fallback in case the triggerCommand doesn't work.
if ( command && typeof command.callback === 'function' ) {
const closeFunction = () => {
if ( typeof closeCommandPalette === 'function' ) {
closeCommandPalette();
}
};

command.callback( {
close: closeFunction,
} );
} else {
triggerCommand( commandName );
}
},
{ isDisabled }
);
}
1 change: 1 addition & 0 deletions packages/commands/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { CommandMenu } from './components/command-menu';
export { privateApis } from './private-apis';
export { default as useCommand } from './hooks/use-command';
export { default as useCommandLoader } from './hooks/use-command-loader';
export { default as useShortcutCommand } from './hooks/use-shortcut-command';
export { store } from './store';
14 changes: 14 additions & 0 deletions packages/commands/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,17 @@ export function close() {
type: 'CLOSE',
};
}

/**
* Triggers a command by name.
*
* @param {string} name Command name.
*
* @return {Object} action.
*/
export function triggerCommand( name ) {
return {
type: 'TRIGGER_COMMAND',
name,
};
}
2 changes: 2 additions & 0 deletions packages/commands/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import reducer from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';
import * as privateActions from './private-actions';
import { commandTriggerer } from './middleware';
import { unlock } from '../lock-unlock';

const STORE_NAME = 'core/commands';
Expand All @@ -33,6 +34,7 @@ export const store = createReduxStore( STORE_NAME, {
reducer,
actions,
selectors,
middleware: [ commandTriggerer ],
} );

register( store );
Expand Down
44 changes: 44 additions & 0 deletions packages/commands/src/store/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* WordPress dependencies
*/
import { select } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as commandsStore } from './index';

/**
* Middleware for handling command triggering.
* When a TRIGGER_COMMAND action is dispatched, it finds the
* corresponding registered command and executes its callback.
*
* @param {Object} store Redux store instance.
*
* @return {Function} Redux middleware.
*/
export const commandTriggerer = ( store ) => ( next ) => ( action ) => {
const result = next( action );

if ( action.type === 'TRIGGER_COMMAND' ) {
const { name } = action;
const allCommands = select( commandsStore ).getCommands();
let command = select( commandsStore ).getCommand( name );

if ( ! command ) {
command = allCommands.find( ( cmd ) => cmd.name === name );
}

if ( command && typeof command.callback === 'function' ) {
const close = () => {
store.dispatch( { type: 'CLOSE' } );
};

command.callback( { close } );
}
}

return result;
};

export default commandTriggerer;
12 changes: 12 additions & 0 deletions packages/commands/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,15 @@ export function isOpen( state ) {
export function getContext( state ) {
return state.context;
}

/**
* Returns a specific command by name.
*
* @param {Object} state State tree.
* @param {string} name Command name.
*
* @return {import('./actions').WPCommandConfig|undefined} The requested command, if it exists.
*/
export function getCommand( state, name ) {
return state.commands[ name ];
}
Loading