diff --git a/packages/editor/src/components/collab-sidebar/README.md b/packages/editor/src/components/collab-sidebar/README.md index eb5bb7f9416c96..7fac76541f0956 100644 --- a/packages/editor/src/components/collab-sidebar/README.md +++ b/packages/editor/src/components/collab-sidebar/README.md @@ -5,7 +5,7 @@ The Notes sidebar (a.k.a. collab sidebar) lets users attach threaded notes to in - **All notes** - a full sidebar (opened from the editor's More menu) listing every note thread on the current post. - **Floating notes** - on larger viewports, unresolved notes float next to their associated blocks, overlaying space reserved at the right edge of the canvas, positioned to track scroll and avoid overlap. The floating notes are part of the canvas surface: they occupy no sidebar, so they can coexist with the Settings sidebar and leave notices and the canvas scrollbar at the full editor width. -The floating notes render full threads, minimized avatar pills, or nothing; the mode is chosen from a "Notes" submenu in the Tools group of the editor's Options (ellipsis) menu, and also adapts automatically to the canvas width: full threads collapse to minimized pills when the canvas gets too narrow to fit them next to a readable content column, and even the pills yield when the canvas can't spare their reserved space (the "All notes" sidebar remains available throughout). +The floating notes render full threads, minimized avatar pills, or nothing; the mode is chosen from a "Notes" submenu in the Tools group of the editor's Options (ellipsis) menu (or with the `access` shortcuts `E`, `I` and `J`, shown alongside each choice), and also adapts automatically to the canvas width: full threads collapse to minimized pills when the canvas gets too narrow to fit them next to a readable content column, and even the pills yield when the canvas can't spare their reserved space (the "All notes" sidebar remains available throughout). Notes are stored as WordPress comments (`type: 'note'`) attached to the post. A block references its thread via `metadata.noteId` on block attributes. Each thread has a top-level note plus replies; threads can be resolved (stored as status `approved`) or reopened. diff --git a/packages/editor/src/components/collab-sidebar/index.jsx b/packages/editor/src/components/collab-sidebar/index.jsx index 16f82153360850..8b47559f694ab4 100644 --- a/packages/editor/src/components/collab-sidebar/index.jsx +++ b/packages/editor/src/components/collab-sidebar/index.jsx @@ -165,6 +165,35 @@ function NotesSidebar( { postId } ) { } ); + // Keyboard equivalents for the display-mode choices in the Options menu, + // available wherever those choices are. + const notesDisplayShortcutsDisabled = + isDistractionFree || ! showNotesDisplayOptions; + useShortcut( + 'core/editor/expand-notes', + ( event ) => { + event.preventDefault(); + applyNotesDisplayMode( 'full' ); + }, + { isDisabled: notesDisplayShortcutsDisabled } + ); + useShortcut( + 'core/editor/minimize-notes', + ( event ) => { + event.preventDefault(); + applyNotesDisplayMode( 'minimized' ); + }, + { isDisabled: notesDisplayShortcutsDisabled } + ); + useShortcut( + 'core/editor/hide-notes', + ( event ) => { + event.preventDefault(); + applyNotesDisplayMode( 'hidden' ); + }, + { isDisabled: notesDisplayShortcutsDisabled } + ); + // Surface one thread for the avatar indicator. const currentThreads = blockNoteIds.length > 0 diff --git a/packages/editor/src/components/collab-sidebar/notes-display-mode-menu.tsx b/packages/editor/src/components/collab-sidebar/notes-display-mode-menu.tsx index 59d69c1e8caa87..2ab2b2dcb860c4 100644 --- a/packages/editor/src/components/collab-sidebar/notes-display-mode-menu.tsx +++ b/packages/editor/src/components/collab-sidebar/notes-display-mode-menu.tsx @@ -1,7 +1,10 @@ import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { comment as commentIcon } from '@wordpress/icons'; // eslint-disable-next-line @wordpress/use-recommended-components import { Menu } from '@wordpress/ui'; +import type { ComponentProps } from 'react'; import NotesMoreMenuGroup from '../more-menu/notes-more-menu-group'; /** @@ -17,6 +20,71 @@ type NotesDisplayModeMenuProps = { onChange: ( value: NotesDisplayMode ) => void; }; +const CHOICES: { + value: NotesDisplayMode; + label: string; + shortcutName: string; +}[] = [ + { + value: 'hidden', + label: __( 'Hide notes' ), + shortcutName: 'core/editor/hide-notes', + }, + { + value: 'minimized', + label: __( 'Minimize notes' ), + shortcutName: 'core/editor/minimize-notes', + }, + { + value: 'full', + label: __( 'Expand notes' ), + shortcutName: 'core/editor/expand-notes', + }, +]; + +type NotesDisplayModeItemProps = { + value: NotesDisplayMode; + label: string; + shortcutName: string; +}; + +/** + * Renders one display-mode choice, advertising its keyboard shortcut + * alongside the label. + * + * @param props Component props. + * @param props.value The display mode the choice selects. + * @param props.label The label of the choice. + * @param props.shortcutName The registered shortcut that selects the mode. + */ +function NotesDisplayModeItem( { + value, + label, + shortcutName, +}: NotesDisplayModeItemProps ) { + const shortcut = useSelect( + ( select ) => + select( keyboardShortcutsStore ).getKeyboardShortcut( + shortcutName + ), + [ shortcutName ] + ); + + return ( + [ 'shortcut' ] + } + closeOnClick + > + { label } + + ); +} + /** * Renders the "Notes" submenu of the editor's Options menu, which holds the * display-mode choices for the floating notes. @@ -29,12 +97,6 @@ export function NotesDisplayModeMenu( { value, onChange, }: NotesDisplayModeMenuProps ) { - const choices: { value: NotesDisplayMode; label: string }[] = [ - { value: 'hidden', label: __( 'Hide notes' ) }, - { value: 'minimized', label: __( 'Minimize notes' ) }, - { value: 'full', label: __( 'Expand notes' ) }, - ]; - return ( @@ -50,16 +112,11 @@ export function NotesDisplayModeMenu( { onChange( mode as NotesDisplayMode ) } > - { choices.map( ( choice ) => ( - ( + - - { choice.label } - - + { ...choice } + /> ) ) } diff --git a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.jsx b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.jsx index 479e20bbbbb258..c7406513219eb7 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.jsx +++ b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.jsx @@ -116,6 +116,36 @@ function EditorKeyboardShortcutsRegister() { }, } ); + registerShortcut( { + name: 'core/editor/expand-notes', + category: 'global', + description: __( 'Expand notes.' ), + keyCombination: { + modifier: 'access', + character: 'e', + }, + } ); + + registerShortcut( { + name: 'core/editor/minimize-notes', + category: 'global', + description: __( 'Minimize notes.' ), + keyCombination: { + modifier: 'access', + character: 'i', + }, + } ); + + registerShortcut( { + name: 'core/editor/hide-notes', + category: 'global', + description: __( 'Hide notes.' ), + keyCombination: { + modifier: 'access', + character: 'j', + }, + } ); + registerShortcut( { name: 'core/editor/next-region', category: 'global', diff --git a/test/e2e/specs/editor/various/block-notes-floating.spec.js b/test/e2e/specs/editor/various/block-notes-floating.spec.js index 1f0077f24e0880..ff6e8e471a1aed 100644 --- a/test/e2e/specs/editor/various/block-notes-floating.spec.js +++ b/test/e2e/specs/editor/various/block-notes-floating.spec.js @@ -584,4 +584,47 @@ test.describe( 'Block Notes: floating panel', () => { await pageUtils.pressKeys( 'Enter' ); await expect( notes ).toBeHidden(); } ); + + test( 'notes display modes have keyboard shortcuts', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Paragraph with a note' }, + } ); + await addNote( page, editor, 'Shortcut mode note' ); + // Keep the canvas wide enough for the full-notes tier so the chosen + // mode (not the canvas width) drives the presentation. + await page.setViewportSize( { width: 1450, height: 800 } ); + + const notes = page.getByRole( 'region', { name: 'Notes' } ); + const overlay = page.locator( '.editor-collab-sidebar-overlay' ); + + await expect( notes ).toBeVisible(); + await expect( overlay ).not.toHaveClass( /is-compact/ ); + + await pageUtils.pressKeys( 'access+i' ); + await expect( overlay ).toHaveClass( /is-compact/ ); + + await pageUtils.pressKeys( 'access+j' ); + await expect( notes ).toBeHidden(); + + await pageUtils.pressKeys( 'access+e' ); + await expect( notes ).toBeVisible(); + await expect( overlay ).not.toHaveClass( /is-compact/ ); + + // The combinations are advertised on the menu choices themselves. + await openNotesDisplayModeMenu( page ); + await expect( + page.getByRole( 'menuitemradio', { name: 'Expand notes' } ) + ).toContainText( /E$/ ); + await expect( + page.getByRole( 'menuitemradio', { name: 'Minimize notes' } ) + ).toContainText( /I$/ ); + await expect( + page.getByRole( 'menuitemradio', { name: 'Hide notes' } ) + ).toContainText( /J$/ ); + } ); } );