From a78f204df645ce86890814796b41498248f70dad Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Thu, 29 Aug 2019 16:53:06 +0200 Subject: [PATCH] Add native support for the MediaText block (#16305) * First working version of the MediaText component for native mobile * Fix adding a block to an innerblock list * Disable mediaText on production * MediaText native: improve editor visuals * Move BlockToolbar from BlockList to Layout * Remove BlockEditorProvider from BlockList and add native version of EditorProvider to Editor. Plus support InsertionPoint and BlockListAppender * Update BlockMover for native to hide if locked or if it's the only block * Make the vertical align button work, add more styling options for toolbar buttons * Make sure registerCoreBlocks does not break in production * Copy docblock comment from the web version for registerCoreBlocks * Fix focusing on the media placeholder * Only support adding image for now * Update usage of MediaPlaceholder in MediaContainer * Enable autoScroll for just the out most block list * Fix JS Unit tests * Roll back to IconButton refactor and fix tests * Fix BlockVerticalAlignmentToolbar buttons style on mobile * Fix thing for web and ensure ariaPressed is always passed down * Use AriaPressed directly to style SVG on mobile * Update snapshots --- .../src/components/block-icon/index.native.js | 30 +++ .../block-list-appender/index.native.js | 54 +++++ .../block-list-appender/style.native.scss | 9 + .../src/components/block-list/index.native.js | 1 + .../block-list/insertion-point.native.js | 46 +++++ .../components/block-mover/index.native.js | 82 ++++---- .../src/components/index.native.js | 3 + .../components/inner-blocks/index.native.js | 182 +++++++++++++++++ .../src/components/url-input/test/button.js | 8 +- packages/block-library/src/index.native.js | 42 +++- .../src/media-text/edit.native.js | 186 +++++++++++++++++ .../src/media-text/media-container.native.js | 191 ++++++++++++++++++ .../src/media-text/style.native.scss | 29 +++ packages/blocks/src/api/index.native.js | 4 + packages/components/src/icon-button/index.js | 6 +- .../components/src/icon-button/test/index.js | 2 +- packages/components/src/icon/index.js | 20 +- packages/components/src/icon/test/index.js | 10 +- .../keyboard-aware-flat-list/index.ios.js | 2 + .../src/primitives/svg/index.native.js | 4 +- .../src/primitives/svg/style.native.scss | 6 +- .../test/__snapshots__/index.js.snap | 45 +++-- .../components/visual-editor/index.native.js | 1 + 23 files changed, 879 insertions(+), 84 deletions(-) create mode 100644 packages/block-editor/src/components/block-icon/index.native.js create mode 100644 packages/block-editor/src/components/block-list-appender/index.native.js create mode 100644 packages/block-editor/src/components/block-list-appender/style.native.scss create mode 100644 packages/block-editor/src/components/block-list/insertion-point.native.js create mode 100644 packages/block-editor/src/components/inner-blocks/index.native.js create mode 100644 packages/block-library/src/media-text/edit.native.js create mode 100644 packages/block-library/src/media-text/media-container.native.js create mode 100644 packages/block-library/src/media-text/style.native.scss diff --git a/packages/block-editor/src/components/block-icon/index.native.js b/packages/block-editor/src/components/block-icon/index.native.js new file mode 100644 index 0000000000000..4dddda5ecce8f --- /dev/null +++ b/packages/block-editor/src/components/block-icon/index.native.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Path, Icon, SVG } from '@wordpress/components'; + +export default function BlockIcon( { icon, showColors = false } ) { + if ( get( icon, [ 'src' ] ) === 'block-default' ) { + icon = { + src: , + }; + } + + const renderedIcon = ; + const style = showColors ? { + backgroundColor: icon && icon.background, + color: icon && icon.foreground, + } : {}; + + return ( + + { renderedIcon } + + ); +} diff --git a/packages/block-editor/src/components/block-list-appender/index.native.js b/packages/block-editor/src/components/block-list-appender/index.native.js new file mode 100644 index 0000000000000..9a6a24c557517 --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/index.native.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { last } from 'lodash'; + +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { getDefaultBlockName } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import DefaultBlockAppender from '../default-block-appender'; +import styles from './style.scss'; + +function BlockListAppender( { + blockClientIds, + rootClientId, + canInsertDefaultBlock, + isLocked, +} ) { + if ( isLocked ) { + return null; + } + + if ( canInsertDefaultBlock ) { + return ( + 0 ? '' : null } + /> + ); + } + + return null; +} + +export default withSelect( ( select, { rootClientId } ) => { + const { + getBlockOrder, + canInsertBlockType, + getTemplateLock, + } = select( 'core/block-editor' ); + + return { + isLocked: !! getTemplateLock( rootClientId ), + blockClientIds: getBlockOrder( rootClientId ), + canInsertDefaultBlock: canInsertBlockType( getDefaultBlockName(), rootClientId ), + }; +} )( BlockListAppender ); diff --git a/packages/block-editor/src/components/block-list-appender/style.native.scss b/packages/block-editor/src/components/block-list-appender/style.native.scss new file mode 100644 index 0000000000000..60734f5e9c210 --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/style.native.scss @@ -0,0 +1,9 @@ + +.blockListAppender { + background-color: $white; + padding-left: 16; + padding-right: 16; + padding-top: 12; + padding-bottom: 0; // will be flushed into inline toolbar height + border-color: transparent; +} diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 10a5cfcbcf1ba..7c67de71c342f 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -100,6 +100,7 @@ export class BlockList extends Component { { + if ( ! showInsertionPoint ) { + return null; + } + + return ( + + + { __( 'ADD BLOCK HERE' ) } + + + ); +}; + +export default withSelect( ( select, { clientId, rootClientId } ) => { + const { + getBlockIndex, + getBlockInsertionPoint, + isBlockInsertionPointVisible, + } = select( 'core/block-editor' ); + const blockIndex = getBlockIndex( clientId, rootClientId ); + const insertionPoint = getBlockInsertionPoint(); + const showInsertionPoint = ( + isBlockInsertionPointVisible() && + insertionPoint.index === blockIndex && + insertionPoint.rootClientId === rootClientId + ); + + return { showInsertionPoint }; +} )( BlockInsertionPoint ); diff --git a/packages/block-editor/src/components/block-mover/index.native.js b/packages/block-editor/src/components/block-mover/index.native.js index 8962dc22f49e4..40bc4d550b7ab 100644 --- a/packages/block-editor/src/components/block-mover/index.native.js +++ b/packages/block-editor/src/components/block-mover/index.native.js @@ -14,51 +14,59 @@ import { withInstanceId, compose } from '@wordpress/compose'; const BlockMover = ( { isFirst, isLast, + isLocked, onMoveDown, onMoveUp, firstIndex, -} ) => ( - <> - + rootClientId, +} ) => { + if ( isLocked || ( isFirst && isLast && ! rootClientId ) ) { + return null; + } - - -); + return ( + <> + + + + + ); +}; export default compose( withSelect( ( select, { clientIds } ) => { - const { getBlockIndex, getBlockRootClientId, getBlockOrder } = select( 'core/block-editor' ); + const { getBlockIndex, getTemplateLock, getBlockRootClientId, getBlockOrder } = select( 'core/block-editor' ); const normalizedClientIds = castArray( clientIds ); const firstClientId = first( normalizedClientIds ); - const rootClientId = getBlockRootClientId( first( normalizedClientIds ) ); + const rootClientId = getBlockRootClientId( firstClientId ); const blockOrder = getBlockOrder( rootClientId ); const firstIndex = getBlockIndex( firstClientId, rootClientId ); const lastIndex = getBlockIndex( last( normalizedClientIds ), rootClientId ); @@ -67,6 +75,8 @@ export default compose( firstIndex, isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, + isLocked: getTemplateLock( rootClientId ) === 'all', + rootClientId, }; } ), withDispatch( ( dispatch, { clientIds, rootClientId } ) => { diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 5f13de7d91b4c..7c6906da0066a 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -2,9 +2,12 @@ export { default as BlockControls } from './block-controls'; export { default as BlockEdit } from './block-edit'; export { default as BlockFormatControls } from './block-format-controls'; +export { default as BlockIcon } from './block-icon'; +export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; export * from './colors'; export * from './font-sizes'; export { default as AlignmentToolbar } from './alignment-toolbar'; +export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorControls } from './inspector-controls'; export { default as PlainText } from './plain-text'; export { diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js new file mode 100644 index 0000000000000..d0515d6d8a58a --- /dev/null +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import { pick, isEqual } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { synchronizeBlocksWithTemplate, withBlockContentContext } from '@wordpress/blocks'; +import isShallowEqual from '@wordpress/is-shallow-equal'; +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import ButtonBlockAppender from './button-block-appender'; +import DefaultBlockAppender from './default-block-appender'; + +/** + * Internal dependencies + */ +import BlockList from '../block-list'; +import { withBlockEditContext } from '../block-edit/context'; + +class InnerBlocks extends Component { + constructor() { + super( ...arguments ); + this.state = { + templateInProcess: !! this.props.template, + }; + this.updateNestedSettings(); + } + + getTemplateLock() { + const { + templateLock, + parentLock, + } = this.props; + return templateLock === undefined ? parentLock : templateLock; + } + + componentDidMount() { + const { innerBlocks } = this.props.block; + // only synchronize innerBlocks with template if innerBlocks are empty or a locking all exists + if ( innerBlocks.length === 0 || this.getTemplateLock() === 'all' ) { + this.synchronizeBlocksWithTemplate(); + } + + if ( this.state.templateInProcess ) { + this.setState( { + templateInProcess: false, + } ); + } + } + + componentDidUpdate( prevProps ) { + const { template, block } = this.props; + const { innerBlocks } = block; + + this.updateNestedSettings(); + // only synchronize innerBlocks with template if innerBlocks are empty or a locking all exists + if ( innerBlocks.length === 0 || this.getTemplateLock() === 'all' ) { + const hasTemplateChanged = ! isEqual( template, prevProps.template ); + if ( hasTemplateChanged ) { + this.synchronizeBlocksWithTemplate(); + } + } + } + + /** + * Called on mount or when a mismatch exists between the templates and + * inner blocks, synchronizes inner blocks with the template, replacing + * current blocks. + */ + synchronizeBlocksWithTemplate() { + const { template, block, replaceInnerBlocks } = this.props; + const { innerBlocks } = block; + + // Synchronize with templates. If the next set differs, replace. + const nextBlocks = synchronizeBlocksWithTemplate( innerBlocks, template ); + if ( ! isEqual( nextBlocks, innerBlocks ) ) { + replaceInnerBlocks( nextBlocks ); + } + } + + updateNestedSettings() { + const { + blockListSettings, + allowedBlocks, + updateNestedSettings, + } = this.props; + + const newSettings = { + allowedBlocks, + templateLock: this.getTemplateLock(), + }; + + if ( ! isShallowEqual( blockListSettings, newSettings ) ) { + updateNestedSettings( newSettings ); + } + } + + render() { + const { + clientId, + renderAppender, + template, + __experimentalTemplateOptions: templateOptions, + } = this.props; + const { templateInProcess } = this.state; + + const isPlaceholder = template === null && !! templateOptions; + + return ( + <> + { ! templateInProcess && ( + isPlaceholder ? + null : + + ) } + + ); + } +} + +InnerBlocks = compose( [ + withBlockEditContext( ( context ) => pick( context, [ 'clientId' ] ) ), + withSelect( ( select, ownProps ) => { + const { + isBlockSelected, + hasSelectedInnerBlock, + getBlock, + getBlockListSettings, + getBlockRootClientId, + getTemplateLock, + } = select( 'core/block-editor' ); + const { clientId } = ownProps; + const block = getBlock( clientId ); + const rootClientId = getBlockRootClientId( clientId ); + + return { + block, + blockListSettings: getBlockListSettings( clientId ), + hasOverlay: block.name !== 'core/template' && ! isBlockSelected( clientId ) && ! hasSelectedInnerBlock( clientId, true ), + parentLock: getTemplateLock( rootClientId ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + replaceInnerBlocks, + updateBlockListSettings, + } = dispatch( 'core/block-editor' ); + const { block, clientId, templateInsertUpdatesSelection = true } = ownProps; + + return { + replaceInnerBlocks( blocks ) { + replaceInnerBlocks( clientId, blocks, block.innerBlocks.length === 0 && templateInsertUpdatesSelection ); + }, + updateNestedSettings( settings ) { + dispatch( updateBlockListSettings( clientId, settings ) ); + }, + }; + } ), +] )( InnerBlocks ); + +// Expose default appender placeholders as components. +InnerBlocks.DefaultBlockAppender = DefaultBlockAppender; +InnerBlocks.ButtonBlockAppender = ButtonBlockAppender; + +InnerBlocks.Content = withBlockContentContext( + ( { BlockContent } ) => +); + +/** + * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/inner-blocks/README.md + */ +export default InnerBlocks; diff --git a/packages/block-editor/src/components/url-input/test/button.js b/packages/block-editor/src/components/url-input/test/button.js index 427ea0ca8531e..8088d61ea5016 100644 --- a/packages/block-editor/src/components/url-input/test/button.js +++ b/packages/block-editor/src/components/url-input/test/button.js @@ -63,17 +63,17 @@ describe( 'URLInputButton', () => { } ); it( 'should close the form when user submits it', () => { const wrapper = TestUtils.renderIntoDocument( ); - const buttonElement = () => TestUtils.findRenderedDOMComponentWithClass( + const buttonElement = () => TestUtils.scryRenderedDOMComponentsWithClass( wrapper, 'components-toolbar__control' ); - const formElement = () => TestUtils.findRenderedDOMComponentWithTag( + const formElement = () => TestUtils.scryRenderedDOMComponentsWithTag( wrapper, 'form' ); - TestUtils.Simulate.click( buttonElement() ); + TestUtils.Simulate.click( buttonElement().shift() ); expect( wrapper.state.expanded ).toBe( true ); - TestUtils.Simulate.submit( formElement() ); + TestUtils.Simulate.submit( formElement().shift() ); expect( wrapper.state.expanded ).toBe( false ); // eslint-disable-next-line react/no-find-dom-node ReactDOM.unmountComponentAtNode( ReactDOM.findDOMNode( wrapper ).parentNode ); diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index b3411411ee75f..ebfeefe38b9dd 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -101,6 +101,33 @@ export const coreBlocks = [ return memo; }, {} ); +/** + * Function to register an individual block. + * + * @param {Object} block The block to be registered. + * + */ +const registerBlock = ( block ) => { + if ( ! block ) { + return; + } + const { metadata, settings, name } = block; + registerBlockType( name, { + ...metadata, + ...settings, + } ); +}; + +/** + * Function to register core blocks provided by the block editor. + * + * @example + * ```js + * import { registerCoreBlocks } from '@wordpress/block-library'; + * + * registerCoreBlocks(); + * ``` + */ export const registerCoreBlocks = () => { [ paragraph, @@ -114,13 +141,10 @@ export const registerCoreBlocks = () => { separator, list, quote, - ].forEach( ( { metadata, name, settings } ) => { - registerBlockType( name, { - ...metadata, - ...settings, - } ); - } ); -}; + // eslint-disable-next-line no-undef + typeof __DEV__ !== 'undefined' && __DEV__ ? mediaText : null, + ].forEach( registerBlock ); -setDefaultBlockName( paragraph.name ); -setUnregisteredTypeHandlerName( missing.name ); + setDefaultBlockName( paragraph.name ); + setUnregisteredTypeHandlerName( missing.name ); +}; diff --git a/packages/block-library/src/media-text/edit.native.js b/packages/block-library/src/media-text/edit.native.js new file mode 100644 index 0000000000000..3cfb91b48ce94 --- /dev/null +++ b/packages/block-library/src/media-text/edit.native.js @@ -0,0 +1,186 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { + BlockControls, + BlockVerticalAlignmentToolbar, + InnerBlocks, + withColors, +} from '@wordpress/block-editor'; +import { Component } from '@wordpress/element'; +import { + Toolbar, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import MediaContainer from './media-container'; +import styles from './style.scss'; + +/** + * Constants + */ +const ALLOWED_BLOCKS = [ 'core/button', 'core/paragraph', 'core/heading', 'core/list' ]; +const TEMPLATE = [ + [ 'core/paragraph', { fontSize: 'large', placeholder: _x( 'Content…', 'content placeholder' ) } ], +]; +// this limits the resize to a safe zone to avoid making broken layouts +const WIDTH_CONSTRAINT_PERCENTAGE = 15; +const applyWidthConstraints = ( width ) => Math.max( WIDTH_CONSTRAINT_PERCENTAGE, Math.min( width, 100 - WIDTH_CONSTRAINT_PERCENTAGE ) ); + +class MediaTextEdit extends Component { + constructor() { + super( ...arguments ); + + this.onSelectMedia = this.onSelectMedia.bind( this ); + this.onWidthChange = this.onWidthChange.bind( this ); + this.commitWidthChange = this.commitWidthChange.bind( this ); + this.state = { + mediaWidth: null, + }; + } + + onSelectMedia( media ) { + const { setAttributes } = this.props; + + let mediaType; + let src; + // for media selections originated from a file upload. + if ( media.media_type ) { + if ( media.media_type === 'image' ) { + mediaType = 'image'; + } else { + // only images and videos are accepted so if the media_type is not an image we can assume it is a video. + // video contain the media type of 'file' in the object returned from the rest api. + mediaType = 'video'; + } + } else { // for media selections originated from existing files in the media library. + mediaType = media.type; + } + + if ( mediaType === 'image' ) { + // Try the "large" size URL, falling back to the "full" size URL below. + src = get( media, [ 'sizes', 'large', 'url' ] ) || get( media, [ 'media_details', 'sizes', 'large', 'source_url' ] ); + } + + setAttributes( { + mediaAlt: media.alt, + mediaId: media.id, + mediaType, + mediaUrl: src || media.url, + imageFill: undefined, + focalPoint: undefined, + } ); + } + + onWidthChange( width ) { + this.setState( { + mediaWidth: applyWidthConstraints( width ), + } ); + } + + commitWidthChange( width ) { + const { setAttributes } = this.props; + + setAttributes( { + mediaWidth: applyWidthConstraints( width ), + } ); + this.setState( { + mediaWidth: null, + } ); + } + + renderMediaArea() { + const { attributes } = this.props; + const { mediaAlt, mediaId, mediaPosition, mediaType, mediaUrl, mediaWidth, imageFill, focalPoint } = attributes; + + return ( + + ); + } + + render() { + const { + attributes, + backgroundColor, + setAttributes, + } = this.props; + const { + isStackedOnMobile, + mediaPosition, + mediaWidth, + verticalAlignment, + } = attributes; + const temporaryMediaWidth = this.state.mediaWidth || mediaWidth; + const widthString = `${ temporaryMediaWidth }%`; + const containerStyles = { + ...styles[ 'wp-block-media-text' ], + ...styles[ `is-vertically-aligned-${ verticalAlignment }` ], + ...( mediaPosition === 'right' ? styles[ 'has-media-on-the-right' ] : {} ), + ...( isStackedOnMobile ? styles[ 'is-stacked-on-mobile' ] : {} ), + ...( isStackedOnMobile && mediaPosition === 'right' ? styles[ 'is-stacked-on-mobile.has-media-on-the-right' ] : {} ), + backgroundColor: backgroundColor.color, + }; + const innerBlockWidth = 100 - temporaryMediaWidth; + const innerBlockWidthString = `${ innerBlockWidth }%`; + + const toolbarControls = [ { + icon: 'align-pull-left', + title: __( 'Show media on left' ), + isActive: mediaPosition === 'left', + onClick: () => setAttributes( { mediaPosition: 'left' } ), + }, { + icon: 'align-pull-right', + title: __( 'Show media on right' ), + isActive: mediaPosition === 'right', + onClick: () => setAttributes( { mediaPosition: 'right' } ), + } ]; + + const onVerticalAlignmentChange = ( alignment ) => { + setAttributes( { verticalAlignment: alignment } ); + }; + + return ( + <> + + + + + + + { this.renderMediaArea() } + + + + + + + ); + } +} + +export default withColors( 'backgroundColor' )( MediaTextEdit ); diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js new file mode 100644 index 0000000000000..ab9056cf46dc3 --- /dev/null +++ b/packages/block-library/src/media-text/media-container.native.js @@ -0,0 +1,191 @@ +/** + * External dependencies + */ +import { View, Image, ImageBackground } from 'react-native'; + +/** + * WordPress dependencies + */ +import { IconButton, Toolbar, withNotices } from '@wordpress/components'; +import { + BlockControls, + BlockIcon, + MediaPlaceholder, + MEDIA_TYPE_IMAGE, + MediaUpload, +} from '@wordpress/block-editor'; +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import icon from './media-container-icon'; + +export function calculatePreferedImageSize( image, container ) { + const maxWidth = container.clientWidth; + const exceedMaxWidth = image.width > maxWidth; + const ratio = image.height / image.width; + const width = exceedMaxWidth ? maxWidth : image.width; + const height = exceedMaxWidth ? maxWidth * ratio : image.height; + return { width, height }; +} + +class MediaContainer extends Component { + constructor() { + super( ...arguments ); + this.onUploadError = this.onUploadError.bind( this ); + this.calculateSize = this.calculateSize.bind( this ); + this.onLayout = this.onLayout.bind( this ); + this.onSelectURL = this.onSelectURL.bind( this ); + + this.state = { + width: 0, + height: 0, + }; + + if ( this.props.mediaUrl ) { + this.onMediaChange(); + } + } + + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + + onSelectURL( mediaId, mediaUrl ) { + const { onSelectMedia } = this.props; + + onSelectMedia( { + media_type: 'image', + id: mediaId, + src: mediaUrl, + } ); + } + + renderToolbarEditButton() { + const { mediaId } = this.props; + return ( + + + ( + + ) } + /> + + + ); + } + + componentDidUpdate( prevProps ) { + if ( prevProps.mediaUrl !== this.props.mediaUrl ) { + this.onMediaChange(); + } + } + + onMediaChange() { + const mediaType = this.props.mediaType; + if ( mediaType === 'video' ) { + + } else if ( mediaType === 'image' ) { + Image.getSize( this.props.mediaUrl, ( width, height ) => { + this.media = { width, height }; + this.calculateSize(); + } ); + } + } + + calculateSize() { + if ( this.media === undefined || this.container === undefined ) { + return; + } + + const { width, height } = calculatePreferedImageSize( this.media, this.container ); + this.setState( { width, height } ); + } + + onLayout( event ) { + const { width, height } = event.nativeEvent.layout; + this.container = { + clientWidth: width, + clientHeight: height, + }; + this.calculateSize(); + } + + renderImage() { + const { mediaAlt, mediaUrl } = this.props; + + return ( + + + + + ); + } + + renderVideo() { + const style = { videoContainer: {} }; + return ( + + + { /* TODO: show video preview */ } + + + ); + } + + renderPlaceholder() { + return ( + } + labels={ { + title: __( 'Media area' ), + } } + onSelectURL={ this.onSelectURL } + mediaType={ MEDIA_TYPE_IMAGE } + onFocus={ this.props.onFocus } + /> + ); + } + + render() { + const { mediaUrl, mediaType } = this.props; + if ( mediaType && mediaUrl ) { + let mediaElement = null; + switch ( mediaType ) { + case 'image': + mediaElement = this.renderImage(); + break; + case 'video': + mediaElement = this.renderVideo(); + break; + } + return mediaElement; + } + return this.renderPlaceholder(); + } +} + +export default withNotices( MediaContainer ); diff --git a/packages/block-library/src/media-text/style.native.scss b/packages/block-library/src/media-text/style.native.scss new file mode 100644 index 0000000000000..f1c3550f29c1e --- /dev/null +++ b/packages/block-library/src/media-text/style.native.scss @@ -0,0 +1,29 @@ +.wp-block-media-text { + display: flex; + align-items: flex-start; + flex-direction: row; +} + +.has-media-on-the-right { + flex-direction: row-reverse; +} + +.is-stacked-on-mobile { + flex-direction: column; + + &.has-media-on-the-right { + flex-direction: column-reverse; + } +} + +.is-vertically-aligned-top { + align-items: flex-start; +} + +.is-vertically-aligned-center { + align-items: center; +} + +.is-vertically-aligned-bottom { + align-items: flex-end; +} diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index 3b3be8f28c3a4..f8d4c03298aeb 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -35,5 +35,9 @@ export { isUnmodifiedDefaultBlock, normalizeIconObject, } from './utils'; +export { + doBlocksMatchTemplate, + synchronizeBlocksWithTemplate, +} from './templates'; export { pasteHandler, getPhrasingContentSchema } from './raw-handling'; export { default as children } from './children'; diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index f64d7f6777e93..377feab7601cf 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { isArray, isString } from 'lodash'; +import { isArray } from 'lodash'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { forwardRef } from '@wordpress/element'; */ import Tooltip from '../tooltip'; import Button from '../button'; -import Dashicon from '../dashicon'; +import Icon from '../icon'; function IconButton( props, ref ) { const { @@ -56,7 +56,7 @@ function IconButton( props, ref ) { className={ classes } ref={ ref } > - { isString( icon ) ? : icon } + { children } ); diff --git a/packages/components/src/icon-button/test/index.js b/packages/components/src/icon-button/test/index.js index e824ed1d556ec..f4a8d4330d3cc 100644 --- a/packages/components/src/icon-button/test/index.js +++ b/packages/components/src/icon-button/test/index.js @@ -24,7 +24,7 @@ describe( 'IconButton', () => { it( 'should render a Dashicon component matching the wordpress icon', () => { const iconButton = shallow( ); - expect( iconButton.find( 'Dashicon' ).shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); + expect( iconButton.find( 'Icon' ).dive().shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); } ); it( 'should render child elements when passed as children', () => { diff --git a/packages/components/src/icon/index.js b/packages/components/src/icon/index.js index 61a4fb0a2d6c4..5e762d1d5c8a9 100644 --- a/packages/components/src/icon/index.js +++ b/packages/components/src/icon/index.js @@ -6,20 +6,26 @@ import { cloneElement, createElement, Component, isValidElement } from '@wordpre /** * Internal dependencies */ -import { Dashicon, SVG } from '../'; +import Dashicon from '../dashicon'; +import { SVG } from '../primitives'; function Icon( { icon = null, size, ...additionalProps } ) { - let iconSize; + // Dashicons should be 20x20 by default. + const dashiconSize = size || 20; if ( 'string' === typeof icon ) { - // Dashicons should be 20x20 by default - iconSize = size || 20; - return ; + return ; } - // Any other icons should be 24x24 by default - iconSize = size || 24; + if ( icon && Dashicon === icon.type ) { + return cloneElement( icon, { + size: dashiconSize, + ...additionalProps, + } ); + } + // Icons should be 24x24 by default. + const iconSize = size || 24; if ( 'function' === typeof icon ) { if ( icon.prototype instanceof Component ) { return createElement( icon, { size: iconSize, ...additionalProps } ); diff --git a/packages/components/src/icon/test/index.js b/packages/components/src/icon/test/index.js index 053b0cf390ff5..a645568c1e2ee 100644 --- a/packages/components/src/icon/test/index.js +++ b/packages/components/src/icon/test/index.js @@ -11,6 +11,7 @@ import { Component } from '@wordpress/element'; /** * Internal dependencies */ +import Dashicon from '../../dashicon'; import Icon from '../'; import { Path, SVG } from '../../'; @@ -31,12 +32,18 @@ describe( 'Icon', () => { expect( wrapper.find( 'Dashicon' ).prop( 'icon' ) ).toBe( 'format-image' ); } ); - it( 'renders a dashicon and with a default size of 20', () => { + it( 'renders a dashicon by slug and with a default size of 20', () => { const wrapper = shallow( ); expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); } ); + it( 'renders a dashicon by element and with a default size of 20', () => { + const wrapper = shallow( } /> ); + + expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); + } ); + it( 'renders a function', () => { const wrapper = shallow( } /> ); @@ -98,6 +105,7 @@ describe( 'Icon', () => { describe.each( [ [ 'dashicon', { icon: 'format-image' } ], + [ 'dashicon element', { icon: } ], [ 'element', { icon: } ], [ 'svg element', { icon: svg } ], [ 'component', { icon: MyComponent } ], diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index fb9dab39a03f2..33c5671526897 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -8,6 +8,7 @@ export const KeyboardAwareFlatList = ( { extraScrollHeight, shouldPreventAutomaticScroll, innerRef, + autoScroll, ...listProps } ) => ( { this.scrollViewRef = ref; innerRef( ref ); diff --git a/packages/components/src/primitives/svg/index.native.js b/packages/components/src/primitives/svg/index.native.js index b0272e6b5a7b9..4ee8dbae9b798 100644 --- a/packages/components/src/primitives/svg/index.native.js +++ b/packages/components/src/primitives/svg/index.native.js @@ -18,7 +18,9 @@ export { export const SVG = ( props ) => { const stylesFromClasses = ( props.className || '' ).split( ' ' ).map( ( element ) => styles[ element ] ).filter( Boolean ); - const styleValues = Object.assign( {}, props.style, ...stylesFromClasses ); + const stylesFromAriaPressed = props.ariaPressed ? styles[ 'is-active' ] : styles[ 'components-toolbar__control' ]; + const styleValues = Object.assign( {}, props.style, stylesFromAriaPressed, ...stylesFromClasses ); + const safeProps = { ...props, style: styleValues }; return ( diff --git a/packages/components/src/primitives/svg/style.native.scss b/packages/components/src/primitives/svg/style.native.scss index 595372b06329e..95dd5b9856bd7 100644 --- a/packages/components/src/primitives/svg/style.native.scss +++ b/packages/components/src/primitives/svg/style.native.scss @@ -1,9 +1,11 @@ -.dashicon { +.dashicon, +.components-toolbar__control { color: #7b9ab1; fill: currentColor; } -.dashicon-active { +.dashicon-active, +.is-active { color: #fff; fill: currentColor; } diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap index 3f01ba28572d4..5096eaa7c803b 100644 --- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap @@ -74,22 +74,16 @@ exports[`MoreMenu should match snapshot 1`] = ` onMouseLeave={[Function]} type="button" > - - - - - + > + + + + + + diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index 9d1925356a913..b7c53b12aad54 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -53,6 +53,7 @@ class VisualEditor extends Component { header={ this.renderHeader() } isFullyBordered={ isFullyBordered } safeAreaBottomInset={ safeAreaBottomInset } + autoScroll={ true } /> ); }