diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index 6a4ac798b7490..bcd60a687a36f 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -30,8 +30,8 @@ import { PatternCategoryPreviewPanel } from './block-patterns-tab/pattern-catego
import { MediaTab, MediaCategoryPanel } from './media-tab';
import InserterSearchResults from './search-results';
import useInsertionPoint from './hooks/use-insertion-point';
-import InserterTabs from './tabs';
import { store as blockEditorStore } from '../../store';
+import TabbedSidebar from '../tabbed-sidebar';
const NOOP = () => {};
function InserterMenu(
@@ -315,21 +315,49 @@ function InserterMenu(
ref={ ref }
>
-
- { inserterSearch }
- { selectedTab === 'blocks' &&
- ! delayedFilterValue &&
- blocksTab }
- { selectedTab === 'patterns' &&
- ! delayedFilterValue &&
- patternsTab }
- { selectedTab === 'media' && mediaTab }
-
+ closeButtonLabel={ __( 'Close block inserter' ) }
+ tabs={ [
+ {
+ name: 'blocks',
+ title: __( 'Blocks' ),
+ panel: (
+ <>
+ { inserterSearch }
+ { selectedTab === 'blocks' &&
+ ! delayedFilterValue &&
+ blocksTab }
+ >
+ ),
+ },
+ {
+ name: 'patterns',
+ title: __( 'Patterns' ),
+ panel: (
+ <>
+ { inserterSearch }
+ { selectedTab === 'patterns' &&
+ ! delayedFilterValue &&
+ patternsTab }
+ >
+ ),
+ },
+ {
+ name: 'media',
+ title: __( 'Media' ),
+ panel: (
+ <>
+ { inserterSearch }
+ { mediaTab }
+ >
+ ),
+ },
+ ] }
+ />
{ showInserterHelpPanel && hoveredItem && (
(
+ ,
+ panelRef: useRef('an-optional-ref'),
+ },
+ {
+ name: 'slug-2',
+ title: _x( 'Title 2', 'context' ),
+ panel: ,
+ },
+ ] }
+ onClose={ onClickCloseButton }
+ onSelect={ onSelectTab }
+ defaultTabId="slug-1"
+ ref={ tabsRef }
+ />
+);
+```
+
+### Props
+
+### `defaultTabId`
+
+- **Type:** `String`
+- **Default:** `undefined`
+
+This is passed to the `Tabs` component so it can handle the tab to select by default when it component renders.
+
+### `onClose`
+
+- **Type:** `Function`
+
+The function that is called when the close button is clicked.
+
+### `onSelect`
+
+- **Type:** `Function`
+
+This is passed to the `Tabs` component - it will be called when a tab has been selected. It is passed the selected tab's ID as an argument.
+
+### `selectedTab`
+
+- **Type:** `String`
+- **Default:** `undefined`
+
+This is passed to the `Tabs` component - it will display this tab as selected.
+
+### `tabs`
+
+- **Type:** `Array`
+- **Default:** `undefined`
+
+An array of tabs which will be rendered as `TabList` and `TabPanel` components.
diff --git a/packages/block-editor/src/components/tabbed-sidebar/index.js b/packages/block-editor/src/components/tabbed-sidebar/index.js
new file mode 100644
index 0000000000000..a0cb510c72090
--- /dev/null
+++ b/packages/block-editor/src/components/tabbed-sidebar/index.js
@@ -0,0 +1,70 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ Button,
+ privateApis as componentsPrivateApis,
+} from '@wordpress/components';
+import { forwardRef } from '@wordpress/element';
+import { closeSmall } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../lock-unlock';
+
+const { Tabs } = unlock( componentsPrivateApis );
+
+function TabbedSidebar(
+ { defaultTabId, onClose, onSelect, selectedTab, tabs, closeButtonLabel },
+ ref
+) {
+ return (
+
+
+
+ onClose() }
+ size="small"
+ />
+
+
+ { tabs.map( ( tab ) => (
+
+ { tab.title }
+
+ ) ) }
+
+
+ { tabs.map( ( tab ) => (
+
+ { tab.panel }
+
+ ) ) }
+
+
+ );
+}
+
+export default forwardRef( TabbedSidebar );
diff --git a/packages/block-editor/src/components/tabbed-sidebar/style.scss b/packages/block-editor/src/components/tabbed-sidebar/style.scss
new file mode 100644
index 0000000000000..e392cf955ed06
--- /dev/null
+++ b/packages/block-editor/src/components/tabbed-sidebar/style.scss
@@ -0,0 +1,53 @@
+.block-editor-tabbed-sidebar {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: hidden;
+
+ @include break-medium() {
+ width: 350px;
+ }
+}
+
+.block-editor-tabbed-sidebar__tablist-and-close-button {
+ border-bottom: $border-width solid $gray-300;
+ display: flex;
+ justify-content: space-between;
+ padding-right: $grid-unit-15;
+}
+
+
+.block-editor-tabbed-sidebar__close-button {
+ background: $white;
+ /* stylelint-disable-next-line property-disallowed-list -- This should be removed when https://github.com/WordPress/gutenberg/issues/59013 is fixed. */
+ order: 1;
+ align-self: center;
+}
+
+.block-editor-tabbed-sidebar__tablist {
+ box-sizing: border-box;
+ flex-grow: 1;
+ margin-bottom: -$border-width;
+ width: 100%;
+}
+
+.block-editor-tabbed-sidebar__tab {
+ flex-grow: 1;
+ margin-bottom: -$border-width;
+
+ &[id$="reusable"] {
+ flex-grow: inherit;
+ // These are to align the `reusable` icon with the search icon.
+ padding-left: $grid-unit-20;
+ padding-right: $grid-unit-20;
+ }
+}
+
+.block-editor-tabbed-sidebar__tabpanel {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ overflow-y: auto;
+ scrollbar-gutter: auto;
+}
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index bfa6ac0c90c84..44f68670556a3 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -48,6 +48,7 @@ import { PrivateInserterLibrary } from './components/inserter/library';
import { PrivatePublishDateTimePicker } from './components/publish-date-time-picker';
import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes';
import useBlockDisplayTitle from './components/block-title/use-block-display-title';
+import TabbedSidebar from './components/tabbed-sidebar';
/**
* Private @wordpress/block-editor APIs.
@@ -77,6 +78,7 @@ lock( privateApis, {
useLayoutStyles,
DimensionsTool,
ResolutionTool,
+ TabbedSidebar,
TextAlignmentControl,
ReusableBlocksRenameHint,
useReusableBlocksRenameHint,
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 0772ddf0d31e5..b2e753bea986e 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -42,6 +42,7 @@
@import "./components/rich-text/style.scss";
@import "./components/segmented-text-control/style.scss";
@import "./components/skip-to-selected-block/style.scss";
+@import "./components/tabbed-sidebar/style.scss";
@import "./components/tool-selector/style.scss";
@import "./components/url-input/style.scss";
@import "./components/url-popover/style.scss";
diff --git a/packages/editor/src/components/list-view-sidebar/index.js b/packages/editor/src/components/list-view-sidebar/index.js
index 79a63ecdec4d9..c90479c23ec70 100644
--- a/packages/editor/src/components/list-view-sidebar/index.js
+++ b/packages/editor/src/components/list-view-sidebar/index.js
@@ -1,17 +1,15 @@
/**
* WordPress dependencies
*/
-import { __experimentalListView as ListView } from '@wordpress/block-editor';
import {
- Button,
- privateApis as componentsPrivateApis,
-} from '@wordpress/components';
+ __experimentalListView as ListView,
+ privateApis as blockEditorPrivateApis,
+} from '@wordpress/block-editor';
import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { focus } from '@wordpress/dom';
import { useCallback, useRef, useState } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
-import { closeSmall } from '@wordpress/icons';
import { useShortcut } from '@wordpress/keyboard-shortcuts';
import { ESCAPE } from '@wordpress/keycodes';
@@ -22,7 +20,7 @@ import ListViewOutline from './list-view-outline';
import { unlock } from '../../lock-unlock';
import { store as editorStore } from '../../store';
-const { Tabs } = unlock( componentsPrivateApis );
+const { TabbedSidebar } = unlock( blockEditorPrivateApis );
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editorStore );
@@ -120,64 +118,38 @@ export default function ListViewSidebar() {
onKeyDown={ closeOnEscape }
ref={ sidebarRef }
>
-
+
+
+
+
+ ),
+ panelRef: listViewContainerRef,
+ },
+ {
+ name: 'outline',
+ title: _x( 'Outline', 'Post overview' ),
+ panel: (
+
+
+
+ ),
+ },
+ ] }
+ onClose={ closeListView }
onSelect={ ( tabName ) => setTab( tabName ) }
- selectOnMove={ false }
- // The initial tab value is set explicitly to avoid an initial
- // render where no tab is selected. This ensures that the
- // tabpanel height is correct so the relevant scroll container
- // can be rendered internally.
defaultTabId="list-view"
- >
-
-
-
-
- { _x( 'List View', 'Post overview' ) }
-
-
- { _x( 'Outline', 'Post overview' ) }
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ref={ tabsRef }
+ closeButtonLabel={ __( 'Close' ) }
+ />
);
}
diff --git a/packages/editor/src/components/list-view-sidebar/style.scss b/packages/editor/src/components/list-view-sidebar/style.scss
index 973defca41f1c..3bf56b2c80760 100644
--- a/packages/editor/src/components/list-view-sidebar/style.scss
+++ b/packages/editor/src/components/list-view-sidebar/style.scss
@@ -1,39 +1,5 @@
.editor-list-view-sidebar {
height: 100%;
- display: flex;
- flex-direction: column;
-
- @include break-medium() {
- // Same width as the Inserter.
- // @see packages/block-editor/src/components/inserter/style.scss
- width: 350px;
- }
- .editor-list-view-sidebar__header {
- display: flex;
- border-bottom: $border-width solid $gray-300;
- }
- .editor-list-view-sidebar__close-button {
- background: $white;
- /* stylelint-disable-next-line property-disallowed-list -- This should be removed when https://github.com/WordPress/gutenberg/issues/59013 is fixed. */
- order: 1;
- align-self: center;
- margin-right: $grid-unit-15;
- }
-}
-
-.editor-list-view-sidebar__tabs-tablist {
- box-sizing: border-box;
- flex-grow: 1;
-
-}
-
-.editor-list-view-sidebar__tabs-tab {
- width: 50%;
- margin-bottom: -$border-width;
-}
-
-.editor-list-view-sidebar__tabs-tabpanel {
- height: calc(100% - #{$grid-unit-60 - $border-width});
}
.editor-list-view-sidebar__list-view-panel-content,