diff --git a/lib/compat/wordpress-6.2/edit-form-blocks.php b/lib/compat/wordpress-6.2/edit-form-blocks.php
new file mode 100644
index 00000000000000..5031550abc1541
--- /dev/null
+++ b/lib/compat/wordpress-6.2/edit-form-blocks.php
@@ -0,0 +1,24 @@
+ {
+ if ( debounced !== input ) {
+ setDebounced( input );
+ }
+ }, [ debounced, input ] );
+ return [ input, setInput, debounced ];
+}
+
+export function useMediaResults( options = {} ) {
+ const [ results, setResults ] = useState();
+ const settings = useSelect(
+ ( select ) => select( blockEditorStore ).getSettings(),
+ []
+ );
+ useEffect( () => {
+ ( async () => {
+ setResults();
+ const _media = await settings?.__unstableFetchMedia( options );
+ if ( _media ) setResults( _media );
+ } )();
+ }, Object.values( options ) );
+ return results;
+}
+
+const MEDIA_CATEGORIES = [
+ { label: __( 'Images' ), name: 'images', mediaType: 'image' },
+ { label: __( 'Videos' ), name: 'videos', mediaType: 'video' },
+ { label: __( 'Audio' ), name: 'audio', mediaType: 'audio' },
+];
+export function useMediaCategories( rootClientId ) {
+ const [ categories, setCategories ] = useState( [] );
+ const { canInsertImage, canInsertVideo, canInsertAudio, fetchMedia } =
+ useSelect(
+ ( select ) => {
+ const { canInsertBlockType, getSettings } =
+ select( blockEditorStore );
+ return {
+ fetchMedia: getSettings().__unstableFetchMedia,
+ canInsertImage: canInsertBlockType(
+ 'core/image',
+ rootClientId
+ ),
+ canInsertVideo: canInsertBlockType(
+ 'core/video',
+ rootClientId
+ ),
+ canInsertAudio: canInsertBlockType(
+ 'core/audio',
+ rootClientId
+ ),
+ };
+ },
+ [ rootClientId ]
+ );
+ useEffect( () => {
+ ( async () => {
+ // If `__unstableFetchMedia` is not defined in block
+ // editor settings, do not set any media categories.
+ if ( ! fetchMedia ) return;
+ const query = {
+ context: 'view',
+ per_page: 1,
+ _fields: [ 'id' ],
+ };
+ const [ image, video, audio ] = await Promise.all( [
+ fetchMedia( { ...query, media_type: 'image' } ),
+ fetchMedia( { ...query, media_type: 'video' } ),
+ fetchMedia( { ...query, media_type: 'audio' } ),
+ ] );
+ const showImage = canInsertImage && !! image.length;
+ const showVideo = canInsertVideo && !! video.length;
+ const showAudio = canInsertAudio && !! audio.length;
+ setCategories(
+ MEDIA_CATEGORIES.filter(
+ ( { mediaType } ) =>
+ ( mediaType === 'image' && showImage ) ||
+ ( mediaType === 'video' && showVideo ) ||
+ ( mediaType === 'audio' && showAudio )
+ )
+ );
+ } )();
+ }, [ canInsertImage, canInsertVideo, canInsertAudio, fetchMedia ] );
+ return categories;
+}
diff --git a/packages/block-editor/src/components/inserter/media-tab/index.js b/packages/block-editor/src/components/inserter/media-tab/index.js
new file mode 100644
index 00000000000000..6e4c518ff63b5e
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/media-tab/index.js
@@ -0,0 +1,3 @@
+export { default as MediaTab } from './media-tab';
+export { MediaCategoryDialog } from './media-panel';
+export { useMediaCategories } from './hooks';
diff --git a/packages/block-editor/src/components/inserter/media-tab/media-list.js b/packages/block-editor/src/components/inserter/media-tab/media-list.js
new file mode 100644
index 00000000000000..da77585583d8c2
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/media-tab/media-list.js
@@ -0,0 +1,93 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __unstableComposite as Composite,
+ __unstableUseCompositeState as useCompositeState,
+ __unstableCompositeItem as CompositeItem,
+ Tooltip,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useMemo, useCallback } from '@wordpress/element';
+import { cloneBlock } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import InserterDraggableBlocks from '../../inserter-draggable-blocks';
+import { getBlockAndPreviewFromMedia } from './utils';
+
+function MediaPreview( { media, onClick, composite, mediaType } ) {
+ const [ block, preview ] = useMemo(
+ () => getBlockAndPreviewFromMedia( media, mediaType ),
+ [ media, mediaType ]
+ );
+ const title = media.title?.rendered || media.title;
+ const baseCssClass = 'block-editor-inserter__media-list';
+ return (
+
+ { ( { draggable, onDragStart, onDragEnd } ) => (
+
+
+ {
+ onClick( block );
+ } }
+ aria-label={ title }
+ >
+
+ { preview }
+
+
+
+
+ ) }
+
+ );
+}
+
+function MediaList( {
+ mediaList,
+ mediaType,
+ onClick,
+ label = __( 'Media List' ),
+} ) {
+ const composite = useCompositeState();
+ const onPreviewClick = useCallback(
+ ( block ) => {
+ onClick( cloneBlock( block ) );
+ },
+ [ onClick ]
+ );
+ return (
+
+ { mediaList.map( ( media ) => (
+
+ ) ) }
+
+ );
+}
+
+export default MediaList;
diff --git a/packages/block-editor/src/components/inserter/media-tab/media-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js
new file mode 100644
index 00000000000000..a73c51bc8e8e00
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js
@@ -0,0 +1,82 @@
+/**
+ * WordPress dependencies
+ */
+import { useRef, useEffect } from '@wordpress/element';
+import { Spinner, SearchControl } from '@wordpress/components';
+import { focus } from '@wordpress/dom';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import MediaList from './media-list';
+import { useMediaResults, useDebouncedInput } from './hooks';
+import InserterNoResults from '../no-results';
+
+const INITIAL_MEDIA_ITEMS_PER_PAGE = 10;
+
+export function MediaCategoryDialog( { rootClientId, onInsert, category } ) {
+ const container = useRef();
+ useEffect( () => {
+ const timeout = setTimeout( () => {
+ const [ firstTabbable ] = focus.tabbable.find( container.current );
+ firstTabbable?.focus();
+ } );
+ return () => clearTimeout( timeout );
+ }, [ category ] );
+ return (
+
+
+
+ );
+}
+
+export function MediaCategoryPanel( { rootClientId, onInsert, category } ) {
+ const [ search, setSearch, debouncedSearch ] = useDebouncedInput();
+ const mediaList = useMediaResults( {
+ per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE,
+ media_type: category.mediaType,
+ search: debouncedSearch,
+ orderBy: !! debouncedSearch ? 'relevance' : 'date',
+ } );
+ const baseCssClass = 'block-editor-inserter__media-panel';
+ return (
+
+
+ { ! mediaList && (
+
+
+
+ ) }
+ { Array.isArray( mediaList ) && ! mediaList.length && (
+
+ ) }
+ { !! mediaList?.length && (
+
+ ) }
+
+ );
+}
diff --git a/packages/block-editor/src/components/inserter/media-tab/media-tab.js b/packages/block-editor/src/components/inserter/media-tab/media-tab.js
new file mode 100644
index 00000000000000..639a63df271d00
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/media-tab/media-tab.js
@@ -0,0 +1,182 @@
+/**
+ * External dependencies
+ */
+import classNames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { __, isRTL } from '@wordpress/i18n';
+import { useViewportMatch } from '@wordpress/compose';
+import {
+ __experimentalItemGroup as ItemGroup,
+ __experimentalItem as Item,
+ __experimentalHStack as HStack,
+ __experimentalNavigatorProvider as NavigatorProvider,
+ __experimentalNavigatorScreen as NavigatorScreen,
+ __experimentalNavigatorButton as NavigatorButton,
+ __experimentalNavigatorBackButton as NavigatorBackButton,
+ FlexBlock,
+ Button,
+} from '@wordpress/components';
+import { useCallback } from '@wordpress/element';
+import { Icon, chevronRight, chevronLeft } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { MediaCategoryPanel } from './media-panel';
+import MediaUploadCheck from '../../media-upload/check';
+import MediaUpload from '../../media-upload';
+import { useMediaCategories } from './hooks';
+import { getBlockAndPreviewFromMedia } from './utils';
+
+const ALLOWED_MEDIA_TYPES = [ 'image', 'video', 'audio' ];
+
+function MediaTab( {
+ rootClientId,
+ selectedCategory,
+ onSelectCategory,
+ onInsert,
+} ) {
+ const mediaCategories = useMediaCategories( rootClientId );
+ const isMobile = useViewportMatch( 'medium', '<' );
+ const baseCssClass = 'block-editor-inserter__media-tabs';
+ const onSelectMedia = useCallback(
+ ( media ) => {
+ if ( ! media?.url ) {
+ return;
+ }
+ const [ block ] = getBlockAndPreviewFromMedia( media, media.type );
+ onInsert( block );
+ },
+ [ onInsert ]
+ );
+ return (
+ <>
+ { ! isMobile && (
+
+
+
+ ) }
+ { isMobile && (
+
+ ) }
+ >
+ );
+}
+
+function MediaTabNavigation( { onInsert, rootClientId, mediaCategories } ) {
+ return (
+
+
+
+ { mediaCategories.map( ( category ) => (
+
+
+ { category.label }
+
+
+
+ ) ) }
+
+
+ { mediaCategories.map( ( category ) => (
+
+
+ { __( 'Back' ) }
+
+
+
+ ) ) }
+
+ );
+}
+
+export default MediaTab;
diff --git a/packages/block-editor/src/components/inserter/media-tab/utils.js b/packages/block-editor/src/components/inserter/media-tab/utils.js
new file mode 100644
index 00000000000000..f6a18514d728c7
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/media-tab/utils.js
@@ -0,0 +1,37 @@
+/**
+ * WordPress dependencies
+ */
+import { createBlock } from '@wordpress/blocks';
+
+const mediaTypeTag = { image: 'img', video: 'video', audio: 'audio' };
+
+export function getBlockAndPreviewFromMedia( media, mediaType ) {
+ // Add the common attributes between the different media types.
+ const attributes = {
+ id: media.id,
+ };
+ // Some props are named differently between the Media REST API and Media Library API.
+ // For example `source_url` is used in the former and `url` is used in the latter.
+ const mediaSrc = media.source_url || media.url;
+ const alt = media.alt_text || media.alt || undefined;
+ const caption = media.caption?.raw || media.caption;
+ if ( caption && typeof caption === 'string' ) {
+ attributes.caption = caption;
+ }
+ if ( mediaType === 'image' ) {
+ attributes.url = mediaSrc;
+ attributes.alt = alt;
+ } else if ( [ 'video', 'audio' ].includes( mediaType ) ) {
+ attributes.src = mediaSrc;
+ }
+ const PreviewTag = mediaTypeTag[ mediaType ];
+ const preview = (
+
+ );
+ return [ createBlock( `core/${ mediaType }`, attributes ), preview ];
+}
diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index c6d019c8ff9a74..37a09dd4488700 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -28,6 +28,7 @@ import BlockPatternsTabs, {
BlockPatternsCategoryDialog,
} from './block-patterns-tab';
import ReusableBlocksTab from './reusable-blocks-tab';
+import { MediaTab, MediaCategoryDialog, useMediaCategories } from './media-tab';
import InserterSearchResults from './search-results';
import useInsertionPoint from './hooks/use-insertion-point';
import InserterTabs from './tabs';
@@ -54,6 +55,8 @@ function InserterMenu(
const [ hoveredItem, setHoveredItem ] = useState( null );
const [ selectedPatternCategory, setSelectedPatternCategory ] =
useState( null );
+ const [ selectedMediaCategory, setSelectedMediaCategory ] =
+ useState( null );
const [ selectedTab, setSelectedTab ] = useState( null );
const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] =
@@ -68,7 +71,6 @@ function InserterMenu(
( select ) => {
const { __experimentalGetAllowedPatterns, getSettings } =
select( blockEditorStore );
-
return {
showPatterns: !! __experimentalGetAllowedPatterns(
destinationRootClientId
@@ -80,6 +82,9 @@ function InserterMenu(
[ destinationRootClientId ]
);
+ const mediaCategories = useMediaCategories( destinationRootClientId );
+ const showMedia = !! mediaCategories.length;
+
const onInsert = useCallback(
( blocks, meta, shouldForceFocusBlock ) => {
onInsertBlocks( blocks, meta, shouldForceFocusBlock );
@@ -170,16 +175,36 @@ function InserterMenu(
[ destinationRootClientId, onInsert, onHover ]
);
+ const mediaTab = useMemo(
+ () => (
+
+ ),
+ [
+ destinationRootClientId,
+ onInsert,
+ selectedMediaCategory,
+ setSelectedMediaCategory,
+ ]
+ );
+
const getCurrentTab = useCallback(
( tab ) => {
if ( tab.name === 'blocks' ) {
return blocksTab;
} else if ( tab.name === 'patterns' ) {
return patternsTab;
+ } else if ( tab.name === 'reusable' ) {
+ return reusableBlocksTab;
+ } else if ( tab.name === 'media' ) {
+ return mediaTab;
}
- return reusableBlocksTab;
},
- [ blocksTab, patternsTab, reusableBlocksTab ]
+ [ blocksTab, patternsTab, reusableBlocksTab, mediaTab ]
);
const searchRef = useRef();
@@ -191,8 +216,10 @@ function InserterMenu(
const showPatternPanel =
selectedTab === 'patterns' && ! filterValue && selectedPatternCategory;
- const showAsTabs = ! filterValue && ( showPatterns || hasReusableBlocks );
-
+ const showAsTabs =
+ ! filterValue && ( showPatterns || hasReusableBlocks || showMedia );
+ const showMediaPanel =
+ selectedTab === 'media' && ! filterValue && selectedMediaCategory;
return (
@@ -244,6 +272,13 @@ function InserterMenu(
) }
+ { showMediaPanel && (
+
+ ) }
{ showInserterHelpPanel && hoveredItem && (
) }
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index ad5a2d6642b4a2..dc1036a5b5c260 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -480,3 +480,151 @@ $block-inserter-tabs-height: 44px;
.block-editor-inserter__patterns-category-panel-title {
font-size: calc(1.25 * 13px);
}
+
+.block-editor-inserter__media-tabs-container {
+ height: 100%;
+
+ nav {
+ height: 100%;
+ }
+
+ .block-editor-inserter__media-library-button {
+ padding: $grid-unit-20;
+ justify-content: center;
+ margin-top: $grid-unit-20;
+ width: 100%;
+ }
+}
+
+.block-editor-inserter__media-tabs {
+ display: flex;
+ flex-direction: column;
+ padding: $grid-unit-20;
+ overflow-y: auto;
+ height: 100%;
+
+ // Push the listitem wrapping the "open media library" button to the bottom of the panel.
+ div[role="listitem"]:last-child {
+ margin-top: auto;
+ }
+
+ &__media-category {
+ &.is-selected {
+ color: var(--wp-admin-theme-color);
+ position: relative;
+
+ .components-flex-item {
+ filter: brightness(0.95);
+ }
+
+ svg {
+ fill: var(--wp-admin-theme-color);
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: $radius-block-ui;
+ opacity: 0.04;
+ background: var(--wp-admin-theme-color);
+ }
+ }
+ }
+}
+
+.block-editor-inserter__media-dialog {
+ background: $gray-100;
+ border-left: $border-width solid $gray-200;
+ border-right: $border-width solid $gray-200;
+ position: absolute;
+ padding: $grid-unit-20 $grid-unit-30;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ scrollbar-gutter: stable both-edges;
+
+ @include break-medium {
+ left: 100%;
+ display: block;
+ width: 300px;
+ }
+
+ .block-editor-block-preview__container {
+ box-shadow: 0 15px 25px rgb(0 0 0 / 7%);
+ &:hover {
+ box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%);
+ }
+ }
+
+ .block-editor-inserter__media-panel {
+ height: 100%;
+
+ &-title {
+ font-size: calc(1.25 * 13px);
+ }
+
+ &-spinner {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &-search {
+ &.components-search-control {
+ input[type="search"].components-search-control__input {
+ background: $white;
+ }
+ }
+ }
+ }
+}
+
+.block-editor-inserter__media-list {
+ margin-top: $grid-unit-20;
+ &__list-item {
+ cursor: pointer;
+ margin-bottom: $grid-unit-30;
+
+ &.is-placeholder {
+ min-height: 100px;
+ }
+
+ &[draggable="true"] .block-editor-block-preview__container {
+ cursor: grab;
+ }
+ }
+
+ &__item {
+ height: 100%;
+
+ &-preview {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ border-radius: 4px;
+
+ > * {
+ margin: 0 auto;
+ max-width: 100%;
+ }
+ }
+
+ &:hover &-preview {
+ box-shadow: 0 0 0 2px var(--wp-admin-theme-color);
+ }
+
+ &:focus &-preview {
+ box-shadow: inset 0 0 0 1px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+
+ // Windows High Contrast mode will show this outline, but not the box-shadow.
+ outline: 2px solid transparent;
+ }
+ }
+}
diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js
index 24bfade8b2f4be..8a80e96cb78cc4 100644
--- a/packages/block-editor/src/components/inserter/tabs.js
+++ b/packages/block-editor/src/components/inserter/tabs.js
@@ -22,11 +22,17 @@ const reusableBlocksTab = {
title: __( 'Reusable' ),
icon: reusableBlockIcon,
};
+const mediaTab = {
+ name: 'media',
+ /* translators: Media tab title in the block inserter. */
+ title: __( 'Media' ),
+};
function InserterTabs( {
children,
showPatterns = false,
showReusableBlocks = false,
+ showMedia = false,
onSelect,
prioritizePatterns,
} ) {
@@ -39,10 +45,12 @@ function InserterTabs( {
if ( ! prioritizePatterns && showPatterns ) {
tempTabs.push( patternsTab );
}
+ if ( showMedia ) {
+ tempTabs.push( mediaTab );
+ }
if ( showReusableBlocks ) {
tempTabs.push( reusableBlocksTab );
}
-
return tempTabs;
}, [
prioritizePatterns,
@@ -50,6 +58,7 @@ function InserterTabs( {
showPatterns,
patternsTab,
showReusableBlocks,
+ showMedia,
reusableBlocksTab,
] );
diff --git a/packages/compose/src/hooks/use-focus-outside/index.ts b/packages/compose/src/hooks/use-focus-outside/index.ts
index cbf1d45d8c9e47..0d62318d38eca4 100644
--- a/packages/compose/src/hooks/use-focus-outside/index.ts
+++ b/packages/compose/src/hooks/use-focus-outside/index.ts
@@ -151,6 +151,23 @@ export default function useFocusOutside(
return;
}
+ // The usage of this attribute should be avoided. The only use case
+ // would be when we load modals that are not React components and
+ // therefore don't exist in the React tree. An example is opening
+ // the Media Library modal from another dialog.
+ // This attribute should contain a selector of the related target
+ // we want to ignore, because we still need to trigger the blur event
+ // on all other cases.
+ const ignoreForRelatedTarget = event.target.getAttribute(
+ 'data-unstable-ignore-focus-outside-for-relatedtarget'
+ );
+ if (
+ ignoreForRelatedTarget &&
+ event.relatedTarget?.closest( ignoreForRelatedTarget )
+ ) {
+ return;
+ }
+
blurCheckTimeoutId.current = setTimeout( () => {
// If document is not focused then focus should remain
// inside the wrapped component and therefore we cancel
diff --git a/packages/core-data/src/fetch/fetch-media.js b/packages/core-data/src/fetch/fetch-media.js
new file mode 100644
index 00000000000000..c7a3a429a8627b
--- /dev/null
+++ b/packages/core-data/src/fetch/fetch-media.js
@@ -0,0 +1,13 @@
+/**
+ * WordPress dependencies
+ */
+import { resolveSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { STORE_NAME as coreStore } from '../name';
+
+export default async function fetchMedia( settings = {} ) {
+ return resolveSelect( coreStore ).getMediaItems( settings );
+}
diff --git a/packages/core-data/src/fetch/index.js b/packages/core-data/src/fetch/index.js
index 8d4d28e3b0db82..2f765825f3e702 100644
--- a/packages/core-data/src/fetch/index.js
+++ b/packages/core-data/src/fetch/index.js
@@ -1,2 +1,3 @@
export { default as __experimentalFetchLinkSuggestions } from './__experimental-fetch-link-suggestions';
export { default as __experimentalFetchUrlData } from './__experimental-fetch-url-data';
+export { default as __experimentalFetchMedia } from './fetch-media';
diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js
index 0f42214b454421..f990ee7f15f697 100644
--- a/packages/edit-site/src/components/block-editor/index.js
+++ b/packages/edit-site/src/components/block-editor/index.js
@@ -8,7 +8,11 @@ import classnames from 'classnames';
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useMemo, useRef, Fragment } from '@wordpress/element';
-import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data';
+import {
+ useEntityBlockEditor,
+ __experimentalFetchMedia as fetchMedia,
+ store as coreStore,
+} from '@wordpress/core-data';
import {
BlockList,
BlockEditorProvider,
@@ -131,6 +135,7 @@ export default function BlockEditor( { setIsInserterOpen } ) {
return {
...restStoredSettings,
+ __unstableFetchMedia: fetchMedia,
__experimentalBlockPatterns: blockPatterns,
__experimentalBlockPatternCategories: blockPatternCategories,
};
diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js
index 0b69dcc66eb81c..69b4f9b42cfc63 100644
--- a/packages/editor/src/components/provider/use-block-editor-settings.js
+++ b/packages/editor/src/components/provider/use-block-editor-settings.js
@@ -7,6 +7,7 @@ import {
store as coreStore,
__experimentalFetchLinkSuggestions as fetchLinkSuggestions,
__experimentalFetchUrlData as fetchUrlData,
+ __experimentalFetchMedia as fetchMedia,
} from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
@@ -182,6 +183,9 @@ function useBlockEditorSettings( settings, hasTemplate ) {
__experimentalBlockPatternCategories: blockPatternCategories,
__experimentalFetchLinkSuggestions: ( search, searchOptions ) =>
fetchLinkSuggestions( search, searchOptions, settings ),
+ // TODO: We should find a proper way to consolidate similar cases
+ // like reusable blocks, fetch entities, etc.
+ __unstableFetchMedia: fetchMedia,
__experimentalFetchRichUrlData: fetchUrlData,
__experimentalCanUserUseUnfilteredHTML: canUseUnfilteredHTML,
__experimentalUndo: undo,
diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js
index cc75245d48bac3..f45a9116508233 100644
--- a/test/e2e/specs/editor/various/inserting-blocks.spec.js
+++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+const path = require( 'path' );
+
/**
* WordPress dependencies
*/
@@ -286,6 +291,50 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
} );
} );
+test.describe( 'insert media from inserter', () => {
+ let uploadedMedia;
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllMedia();
+ uploadedMedia = await requestUtils.uploadMedia(
+ path.resolve(
+ process.cwd(),
+ 'test/e2e/assets/10x10_e2e_test_image_z9T8jK.png'
+ )
+ );
+ } );
+ test.afterAll( async ( { requestUtils } ) => {
+ Promise.all( [
+ requestUtils.deleteAllMedia(),
+ requestUtils.deleteAllPosts(),
+ ] );
+ } );
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+ test( 'insert media from the global inserter', async ( {
+ page,
+ editor,
+ } ) => {
+ await page.click(
+ 'role=region[name="Editor top bar"i] >> role=button[name="Toggle block inserter"i]'
+ );
+ await page.click(
+ 'role=region[name="Block Library"i] >> role=tab[name="Media"i]'
+ );
+ await page.click(
+ '[aria-label="Media categories"i] >> role=button[name="Images"i]'
+ );
+ await page.click(
+ `role=listbox[name="Media List"i] >> role=option[name="${ uploadedMedia.title.raw }"]`
+ );
+ await expect.poll( editor.getEditedPostContent ).toBe(
+ `
+
+`
+ );
+ } );
+} );
+
class InsertingBlocksUtils {
constructor( { page, editor } ) {
this.page = page;