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 2e375ec1684833..e120c53daf2c01 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -2,7 +2,7 @@ * External dependencies */ import { View, Text, TouchableWithoutFeedback } from 'react-native'; -import { uniqBy } from 'lodash'; +import { uniqWith } from 'lodash'; /** * WordPress dependencies @@ -36,7 +36,9 @@ function MediaPlaceholder( props ) { } = props; const setMedia = multiple && addToGallery ? - ( selected ) => onSelect( uniqBy( [ ...value, ...selected ], 'id' ) ) : + ( selected ) => onSelect( uniqWith( [ ...value, ...selected ], ( media1, media2 ) => { + return media1.id === media2.id || media1.url === media2.url; + } ) ) : onSelect; const isOneType = allowedTypes.length === 1; diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index e25885c73503bb..72a337cc921bd1 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -86,7 +86,15 @@ export class MediaUpload extends React.Component { } getMediaOptionsItems() { - const { allowedTypes = [] } = this.props; + const { allowedTypes = [], multiple = false } = this.props; + + // disable upload sources for now when multiple flag is set + // eslint-disable-next-line no-undef + if ( ! __DEV__ ) { + if ( allowedTypes.includes( MEDIA_TYPE_IMAGE ) && multiple ) { + return [ siteLibrarySource ]; + } + } return this.getAllSources().filter( ( source ) => { return allowedTypes.filter( ( allowedType ) => source.types.includes( allowedType ) ).length > 0; diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index d4f51b1565cffe..2a5767b71be071 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import classnames from 'classnames'; import { every, filter, @@ -23,22 +22,21 @@ import { withNotices, } from '@wordpress/components'; import { - BlockIcon, MediaPlaceholder, InspectorControls, - RichText, } from '@wordpress/block-editor'; -import { Component } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { Component, Platform } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; import { withSelect } from '@wordpress/data'; +import { withViewportMatch } from '@wordpress/viewport'; /** * Internal dependencies */ -import GalleryImage from './gallery-image'; -import { icon } from './icons'; +import { sharedIcon } from './shared-icon'; import { defaultColumnsNumber, pickRelevantMediaFiles } from './shared'; +import Gallery from './gallery'; const MAX_COLUMNS = 8; const linkOptions = [ @@ -48,6 +46,18 @@ const linkOptions = [ ]; const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const PLACEHOLDER_TEXT = Platform.select( { + web: __( 'Drag images, upload new ones or select files from your library.' ), + native: __( 'ADD MEDIA' ), +} ); + +// currently this is needed for consistent controls UI on mobile +// this can be removed after control components settle on consistent defaults +const MOBILE_CONTROL_PROPS = Platform.select( { + web: {}, + native: { separatorType: 'fullWidth' }, +} ); + class GalleryEdit extends Component { constructor() { super( ...arguments ); @@ -227,7 +237,7 @@ class GalleryEdit extends Component { componentDidMount() { const { attributes, mediaUpload } = this.props; const { images } = attributes; - if ( every( images, ( { url } ) => isBlobURL( url ) ) ) { + if ( Platform.OS === 'web' && every( images, ( { url } ) => isBlobURL( url ) ) ) { const filesList = map( images, ( { url } ) => getBlobByURL( url ) ); forEach( images, ( { url } ) => revokeBlobURL( url ) ); mediaUpload( { @@ -254,12 +264,9 @@ class GalleryEdit extends Component { className, isSelected, noticeUI, - setAttributes, } = this.props; const { - align, columns = defaultColumnsNumber( attributes ), - caption, imageCrop, images, linkTo, @@ -274,10 +281,10 @@ class GalleryEdit extends Component { isAppender={ hasImages } className={ className } disableMediaButtons={ hasImages && ! isSelected } - icon={ ! hasImages && } + icon={ ! hasImages && sharedIcon } labels={ { title: ! hasImages && __( 'Gallery' ), - instructions: ! hasImages && __( 'Drag images, upload new ones or select files from your library.' ), + instructions: ! hasImages && PLACEHOLDER_TEXT, } } onSelect={ this.onSelectImages } accept="image/*" @@ -286,25 +293,20 @@ class GalleryEdit extends Component { value={ hasImagesWithId ? images : undefined } onError={ this.onUploadError } notices={ hasImages ? undefined : noticeUI } + onFocus={ this.props.onFocus } /> ); if ( ! hasImages ) { return mediaPlaceholder; } - - const captionClassNames = classnames( - 'blocks-gallery-caption', - { - 'screen-reader-text': ! isSelected && RichText.isEmpty( caption ), - } - ); return ( <> { images.length > 1 && } { noticeUI } -
- - { mediaPlaceholder } - setAttributes( { caption: value } ) } - inlineToolbar - /> -
+ ); } @@ -383,4 +352,5 @@ export default compose( [ return { mediaUpload }; } ), withNotices, + withViewportMatch( { isNarrow: '< small' } ), ] )( GalleryEdit ); diff --git a/packages/block-library/src/gallery/gallery-image.native.js b/packages/block-library/src/gallery/gallery-image.native.js index 38623d565e3391..60d6138521c5fd 100644 --- a/packages/block-library/src/gallery/gallery-image.native.js +++ b/packages/block-library/src/gallery/gallery-image.native.js @@ -6,13 +6,14 @@ import { requestImageFailedRetryDialog, requestImageUploadCancelDialog, } from 'react-native-gutenberg-bridge'; +import { isEmpty } from 'lodash'; /** * WordPress dependencies */ import { Component } from '@wordpress/element'; import { Icon } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { RichText, MediaUploadProgress } from '@wordpress/block-editor'; import { isURL } from '@wordpress/url'; import { withPreferredColorScheme } from '@wordpress/compose'; @@ -258,7 +259,7 @@ class GalleryImage extends Component { } render() { - const { id, onRemove, getStylesFromColorScheme } = this.props; + const { id, onRemove, getStylesFromColorScheme, isSelected } = this.props; const containerStyle = getStylesFromColorScheme( style.galleryImageContainer, style.galleryImageContainerDark ); @@ -266,6 +267,9 @@ class GalleryImage extends Component { return ( ); } + + accessibilityLabelImageContainer() { + const { caption, 'aria-label': ariaLabel } = this.props; + + return isEmpty( caption ) ? ariaLabel : ( ariaLabel + '. ' + sprintf( + /* translators: accessibility text. %s: image caption. */ + __( 'Image caption. %s' ), caption + ) ); + } } export default withPreferredColorScheme( GalleryImage ); diff --git a/packages/block-library/src/gallery/gallery.js b/packages/block-library/src/gallery/gallery.js new file mode 100644 index 00000000000000..2f1a53156c522f --- /dev/null +++ b/packages/block-library/src/gallery/gallery.js @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + RichText, +} from '@wordpress/block-editor'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import GalleryImage from './gallery-image'; +import { defaultColumnsNumber } from './shared'; + +export const Gallery = ( props ) => { + const { + attributes, + className, + isSelected, + setAttributes, + selectedImage, + mediaPlaceholder, + onMoveBackward, + onMoveForward, + onRemoveImage, + onSelectImage, + onSetImageAttributes, + onFocusGalleryCaption, + } = props; + + const { + align, + columns = defaultColumnsNumber( attributes ), + caption, + imageCrop, + images, + } = attributes; + + const captionClassNames = classnames( + 'blocks-gallery-caption', + { + 'screen-reader-text': ! isSelected && RichText.isEmpty( caption ), + } + ); + + return ( +
+
    + { images.map( ( img, index ) => { + /* translators: %1$d is the order number of the image, %2$d is the total number of images. */ + const ariaLabel = sprintf( __( 'image %1$d of %2$d in gallery' ), ( index + 1 ), images.length ); + + return ( +
  • + onSetImageAttributes( index, attrs ) } + caption={ img.caption } + aria-label={ ariaLabel } + /> +
  • + ); + } ) } +
+ { mediaPlaceholder } + setAttributes( { caption: value } ) } + inlineToolbar + /> +
+ ); +}; + +export default Gallery; diff --git a/packages/block-library/src/gallery/shared-icon.js b/packages/block-library/src/gallery/shared-icon.js new file mode 100644 index 00000000000000..68ec4aa0f75dda --- /dev/null +++ b/packages/block-library/src/gallery/shared-icon.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { BlockIcon } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { icon } from './icons.js'; + +export const sharedIcon = ; diff --git a/packages/block-library/src/gallery/shared-icon.native.js b/packages/block-library/src/gallery/shared-icon.native.js new file mode 100644 index 00000000000000..30ccbf2929dc5e --- /dev/null +++ b/packages/block-library/src/gallery/shared-icon.native.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; +import { withPreferredColorScheme } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { icon } from './icons.js'; +import styles from './styles.scss'; + +const IconWithColorScheme = withPreferredColorScheme( ( { getStylesFromColorScheme } ) => { + const colorSchemeStyles = getStylesFromColorScheme( styles.icon, styles.iconDark ); + return ; +} ); + +export const sharedIcon = ; diff --git a/packages/block-library/src/gallery/styles.native.scss b/packages/block-library/src/gallery/styles.native.scss new file mode 100644 index 00000000000000..f5c00393677262 --- /dev/null +++ b/packages/block-library/src/gallery/styles.native.scss @@ -0,0 +1,7 @@ +.icon { + fill: $gray-dark; +} + +.iconDark { + fill: $white; +} diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 22e534fb2669e5..bdfc5cbb0fabf6 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -146,6 +146,7 @@ export const registerCoreBlocks = () => { list, quote, mediaText, + gallery, // eslint-disable-next-line no-undef ( ( Platform.OS === 'ios' ) || ( !! __DEV__ ) ) ? preformatted : null, // eslint-disable-next-line no-undef diff --git a/packages/editor/src/utils/index.native.js b/packages/editor/src/utils/index.native.js index e69de29bb2d1d6..3a49f66b86c902 100644 --- a/packages/editor/src/utils/index.native.js +++ b/packages/editor/src/utils/index.native.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import mediaUpload from './media-upload'; + +export { mediaUpload }; diff --git a/packages/editor/src/utils/media-upload/index.native.js b/packages/editor/src/utils/media-upload/index.native.js new file mode 100644 index 00000000000000..9b7a53d3376d55 --- /dev/null +++ b/packages/editor/src/utils/media-upload/index.native.js @@ -0,0 +1 @@ +export default function() { }