Skip to content

Widget Dashboard: use Menu from @wordpress/ui - #81970

Closed
retrofox wants to merge 6 commits into
trunkfrom
update/dashboard-menus-ui
Closed

retrofox wants to merge 6 commits into
trunkfrom
update/dashboard-menus-ui

Conversation

@retrofox

@retrofox retrofox commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

What?

Part of #81230.

Migrates every menu in @wordpress/widget-dashboard from the private Menu in @wordpress/components to the Menu in @wordpress/ui:

  • The dashboard overflow menu in the page header (ActionsMenu).
  • The widget chrome "More" menu that materializes widget actions (WidgetActions).
  • The customize-mode width menu (WidgetLayoutControls).

No usages of the old component remain in the package, and routes/dashboard never had any: it mounts WidgetDashboard.Actions into the Page header, so the menu lives inside the package.

Also applies two Menu usage guidelines the migration surfaced: the ellipsis convention on "Reset to default…", and folding widget removal into the customize-mode menu so its radio group sits inside a command menu.

Follows #81783, which did the same for @wordpress/dataviews.

Why?

See #81230.

Dogfoods the design-system Menu (#79560) in another product surface, and removes this package's last consumers of the @wordpress/components private APIs.

How?

Mostly a mechanical API mapping: Menu to Menu.Root, Menu.TriggerButton to Menu.Trigger, Menu.Popover to Menu.Popup. Three places where the new component changes what gets rendered.

Widget actions mount as Menu.LinkItem. They were a Menu.Item rendering a Link. Menu.LinkItem is the part meant for navigation destinations: it renders the <a role="menuitem"> natively and handles download, target="_blank" and the "(opens in a new tab)" indicator. That leaves the wrapping Menu.Group and the two CSS rules that only existed to force display: inline-flex on the link with nothing to do.

Overflow menu items no longer render a Button. The new Menu styles its own items, so the nested Button was redundant.

The width menu uses Menu.RadioGroup. The two widths are mutually exclusive states, and the active one was communicated by disabling it. The Menu usage guidelines put that in a radio group, which is also what #81783 did for the sorting directions and the layout switcher.

"Reset to default" becomes "Reset to default…". The Menu usage guidelines reserve the ellipsis for items that open another interface requiring more input before the command can finish, naming a confirmation dialog as the example. That item opens an AlertDialog with intent="irreversible" and a "Reset" confirm button, so the command does not complete on activation. The command palette entry keeps its own wording; the convention covers menu items.

Widget removal moves into the customize-mode menu. The width options were the menu's only content, with removal sitting beside the trigger as a separate trash button. The guidelines reserve radio items for state that "belongs inside a broader command menu", and warn against using them to recreate a Select when choosing a value is the control's whole purpose. Removal now renders as a Menu.Item under a Menu.Separator, keeping the trash icon as its prefix. It sits below the width group rather than above it, so neither the pointer nor the first ArrowDown lands on the destructive item.

One behaviour detail the mapping could have dropped silently: the old Menu.Item closed the menu on click (hideOnClick defaults to true), while Menu.RadioItem and Menu.LinkItem default closeOnClick to false. Both now pass it explicitly.

Adds packages/widget-dashboard/src/test/menus.test.tsx. Nothing in the suite opened these menus, and the migration changes the rendered DOM: real anchors carrying href / download / target, and menuitemradio reporting the current width.

Testing Instructions

  1. Enable the "New Dashboard experience" experiment under Gutenberg > Experiments, then open "Dashboard (Beta)" in wp-admin.
  2. Page header: open the "More options" menu. "Reset to default…" opens the reset dialog. Selecting an item closes the menu and focus returns to the trigger.
  3. Widget chrome: the Hello Dolly widget declares a 'low' relevance action, so it lands in the menu. Open its "More" menu and verify "WordPress.org plugin page" navigates, opens in a new tab, and that middle-click and "Copy link address" work on it. Site Health has two more (Status, Info) as plain links.
  4. Customize mode: press "Customize", then open a widget's "Widget options". The current width shows as the selected option rather than a disabled one. Pick the other width; the tile resizes and the menu closes. "Remove" is now in that menu instead of a separate button beside it, and removing a tile still leaves customize mode able to cancel the change.
  5. Check RTL (?wp_lang=ar) for popup alignment.

No bundled widget declares a download action, so that path is covered by the unit test rather than manually.

Testing Instructions for Keyboard

  1. Tab to each trigger and open it with Enter, Space, or ArrowDown.
  2. Arrow keys move through the items, typeahead works, Esc closes the menu and returns focus to the trigger.
  3. In the width menu, arrow keys move between the radio options and Enter selects one, closing the menu.

Screenshots or screencast

Two visual changes in the customize-mode widget chrome: the active width carries a selection indicator instead of appearing disabled, and the trash button beside the trigger is now a "Remove" command inside the menu.

Before After

Follow-ups

  • The @wordpress/ui imports in these files carry a bare eslint-disable for @wordpress/use-recommended-components. The Menu is use-with-caution pending Prepare @wordpress/ui for use in Gutenberg #76135, so the disables should name that reason, as DataViews: use Menu from @wordpress/ui #81783 does.
  • ActionsMenuItem.disabledTooltip is public API with no in-tree caller. Now that Menu.ItemDescription exists for supplementary content announced to assistive technology, it is a better fit than a tooltip on a disabled item.

@retrofox retrofox self-assigned this Aug 24, 2026
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: retrofox <retrofox@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.78 MB

compressed-size-action

the Menu guidelines reserve the ellipsis for items needing more input
the items drop the nested Button; the new Menu styles its own
link actions mount as LinkItem, which renders the anchor natively
the current width becomes a radio selection instead of a disabled item
@retrofox
retrofox force-pushed the update/dashboard-menus-ui branch from e1eae9c to 31aa785 Compare August 24, 2026 05:59
a radio group belongs inside a command menu, not alone behind a trigger
/>
}
/>
<Menu.Root>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now conflicts with the policy gates added in #81967. When rebasing, could we keep one menu but render the width group only when canResize and the Remove item only when canRemove? Taking this side wholesale would let a policy-denied resize change the staged layout.

Comment on lines +53 to +69
<Menu.Popup>
{ actions.map( ( action ) => (
<Menu.LinkItem
key={ action.id }
href={ action.href }
download={ action.download }
openInNewTab={ action.openInNewTab }
closeOnClick
prefix={
action.icon ? (
<Icon icon={ action.icon } />
) : undefined
}
>
<Menu.ItemLabel>{ action.label }</Menu.ItemLabel>
</Menu.LinkItem>
) ) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the rebased Menu.LinkItem preserve the host routing from #81740 and the empty-string download handling from #82073? Matched in-app routes should render the host Link; only the plain-anchor branch should receive href, download, and openInNewTab.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#80991 now routes DOM suites only through the *.jsdom.test.* suffix. Could you rename this to menus.jsdom.test.tsx during the rebase so these rendered menu tests keep a JSDOM environment?

Comment on lines +89 to +100
it( 'surfaces the dashboard actions in the overflow menu', async () => {
const user = userEvent.setup();
render( <Harness /> );

await user.click(
screen.getByRole( 'button', { name: 'More options' } )
);

expect(
await screen.findByRole( 'menuitem', { name: 'Reset to default…' } )
).toBeInTheDocument();
} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this click "Reset to default…" and assert that the "Reset dashboard to default?" alert dialog opens? That gives the migrated command a behavioral regression test, rather than only checking that its label renders.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After rebasing, these entries auto-merge into the already released 0.6.0 section, and none links #81970. Could you move them to the current ## Unreleased section and add [#81970](https://github.com/WordPress/gutenberg/pull/81970)? And potentially merge them into one entry, if it makes sense

@ciampo

ciampo commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Closing as superseded by #81929

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants