diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index c71e28a6b69c4..2c538144ec528 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -36,6 +36,8 @@ function InserterMenu( { insertDefaultBlock, } = useDispatch( blockEditorStore ); + const { addLastBlockInserted } = useDispatch( 'core/editor' ); + const { items, destinationRootClientId, @@ -110,6 +112,8 @@ function InserterMenu( { innerBlocks ); + addLastBlockInserted( newBlock.clientId ); + insertBlock( newBlock, insertionIndex, destinationRootClientId ); }, [ 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 da47806e7b14f..14647d2ab27c4 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -63,6 +63,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..0807aedd89004 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 } + autoOpenMediaUpload={ autoOpenMediaUpload } render={ ( { open, getMediaOptions } ) => { return ( { const otherMediaOptionsWithIcons = otherMediaOptions.map( ( option ) => { @@ -54,6 +62,10 @@ export class MediaUpload extends Component { this.setState( { otherMediaOptions: otherMediaOptionsWithIcons } ); } ); + + if ( autoOpenMediaUpload ) { + this.onPickerPresent(); + } } getAllSources() { @@ -136,8 +148,20 @@ export class MediaUpload extends Component { } onPickerPresent() { + const { autoOpenMediaUpload } = 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 ( autoOpenMediaUpload && isIOS ) { + delay( + () => this.picker.presentPicker(), + PICKER_OPENING_DELAY + ); + } else { + this.picker.presentPicker(); + } } } diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 184efb0a6f570..79665447766ce 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, withSelect, withDispatch } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { View } from '@wordpress/primitives'; import { store as coreStore } from '@wordpress/core-data'; @@ -81,6 +81,7 @@ function GalleryEdit( props ) { imageSizes, resizedImages, onFocus, + wasBlockJustInserted, } = props; const { columns = defaultColumnsNumber( attributes ), @@ -343,6 +344,9 @@ function GalleryEdit( props ) { onError={ onUploadError } notices={ hasImages ? undefined : noticeUI } onFocus={ onFocus } + autoOpenMediaUpload={ + ! hasImages && isSelected && wasBlockJustInserted() + } /> ); @@ -464,6 +468,22 @@ export default compose( [ resizedImages, }; } ), + withDispatch( ( dispatch, { clientId }, { select } ) => { + return { + wasBlockJustInserted() { + const { clearLastBlockInserted } = dispatch( 'core/editor' ); + const { wasBlockJustInserted } = select( 'core/editor' ); + + const result = wasBlockJustInserted( clientId ); + + if ( result ) { + clearLastBlockInserted(); + return true; + } + return false; + }, + }; + } ), 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 3f9baab48611f..77894ad69320c 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -42,7 +42,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { getProtocol, hasQueryArg } from '@wordpress/url'; import { doAction, hasAction } from '@wordpress/hooks'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { withSelect, withDispatch } from '@wordpress/data'; import { image as placeholderIcon, textColor, @@ -66,7 +66,6 @@ const getUrlForSlug = ( image, { sizeSlug } ) => { export class ImageEdit extends Component { constructor( props ) { super( props ); - this.state = { isCaptionSelected: false, }; @@ -116,6 +115,7 @@ export class ImageEdit extends Component { value: slug, name, } ) ); + this.onClearMedia = this.onClearMedia.bind( this ); } componentDidMount() { @@ -340,6 +340,10 @@ export class ImageEdit extends Component { } } + onClearMedia() { + this.props.setAttributes( { id: null, url: null } ); + } + getPlaceholderIcon() { return ( ); @@ -520,6 +533,16 @@ export class ImageEdit extends Component { this.onSelectMediaUploadOption } openMediaOptions={ openMediaOptions } + mediaPickerOptions={ [ + { + destructiveButton: true, + id: 'clearMedia', + label: __( 'Clear Media' ), + onPress: this.onClearMedia, + separated: true, + value: 'clearMedia', + }, + ] } retryMessage={ retryMessage } url={ url } shapeStyle={ styles[ className ] } @@ -579,5 +602,21 @@ export default compose( [ imageSizes, }; } ), + withDispatch( ( dispatch, { clientId }, { select } ) => { + return { + wasBlockJustInserted() { + const { clearLastBlockInserted } = dispatch( 'core/editor' ); + const { wasBlockJustInserted } = select( 'core/editor' ); + + const result = wasBlockJustInserted( clientId ); + + if ( result ) { + clearLastBlockInserted(); + return true; + } + return false; + }, + }; + } ), withPreferredColorScheme, ] )( ImageEdit ); diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 2d0cf9fbe014e..3f170fa37a9d7 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, @@ -35,6 +35,7 @@ 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 { withDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -189,7 +190,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 +227,9 @@ class VideoEdit extends Component { onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( ICON_TYPE.PLACEHOLDER ) } onFocus={ this.props.onFocus } + autoOpenMediaUpload={ + isSelected && ! src && wasBlockJustInserted() + } /> ); @@ -361,4 +370,22 @@ class VideoEdit extends Component { } } -export default withPreferredColorScheme( VideoEdit ); +export default compose( [ + withDispatch( ( dispatch, { clientId }, { select } ) => { + return { + wasBlockJustInserted() { + const { clearLastBlockInserted } = dispatch( 'core/editor' ); + const { wasBlockJustInserted } = select( 'core/editor' ); + + const result = wasBlockJustInserted( clientId ); + + if ( result ) { + clearLastBlockInserted(); + return true; + } + return false; + }, + }; + } ), + withPreferredColorScheme, +] )( VideoEdit ); diff --git a/packages/components/src/mobile/picker/index.ios.js b/packages/components/src/mobile/picker/index.ios.js index bd99917ec4042..14bdf987b37e5 100644 --- a/packages/components/src/mobile/picker/index.ios.js +++ b/packages/components/src/mobile/picker/index.ios.js @@ -25,7 +25,7 @@ class Picker extends Component { onHandleClosingBottomSheet, } = this.props; const labels = options.map( ( { label } ) => label ); - const fullOptions = [ __( 'Cancel' ) ].concat( labels ); + const fullOptions = [ __( 'Dismiss' ) ].concat( labels ); ActionSheetIOS.showActionSheetWithOptions( { diff --git a/packages/editor/src/store/actions.native.js b/packages/editor/src/store/actions.native.js index 49d40749cb1c7..f45eec5ea3ce3 100644 --- a/packages/editor/src/store/actions.native.js +++ b/packages/editor/src/store/actions.native.js @@ -25,3 +25,28 @@ export function togglePostTitleSelection( isSelected = true ) { export function* autosave() { RNReactNativeGutenbergBridge.editorDidAutosave(); } + +/** + * Returns an action object to track the last block that was inserted. + * + * @param {Object} clientId The client id of the block. + * + * @return {Object} Action object. + */ +export function addLastBlockInserted( clientId ) { + return { + type: 'ADD_LAST_BLOCK_INSERTED', + clientId, + }; +} + +/** + * Returns an action object to clear the last block that was inserted. + * + * @return {Object} Action object. + */ +export function clearLastBlockInserted() { + return { + type: 'CLEAR_LAST_BLOCK_INSERTED', + }; +} diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index d9b3fbcd67888..ec4c91c6b0c26 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -80,6 +80,25 @@ export function notices( 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 'ADD_LAST_BLOCK_INSERTED': + return { ...state, clientId: action.clientId }; + + case 'CLEAR_LAST_BLOCK_INSERTED': + return {}; + } + return state; +} + export default combineReducers( { postId, postType, @@ -93,4 +112,5 @@ export default combineReducers( { editorSettings, clipboard, notices, + lastBlockInserted, } ); diff --git a/packages/editor/src/store/selectors.native.js b/packages/editor/src/store/selectors.native.js index 2edfc373213fa..da65a8d08e3e8 100644 --- a/packages/editor/src/store/selectors.native.js +++ b/packages/editor/src/store/selectors.native.js @@ -55,3 +55,14 @@ export const isEditedPostAutosaveable = createRegistrySelector( return false; } ); + +/** + * 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. + * @return {boolean} If the client id exists within the lastBlockInserted state then the block was just inserted. + */ +export function wasBlockJustInserted( state, clientId ) { + return state.lastBlockInserted.clientId === clientId; +} diff --git a/packages/editor/src/store/test/actions.native.js b/packages/editor/src/store/test/actions.native.js index 577a38e95fe8e..ed00382fa3b7f 100644 --- a/packages/editor/src/store/test/actions.native.js +++ b/packages/editor/src/store/test/actions.native.js @@ -1,9 +1,13 @@ /** * Internal dependencies */ -import { togglePostTitleSelection } from '../actions'; +import { + togglePostTitleSelection, + addLastBlockInserted, + clearLastBlockInserted, +} from '../actions'; -describe( 'Editor actions', () => { +describe( 'actions native', () => { describe( 'togglePostTitleSelection', () => { it( 'should return the TOGGLE_POST_TITLE_SELECTION action', () => { const result = togglePostTitleSelection( true ); @@ -13,4 +17,24 @@ describe( 'Editor actions', () => { } ); } ); } ); + + describe( 'addLastBlockInserted', () => { + it( 'should return the ADD_LAST_BLOCK_INSERTED action', () => { + const expectedClientId = 1; + const result = addLastBlockInserted( expectedClientId ); + expect( result ).toEqual( { + type: 'ADD_LAST_BLOCK_INSERTED', + clientId: expectedClientId, + } ); + } ); + } ); + + describe( 'clearLastBlockInserted', () => { + it( 'should return the CLEAR_LAST_BLOCK_INSERTED action', () => { + const result = clearLastBlockInserted(); + expect( result ).toEqual( { + type: 'CLEAR_LAST_BLOCK_INSERTED', + } ); + } ); + } ); } ); diff --git a/packages/editor/src/store/test/reducer.native.js b/packages/editor/src/store/test/reducer.native.js index ba691a98513d9..741c157075eab 100644 --- a/packages/editor/src/store/test/reducer.native.js +++ b/packages/editor/src/store/test/reducer.native.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { postTitle } from '../reducer'; +import { postTitle, lastBlockInserted } from '../reducer'; describe( 'state native', () => { describe( 'postTitle', () => { @@ -32,5 +32,28 @@ describe( 'state native', () => { ).toBe( true ); } ); } ); + + describe( 'lastBlockInserted()', () => { + it( 'should return client id of last block inserted', () => { + const expectedClientId = 1; + const action = { + type: 'ADD_LAST_BLOCK_INSERTED', + clientId: expectedClientId, + }; + + expect( + lastBlockInserted( { clientId: expectedClientId }, action ) + .clientId + ).toBe( expectedClientId ); + } ); + + it( 'should return empty state if last block has been cleared', () => { + const action = { + type: 'CLEAR_LAST_BLOCK_INSERTED', + }; + + expect( lastBlockInserted( {}, action ) ).toStrictEqual( {} ); + } ); + } ); } ); } ); diff --git a/packages/editor/src/store/test/selectors.native.js b/packages/editor/src/store/test/selectors.native.js index 958a68e651f74..f16f9fc1554a5 100644 --- a/packages/editor/src/store/test/selectors.native.js +++ b/packages/editor/src/store/test/selectors.native.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { isPostTitleSelected } from '../selectors'; +import { isPostTitleSelected, wasBlockJustInserted } from '../selectors'; describe( 'selectors native', () => { describe( 'isPostTitleSelected', () => { @@ -25,4 +25,33 @@ describe( 'selectors native', () => { expect( isPostTitleSelected( state ) ).toBe( false ); } ); } ); + + describe( 'wasBlockJustInserted', () => { + it( 'should return true if the client id passed to wasBlockJustInserted is found within the state', () => { + const expectedClientId = 1; + const state = { + lastBlockInserted: { + clientId: expectedClientId, + }, + }; + + expect( wasBlockJustInserted( state, expectedClientId ) ).toBe( + true + ); + } ); + + it( 'should return false if the client id passed to wasBlockJustInserted is not found within the state', () => { + const expectedClientId = 1; + const unexpectedClientId = 0; + const state = { + lastBlockInserted: { + clientId: unexpectedClientId, + }, + }; + + expect( wasBlockJustInserted( state, expectedClientId ) ).toBe( + false + ); + } ); + } ); } ); 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 8fd19373bba37..f3b32d587bbd3 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -526,6 +526,17 @@ class EditorPage { return await typeString( this.driver, textViewElement, text, clear ); } + async closePicker() { + if ( isAndroid() ) { + await swipeDown( this.driver ); + } else { + const dismissButton = await this.driver.elementByAccessibilityId( + 'Dismiss' + ); + await dismissButton.click(); + } + } + // ============================= // Unsupported Block functions // =============================