Skip to content
Draft
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
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ function gutenberg_enable_experiments() {
if ( gutenberg_is_experiment_enabled( 'gutenberg-dashboard-widgets' ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalDashboardWidgets = true', 'before' );
}
if ( gutenberg_is_experiment_enabled( 'gutenberg-suggestion-mode' ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalSuggestionMode = true', 'before' );
}
}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
Expand Down
11 changes: 11 additions & 0 deletions lib/experimental/experiments/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ function gutenberg_initialize_experiments_settings() {
),
),
),
array(
'slug' => 'collaboration',
'label' => _x( 'Collaboration', 'experiments group name', 'gutenberg' ),
'items' => array(
array(
'id' => 'gutenberg-suggestion-mode',
'label' => __( 'Suggestion Mode', 'gutenberg' ),
'description' => __( 'Enables Suggestion mode. In Suggestion mode, edits are captured as proposed changes (inline and structural) that the author can apply or reject, similar to suggesting changes in a document.', 'gutenberg' ),
),
),
),
array(
'slug' => 'other',
'label' => _x( 'Other', 'experiments group name', 'gutenberg' ),
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### New Features

- Added a session-scoped editor intent (`edit`, `suggest`, `view`) with a matching private `setEditorIntent` action and private `getEditorIntent` selector (the intent API is private while Suggest mode is experimental). Surfaced as an Edit / Suggest / View menu in the editor options for post types that support notes. The `view` intent puts the block editor into a read-only preview. Keyboard shortcuts follow the Google Docs convention: Ctrl+Alt+Shift+Z (Edit), +X (Suggest), +C (View) on Windows / ⌘⌥⇧Z/X/C on macOS.
- `setEditorIntent` now surfaces mode transitions with a snackbar ('You're suggesting' / 'You're editing' / 'You're viewing') alongside the existing a11y announcement.
- The "Apply globally" control now opens a review modal so you can choose which of a block's modified styles are pushed to Global Styles, showing each style's current and new value ([#79839](https://github.com/WordPress/gutenberg/pull/79839)).

### Bug Fixes
Expand Down
41 changes: 41 additions & 0 deletions packages/editor/src/components/global-keyboard-shortcuts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import {
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
} from '../../store/constants';
import { unlock } from '../../lock-unlock';
import { useCanSuggest } from '../suggestion-mode/gate';

/**
* Handles the keyboard shortcuts for the editor.
Expand All @@ -36,6 +43,8 @@ export default function EditorKeyboardShortcuts() {
switchEditorMode,
toggleDistractionFree,
} = useDispatch( editorStore );
// The intent API is private while Suggest mode is experimental.
const { setEditorIntent } = unlock( useDispatch( editorStore ) );
const {
isEditedPostDirty,
isPostSavingLocked,
Expand Down Expand Up @@ -100,6 +109,38 @@ export default function EditorKeyboardShortcuts() {
}
} );

/*
* The intent shortcuts share the gate the intent menu uses: Suggestion
* Mode experiment on AND the current post type supports `editor.notes`.
* Without the support check the shortcut could switch intent on post
* types where the menu never offers it.
*/
const canSuggest = useCanSuggest();

useShortcut( 'core/editor/intent-edit', ( event ) => {
if ( ! canSuggest ) {
return;
}
event.preventDefault();
setEditorIntent( EDITOR_INTENT_EDIT );
} );

useShortcut( 'core/editor/intent-suggest', ( event ) => {
if ( ! canSuggest ) {
return;
}
event.preventDefault();
setEditorIntent( EDITOR_INTENT_SUGGEST );
} );

useShortcut( 'core/editor/intent-view', ( event ) => {
if ( ! canSuggest ) {
return;
}
event.preventDefault();
setEditorIntent( EDITOR_INTENT_VIEW );
} );

useShortcut( 'core/editor/toggle-sidebar', ( event ) => {
// This shortcut has no known clashes, but use preventDefault to prevent any
// obscure shortcuts from triggering.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { BlockEditorKeyboardShortcuts } from '@wordpress/block-editor';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { isAppleOS } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { isSuggestionModeEnabled } from '../suggestion-mode/gate';

/**
* Component for registering editor keyboard shortcuts.
*
Expand Down Expand Up @@ -119,6 +124,44 @@ function EditorKeyboardShortcutsRegister() {
},
} );

/*
* Only register the intent shortcuts when the Suggestion Mode
* experiment is on — registration alone lists them in the Keyboard
* Shortcuts help modal, which would advertise a feature the
* handlers refuse to run.
*/
if ( isSuggestionModeEnabled() ) {
registerShortcut( {
name: 'core/editor/intent-edit',
category: 'global',
description: __( 'Switch to Edit mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'z',
},
} );

registerShortcut( {
name: 'core/editor/intent-suggest',
category: 'global',
description: __( 'Switch to Suggest mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'x',
},
} );

registerShortcut( {
name: 'core/editor/intent-view',
category: 'global',
description: __( 'Switch to View mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'c',
},
} );
}

registerShortcut( {
name: 'core/editor/next-region',
category: 'global',
Expand Down
85 changes: 85 additions & 0 deletions packages/editor/src/components/intent-switcher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { MenuItemsChoice, MenuGroup } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { displayShortcut } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
import {
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
} from '../../store/constants';
import PostTypeSupportCheck from '../post-type-support-check';

/**
* Available editor intent options surfaced in the more-menu mode picker.
*
* Each option is mirrored across three files; keep them in sync when adding
* or renaming an intent:
* - This file: UI label, description, and shortcut hint.
* - `../global-keyboard-shortcuts/register-shortcuts.js`: registers the
* keyboard binding with `@wordpress/keyboard-shortcuts`.
* - `../global-keyboard-shortcuts/index.js`: wires `useShortcut` so the
* binding dispatches `setEditorIntent`.
*
* The `value` field must be one of the `EDITOR_INTENT_*` constants — the
* `setEditorIntent` action validates against `EDITOR_INTENTS` and silently
* ignores unknown values.
*
* @type {Array<{value: string, label: string, info: string, shortcut: string}>}
*/
const INTENTS = [
{
value: EDITOR_INTENT_EDIT,
label: __( 'Editing' ),
info: __( 'Edit content directly.' ),
shortcut: displayShortcut.secondary( 'z' ),
},
{
value: EDITOR_INTENT_SUGGEST,
label: __( 'Suggesting' ),
info: __( 'Propose changes the author can apply or reject.' ),
shortcut: displayShortcut.secondary( 'x' ),
},
{
value: EDITOR_INTENT_VIEW,
label: __( 'Viewing' ),
info: __( 'Read-only preview of the content.' ),
shortcut: displayShortcut.secondary( 'c' ),
},
];

/**
* Editor intent switcher. Lets the user pick between direct editing,
* suggesting changes, or viewing in read-only. Only rendered for post
* types that declare the `editor.notes` support.
*/
function IntentSwitcher() {
// The intent API is private while Suggest mode is experimental.
const intent = useSelect(
( select ) => unlock( select( editorStore ) ).getEditorIntent(),
[]
);
const { setEditorIntent } = unlock( useDispatch( editorStore ) );

return (
<PostTypeSupportCheck supportKeys="editor.notes">
<MenuGroup label={ __( 'Mode' ) }>
<MenuItemsChoice
choices={ INTENTS }
value={ intent }
onSelect={ setEditorIntent }
/>
</MenuGroup>
</PostTypeSupportCheck>
);
}

export default IntentSwitcher;
3 changes: 3 additions & 0 deletions packages/editor/src/components/more-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { VisuallyHidden } from '@wordpress/ui';
* Internal dependencies
*/
import CopyContentMenuItem from './copy-content-menu-item';
import IntentSwitcher from '../intent-switcher';
import { isSuggestionModeEnabled } from '../suggestion-mode/gate';
import ModeSwitcher from '../mode-switcher';
import ToolsMoreMenuGroup from './tools-more-menu-group';
import ViewMoreMenuGroup from './view-more-menu-group';
Expand Down Expand Up @@ -106,6 +108,7 @@ export default function MoreMenu( { disabled = false } ) {
/>
<ViewMoreMenuGroup.Slot fillProps={ { onClose } } />
</MenuGroup>
{ isSuggestionModeEnabled() && <IntentSwitcher /> }
<ModeSwitcher />
<ActionItem.Slot
name="core/plugin-more-menu"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { store as coreStore } from '@wordpress/core-data';
*/
import { store as editorStore } from '../../store';

function checkSupport( supports = {}, key ) {
export function checkSupport( supports = {}, key ) {
// Check for top-level support keys.
if ( supports[ key ] !== undefined ) {
return !! supports[ key ];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { default as mediaSideloadFromUrl } from '../../utils/media-sideload-from
import { default as mediaFinalize } from '../../utils/media-finalize';
import { default as mediaDelete } from '../../utils/media-delete';
import { store as editorStore } from '../../store';
import { EDITOR_INTENT_VIEW } from '../../store/constants';
import { unlock } from '../../lock-unlock';
import { useGlobalStyles } from '../global-styles';

Expand Down Expand Up @@ -144,6 +145,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
focusMode,
hasFixedToolbar,
isDistractionFree,
isViewIntent,
keepCaretInsideBlock,
hasUploadPermissions,
hiddenBlockTypes,
Expand Down Expand Up @@ -171,9 +173,11 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
const { getBlockTypes } = select( blocksStore );
const { getCurrentPostId, getCurrentPostType } =
select( editorStore );
const { getDeviceType, isRevisionsMode: _isRevisionsMode } = unlock(
select( editorStore )
);
const {
getDeviceType,
isRevisionsMode: _isRevisionsMode,
getEditorIntent,
} = unlock( select( editorStore ) );
const { getBlocksByName, getBlockAttributes } =
select( blockEditorStore );
const siteSettings = canUser( 'read', {
Expand Down Expand Up @@ -232,6 +236,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
get( 'core', 'fixedToolbar' ) || ! isLargeViewport,
hiddenBlockTypes: get( 'core', 'hiddenBlockTypes' ),
isDistractionFree: get( 'core', 'distractionFree' ),
isViewIntent: getEditorIntent() === EDITOR_INTENT_VIEW,
keepCaretInsideBlock: get( 'core', 'keepCaretInsideBlock' ),
hasUploadPermissions:
canUser( 'create', {
Expand Down Expand Up @@ -463,13 +468,14 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
[ isNavigationOverlayContextKey ]: isNavigationOverlayContext,
};

if ( isRevisionsMode ) {
if ( isRevisionsMode || isViewIntent ) {
blockEditorSettings.isPreviewMode = true;
}

return blockEditorSettings;
}, [
isRevisionsMode,
isViewIntent,
allowedBlockTypes,
allowRightClickOverrides,
focusMode,
Expand Down
13 changes: 13 additions & 0 deletions packages/editor/src/components/suggestion-mode/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The editor store is referenced by its registered name rather than being
* imported directly to avoid a module cycle between the suggestion-mode
* subsystem, the editor store, and the editor provider (which mounts
* `SuggestionOverlayProvider`).
*/
export const EDITOR_STORE_NAME = 'core/editor';

/**
* Mirror of the `suggest` intent value defined in the editor store's
* constants. Duplicated here to avoid the module cycle described above.
*/
export const SUGGEST_INTENT = 'suggest';
Loading
Loading