Skip to content

Commit c84cb24

Browse files
Editor: Gather the View group of the Options menu into submenus
Appearance holds the toolbar, distraction free and spotlight toggles plus the fills of the View slot; Panels holds the sidebars plugins register. Follows the design on #79963. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDD2VErzTrjyiB4LJcvhN1
1 parent b816c07 commit c84cb24

7 files changed

Lines changed: 180 additions & 78 deletions

File tree

‎packages/editor/src/components/more-menu/index.jsx‎

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { __, _x } from '@wordpress/i18n';
22
import { useSelect, useDispatch } from '@wordpress/data';
33
import { Button } from '@wordpress/components';
4-
import { moreVertical } from '@wordpress/icons';
4+
import { brush, drawerRight, moreVertical } from '@wordpress/icons';
55
import { store as preferencesStore } from '@wordpress/preferences';
66
import { store as interfaceStore, ActionItem } from '@wordpress/interface';
77
import { keyboardShortcut } from '@wordpress/keycodes';
@@ -10,8 +10,9 @@ import { Menu } from '@wordpress/ui';
1010
import CopyContentMenuItem from './copy-content-menu-item';
1111
import MoreMenuItem from './more-menu-item';
1212
import ModeSwitcher from '../mode-switcher';
13-
import MoreMenuGroup from './more-menu-group';
13+
import { toMenuItems } from './more-menu-group';
1414
import MoreMenuPreferenceItem from './more-menu-preference-item';
15+
import MoreMenuSubmenu from './more-menu-submenu';
1516
import ToolsMoreMenuGroup from './tools-more-menu-group';
1617
import ViewMoreMenuGroup from './view-more-menu-group';
1718
import { store as editorStore } from '../../store';
@@ -69,58 +70,70 @@ export default function MoreMenu( { isRevisionMode = false } ) {
6970
>
7071
<Menu.Group>
7172
<Menu.GroupLabel>{ _x( 'View', 'noun' ) }</Menu.GroupLabel>
72-
<MoreMenuPreferenceItem
73-
scope="core"
74-
name="fixedToolbar"
75-
onToggle={ turnOffDistractionFree }
76-
label={ __( 'Top toolbar' ) }
77-
info={ __(
78-
'Access all block and document tools in a single place'
79-
) }
80-
messageActivated={ __( 'Top toolbar activated.' ) }
81-
messageDeactivated={ __( 'Top toolbar deactivated.' ) }
82-
/>
83-
<MoreMenuPreferenceItem
84-
scope="core"
85-
name="distractionFree"
86-
label={ __( 'Distraction free' ) }
87-
info={ __( 'Write with calmness' ) }
88-
handleToggling={ false }
89-
onToggle={ () =>
90-
toggleDistractionFree( { createNotice: false } )
91-
}
92-
messageActivated={ __(
93-
'Distraction free mode activated.'
94-
) }
95-
messageDeactivated={ __(
96-
'Distraction free mode deactivated.'
97-
) }
98-
shortcut={ keyboardShortcut.primaryShift( '\\' ) }
99-
/>
100-
<MoreMenuPreferenceItem
101-
scope="core"
102-
name="focusMode"
103-
label={ __( 'Spotlight mode' ) }
104-
info={ __( 'Focus on one block at a time' ) }
105-
messageActivated={ __( 'Spotlight mode activated.' ) }
106-
messageDeactivated={ __(
107-
'Spotlight mode deactivated.'
73+
<MoreMenuSubmenu
74+
icon={ brush }
75+
label={ __( 'Appearance' ) }
76+
>
77+
<MoreMenuPreferenceItem
78+
scope="core"
79+
name="fixedToolbar"
80+
onToggle={ turnOffDistractionFree }
81+
label={ __( 'Top toolbar' ) }
82+
info={ __(
83+
'Access all block and document tools in a single place'
84+
) }
85+
messageActivated={ __( 'Top toolbar activated.' ) }
86+
messageDeactivated={ __(
87+
'Top toolbar deactivated.'
88+
) }
89+
/>
90+
<MoreMenuPreferenceItem
91+
scope="core"
92+
name="distractionFree"
93+
label={ __( 'Distraction free' ) }
94+
info={ __( 'Write with calmness' ) }
95+
handleToggling={ false }
96+
onToggle={ () =>
97+
toggleDistractionFree( { createNotice: false } )
98+
}
99+
messageActivated={ __(
100+
'Distraction free mode activated.'
101+
) }
102+
messageDeactivated={ __(
103+
'Distraction free mode deactivated.'
104+
) }
105+
shortcut={ keyboardShortcut.primaryShift( '\\' ) }
106+
/>
107+
<MoreMenuPreferenceItem
108+
scope="core"
109+
name="focusMode"
110+
label={ __( 'Spotlight mode' ) }
111+
info={ __( 'Focus on one block at a time' ) }
112+
messageActivated={ __(
113+
'Spotlight mode activated.'
114+
) }
115+
messageDeactivated={ __(
116+
'Spotlight mode deactivated.'
117+
) }
118+
/>
119+
<ViewMoreMenuGroup.Slot />
120+
</MoreMenuSubmenu>
121+
<ActionItem.Slot
122+
name="core/plugin-more-menu"
123+
fillProps={ { as: MoreMenuItem } }
124+
>
125+
{ ( items ) => (
126+
<MoreMenuSubmenu
127+
icon={ drawerRight }
128+
label={ __( 'Panels' ) }
129+
>
130+
{ toMenuItems( items ) }
131+
</MoreMenuSubmenu>
108132
) }
109-
/>
110-
<ViewMoreMenuGroup.Slot />
133+
</ActionItem.Slot>
111134
</Menu.Group>
112135
<Menu.Separator />
113136
<ModeSwitcher />
114-
<ActionItem.Slot
115-
name="core/plugin-more-menu"
116-
fillProps={ { as: MoreMenuItem } }
117-
>
118-
{ ( items ) => (
119-
<MoreMenuGroup label={ __( 'Panels' ) }>
120-
{ items }
121-
</MoreMenuGroup>
122-
) }
123-
</ActionItem.Slot>
124137
<Menu.Separator />
125138
<Menu.Group>
126139
<Menu.GroupLabel>{ __( 'Tools' ) }</Menu.GroupLabel>

‎packages/editor/src/components/more-menu/more-menu-group.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function MoreMenuGroup( {
4646
*
4747
* @return The fills as menu items.
4848
*/
49-
function toMenuItems( fills: ReactNode ) {
49+
export function toMenuItems( fills: ReactNode ) {
5050
return Children.map( fills, ( fill ) => {
5151
if (
5252
! isValidElement< { href?: string } >( fill ) ||
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// eslint-disable-next-line @wordpress/use-recommended-components
2+
import { Menu } from '@wordpress/ui';
3+
import type { ComponentProps, ReactNode } from 'react';
4+
5+
type MoreMenuSubmenuProps = {
6+
/**
7+
* Label of the item that opens the submenu.
8+
*/
9+
label: string;
10+
11+
/**
12+
* Icon shown before the label.
13+
*/
14+
icon: ComponentProps< typeof Menu.PrefixIcon >[ 'icon' ];
15+
16+
/**
17+
* Items of the submenu.
18+
*/
19+
children: ReactNode;
20+
};
21+
22+
/**
23+
* Renders a submenu of the editor's Options menu, opened from an item that
24+
* carries an icon and a label.
25+
*/
26+
export default function MoreMenuSubmenu( {
27+
label,
28+
icon,
29+
children,
30+
}: MoreMenuSubmenuProps ) {
31+
return (
32+
<Menu.SubmenuRoot>
33+
<Menu.SubmenuTrigger prefix={ <Menu.PrefixIcon icon={ icon } /> }>
34+
<Menu.ItemLabel>{ label }</Menu.ItemLabel>
35+
</Menu.SubmenuTrigger>
36+
<Menu.Popup>{ children }</Menu.Popup>
37+
</Menu.SubmenuRoot>
38+
);
39+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, expect, it } from 'vitest';
4+
import { MenuItem } from '@wordpress/components';
5+
import { brush } from '@wordpress/icons';
6+
// eslint-disable-next-line @wordpress/use-recommended-components
7+
import { Menu } from '@wordpress/ui';
8+
import { toMenuItems } from '../more-menu-group';
9+
import MoreMenuSubmenu from '../more-menu-submenu';
10+
11+
describe( 'MoreMenuSubmenu', () => {
12+
it( 'opens its items from a labelled menu item', async () => {
13+
const user = userEvent.setup();
14+
15+
render(
16+
<Menu.Root>
17+
<Menu.Trigger>Options</Menu.Trigger>
18+
<Menu.Popup>
19+
<MoreMenuSubmenu icon={ brush } label="Panels">
20+
{ toMenuItems( <MenuItem>Legacy item</MenuItem> ) }
21+
</MoreMenuSubmenu>
22+
</Menu.Popup>
23+
</Menu.Root>
24+
);
25+
26+
await user.click( screen.getByRole( 'button', { name: 'Options' } ) );
27+
28+
const trigger = await screen.findByRole( 'menuitem', {
29+
name: 'Panels',
30+
} );
31+
expect( trigger ).toHaveAttribute( 'aria-haspopup', 'menu' );
32+
expect(
33+
screen.queryByRole( 'menuitem', { name: 'Legacy item' } )
34+
).not.toBeInTheDocument();
35+
36+
await user.click( trigger );
37+
38+
expect(
39+
await screen.findByRole( 'menuitem', { name: 'Legacy item' } )
40+
).toBeVisible();
41+
} );
42+
} );

‎test/e2e/specs/editor/plugins/plugins-api.spec.js‎

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
22

3+
/**
4+
* Opens the "Panels" submenu of the editor's Options menu, which lists the
5+
* sidebars plugins register.
6+
*
7+
* @param {import('@playwright/test').Page} page
8+
*/
9+
async function openPanelsMenu( page ) {
10+
await page
11+
.getByRole( 'region', { name: 'Editor top bar' } )
12+
.getByRole( 'button', { name: 'Options' } )
13+
.click();
14+
await page
15+
.getByRole( 'menu', { name: 'Options' } )
16+
.getByRole( 'menuitem', { name: 'Panels', exact: true } )
17+
.click();
18+
}
19+
320
test.describe( 'Plugins API', () => {
421
test.beforeAll( async ( { requestUtils } ) => {
522
await requestUtils.activatePlugin(
@@ -75,10 +92,7 @@ test.describe( 'Plugins API', () => {
7592
test( 'Should open plugins sidebar using More Menu item and render content', async ( {
7693
page,
7794
} ) => {
78-
await page
79-
.getByRole( 'region', { name: 'Editor top bar' } )
80-
.getByRole( 'button', { name: 'Options' } )
81-
.click();
95+
await openPanelsMenu( page );
8296
await page
8397
.getByRole( 'menuitemcheckbox', {
8498
name: 'Plugin more menu title',
@@ -104,10 +118,7 @@ test.describe( 'Plugins API', () => {
104118
test( 'Should render a single More Menu item for a sidebar that renders one of its own', async ( {
105119
page,
106120
} ) => {
107-
await page
108-
.getByRole( 'region', { name: 'Editor top bar' } )
109-
.getByRole( 'button', { name: 'Options' } )
110-
.click();
121+
await openPanelsMenu( page );
111122

112123
// The menu item PluginSidebar injects is labelled with the sidebar
113124
// title, the one the plugin renders with its own children.
@@ -171,10 +182,7 @@ test.describe( 'Plugins API', () => {
171182

172183
await expect( pinnedButton ).toBeHidden();
173184

174-
await page
175-
.getByRole( 'region', { name: 'Editor top bar' } )
176-
.getByRole( 'button', { name: 'Options' } )
177-
.click();
185+
await openPanelsMenu( page );
178186
await page
179187
.getByRole( 'menuitemcheckbox', {
180188
name: 'Plugin more menu title',
@@ -192,9 +200,6 @@ test.describe( 'Plugins API', () => {
192200
test( 'Should close plugins sidebar using More Menu item', async ( {
193201
page,
194202
} ) => {
195-
const options = page
196-
.getByRole( 'region', { name: 'Editor top bar' } )
197-
.getByRole( 'button', { name: 'Options' } );
198203
const moreMenuItem = page.getByRole( 'menuitemcheckbox', {
199204
name: 'Plugin more menu title',
200205
} );
@@ -206,11 +211,11 @@ test.describe( 'Plugins API', () => {
206211
name: 'Title',
207212
} );
208213

209-
await options.click();
214+
await openPanelsMenu( page );
210215
await moreMenuItem.click();
211216
await expect( pluginField ).toBeVisible();
212217

213-
await options.click();
218+
await openPanelsMenu( page );
214219
await moreMenuItem.click();
215220
await expect( pluginField ).toBeHidden();
216221
} );
@@ -221,10 +226,7 @@ test.describe( 'Plugins API', () => {
221226
} ) => {
222227
await pageUtils.setBrowserViewport( 'medium' );
223228

224-
await page
225-
.getByRole( 'region', { name: 'Editor top bar' } )
226-
.getByRole( 'button', { name: 'Options' } )
227-
.click();
229+
await openPanelsMenu( page );
228230
await page
229231
.getByRole( 'menuitemcheckbox', {
230232
name: 'Plugin more menu title',

‎test/e2e/specs/editor/various/dropdown-menu.spec.js‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,17 @@ test.describe( 'Dropdown Menu', () => {
5050
} );
5151

5252
await test.step( 'ignores arrow key navigation that is orthogonal to the orientation of the menu, but stays open', async () => {
53-
// Expect the first menu item to be focused.
54-
await expect( menuItems.first() ).toBeFocused();
53+
// The first item opens a submenu with the right arrow, so test
54+
// on the last item, which is a plain one.
55+
await page.keyboard.press( 'ArrowUp' );
56+
await expect( menuItems.last() ).toBeFocused();
5557

5658
// Press left and right keys an arbitrary (but > 1) number of times.
5759
await pageUtils.pressKeys( 'ArrowLeft', { times: 5 } );
5860
await pageUtils.pressKeys( 'ArrowRight', { times: 5 } );
5961

60-
// Expect the first menu item to still be focused.
61-
await expect( menuItems.first() ).toBeFocused();
62+
// Expect the last menu item to still be focused.
63+
await expect( menuItems.last() ).toBeFocused();
6264
} );
6365
} );
6466
} );

‎test/e2e/specs/editor/various/fullscreen-mode.spec.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ async function enableFullscreenMode( page ) {
77
.getByRole( 'button', { name: 'Options' } )
88
.click();
99

10-
// Select Full Screen Mode
10+
// Fullscreen mode sits in the Appearance submenu.
1111
await page
12-
.locator( 'role=menuitemcheckbox', { hasText: 'Fullscreen mode' } )
12+
.getByRole( 'menu', { name: 'Options' } )
13+
.getByRole( 'menuitem', { name: 'Appearance', exact: true } )
14+
.click();
15+
await page
16+
.getByRole( 'menuitemcheckbox', { name: 'Fullscreen mode' } )
1317
.click();
1418
}
1519

0 commit comments

Comments
 (0)