diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 73bb0e074281b..ddf022e4ffaec 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -133,7 +133,13 @@ function InserterMenu( { innerBlocks ); - insertBlock( newBlock, insertionIndex, destinationRootClientId ); + insertBlock( + newBlock, + insertionIndex, + destinationRootClientId, + true, + { source: 'inserter_menu' } + ); }, [ insertBlock, destinationRootClientId, insertionIndex ] ); diff --git a/packages/block-editor/src/components/media-placeholder/README.md b/packages/block-editor/src/components/media-placeholder/README.md index dc50d50f78a26..1a2c9693f6173 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -62,6 +62,15 @@ This property is similar to the `accept` property. The difference is the format - Required: No - Platform: Web | Mobile +### autoOpenMediaUpload + +If true, the MediaUpload component auto-opens the picker of the respective platform. + +- Type: `Boolean` +- Required: No +- Default: `false` +- Platform: Mobile + ### className Class name added to the placeholder. diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index 1193b8a4122d4..9760fec58e00e 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -48,6 +48,7 @@ function MediaPlaceholder( props ) { height, backgroundColor, hideContent, + autoOpenMediaUpload, } = props; // use ref to keep media array current for callbacks during rerenders @@ -160,6 +161,7 @@ function MediaPlaceholder( props ) { } multiple={ multiple } isReplacingMedia={ false } + autoOpen={ autoOpenMediaUpload } render={ ( { open, getMediaOptions } ) => { return ( { const otherMediaOptionsWithIcons = otherMediaOptions.map( ( option ) => { @@ -54,6 +62,10 @@ export class MediaUpload extends Component { this.setState( { otherMediaOptions: otherMediaOptionsWithIcons } ); } ); + + if ( autoOpen ) { + this.onPickerPresent(); + } } getAllSources() { @@ -136,8 +148,20 @@ export class MediaUpload extends Component { } onPickerPresent() { + const { autoOpen } = this.props; + const isIOS = Platform.OS === 'ios'; + if ( this.picker ) { - this.picker.presentPicker(); + // the delay below is required because on iOS this action sheet gets dismissed by the close event of the Inserter + // so this delay allows the Inserter to be closed fully before presenting action sheet. + if ( autoOpen && isIOS ) { + delay( + () => this.picker.presentPicker(), + PICKER_OPENING_DELAY + ); + } else { + this.picker.presentPicker(); + } } } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index aad215e14ae50..b0144c19e4f47 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -538,6 +538,7 @@ export function* moveBlockToPosition( * @param {?number} index Index at which block should be inserted. * @param {?string} rootClientId Optional root client ID of block list on which to insert. * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * @param {?Object} meta Optional Meta values to be passed to the action object. * * @return {Object} Action object. */ @@ -545,9 +546,17 @@ export function insertBlock( block, index, rootClientId, - updateSelection = true + updateSelection = true, + meta ) { - return insertBlocks( [ block ], index, rootClientId, updateSelection ); + return insertBlocks( + [ block ], + index, + rootClientId, + updateSelection, + 0, + meta + ); } /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 0930f57bfc5d4..68f0f9b684e95 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1727,6 +1727,31 @@ export function highlightedBlock( state, action ) { return state; } +/** + * Reducer returning the block insertion event list state. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function lastBlockInserted( state = {}, action ) { + switch ( action.type ) { + case 'INSERT_BLOCKS': + if ( ! action.blocks.length ) { + return state; + } + + const clientId = action.blocks[ 0 ].clientId; + const source = action.meta?.source; + + return { clientId, source }; + case 'RESET_BLOCKS': + return {}; + } + return state; +} + export default combineReducers( { blocks, isTyping, @@ -1748,4 +1773,5 @@ export default combineReducers( { hasBlockMovingClientId, automaticChangeStatus, highlightedBlock, + lastBlockInserted, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index ffac34cea81fb..7d8b9d0b4cca8 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2193,3 +2193,19 @@ export const __experimentalGetActiveBlockIdByBlockNames = createSelector( validBlockNames, ] ); + +/** + * Tells if the block with the passed clientId was just inserted. + * + * @param {Object} state Global application state. + * @param {Object} clientId Client Id of the block. + * @param {?string} source Optional insertion source of the block. + * @return {boolean} True if the block matches the last block inserted from the specified source. + */ +export function wasBlockJustInserted( state, clientId, source ) { + const { lastBlockInserted } = state; + return ( + lastBlockInserted.clientId === clientId && + lastBlockInserted.source === source + ); +} diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 826894c8814f5..13e6fe57ae980 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -33,6 +33,7 @@ import { template, blockListSettings, lastBlockAttributesChange, + lastBlockInserted, } from '../reducer'; describe( 'state', () => { @@ -2944,4 +2945,76 @@ describe( 'state', () => { expect( state ).toBe( null ); } ); } ); + + describe( 'lastBlockInserted', () => { + it( 'should return client id if last block inserted is called with action INSERT_BLOCKS', () => { + const expectedClientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; + + const action = { + blocks: [ + { + clientId: expectedClientId, + }, + ], + meta: { + source: 'inserter_menu', + }, + type: 'INSERT_BLOCKS', + }; + + const state = lastBlockInserted( {}, action ); + + expect( state.clientId ).toBe( expectedClientId ); + } ); + + it( 'should return inserter_menu source if last block inserted is called with action INSERT_BLOCKS', () => { + const expectedSource = 'inserter_menu'; + + const action = { + blocks: [ + { + clientId: '62bfef6e-d5e9-43ba-b7f9-c77cf354141f', + }, + ], + meta: { + source: expectedSource, + }, + type: 'INSERT_BLOCKS', + }; + + const state = lastBlockInserted( {}, action ); + + expect( state.source ).toBe( expectedSource ); + } ); + + it( 'should return state if last block inserted is called with action INSERT_BLOCKS and block list is empty', () => { + const expectedState = { + clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189', + }; + + const action = { + blocks: [], + meta: { + source: 'inserter_menu', + }, + type: 'INSERT_BLOCKS', + }; + + const state = lastBlockInserted( expectedState, action ); + + expect( state ).toEqual( expectedState ); + } ); + + it( 'should return empty state if last block inserted is called with action RESET_BLOCKS', () => { + const expectedState = {}; + + const action = { + type: 'RESET_BLOCKS', + }; + + const state = lastBlockInserted( expectedState, action ); + + expect( state ).toEqual( expectedState ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 6563be65dad33..78f1342fa4ce6 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -75,6 +75,7 @@ const { __experimentalGetPatternsByBlockTypes, __unstableGetClientIdWithClientIdsTree, __unstableGetClientIdsTree, + wasBlockJustInserted, } = selectors; describe( 'selectors', () => { @@ -3481,6 +3482,56 @@ describe( 'selectors', () => { ); } ); } ); + + describe( 'wasBlockJustInserted', () => { + it( 'should return true if the client id passed to wasBlockJustInserted is found within the state', () => { + const expectedClientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; + const source = 'inserter_menu'; + + const state = { + lastBlockInserted: { + clientId: expectedClientId, + source, + }, + }; + + expect( + wasBlockJustInserted( state, expectedClientId, source ) + ).toBe( true ); + } ); + + it( 'should return false if the client id passed to wasBlockJustInserted is not found within the state', () => { + const expectedClientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; + const unexpectedClientId = '62bfsed4-d5e9-43ba-b7f9-c77cf565756s'; + const source = 'inserter_menu'; + + const state = { + lastBlockInserted: { + clientId: unexpectedClientId, + source, + }, + }; + + expect( + wasBlockJustInserted( state, expectedClientId, source ) + ).toBe( false ); + } ); + + it( 'should return false if the source passed to wasBlockJustInserted is not found within the state', () => { + const clientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; + const expectedSource = 'inserter_menu'; + + const state = { + lastBlockInserted: { + clientId, + }, + }; + + expect( + wasBlockJustInserted( state, clientId, expectedSource ) + ).toBe( false ); + } ); + } ); } ); describe( '__experimentalGetParsedReusableBlock', () => { diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index a888ead72fbb9..dfa960d5d1660 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -34,7 +34,7 @@ import { import { Platform, useEffect, useState, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; -import { useDispatch, withSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { View } from '@wordpress/primitives'; import { store as coreStore } from '@wordpress/core-data'; @@ -74,12 +74,10 @@ const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( { function GalleryEdit( props ) { const { attributes, + clientId, isSelected, noticeUI, noticeOperations, - mediaUpload, - imageSizes, - resizedImages, onFocus, } = props; const { @@ -95,6 +93,65 @@ function GalleryEdit( props ) { blockEditorStore ); + const { + imageSizes, + mediaUpload, + getMedia, + wasBlockJustInserted, + } = useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + + return { + imageSizes: settings.imageSizes, + mediaUpload: settings.mediaUpload, + getMedia: select( coreStore ).getMedia, + wasBlockJustInserted: select( + blockEditorStore + ).wasBlockJustInserted( clientId, 'inserter_menu' ), + }; + } ); + + const { resizedImages } = useMemo( () => { + if ( isSelected ) { + return reduce( + attributes.ids, + ( currentResizedImages, id ) => { + if ( ! id ) { + return currentResizedImages; + } + const image = getMedia( id ); + const sizes = reduce( + imageSizes, + ( currentSizes, size ) => { + const defaultUrl = get( image, [ + 'sizes', + size.slug, + 'url', + ] ); + const mediaDetailsUrl = get( image, [ + 'media_details', + 'sizes', + size.slug, + 'source_url', + ] ); + return { + ...currentSizes, + [ size.slug ]: defaultUrl || mediaDetailsUrl, + }; + }, + {} + ); + return { + ...currentResizedImages, + [ parseInt( id, 10 ) ]: sizes, + }; + }, + {} + ); + } + return {}; + }, [ isSelected, attributes.ids, imageSizes ] ); + function setAttributes( newAttrs ) { if ( newAttrs.ids ) { throw new Error( @@ -344,6 +401,9 @@ function GalleryEdit( props ) { onError={ onUploadError } notices={ hasImages ? undefined : noticeUI } onFocus={ onFocus } + autoOpenMediaUpload={ + ! hasImages && isSelected && wasBlockJustInserted + } /> ); @@ -414,59 +474,6 @@ function GalleryEdit( props ) { } export default compose( [ - withSelect( ( select, { attributes: { ids }, isSelected } ) => { - const { getMedia } = select( coreStore ); - const { getSettings } = select( blockEditorStore ); - const { imageSizes, mediaUpload } = getSettings(); - - const resizedImages = useMemo( () => { - if ( isSelected ) { - return reduce( - ids, - ( currentResizedImages, id ) => { - if ( ! id ) { - return currentResizedImages; - } - const image = getMedia( id ); - const sizes = reduce( - imageSizes, - ( currentSizes, size ) => { - const defaultUrl = get( image, [ - 'sizes', - size.slug, - 'url', - ] ); - const mediaDetailsUrl = get( image, [ - 'media_details', - 'sizes', - size.slug, - 'source_url', - ] ); - return { - ...currentSizes, - [ size.slug ]: - defaultUrl || mediaDetailsUrl, - }; - }, - {} - ); - return { - ...currentResizedImages, - [ parseInt( id, 10 ) ]: sizes, - }; - }, - {} - ); - } - return {}; - }, [ isSelected, ids, imageSizes ] ); - - return { - imageSizes, - mediaUpload, - resizedImages, - }; - } ), withNotices, withViewportMatch( { isNarrow: '< small' } ), ] )( GalleryEdit ); diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 285675c1585ab..09670a643f6ba 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -439,6 +439,7 @@ export class ImageEdit extends Component { clientId, imageDefaultSize, featuredImageId, + wasBlockJustInserted, } = this.props; const { align, url, alt, id, sizeSlug, className } = attributes; @@ -497,6 +498,9 @@ export class ImageEdit extends Component { onSelect={ this.onSelectMediaUploadOption } icon={ this.getPlaceholderIcon() } onFocus={ this.props.onFocus } + autoOpenMediaUpload={ + isSelected && ! url && wasBlockJustInserted + } /> ); @@ -594,11 +598,14 @@ export class ImageEdit extends Component { export default compose( [ withSelect( ( select, props ) => { const { getMedia } = select( coreStore ); - const { getSettings } = select( blockEditorStore ); + const { getSettings, wasBlockJustInserted } = select( + blockEditorStore + ); const { getEditedPostAttribute } = select( 'core/editor' ); const { attributes: { id, url }, isSelected, + clientId, } = props; const { imageSizes, imageDefaultSize } = getSettings(); const isNotFileUrl = id && getProtocol( url ) !== 'file:'; @@ -612,11 +619,16 @@ export default compose( [ isNotFileUrl && url && ! hasQueryArg( url, 'w' ) ); + return { image: shouldGetMedia ? getMedia( id ) : null, imageSizes, imageDefaultSize, featuredImageId, + wasBlockJustInserted: wasBlockJustInserted( + clientId, + 'inserter_menu' + ), }; } ), withPreferredColorScheme, diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 2d0cf9fbe014e..9ab998ea579da 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -19,7 +19,7 @@ import { ToolbarGroup, PanelBody, } from '@wordpress/components'; -import { withPreferredColorScheme } from '@wordpress/compose'; +import { withPreferredColorScheme, compose } from '@wordpress/compose'; import { BlockCaption, MediaPlaceholder, @@ -30,11 +30,13 @@ import { VIDEO_ASPECT_RATIO, VideoPlayer, InspectorControls, + store as blockEditorStore, } from '@wordpress/block-editor'; import { __, sprintf } from '@wordpress/i18n'; import { isURL, getProtocol } from '@wordpress/url'; import { doAction, hasAction } from '@wordpress/hooks'; import { video as SvgIcon, replace } from '@wordpress/icons'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -189,7 +191,12 @@ class VideoEdit extends Component { } render() { - const { setAttributes, attributes, isSelected } = this.props; + const { + setAttributes, + attributes, + isSelected, + wasBlockJustInserted, + } = this.props; const { id, src } = attributes; const { videoContainerHeight } = this.state; @@ -221,6 +228,9 @@ class VideoEdit extends Component { onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( ICON_TYPE.PLACEHOLDER ) } onFocus={ this.props.onFocus } + autoOpenMediaUpload={ + isSelected && ! src && wasBlockJustInserted + } /> ); @@ -361,4 +371,12 @@ class VideoEdit extends Component { } } -export default withPreferredColorScheme( VideoEdit ); +export default compose( [ + withSelect( ( select, { clientId } ) => ( { + wasBlockJustInserted: select( blockEditorStore ).wasBlockJustInserted( + clientId, + 'inserter_menu' + ), + } ) ), + withPreferredColorScheme, +] )( VideoEdit ); diff --git a/packages/editor/src/store/test/actions.native.js b/packages/editor/src/store/test/actions.native.js index 577a38e95fe8e..9449d13e164d7 100644 --- a/packages/editor/src/store/test/actions.native.js +++ b/packages/editor/src/store/test/actions.native.js @@ -3,7 +3,7 @@ */ import { togglePostTitleSelection } from '../actions'; -describe( 'Editor actions', () => { +describe( 'actions native', () => { describe( 'togglePostTitleSelection', () => { it( 'should return the TOGGLE_POST_TITLE_SELECTION action', () => { const result = togglePostTitleSelection( true ); diff --git a/packages/editor/src/store/test/reducer.native.js b/packages/editor/src/store/test/reducer.native.js index ba691a98513d9..c757b67153b6b 100644 --- a/packages/editor/src/store/test/reducer.native.js +++ b/packages/editor/src/store/test/reducer.native.js @@ -5,7 +5,7 @@ import { postTitle } from '../reducer'; describe( 'state native', () => { describe( 'postTitle', () => { - describe( 'isSelected()', () => { + describe( 'isSelected', () => { it( 'should not be selected by default', () => { expect( postTitle( undefined, {} ).isSelected ).toBe( false ); } ); diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 2f78a93702160..541155186660a 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,8 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Image block: Add a "featured" banner. (Android only) [#30806] +- [**] The media upload options of the Image, Video and Gallery block automatically opens when the respective block is inserted. [#29546] + ## 1.51.0 diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js index 37211a5db2e0b..4789aa0bd5776 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js @@ -6,6 +6,9 @@ import { blockNames } from './pages/editor-page'; describe( 'Gutenberg Editor Gallery Block tests', () => { it( 'should be able to add a gallery block', async () => { await editorPage.addNewBlock( blockNames.gallery ); + await editorPage.driver.sleep( 1000 ); + await editorPage.closePicker(); + const galleryBlock = await editorPage.getBlockAtPosition( blockNames.gallery ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js index 30d135e4cdc1e..40a9a1b38a96f 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js @@ -8,6 +8,9 @@ import testData from './helpers/test-data'; describe( 'Gutenberg Editor Image Block tests', () => { it( 'should be able to add an image block', async () => { await editorPage.addNewBlock( blockNames.image ); + await editorPage.driver.sleep( 1000 ); + await editorPage.closePicker(); + let imageBlock = await editorPage.getBlockAtPosition( blockNames.image ); diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 492d16f62c028..83cfd9a21ac07 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -570,6 +570,17 @@ class EditorPage { return await typeString( this.driver, textViewElement, text, clear ); } + async closePicker() { + if ( isAndroid() ) { + await swipeDown( this.driver ); + } else { + const cancelButton = await this.driver.elementByAccessibilityId( + 'Cancel' + ); + await cancelButton.click(); + } + } + // ============================= // Unsupported Block functions // =============================