Skip to content
2 changes: 1 addition & 1 deletion packages/editor/src/components/collab-sidebar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
29 changes: 29 additions & 0 deletions packages/editor/src/components/collab-sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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 (
<Menu.RadioItem
value={ value }
shortcut={
( shortcut ?? undefined ) as ComponentProps<
typeof Menu.RadioItem
>[ 'shortcut' ]
}
closeOnClick
>
<Menu.ItemLabel>{ label }</Menu.ItemLabel>
</Menu.RadioItem>
);
}

/**
* Renders the "Notes" submenu of the editor's Options menu, which holds the
* display-mode choices for the floating notes.
Expand All @@ -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 (
<NotesMoreMenuGroup.Fill>
<Menu.SubmenuRoot>
Expand All @@ -50,16 +112,11 @@ export function NotesDisplayModeMenu( {
onChange( mode as NotesDisplayMode )
}
>
{ choices.map( ( choice ) => (
<Menu.RadioItem
{ CHOICES.map( ( choice ) => (
<NotesDisplayModeItem
key={ choice.value }
value={ choice.value }
closeOnClick
>
<Menu.ItemLabel>
{ choice.label }
</Menu.ItemLabel>
</Menu.RadioItem>
{ ...choice }
/>
) ) }
</Menu.RadioGroup>
</Menu.Popup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
43 changes: 43 additions & 0 deletions test/e2e/specs/editor/various/block-notes-floating.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$/ );
} );
} );
Loading