Skip to content
WP EngineDocumentation

Editor Tabs

MediaPress Editor Tabs enables a tabbed view of the block editor interface, by allowing other packages or code to render custom tabs in a central location within the top toolbar of the block editor.

Whilst this package handles the rendering of the tab selection UI, this is essentially just a group of buttons. It is up to external code to handle how content is rendered when each button is clicked, and the conditions under which the tab/button is deemed to be ‘active’.

Editor Tabs must be activated via the “MediaPress” settings page. Initially, it will render a single tab for the “Post” view, but other packages or code may filter this to introduce additional tabs.

mediapress_editor_tabs_supported_post_types
Section titled “mediapress_editor_tabs_supported_post_types”

Defines the post types on which the editor tabs functionality will be enabled.

Default Value

[ 'post', 'page' ]

Parameters

Name Type Description
post_types array Array of post type slugs

Usage

add_filter( 'mediapress_editor_tabs_supported_post_types', 'my_plugin_add_editor_tabs_support' );
function my_plugin_add_editor_tabs_support( array $post_types ): array {
$post_types[] = 'my_custom_post_type';
return $post_types;
}

Allows filtering of the tab buttons shown in the block editor toolbar when this package is enabled.

Each tab is a button which can be provided with a click handler, as well state to determine whether that tab is ‘active’ or not.

Parameters

Name Type Description
tabs Object Object containing individual tab objects, keyed by the tab name
tabs.key Object Individual tab object
tabs.key.label args.type The user-facing label to show within the tab button
tabs.key.onClick Function Callback to run when the tab is clicked by a user. Not run directly while the tab renders as a dropdown, but reachable from a sub-menu fill through activate — see “Tab sub-menus” below
tabs.key.isActive boolean Expected to contain conditions to determine whether the tab is currently active or not
tabs.key.order number The priority order in which this tab should appear in the list
tabs.key.hasSubMenu boolean Render the tab as a dropdown rather than a plain button. See “Tab sub-menus” below

Usage

addFilter('mediaPress.editorTabs.tabs', 'my-plugin/add-media-tab', (tabs) => {
return {
...tabs,
meta: {
label: __('Media', 'default'),
onClick: () =>
dispatch(editPostStore).openGeneralSidebar(MEDIA_SIDEBAR_NAME),
isActive:
select(editPostStore).getActiveGeneralSidebarName() ===
MEDIA_SIDEBAR_NAME,
order: 2,
},
};
});

Any tab can render as a dropdown instead of a plain button — the tab this package provides itself, and any tab added through the filter. This takes two steps, because the tab filter carries data only and cannot express arbitrary markup:

  1. Set hasSubMenu: true on the tab through the mediaPress.editorTabs.tabs filter. A tab rendering as a dropdown never runs its own onClick from clicking the tab itself, open or closed; clicking it opens the menu instead. The onClick is still reachable from inside the menu through activate, described under “Returning to the tab’s own destination” below, and is run directly only if the tab falls back to a plain button, described further down.
  2. Render the menu items into the tab’s slot.

The slot name is MediaPressEditorTab/<tab key>, and is part of this package’s public API. Fill it with Fill from @wordpress/components, using the children-as-function form to receive the fill props — onClose here, activate covered under “Returning to the tab’s own destination”, and ContextMenu covered in “Context menus”:

import { Fill, MenuGroup, MenuItem } from '@wordpress/components';
<Fill name="MediaPressEditorTab/channels">
{({ onClose }) => (
<MenuGroup label={__('My things', 'my-plugin')}>
<MenuItem
onClick={() => {
doSomething();
onClose();
}}
>
{__('My item', 'my-plugin')}
</MenuItem>
</MenuGroup>
)}
</Fill>;

Both steps work the same way whichever tab you are addressing, so there are two things you can do with them. A new tab can be created as a sub-menu, by adding it through the filter with hasSubMenu set and filling its slot. An existing tab can be transformed into one, by spreading it and adding hasSubMenu — including a tab belonging to another module, or the editor tab this package provides:

addFilter(
'mediaPress.editorTabs.tabs',
'my-plugin/add-channels-tab',
(tabs) => ({
...tabs,
// A new tab, created as a sub-menu.
channels: {
label: __('Channels', 'my-plugin'),
hasSubMenu: true,
onClick: openChannelsPanel,
isActive: isChannelsPanelOpen,
order: 20,
},
// An existing tab, transformed into one.
editor: { ...tabs.editor, hasSubMenu: true },
}),
);

The slot is rendered with bubblesVirtually, so a fill stays inside the React tree of the module that declares it. Your menu items keep their own context providers and hooks, even though the markup ends up inside the dropdown. This also means the slot name is the only coupling: a module in a separate plugin can fill it without importing anything from this package.

Any number of modules may fill the same tab; their items appear in fill registration order.

Nothing is rendered around the slot, so a fill decides its own grouping. Wrap your items in a MenuGroup with a label, as above: the label becomes a heading over the group and its accessible name, which tells a user what that part of the menu is for.

This is worth doing even for a single group, because a tab is shared. When two modules fill the same tab, each brings its own MenuGroup, so their items are headed separately and divided from one another instead of running together in one undifferentiated list. A module cannot know what else is in the menu alongside it, so it should always say what its own items are.

A fill may render more than one group if its items divide into more than one kind, and may render none at all — bare items are still valid, they simply get no heading and no padding of their own.

A tab renders as a dropdown only while at least one module is filling its slot. With no fills it falls back to a plain button and runs its own onClick, exactly as a tab that never set hasSubMenu.

This matters because the two steps above are usually taken by different modules, and the one declaring the sub-menu cannot see whether anything will fill it — the filling modules are individually toggleable and may live in another plugin. The fallback means you can set hasSubMenu unconditionally and let the tab degrade on its own, rather than modelling the presence of modules you cannot see:

channels: {
label: __('Channels', 'my-plugin'),
hasSubMenu: true,
// Used when nothing fills the sub-menu. Worth giving to any tab that sets `hasSubMenu`.
onClick: openChannelsPanel,
isActive: isChannelsPanelOpen,
order: 20,
}

A tab that sets hasSubMenu and gives no onClick becomes an inert button when nothing fills it, so give one to any tab that declares a sub-menu.

A dropdown tab’s own onClick stops being reachable from the tab itself once it has at least one fill — clicking the tab only opens the menu. That leaves nothing to take a user back to whatever the tab represents, if one of the items in its own menu is “the thing this tab is already showing”.

activate, given to every fill alongside onClose, runs that onClick on request. It is a no-op while the tab is already isActive, so it is safe to call without checking that yourself:

<Fill name="MediaPressEditorTab/channels">
{({ onClose, activate }) => (
<MenuGroup label={__('Channels', 'my-plugin')}>
<MenuItem
role="menuitemcheckbox"
isSelected={isOnThisTabsOwnDestination}
onClick={() => {
activate();
onClose();
}}
>
{__('My destination', 'my-plugin')}
</MenuItem>
</MenuGroup>
)}
</Fill>

This needs no extra property on the tab: onClick and isActive are already required by every tab, including ones that set hasSubMenu (see “Falling back to a single tab” above), so activate is built from state the tab already carries.

An item in a sub-menu can carry its own menu of secondary actions, opened from a trigger at the end of the row. This lets a user act on something the item represents without having to navigate to it first.

The ContextMenu component that provides this arrives through the slot’s fill props, in the same children-as-function form as onClose. It is not exported, so nothing needs to be imported and a module in a separate plugin can use it on exactly the same terms as one in this repository.

Wrap the item you want the menu on, and describe the actions with controls:

import { Fill, MenuGroup, MenuItem } from '@wordpress/components';
import { trash, unlink } from '@wordpress/icons';
<Fill name="MediaPressEditorTab/channels">
{({ onClose, ContextMenu }) => (
<MenuGroup label={__('Channels', 'my-plugin')}>
<ContextMenu
label={__('Apple News actions', 'my-plugin')}
controls={[
{
title: __('Delete', 'my-plugin'),
icon: trash,
onClick: () => deleteChannel(),
},
{
title: __('Decouple', 'my-plugin'),
icon: unlink,
onClick: () => decoupleChannel(),
},
]}
>
<MenuItem
onClick={() => {
navigateToChannel();
onClose();
}}
>
{__('Apple News', 'my-plugin')}
</MenuItem>
</ContextMenu>
</MenuGroup>
)}
</Fill>;

Props

Name Type Description
children JSX.Element The item the context menu belongs to
controls Array The actions to offer, in the controls form used by DropdownMenu. Any number of actions may be given
label string Accessible name for the trigger. Include what the actions apply to, since a menu may hold several triggers

Every action is defined by the module supplying the item, and its onClick runs in that module’s own React tree, so it keeps whatever context and hooks the fill has around it.

Passing no controls, or an empty array, renders the item exactly as it was given with no trigger. Items in the same sub-menu may therefore mix freely — you can wrap each one unconditionally and let those with actions be the only ones that grow a trigger.

The sub-menu stays open behind an open context menu, so an action can be run and the menu returned to without reopening the tab. It still closes on escape, on a click elsewhere in the editor, and on its tab being pressed again.

Last updated: