diff --git a/.travis.yml b/.travis.yml index 99f72bbbd5f4e5..b8184b6f8b8feb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ cache: branches: only: - master + - rnmobile/master env: global: diff --git a/packages/api-fetch/src/index.native.js b/packages/api-fetch/src/index.native.js new file mode 100644 index 00000000000000..802ac8f7c42aea --- /dev/null +++ b/packages/api-fetch/src/index.native.js @@ -0,0 +1,10 @@ + +function apiFetch( options ) { + // eslint-disable-next-line no-console + console.warn( 'apiFetch called with options', options ); + + // return a promise that never resolves + return new Promise( () => {} ); +} + +export default apiFetch; diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 541f276f0df45d..7c6af26534df97 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -19,6 +19,7 @@ }, "main": "build/index.js", "module": "build-module/index.js", + "react-native": "src/index", "dependencies": { "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 07abd2b81221bb..8b2a631518de53 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## Master + +### Deprecation + +- `dropZoneUIOnly` prop in `MediaPlaceholder` component has been deprecated in favor of `disableMediaButtons` prop. + ## 3.0.0 (2019-08-05) ### New Features 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 00000000000000..4dddda5ecce8f0 --- /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 00000000000000..6d8fb4e6cee976 --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/index.native.js @@ -0,0 +1,61 @@ +/** + * 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, + renderAppender: CustomAppender, +} ) { + if ( isLocked ) { + return null; + } + + if ( CustomAppender ) { + return ( + + ); + } + + 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 00000000000000..edb2c6f809ebcc --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/style.native.scss @@ -0,0 +1,8 @@ + +.blockListAppender { + 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/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index b87f95445e11f2..69503bf7151e24 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -131,7 +131,7 @@ class BlockListBlock extends Component { > { isValid && this.getBlockForType() } { ! isValid && - + } { isSelected && } diff --git a/packages/block-editor/src/components/block-list/block.native.scss b/packages/block-editor/src/components/block-list/block.native.scss index b997cddc723073..a7dbae3b46516b 100644 --- a/packages/block-editor/src/components/block-list/block.native.scss +++ b/packages/block-editor/src/components/block-list/block.native.scss @@ -3,7 +3,6 @@ } .blockContainer { - background-color: $white; padding-left: 16px; padding-right: 16px; padding-top: 12px; @@ -11,7 +10,6 @@ } .blockContainerFocused { - background-color: $white; padding-left: 16px; padding-right: 16px; padding-top: 12px; 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 d25dcc85a90753..0eefa1274378c8 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -10,7 +10,7 @@ import { Text, View, Platform, TouchableWithoutFeedback } from 'react-native'; import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components'; @@ -19,7 +19,7 @@ import { KeyboardAwareFlatList, ReadableContentView } from '@wordpress/component */ import styles from './style.scss'; import BlockListBlock from './block'; -import DefaultBlockAppender from '../default-block-appender'; +import BlockListAppender from '../block-list-appender'; const innerToolbarHeight = 44; @@ -60,41 +60,47 @@ export class BlockList extends Component { renderDefaultBlockAppender() { return ( - ); } render() { + const { clearSelectedBlock, blockClientIds, isFullyBordered, title, header, withFooter = true, renderAppender } = this.props; + return ( + + { renderAppender && blockClientIds.length > 0 && + + } ); } @@ -107,6 +113,7 @@ export class BlockList extends Component { } renderItem( { item: clientId, index } ) { + const blockHolderFocusedStyle = this.props.getStylesFromColorScheme( styles.blockHolderFocused, styles.blockHolderFocusedDark ); const { shouldShowBlockAtIndex, shouldShowInsertionPoint } = this.props; return ( @@ -119,18 +126,20 @@ export class BlockList extends Component { rootClientId={ this.props.rootClientId } onCaretVerticalPositionChange={ this.onCaretVerticalPositionChange } borderStyle={ this.blockHolderBorderStyle() } - focusedBorderColor={ styles.blockHolderFocused.borderColor } + focusedBorderColor={ blockHolderFocusedStyle.borderColor } /> ) } ); } renderAddBlockSeparator() { + const lineStyle = this.props.getStylesFromColorScheme( styles.lineStyleAddHere, styles.lineStyleAddHereDark ); + const labelStyle = this.props.getStylesFromColorScheme( styles.labelStyleAddHere, styles.labelStyleAddHereDark ); return ( - - { __( 'ADD BLOCK HERE' ) } - + + { __( 'ADD BLOCK HERE' ) } + ); } @@ -156,12 +165,15 @@ export default compose( [ getSelectedBlockClientId, getBlockInsertionPoint, isBlockInsertionPointVisible, + getSelectedBlock, } = select( 'core/block-editor' ); const selectedBlockClientId = getSelectedBlockClientId(); const blockClientIds = getBlockOrder( rootClientId ); const insertionPoint = getBlockInsertionPoint(); const blockInsertionPointIsVisible = isBlockInsertionPointVisible(); + const selectedBlock = getSelectedBlock(); + const isSelectedGroup = selectedBlock && selectedBlock.name === 'core/group'; const shouldShowInsertionPoint = ( clientId ) => { return ( blockInsertionPointIsVisible && @@ -173,7 +185,7 @@ export default compose( [ const selectedBlockIndex = getBlockIndex( selectedBlockClientId ); const shouldShowBlockAtIndex = ( index ) => { const shouldHideBlockAtIndex = ( - blockInsertionPointIsVisible && + ! isSelectedGroup && blockInsertionPointIsVisible && // if `index` === `insertionPoint.index`, then block is replaceable index === insertionPoint.index && // only hide selected block @@ -204,5 +216,6 @@ export default compose( [ replaceBlock, }; } ), + withPreferredColorScheme, ] )( BlockList ); diff --git a/packages/block-editor/src/components/block-list/insertion-point.native.js b/packages/block-editor/src/components/block-list/insertion-point.native.js new file mode 100644 index 00000000000000..d6caa88f407fba --- /dev/null +++ b/packages/block-editor/src/components/block-list/insertion-point.native.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +const BlockInsertionPoint = ( { showInsertionPoint } ) => { + 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-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index b6961600b95c67..36be89c4c067cd 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -26,6 +26,10 @@ height: 2px; } +.lineStyleAddHereDark { + background-color: $gray-50; +} + .labelStyleAddHere { flex: 1; text-align: center; @@ -34,9 +38,12 @@ font-weight: bold; } +.labelStyleAddHereDark { + color: $gray-20; +} + .containerStyleAddHere { flex-direction: row; - background-color: $white; } .blockHolderSemiBordered { @@ -54,7 +61,6 @@ } .blockContainerFocused { - background-color: $white; padding-left: 16; padding-right: 16; padding-top: 12; @@ -65,6 +71,10 @@ border-color: $gray-lighten-30; } +.blockHolderFocusedDark { + border-color: $gray-70; +} + .blockListFooter { height: 80px; } 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 8962dc22f49e45..40bc4d550b7ab6 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/block-toolbar/index.native.js b/packages/block-editor/src/components/block-toolbar/index.native.js index 5413e4e1f9cded..38908cf68335a7 100644 --- a/packages/block-editor/src/components/block-toolbar/index.native.js +++ b/packages/block-editor/src/components/block-toolbar/index.native.js @@ -4,9 +4,10 @@ import { withSelect } from '@wordpress/data'; /** - * WordPress dependencies + * Internal dependencies */ -import { BlockFormatControls, BlockControls } from '@wordpress/block-editor'; +import BlockControls from '../block-controls'; +import BlockFormatControls from '../block-format-controls'; export const BlockToolbar = ( { blockClientIds, isValid, mode } ) => { if ( blockClientIds.length === 0 ) { diff --git a/packages/block-editor/src/components/button-block-appender/index.native.js b/packages/block-editor/src/components/button-block-appender/index.native.js new file mode 100644 index 00000000000000..b01d3b65109856 --- /dev/null +++ b/packages/block-editor/src/components/button-block-appender/index.native.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Button, Dashicon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import Inserter from '../inserter'; +import styles from './styles.scss'; + +function ButtonBlockAppender( { rootClientId } ) { + return ( + <> + ( + + ) } + isAppender + /> + + ); +} + +/** + * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/button-block-appender/README.md + */ +export default ButtonBlockAppender; diff --git a/packages/block-editor/src/components/button-block-appender/styles.native.scss b/packages/block-editor/src/components/button-block-appender/styles.native.scss new file mode 100644 index 00000000000000..3a6549980af87d --- /dev/null +++ b/packages/block-editor/src/components/button-block-appender/styles.native.scss @@ -0,0 +1,16 @@ +.appender { + align-items: center; + justify-content: center; + padding: 12px; + background-color: $white; + border: $border-width solid $light-gray-500; + border-radius: 4px; +} + +.addBlockButton { + color: $white; + background-color: $gray; + border-radius: $icon-button-size-small / 2; + overflow: hidden; + size: $icon-button-size-small; +} diff --git a/packages/block-editor/src/components/default-block-appender/style.native.scss b/packages/block-editor/src/components/default-block-appender/style.native.scss index 5193611fa45d58..9e2ffd2f293b01 100644 --- a/packages/block-editor/src/components/default-block-appender/style.native.scss +++ b/packages/block-editor/src/components/default-block-appender/style.native.scss @@ -5,7 +5,6 @@ } .blockContainer { - background-color: $white; padding-top: 0; padding-left: 16px; padding-right: 16px; diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 5f13de7d91b4ce..b45904361595db 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 { @@ -15,6 +18,7 @@ export { } from './rich-text'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from './media-upload'; +export { default as MediaUploadProgress } from './media-upload-progress'; export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; export { default as Caption } from './caption'; 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 00000000000000..2b6a6c9320c923 --- /dev/null +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -0,0 +1,183 @@ +/** + * 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/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js index b20eb8387992d6..4c06402af38214 100644 --- a/packages/block-editor/src/components/inserter/index.native.js +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { Dropdown, ToolbarButton, Dashicon } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { getUnregisteredTypeHandlerName } from '@wordpress/blocks'; /** @@ -14,10 +14,10 @@ import { getUnregisteredTypeHandlerName } from '@wordpress/blocks'; import styles from './style.scss'; import InserterMenu from './menu'; -const defaultRenderToggle = ( { onToggle, disabled } ) => ( +const defaultRenderToggle = ( { onToggle, disabled, style } ) => ( ) } + icon={ ( ) } onClick={ onToggle } extraProps={ { hint: __( 'Double tap to add a block' ) } } isDisabled={ disabled } @@ -56,9 +56,10 @@ class Inserter extends Component { const { disabled, renderToggle = defaultRenderToggle, + getStylesFromColorScheme, } = this.props; - - return renderToggle( { onToggle, isOpen, disabled } ); + const style = getStylesFromColorScheme( styles.addBlockButton, styles.addBlockButtonDark ); + return renderToggle( { onToggle, isOpen, disabled, style } ); } /** @@ -118,4 +119,5 @@ export default compose( [ items: inserterItems.filter( ( { name } ) => name !== getUnregisteredTypeHandlerName() ), }; } ), + withPreferredColorScheme, ] )( Inserter ); diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 7870f41954afe3..2a990445b284c4 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -13,7 +13,7 @@ import { isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; -import { withInstanceId, compose } from '@wordpress/compose'; +import { withInstanceId, compose, withPreferredColorScheme } from '@wordpress/compose'; import { BottomSheet, Icon } from '@wordpress/components'; /** @@ -61,8 +61,12 @@ export class InserterMenu extends Component { } render() { + const { getStylesFromColorScheme } = this.props; const numberOfColumns = this.calculateNumberOfColumns(); const bottomPadding = styles.contentBottomPadding; + const modalIconWrapperStyle = getStylesFromColorScheme( styles.modalIconWrapper, styles.modalIconWrapperDark ); + const modalIconStyle = getStylesFromColorScheme( styles.modalIcon, styles.modalIconDark ); + const modalItemLabelStyle = getStylesFromColorScheme( styles.modalItemLabel, styles.modalItemLabelDark ); return ( this.props.onSelect( item ) }> - - - + + + - { item.title } + { item.title } } @@ -213,4 +217,5 @@ export default compose( }; } ), withInstanceId, + withPreferredColorScheme, )( InserterMenu ); diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index e10b685dda406c..899b3f676827db 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -37,6 +37,10 @@ align-items: center; } +.modalIconWrapperDark { + background-color: rgba($white, 0.07); +} + .modalIcon { width: 32px; height: 32px; @@ -45,6 +49,10 @@ fill: $gray-dark; } +.modalIconDark { + fill: $white; +} + .modalItemLabel { background-color: transparent; padding-left: 2; @@ -56,9 +64,18 @@ color: $gray-dark; } +.modalItemLabelDark { + color: $white; +} + .addBlockButton { color: $blue-wordpress; border: 2px; border-radius: 10px; border-color: $blue-wordpress; } + +.addBlockButtonDark { + color: $blue-30; + border-color: $blue-30; +} diff --git a/packages/block-editor/src/components/media-placeholder/README.md b/packages/block-editor/src/components/media-placeholder/README.md index b35b00b7d7a53b..b730b9af5cbc9c 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -39,6 +39,7 @@ This property is similar to the `allowedTypes` property. The difference is the f - Type: `String` - Required: No +- Platform: Web ### addToGallery @@ -48,6 +49,7 @@ If false the gallery media modal opens in the edit mode where the user can edit - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### allowedTypes @@ -59,6 +61,7 @@ This property is similar to the `accept` property. The difference is the format - Type: `Array` - Required: No +- Platform: Web | Mobile ### className @@ -66,6 +69,7 @@ Class name added to the placeholder. - Type: `String` - Required: No +- Platform: Web ### disableDropZone @@ -90,6 +94,7 @@ Icon to display left of the title. When passed as a `String`, the icon will be r - Type: `String|WPComponent` - Required: No +- Platform: Web | Mobile ### isAppender @@ -99,6 +104,16 @@ If false the default placeholder style is used. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web | Mobile + +### disableMediaButtons + +If true, only the Drop Zone will be rendered. No UI controls to upload the media will be shown + +- Type: `Boolean` +- Required: No +- Default: `false` +- Platform: Web | Mobile ### labels @@ -106,7 +121,7 @@ An object that can contain a `title` and `instructions` properties. These proper - Type: `Object` - Required: No - +- Platform: Web | Mobile ### multiple @@ -115,6 +130,7 @@ Whether to allow multiple selection of files or not. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### onError @@ -122,6 +138,7 @@ Callback called when an upload error happens. - Type: `Function` - Required: No +- Platform: Web ### onSelect @@ -130,6 +147,11 @@ The call back receives an array with the new files. Each element of the collecti - Type: `Function` - Required: Yes +- Platform: Web | Mobile + +The argument of the callback is an object containing the following properties: +- Web: `{ url, alt, id, link, caption, sizes, media_details }` +- Mobile: `{ id, url }` ### value @@ -137,6 +159,7 @@ Media ID (or media IDs if multiple is true) to be selected by default when openi - Type: `Number|Array` - Required: No +- Platform: Web ## Extend diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index df92ab71ee6ece..d142d6d6c70008 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -25,6 +25,7 @@ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -396,10 +397,17 @@ export class MediaPlaceholder extends Component { render() { const { + disableMediaButtons, dropZoneUIOnly, } = this.props; - if ( dropZoneUIOnly ) { + if ( dropZoneUIOnly || disableMediaButtons ) { + if ( dropZoneUIOnly ) { + deprecated( 'wp.blockEditor.MediaPlaceholder dropZoneUIOnly prop', { + alternative: 'disableMediaButtons', + } ); + } + return ( { this.renderDropZone() } 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 a19a28588200c9..4c92ccab44b790 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -7,7 +7,13 @@ import { View, Text, TouchableWithoutFeedback } from 'react-native'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/block-editor'; +import { + MediaUpload, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, +} from '@wordpress/block-editor'; +import { Dashicon } from '@wordpress/components'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies @@ -15,10 +21,19 @@ import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/bloc import styles from './styles.scss'; function MediaPlaceholder( props ) { - const { mediaType, labels = {}, icon, onSelectURL } = props; + const { + allowedTypes = [], + labels = {}, + icon, + onSelect, + isAppender, + disableMediaButtons, + getStylesFromColorScheme, + } = props; - const isImage = MEDIA_TYPE_IMAGE === mediaType; - const isVideo = MEDIA_TYPE_VIDEO === mediaType; + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); let placeholderTitle = labels.title; if ( placeholderTitle === undefined ) { @@ -46,41 +61,74 @@ function MediaPlaceholder( props ) { accessibilityHint = __( 'Double tap to select a video' ); } + const emptyStateTitleStyle = getStylesFromColorScheme( styles.emptyStateTitle, styles.emptyStateTitleDark ); + + const renderContent = () => { + if ( isAppender === undefined || ! isAppender ) { + return ( + <> + + { icon } + + + { placeholderTitle } + + + { instructions } + + + ); + } else if ( isAppender && ! disableMediaButtons ) { + return ( + + ); + } + }; + + if ( isAppender && disableMediaButtons ) { + return null; + } + + const emptyStateContainerStyle = getStylesFromColorScheme( styles.emptyStateContainer, styles.emptyStateContainerDark ); + return ( - { - return ( - { - props.onFocus( event ); - open(); - } } - > - - { getMediaOptions() } - - { icon } + + { + return ( + { + props.onFocus( event ); + open(); + } }> + + { getMediaOptions() } + { renderContent() } - - { placeholderTitle } - - - { instructions } - - - - ); - } } /> + + ); + } } + /> + ); } -export default MediaPlaceholder; +export default withPreferredColorScheme( MediaPlaceholder ); diff --git a/packages/block-editor/src/components/media-placeholder/styles.native.scss b/packages/block-editor/src/components/media-placeholder/styles.native.scss index 1d55d93c6cd702..bebceb30f15cf7 100644 --- a/packages/block-editor/src/components/media-placeholder/styles.native.scss +++ b/packages/block-editor/src/components/media-placeholder/styles.native.scss @@ -15,6 +15,18 @@ border-bottom-right-radius: 4; } +.emptyStateContainerDark { + background-color: $background-dark-secondary; +} + +.emptyStateContainerDark { + background-color: $background-dark-secondary; +} + +.emptyStateContainerDark { + background-color: $background-dark-secondary; +} + .emptyStateTitle { text-align: center; margin-top: 8; @@ -23,6 +35,10 @@ color: #2e4453; } +.emptyStateTitleDark { + color: $white; +} + .emptyStateDescription { text-align: center; color: $blue-wordpress; @@ -37,3 +53,18 @@ align-items: center; fill: $gray-dark; } + +.isAppender { + height: auto; + background-color: $white; + border: $border-width solid $light-gray-500; + border-radius: 4px; +} + +.addBlockButton { + color: $white; + background-color: $dark-gray-500; + border-radius: $icon-button-size-small / 2; + overflow: hidden; + size: $icon-button-size-small; +} diff --git a/packages/block-editor/src/components/media-upload-progress/README.md b/packages/block-editor/src/components/media-upload-progress/README.md new file mode 100644 index 00000000000000..89ef9243717774 --- /dev/null +++ b/packages/block-editor/src/components/media-upload-progress/README.md @@ -0,0 +1,140 @@ +MediaUploadProgress +=================== + +`MediaUploadProgress` shows a progress bar while a media file associated with a block is being uploaded. + +## Usage + +Usage example + +```jsx +import { ImageBackground, Text, View } from 'react-native'; +import { + MediaUploadProgress, +} from '@wordpress/block-editor'; + +function MediaProgress( { height, width, url, id } ) { + return ( + { + return ( + + { isUploadFailed && + + { retryMessage } + + } + + ); + } } + /> + ); +} +``` + +## Props + +### mediaId + +A media ID that identifies the current upload. + +- Type: `Number` +- Required: Yes +- Platform: Mobile + +### coverUrl + +By passing an image url, it'll calculate the right size depending on the container of the component maintaining its aspect ratio, it'll pass these values through `renderContent`. + +- Type: `String` +- Required: No +- Platform: Mobile + +### renderContent + +Content to be rendered along with the progress bar, usually the thumbnail of the media being uploaded. + +- Type: `React components` +- Required: Yes +- Platform: Mobile + +It passes an object containing the following properties: + +**With** `coverUrl` as a parameter: + +`{ isUploadInProgress, isUploadFailed, finalWidth, finalHeight, imageWidthWithinContainer, retryMessage }` + +**Without** `coverUrl` as a parameter: + +`{ isUploadInProgress, isUploadFailed, retryMessage }` + + +### width + +Forces the final width to be returned in `finalWidth` + +- Type: `Number` +- Required: No +- Platform: Mobile + +### height + +Forces the final height to be returned in `finalHeight` + +- Type: `Number` +- Required: No +- Platform: Mobile + +### onUpdateMediaProgress + +Callback called when the progress of the upload is updated. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, mediaUrl, progress, state }` + +### onFinishMediaUploadWithSuccess + +Callback called when the media file has been uploaded successfully. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, mediaServerId, mediaUrl, progress, state }` + +### onFinishMediaUploadWithFailure + +Callback called when the media file couldn't be uploaded. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, progress, state }` + + +### onMediaUploadStateReset + +Callback called when the media upload is reset + +- Type: `Function` +- Required: No +- Platform: Mobile + diff --git a/packages/block-library/src/image/image-size.native.js b/packages/block-editor/src/components/media-upload-progress/image-size.native.js similarity index 83% rename from packages/block-library/src/image/image-size.native.js rename to packages/block-editor/src/components/media-upload-progress/image-size.native.js index a337a5d14548c7..ade5984c1cadab 100644 --- a/packages/block-library/src/image/image-size.native.js +++ b/packages/block-editor/src/components/media-upload-progress/image-size.native.js @@ -11,7 +11,15 @@ import { View, Image } from 'react-native'; /** * Internal dependencies */ -import { calculatePreferedImageSize } from './utils'; + +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 ImageSize extends Component { constructor() { diff --git a/packages/block-library/src/image/media-upload-progress.native.js b/packages/block-editor/src/components/media-upload-progress/index.native.js similarity index 100% rename from packages/block-library/src/image/media-upload-progress.native.js rename to packages/block-editor/src/components/media-upload-progress/index.native.js diff --git a/packages/block-editor/src/components/media-upload-progress/styles.native.scss b/packages/block-editor/src/components/media-upload-progress/styles.native.scss new file mode 100644 index 00000000000000..5dea1caaf5aeff --- /dev/null +++ b/packages/block-editor/src/components/media-upload-progress/styles.native.scss @@ -0,0 +1,4 @@ +.mediaUploadProgress { + flex: 1; + background-color: $gray-lighten-30; +} diff --git a/packages/block-library/src/image/test/media-upload-progress.native.js b/packages/block-editor/src/components/media-upload-progress/test/index.native.js similarity index 99% rename from packages/block-library/src/image/test/media-upload-progress.native.js rename to packages/block-editor/src/components/media-upload-progress/test/index.native.js index 042f571aeefe58..e7c48e80d05df5 100644 --- a/packages/block-library/src/image/test/media-upload-progress.native.js +++ b/packages/block-editor/src/components/media-upload-progress/test/index.native.js @@ -15,7 +15,7 @@ import { MEDIA_UPLOAD_STATE_SUCCEEDED, MEDIA_UPLOAD_STATE_FAILED, MEDIA_UPLOAD_STATE_RESET, -} from '../media-upload-progress'; +} from '../'; jest.mock( 'react-native-gutenberg-bridge', () => { const callUploadCallback = ( payload ) => { diff --git a/packages/block-editor/src/components/media-upload/README.md b/packages/block-editor/src/components/media-upload/README.md index 56714034ec0ba4..d445348fa0b15d 100644 --- a/packages/block-editor/src/components/media-upload/README.md +++ b/packages/block-editor/src/components/media-upload/README.md @@ -63,6 +63,7 @@ If allowedTypes is unset all mime types should be allowed. - Type: `Array` - Required: No +- Platform: Web | Mobile ### multiple @@ -71,6 +72,7 @@ Whether to allow multiple selections or not. - Type: `Boolean` - Required: No - Default: false +- Platform: Web ### value @@ -78,6 +80,7 @@ Media ID (or media IDs if multiple is true) to be selected by default when openi - Type: `Number|Array` - Required: No +- Platform: Web ### onClose @@ -96,6 +99,7 @@ This is called subsequent to `onClose` when media is selected. The selected medi - Type: `Function` - Required: Yes +- Platform: Web | Mobile ### title @@ -104,6 +108,7 @@ Title displayed in the media modal. - Type: `String` - Required: No - Default: `Select or Upload Media` +- Platform: Web ### modalClass @@ -111,7 +116,7 @@ CSS class added to the media modal frame. - Type: `String` - Required: No - +- Platform: Web ### addToGallery @@ -122,6 +127,7 @@ Only applies if `gallery === true`. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### gallery @@ -130,6 +136,7 @@ If true, the component will initiate all the states required to represent a gall - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ## render @@ -137,6 +144,7 @@ A callback invoked to render the Button opening the media library. - Type: `Function` - Required: Yes +- Platform: Web | Mobile The first argument of the callback is an object containing the following properties: 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 14ed5d8dbe6236..88208d961c3d2a 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -23,16 +23,27 @@ export const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_med export const OPTION_TAKE_VIDEO = __( 'Take a Video' ); export const OPTION_TAKE_PHOTO = __( 'Take a Photo' ); +export const OPTION_TAKE_PHOTO_OR_VIDEO = __( 'Take a Photo or Video' ); export class MediaUpload extends React.Component { + constructor( props ) { + super( props ); + this.onPickerPresent = this.onPickerPresent.bind( this ); + this.onPickerChange = this.onPickerChange.bind( this ); + this.onPickerSelect = this.onPickerSelect.bind( this ); + } getTakeMediaLabel() { - const { mediaType } = this.props; + const { allowedTypes = [] } = this.props; + + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); - if ( mediaType === MEDIA_TYPE_IMAGE ) { + if ( isImage ) { return OPTION_TAKE_PHOTO; - } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + } else if ( isVideo ) { return OPTION_TAKE_VIDEO; - } + } return OPTION_TAKE_PHOTO_OR_VIDEO; } getMediaOptionsItems() { @@ -44,11 +55,15 @@ export class MediaUpload extends React.Component { } getChooseFromDeviceIcon() { - const { mediaType } = this.props; + const { allowedTypes = [] } = this.props; + + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); - if ( mediaType === MEDIA_TYPE_IMAGE ) { + if ( isImage || ! isOneType ) { return 'format-image'; - } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + } else if ( isVideo ) { return 'format-video'; } } @@ -61,58 +76,44 @@ export class MediaUpload extends React.Component { return 'wordpress-alt'; } - render() { - const { mediaType } = this.props; - - const onMediaLibraryButtonPressed = () => { - requestMediaPickFromMediaLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; - - const onMediaUploadButtonPressed = () => { - requestMediaPickFromDeviceLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; - - const onMediaCaptureButtonPressed = () => { - requestMediaPickFromDeviceCamera( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; + onPickerPresent() { + if ( this.picker ) { + this.picker.presentPicker(); + } + } - const mediaOptions = this.getMediaOptionsItems(); + onPickerSelect( requestFunction ) { + const { allowedTypes = [], onSelect } = this.props; + requestFunction( allowedTypes, ( id, url ) => { + if ( id ) { + onSelect( { id, url } ); + } + } ); + } - let picker; + onPickerChange( value ) { + if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { + this.onPickerSelect( requestMediaPickFromDeviceLibrary ); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { + this.onPickerSelect( requestMediaPickFromDeviceCamera ); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { + this.onPickerSelect( requestMediaPickFromMediaLibrary ); + } + } - const onPickerPresent = () => { - picker.presentPicker(); - }; + render() { + const mediaOptions = this.getMediaOptionsItems(); const getMediaOptions = () => ( picker = instance } + ref={ ( instance ) => this.picker = instance } options={ mediaOptions } - onChange={ ( value ) => { - if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { - onMediaUploadButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { - onMediaCaptureButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { - onMediaLibraryButtonPressed(); - } - } } + onChange={ this.onPickerChange } /> ); - return this.props.render( { open: onPickerPresent, getMediaOptions } ); + + return this.props.render( { open: this.onPickerPresent, getMediaOptions } ); } } diff --git a/packages/block-editor/src/components/media-upload/test/index.native.js b/packages/block-editor/src/components/media-upload/test/index.native.js index 6eca7575ac408b..d1eec5a560b7b7 100644 --- a/packages/block-editor/src/components/media-upload/test/index.native.js +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -29,14 +29,14 @@ const MEDIA_ID = 123; describe( 'MediaUpload component', () => { it( 'renders without crashing', () => { const wrapper = shallow( - {} } /> + {} } /> ); expect( wrapper ).toBeTruthy(); } ); it( 'opens media options picker', () => { const wrapper = shallow( - { + { return ( { getMediaOptions() } @@ -51,7 +51,7 @@ describe( 'MediaUpload component', () => { const expectOptionForMediaType = ( mediaType, expectedOption ) => { const wrapper = shallow( { return ( @@ -72,12 +72,12 @@ describe( 'MediaUpload component', () => { callback( MEDIA_ID, MEDIA_URL ); } ); - const onSelectURL = jest.fn(); + const onSelect = jest.fn(); const wrapper = shallow( { return ( @@ -87,10 +87,12 @@ describe( 'MediaUpload component', () => { } } /> ); wrapper.find( 'Picker' ).simulate( 'change', option ); + const media = { id: MEDIA_ID, url: MEDIA_URL }; + expect( requestFunction ).toHaveBeenCalledTimes( 1 ); - expect( onSelectURL ).toHaveBeenCalledTimes( 1 ); - expect( onSelectURL ).toHaveBeenCalledWith( MEDIA_ID, MEDIA_URL ); + expect( onSelect ).toHaveBeenCalledTimes( 1 ); + expect( onSelect ).toHaveBeenCalledWith( media ); }; it( 'can select media from device library', () => { diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 30e882ed49d801..feab0f747a9229 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -11,7 +11,7 @@ import { RawHTML } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; import { pasteHandler, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { withInstanceId, compose } from '@wordpress/compose'; -import { RichText } from '@wordpress/rich-text'; +import { __experimentalRichText as RichText } from '@wordpress/rich-text'; /** * Internal dependencies 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 427ea0ca8531e8..8088d61ea5016e 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-editor/src/components/warning/index.native.js b/packages/block-editor/src/components/warning/index.native.js index 7d0cced3408cca..dc7e83d16509af 100644 --- a/packages/block-editor/src/components/warning/index.native.js +++ b/packages/block-editor/src/components/warning/index.native.js @@ -7,6 +7,7 @@ import { View, Text } from 'react-native'; * WordPress dependencies */ import { Icon } from '@wordpress/components'; +import { withPreferredColorScheme } from '@wordpress/compose'; import { normalizeIconObject } from '@wordpress/blocks'; /** @@ -14,29 +15,33 @@ import { normalizeIconObject } from '@wordpress/blocks'; */ import styles from './style.scss'; -function Warning( { title, message, icon, iconClass, ...viewProps } ) { +function Warning( { title, message, icon, iconClass, preferredColorScheme, getStylesFromColorScheme, ...viewProps } ) { icon = icon && normalizeIconObject( icon ); + const internalIconClass = 'warning-icon' + '-' + preferredColorScheme; + const titleStyle = getStylesFromColorScheme( styles.title, styles.titleDark ); + const messageStyle = getStylesFromColorScheme( styles.message, styles.messageDark ); + return ( { icon && ( ) } { title && ( - { title } + { title } ) } { message && ( - { message } + { message } ) } ); } -export default Warning; +export default withPreferredColorScheme( Warning ); diff --git a/packages/block-editor/src/components/warning/style.native.scss b/packages/block-editor/src/components/warning/style.native.scss index 1897d79890c6ae..7c4a288151a107 100644 --- a/packages/block-editor/src/components/warning/style.native.scss +++ b/packages/block-editor/src/components/warning/style.native.scss @@ -14,6 +14,10 @@ justify-content: center; } +.containerDark { + background-color: $background-dark-secondary; +} + .icon { width: 24px; height: 24px; @@ -28,8 +32,16 @@ color: $gray-dark; } +.titleDark { + color: $white; +} + .message { text-align: center; color: $gray-text-min; font-size: 12; } + +.messageDark { + color: $white; +} diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 3f021790c2a403..d8641b2270d20a 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -6,12 +6,13 @@ import { View } from 'react-native'; /** * WordPress dependencies */ +import { PlainText } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies */ -import { PlainText } from '@wordpress/block-editor'; import { escape, unescape } from './utils'; /** @@ -21,14 +22,16 @@ import styles from './theme.scss'; // Note: styling is applied directly to the (nested) PlainText component. Web-side components // apply it to the container 'div' but we don't have a proper proposal for cascading styling yet. -export default function CodeEdit( props ) { - const { attributes, setAttributes, style, onFocus, onBlur } = props; +export function CodeEdit( props ) { + const { attributes, setAttributes, style, onFocus, onBlur, getStylesFromColorScheme } = props; + const codeStyle = getStylesFromColorScheme( styles.blockCode, styles.blockCodeDark ); + const placeholderStyle = getStylesFromColorScheme( styles.placeholder, styles.placeholderDark ); return ( setAttributes( { content: escape( content ) } ) } @@ -38,8 +41,10 @@ export default function CodeEdit( props ) { onFocus={ onFocus } onBlur={ onBlur } fontFamily={ ( styles.blockCode.fontFamily ) } + placeholderTextColor={ placeholderStyle.color } /> </View> ); } +export default withPreferredColorScheme( CodeEdit ); diff --git a/packages/block-library/src/code/theme.native.scss b/packages/block-library/src/code/theme.native.scss index 668b9f92dd1f53..40a4ba9bfcbfdf 100644 --- a/packages/block-library/src/code/theme.native.scss +++ b/packages/block-library/src/code/theme.native.scss @@ -4,3 +4,14 @@ font-family: $default-monospace-font; } +.blockCodeDark { + color: $white; +} + +.placeholder { + color: $gray; +} + +.placeholderDark { + color: $gray-50; +} diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index fc6a6cd841628c..449711a356ea42 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -273,7 +273,7 @@ class GalleryEdit extends Component { addToGallery={ hasImagesWithId } isAppender={ hasImages } className={ className } - dropZoneUIOnly={ hasImages && ! isSelected } + disableMediaButtons={ hasImages && ! isSelected } icon={ ! hasImages && <BlockIcon icon={ icon } /> } labels={ { title: ! hasImages && __( 'Gallery' ), diff --git a/packages/block-library/src/group/edit.native.js b/packages/block-library/src/group/edit.native.js new file mode 100644 index 00000000000000..32c87e3863c881 --- /dev/null +++ b/packages/block-library/src/group/edit.native.js @@ -0,0 +1,51 @@ + +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { + InnerBlocks, + withColors, +} from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import styles from './editor.scss'; + +function GroupEdit( { + hasInnerBlocks, + isSelected, +} ) { + if ( ! isSelected && ! hasInnerBlocks ) { + return ( + <View style={ styles.groupPlaceholder } /> + ); + } + + return ( + <InnerBlocks + renderAppender={ isSelected && InnerBlocks.ButtonBlockAppender } + /> + ); +} + +export default compose( [ + withColors( 'backgroundColor' ), + withSelect( ( select, { clientId } ) => { + const { + getBlock, + } = select( 'core/block-editor' ); + + const block = getBlock( clientId ); + + return { + hasInnerBlocks: !! ( block && block.innerBlocks.length ), + }; + } ), +] )( GroupEdit ); diff --git a/packages/block-library/src/group/editor.native.scss b/packages/block-library/src/group/editor.native.scss new file mode 100644 index 00000000000000..5edfa582287ef5 --- /dev/null +++ b/packages/block-library/src/group/editor.native.scss @@ -0,0 +1,6 @@ +.groupPlaceholder { + padding: 12px; + background-color: $white; + border: $border-width dashed $light-gray-500; + border-radius: 4px; +} diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 389791663b0541..ac8a0e755278cd 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -28,9 +28,10 @@ const HeadingEdit = ( { <BlockControls> <HeadingToolbar minLevel={ 2 } - maxLevel={ 5 } + maxLevel={ 7 } selectedLevel={ attributes.level } onChange={ ( newLevel ) => setAttributes( { level: newLevel } ) } + isCollapsed={ false } /> </BlockControls> <RichText diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index bbca559088299f..d94182130c3013 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -674,7 +674,7 @@ export class ImageEdit extends Component { allowedTypes={ ALLOWED_MEDIA_TYPES } value={ { id, src } } mediaPreview={ mediaPreview } - dropZoneUIOnly={ ! isEditing && url } + disableMediaButtons={ ! isEditing && url } /> ); if ( isEditing || ! url ) { diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 3eb19a6bfa991b..1839c19c0a58d2 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -20,10 +20,12 @@ import { Toolbar, ToolbarButton, } from '@wordpress/components'; + import { Caption, MediaPlaceholder, MediaUpload, + MediaUploadProgress, MEDIA_TYPE_IMAGE, BlockControls, InspectorControls, @@ -31,12 +33,12 @@ import { import { __, sprintf } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; import { doAction, hasAction } from '@wordpress/hooks'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies */ import styles from './styles.scss'; -import MediaUploadProgress from './media-upload-progress'; import SvgIcon from './icon'; import SvgIconRetry from './icon-retry'; @@ -81,9 +83,9 @@ class ImageEdit extends React.Component { if ( attributes.id && attributes.url && ! isURL( attributes.url ) ) { if ( attributes.url.indexOf( 'file:' ) === 0 ) { - requestMediaImport( attributes.url, ( mediaId, mediaUri ) => { - if ( mediaUri ) { - setAttributes( { url: mediaUri, id: mediaId } ); + requestMediaImport( attributes.url, ( id, url ) => { + if ( url ) { + setAttributes( { id, url } ); } } ); } @@ -175,9 +177,9 @@ class ImageEdit extends React.Component { } ); } - onSelectMediaUploadOption( mediaId, mediaUrl ) { + onSelectMediaUploadOption( { id, url } ) { const { setAttributes } = this.props; - setAttributes( { url: mediaUrl, id: mediaId } ); + setAttributes( { id, url } ); } onFocusCaption() { @@ -196,7 +198,8 @@ class ImageEdit extends React.Component { return <Icon icon={ SvgIconRetry } { ...styles.iconRetry } />; } - return <Icon icon={ SvgIcon } { ...styles.icon } />; + const iconStyle = this.props.getStylesFromColorScheme( styles.icon, styles.iconDark ); + return <Icon icon={ SvgIcon } { ...iconStyle } />; } render() { @@ -260,8 +263,8 @@ class ImageEdit extends React.Component { return ( <View style={ { flex: 1 } } > <MediaPlaceholder - mediaType={ MEDIA_TYPE_IMAGE } - onSelectURL={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_IMAGE ] } + onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( false ) } onFocus={ this.props.onFocus } /> @@ -358,8 +361,8 @@ class ImageEdit extends React.Component { ); return ( - <MediaUpload mediaType={ MEDIA_TYPE_IMAGE } - onSelectURL={ this.onSelectMediaUploadOption } + <MediaUpload allowedTypes={ [ MEDIA_TYPE_IMAGE ] } + onSelect={ this.onSelectMediaUploadOption } render={ ( { open, getMediaOptions } ) => { return getImageComponent( open, getMediaOptions ); } } @@ -368,4 +371,4 @@ class ImageEdit extends React.Component { } } -export default ImageEdit; +export default withPreferredColorScheme( ImageEdit ); diff --git a/packages/block-library/src/image/styles.native.scss b/packages/block-library/src/image/styles.native.scss index 81578bd734ba3e..9c5a9cbf5c45e5 100644 --- a/packages/block-library/src/image/styles.native.scss +++ b/packages/block-library/src/image/styles.native.scss @@ -22,11 +22,6 @@ color: $alert-red; } -.mediaUploadProgress { - flex: 1; - background-color: $gray-lighten-30; -} - .modalIcon { width: 80px; height: 80px; @@ -45,3 +40,7 @@ width: 100%; height: 100%; } + +.iconDark { + fill: $white; +} diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 8d5eab4182e507..41806df19dc178 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -48,6 +48,7 @@ import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; import * as tagCloud from './tag-cloud'; +import * as group from './group'; export const coreBlocks = [ // Common blocks are grouped at the top to prioritize their display @@ -99,6 +100,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, @@ -112,13 +140,12 @@ export const registerCoreBlocks = () => { separator, list, quote, - ].forEach( ( { metadata, name, settings } ) => { - registerBlockType( name, { - ...metadata, - ...settings, - } ); - } ); -}; + // eslint-disable-next-line no-undef + !! __DEV__ ? mediaText : null, + // eslint-disable-next-line no-undef + !! __DEV__ ? group : 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 00000000000000..3cfb91b48ce949 --- /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 ( + <MediaContainer + onSelectMedia={ this.onSelectMedia } + onWidthChange={ this.onWidthChange } + commitWidthChange={ this.commitWidthChange } + onFocus={ this.props.onFocus } + { ...{ mediaAlt, mediaId, mediaType, mediaUrl, mediaPosition, mediaWidth, imageFill, focalPoint } } + /> + ); + } + + 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 ( + <> + <BlockControls> + <Toolbar + controls={ toolbarControls } + /> + <BlockVerticalAlignmentToolbar + onChange={ onVerticalAlignmentChange } + value={ verticalAlignment } + isCollapsed={ false } + /> + </BlockControls> + <View style={ containerStyles }> + <View style={ { width: widthString } }> + { this.renderMediaArea() } + </View> + <View style={ { width: innerBlockWidthString } }> + <InnerBlocks + allowedBlocks={ ALLOWED_BLOCKS } + template={ TEMPLATE } + templateInsertUpdatesSelection={ false } + /> + </View> + </View> + </> + ); + } +} + +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 00000000000000..0b47812e5f5ac0 --- /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.onSelectMediaUploadOption = this.onSelectMediaUploadOption.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 ); + } + + onSelectMediaUploadOption( { id, url } ) { + const { onSelectMedia } = this.props; + + onSelectMedia( { + media_type: 'image', + id, + url, + } ); + } + + renderToolbarEditButton() { + const { mediaId } = this.props; + return ( + <BlockControls> + <Toolbar> + <MediaUpload + onSelect={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_IMAGE ] } + value={ mediaId } + render={ ( { open } ) => ( + <IconButton + className="components-toolbar__control" + label={ __( 'Edit media' ) } + icon="edit" + onClick={ open } + /> + ) } + /> + </Toolbar> + </BlockControls> + ); + } + + 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 ( + <View onLayout={ this.onLayout }> + <ImageBackground + accessible={ true } + //disabled={ ! isSelected } + accessibilityLabel={ mediaAlt } + accessibilityHint={ __( 'Double tap and hold to edit' ) } + accessibilityRole={ 'imagebutton' } + style={ { width: this.state.width, height: this.state.height } } + resizeMethod="scale" + source={ { uri: mediaUrl } } + key={ mediaUrl } + > + </ImageBackground> + </View> + ); + } + + renderVideo() { + const style = { videoContainer: {} }; + return ( + <View onLayout={ this.onLayout }> + <View style={ style.videoContainer }> + { /* TODO: show video preview */ } + </View> + </View> + ); + } + + renderPlaceholder() { + return ( + <MediaPlaceholder + icon={ <BlockIcon icon={ icon } /> } + labels={ { + title: __( 'Media area' ), + } } + onSelect={ this.onSelectMediaUploadOption } + allowedTypes={ [ 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 00000000000000..f1c3550f29c1e9 --- /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/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js index 838c64d88b56c4..cd4898a4e8c692 100644 --- a/packages/block-library/src/missing/edit.native.js +++ b/packages/block-library/src/missing/edit.native.js @@ -7,6 +7,7 @@ import { View, Text } from 'react-native'; * WordPress dependencies */ import { Icon } from '@wordpress/components'; +import { withPreferredColorScheme } from '@wordpress/compose'; import { coreBlocks } from '@wordpress/block-library'; import { normalizeIconObject } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; @@ -17,18 +18,25 @@ import { __ } from '@wordpress/i18n'; */ import styles from './style.scss'; -export default class UnsupportedBlockEdit extends Component { +export class UnsupportedBlockEdit extends Component { render() { const { originalName } = this.props.attributes; + const { getStylesFromColorScheme, preferredColorScheme } = this.props; const blockType = coreBlocks[ originalName ]; + const title = blockType ? blockType.settings.title : __( 'Unsupported' ); - const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; + const titleStyle = getStylesFromColorScheme( styles.unsupportedBlockMessage, styles.unsupportedBlockMessageDark ); + const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; + const iconStyle = getStylesFromColorScheme( styles.unsupportedBlockIcon, styles.unsupportedBlockIconDark ); + const iconClassName = 'unsupported-icon' + '-' + preferredColorScheme; return ( - <View style={ styles.unsupportedBlock }> - <Icon className="unsupported-icon" icon={ icon && icon.src ? icon.src : icon } /> - <Text style={ styles.unsupportedBlockMessage }>{ title }</Text> + <View style={ getStylesFromColorScheme( styles.unsupportedBlock, styles.unsupportedBlockDark ) }> + <Icon className={ iconClassName } icon={ icon && icon.src ? icon.src : icon } color={ iconStyle.color } /> + <Text style={ titleStyle }>{ title }</Text> </View> ); } } + +export default withPreferredColorScheme( UnsupportedBlockEdit ); diff --git a/packages/block-library/src/missing/style.native.scss b/packages/block-library/src/missing/style.native.scss index 6d587beb4f2e7e..63cd4258cd23b0 100644 --- a/packages/block-library/src/missing/style.native.scss +++ b/packages/block-library/src/missing/style.native.scss @@ -14,13 +14,25 @@ justify-content: center; } +.unsupportedBlockDark { + background-color: $background-dark-secondary; +} + .unsupportedBlockIcon { color: $gray-dark; } +.unsupportedBlockIconDark { + color: $white; +} + .unsupportedBlockMessage { margin-top: 2; text-align: center; color: $gray-dark; font-size: 14; } + +.unsupportedBlockMessageDark { + color: $white; +} diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index 8b369284cb5962..88908d3f44837c 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -9,13 +9,14 @@ import Hr from 'react-native-hr'; */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies */ import styles from './editor.scss'; -export default class MoreEdit extends Component { +export class MoreEdit extends Component { constructor() { super( ...arguments ); @@ -25,9 +26,13 @@ export default class MoreEdit extends Component { } render() { - const { customText } = this.props.attributes; + const { attributes, getStylesFromColorScheme } = this.props; + const { customText } = attributes; const { defaultText } = this.state; + const content = customText || defaultText; + const textStyle = getStylesFromColorScheme( styles.moreText, styles.moreTextDark ); + const lineStyle = getStylesFromColorScheme( styles.moreLine, styles.moreLineDark ); return ( <View> @@ -35,10 +40,12 @@ export default class MoreEdit extends Component { text={ content } marginLeft={ 0 } marginRight={ 0 } - textStyle={ styles[ 'block-library-more__text' ] } - lineStyle={ styles[ 'block-library-more__line' ] } + textStyle={ textStyle } + lineStyle={ lineStyle } /> </View> ); } } + +export default withPreferredColorScheme( MoreEdit ); diff --git a/packages/block-library/src/more/editor.native.scss b/packages/block-library/src/more/editor.native.scss index eb4a1d60d9431f..b0dece50736e6b 100644 --- a/packages/block-library/src/more/editor.native.scss +++ b/packages/block-library/src/more/editor.native.scss @@ -1,13 +1,21 @@ // @format -.block-library-more__line { +.moreLine { background-color: $gray-lighten-20; height: 2; } -.block-library-more__text { +.moreLineDark { + background-color: $gray-50; +} + +.moreText { color: $gray; text-decoration-style: solid; text-transform: uppercase; } +.moreTextDark { + color: $gray-20; +} + diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js index e3aa69b15e5e41..25591a00269cb7 100644 --- a/packages/block-library/src/nextpage/edit.native.js +++ b/packages/block-library/src/nextpage/edit.native.js @@ -8,16 +8,19 @@ import Hr from 'react-native-hr'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies */ import styles from './editor.scss'; -export default function NextPageEdit( { attributes, isSelected, onFocus } ) { +export function NextPageEdit( { attributes, isSelected, onFocus, getStylesFromColorScheme } ) { const { customText = __( 'Page break' ) } = attributes; const accessibilityTitle = attributes.customText || ''; const accessibilityState = isSelected ? [ 'selected' ] : []; + const textStyle = getStylesFromColorScheme( styles.nextpageText, styles.nextpageTextDark ); + const lineStyle = getStylesFromColorScheme( styles.nextpageLine, styles.nextpageLineDark ); return ( <View @@ -35,8 +38,10 @@ export default function NextPageEdit( { attributes, isSelected, onFocus } ) { <Hr text={ customText } marginLeft={ 0 } marginRight={ 0 } - textStyle={ styles[ 'block-library-nextpage__text' ] } - lineStyle={ styles[ 'block-library-nextpage__line' ] } /> + textStyle={ textStyle } + lineStyle={ lineStyle } /> </View> ); } + +export default withPreferredColorScheme( NextPageEdit ); diff --git a/packages/block-library/src/nextpage/editor.native.scss b/packages/block-library/src/nextpage/editor.native.scss index 869851fdd37c63..01ed97ac577473 100644 --- a/packages/block-library/src/nextpage/editor.native.scss +++ b/packages/block-library/src/nextpage/editor.native.scss @@ -1,12 +1,21 @@ // @format -.block-library-nextpage__line { +.nextpageLine { background-color: $gray-lighten-20; height: 2; } -.block-library-nextpage__text { +.nextpageLineDark { + background-color: $gray-50; +} + +.nextpageText { color: $gray; text-decoration-style: solid; text-transform: uppercase; } + +.nextpageTextDark { + color: $gray-20; +} + diff --git a/packages/block-library/src/separator/edit.js b/packages/block-library/src/separator/edit.js index 050bd7423fb665..30dd0e85d71549 100644 --- a/packages/block-library/src/separator/edit.js +++ b/packages/block-library/src/separator/edit.js @@ -6,13 +6,12 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { HorizontalRule } from '@wordpress/components'; -import { - InspectorControls, - withColors, - PanelColorSettings, -} from '@wordpress/block-editor'; +import { withColors } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import SeparatorSettings from './separator-settings'; function SeparatorEdit( { color, setColor, className } ) { return ( @@ -29,19 +28,10 @@ function SeparatorEdit( { color, setColor, className } ) { color: color.color, } } /> - <InspectorControls> - <PanelColorSettings - title={ __( 'Color Settings' ) } - colorSettings={ [ - { - value: color.color, - onChange: setColor, - label: __( 'Color' ), - }, - ] } - > - </PanelColorSettings> - </InspectorControls> + <SeparatorSettings + color={ color } + setColor={ setColor } + /> </> ); } diff --git a/packages/block-library/src/separator/separator-settings.js b/packages/block-library/src/separator/separator-settings.js new file mode 100644 index 00000000000000..bb3a3a57aa1fa0 --- /dev/null +++ b/packages/block-library/src/separator/separator-settings.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls, + PanelColorSettings, +} from '@wordpress/block-editor'; + +const SeparatorSettings = ( { color, setColor } ) => ( + <InspectorControls> + <PanelColorSettings + title={ __( 'Color Settings' ) } + colorSettings={ [ + { + value: color.color, + onChange: setColor, + label: __( 'Color' ), + }, + ] } + > + </PanelColorSettings> + </InspectorControls> +); + +export default SeparatorSettings; diff --git a/packages/block-library/src/separator/separator-settings.native.js b/packages/block-library/src/separator/separator-settings.native.js new file mode 100644 index 00000000000000..d2bdd8ef6443a3 --- /dev/null +++ b/packages/block-library/src/separator/separator-settings.native.js @@ -0,0 +1,3 @@ +// Mobile has no separator settings at this time, so render nothing +const SeparatorSettings = () => null; +export default SeparatorSettings; diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 3b0a9d186d3084..c69ae7bb39e8e2 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -21,10 +21,12 @@ import { Toolbar, ToolbarButton, } from '@wordpress/components'; +import { withPreferredColorScheme } from '@wordpress/compose'; import { Caption, MediaPlaceholder, MediaUpload, + MediaUploadProgress, MEDIA_TYPE_VIDEO, BlockControls, InspectorControls, @@ -36,7 +38,6 @@ import { doAction, hasAction } from '@wordpress/hooks'; /** * Internal dependencies */ -import MediaUploadProgress from '../image/media-upload-progress'; import style from './style.scss'; import SvgIcon from './icon'; import SvgIconRetry from './icon-retry'; @@ -133,9 +134,9 @@ class VideoEdit extends React.Component { this.setState( { isUploadInProgress: false } ); } - onSelectMediaUploadOption( mediaId, mediaUrl ) { + onSelectMediaUploadOption( { id, url } ) { const { setAttributes } = this.props; - setAttributes( { id: mediaId, src: mediaUrl } ); + setAttributes( { id, src: url } ); } onVideoContanerLayout( event ) { @@ -151,7 +152,8 @@ class VideoEdit extends React.Component { return <Icon icon={ SvgIconRetry } { ...style.icon } />; } - return <Icon icon={ SvgIcon } { ...( ! isMediaPlaceholder ? style.iconUploading : style.icon ) } />; + const iconStyle = this.props.getStylesFromColorScheme( style.icon, style.iconDark ); + return <Icon icon={ SvgIcon } { ...( ! isMediaPlaceholder ? style.iconUploading : iconStyle ) } />; } render() { @@ -160,8 +162,8 @@ class VideoEdit extends React.Component { const { videoContainerHeight } = this.state; const toolbarEditButton = ( - <MediaUpload mediaType={ MEDIA_TYPE_VIDEO } - onSelectURL={ this.onSelectMediaUploadOption } + <MediaUpload allowedTypes={ [ MEDIA_TYPE_VIDEO ] } + onSelect={ this.onSelectMediaUploadOption } render={ ( { open, getMediaOptions } ) => { return ( <Toolbar> @@ -181,8 +183,8 @@ class VideoEdit extends React.Component { return ( <View style={ { flex: 1 } } > <MediaPlaceholder - mediaType={ MEDIA_TYPE_VIDEO } - onSelectURL={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_VIDEO ] } + onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( false, true ) } onFocus={ this.props.onFocus } /> @@ -262,4 +264,4 @@ class VideoEdit extends React.Component { } } -export default VideoEdit; +export default withPreferredColorScheme( VideoEdit ); diff --git a/packages/block-library/src/video/style.native.scss b/packages/block-library/src/video/style.native.scss index dd4d70ae0a4752..5eb36be46605e3 100644 --- a/packages/block-library/src/video/style.native.scss +++ b/packages/block-library/src/video/style.native.scss @@ -59,6 +59,10 @@ height: 100%; } +.iconDark { + fill: $white; +} + .iconUploading { fill: $gray-lighten-20; width: 100%; diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index 3b3be8f28c3a46..f8d4c03298aebe 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/blocks/src/api/raw-handling/list-reducer.js b/packages/blocks/src/api/raw-handling/list-reducer.js index a5f079dcb24880..a5e85949aea62b 100644 --- a/packages/blocks/src/api/raw-handling/list-reducer.js +++ b/packages/blocks/src/api/raw-handling/list-reducer.js @@ -8,7 +8,7 @@ function isList( node ) { } function shallowTextContent( element ) { - return [ ...element.childNodes ] + return Array.from( element.childNodes ) .map( ( { nodeValue = '' } ) => nodeValue ) .join( '' ); } diff --git a/packages/blocks/src/api/validation/index.js b/packages/blocks/src/api/validation/index.js index 01e5f0857a1ea1..fdf3cb67fac1fd 100644 --- a/packages/blocks/src/api/validation/index.js +++ b/packages/blocks/src/api/validation/index.js @@ -51,7 +51,7 @@ const REGEXP_STYLE_URL_TYPE = /^url\s*\(['"\s]*(.*?)['"\s]*\)$/; * See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes * Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3 * - * Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ] + * Object.keys( Array.from( document.querySelectorAll( '#attributes-1 > tbody > tr' ) ) * .filter( ( tr ) => tr.lastChild.textContent.indexOf( 'Boolean attribute' ) !== -1 ) * .reduce( ( result, tr ) => Object.assign( result, { * [ tr.firstChild.textContent.trim() ]: true @@ -98,7 +98,7 @@ const BOOLEAN_ATTRIBUTES = [ * See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#enumerated-attribute * Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3 * - * Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ] + * Object.keys( Array.from( document.querySelectorAll( '#attributes-1 > tbody > tr' ) ) * .filter( ( tr ) => /^("(.+?)";?\s*)+/.test( tr.lastChild.textContent.trim() ) ) * .reduce( ( result, tr ) => Object.assign( result, { * [ tr.firstChild.textContent.trim() ]: true @@ -165,9 +165,9 @@ const TEXT_NORMALIZATIONS = [ * Tested aginst "12.5 Named character references": * * ``` - * const references = [ ...document.querySelectorAll( + * const references = Array.from( document.querySelectorAll( * '#named-character-references-table tr[id^=entity-] td:first-child' - * ) ].map( ( code ) => code.textContent ) + * ) ).map( ( code ) => code.textContent ) * references.every( ( reference ) => /^[\da-z]+$/i.test( reference ) ) * ``` * diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index 7a90dd53131661..f7fdd94d13f85d 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -19,8 +19,9 @@ const styles = StyleSheet.create( { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', + }, + fixedRatio: { aspectRatio: 1, - backgroundColor: 'white', }, buttonActive: { flex: 1, @@ -29,7 +30,6 @@ const styles = StyleSheet.create( { alignItems: 'center', borderRadius: 6, borderColor: '#2e4453', - aspectRatio: 1, backgroundColor: '#2e4453', }, subscriptInactive: { @@ -56,6 +56,7 @@ export default function Button( props ) { onClick, disabled, hint, + fixedRatio = true, 'aria-disabled': ariaDisabled, 'aria-label': ariaLabel, 'aria-pressed': ariaPressed, @@ -63,8 +64,10 @@ export default function Button( props ) { } = props; const isDisabled = ariaDisabled || disabled; + const buttonViewStyle = { opacity: isDisabled ? 0.2 : 1, + ...( fixedRatio && styles.fixedRatio ), ...( ariaPressed ? styles.buttonActive : styles.buttonInactive ), }; diff --git a/packages/components/src/disabled/test/index.js b/packages/components/src/disabled/test/index.js index bd7c556163e4ec..54120a61a61bc9 100644 --- a/packages/components/src/disabled/test/index.js +++ b/packages/components/src/disabled/test/index.js @@ -25,7 +25,7 @@ jest.mock( '@wordpress/dom', () => { // In JSDOM, all elements have zero'd widths and height. // This is a metric for focusable's `isVisible`, so find // and apply an arbitrary non-zero width. - [ ...context.querySelectorAll( '*' ) ].forEach( ( element ) => { + Array.from( context.querySelectorAll( '*' ) ).forEach( ( element ) => { Object.defineProperties( element, { offsetWidth: { get: () => 1, diff --git a/packages/components/src/draggable/index.js b/packages/components/src/draggable/index.js index ac077468d83777..cd7d8c310e9530 100644 --- a/packages/components/src/draggable/index.js +++ b/packages/components/src/draggable/index.js @@ -116,7 +116,7 @@ class Draggable extends Component { } // Hack: Remove iFrames as it's causing the embeds drag clone to freeze - [ ...clone.querySelectorAll( 'iframe' ) ].forEach( ( child ) => child.parentNode.removeChild( child ) ); + Array.from( clone.querySelectorAll( 'iframe' ) ).forEach( ( child ) => child.parentNode.removeChild( child ) ); this.cloneWrapper.appendChild( clone ); elementWrapper.appendChild( this.cloneWrapper ); diff --git a/packages/components/src/higher-order/navigate-regions/index.js b/packages/components/src/higher-order/navigate-regions/index.js index c716e2d1ea9f14..b8b010a7414f45 100644 --- a/packages/components/src/higher-order/navigate-regions/index.js +++ b/packages/components/src/higher-order/navigate-regions/index.js @@ -34,7 +34,7 @@ export default createHigherOrderComponent( } focusRegion( offset ) { - const regions = [ ...this.container.querySelectorAll( '[role="region"]' ) ]; + const regions = Array.from( this.container.querySelectorAll( '[role="region"]' ) ); if ( ! regions.length ) { return; } diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index f64d7f6777e934..377feab7601cfc 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 ) ? <Dashicon icon={ icon } ariaPressed={ ariaPressed } /> : icon } + <Icon icon={ icon } ariaPressed={ ariaPressed } /> { children } </Button> ); diff --git a/packages/components/src/icon-button/test/index.js b/packages/components/src/icon-button/test/index.js index e824ed1d556ec9..f4a8d4330d3cc2 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( <IconButton icon="wordpress" /> ); - 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 61a4fb0a2d6c47..5e762d1d5c8a98 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 <Dashicon icon={ icon } size={ iconSize } { ...additionalProps } />; + return <Dashicon icon={ icon } size={ dashiconSize } { ...additionalProps } />; } - // 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 053b0cf390ff58..a645568c1e2eee 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( <Icon icon="format-image" /> ); expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); } ); + it( 'renders a dashicon by element and with a default size of 20', () => { + const wrapper = shallow( <Icon icon={ <Dashicon icon="format-image" /> } /> ); + + expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); + } ); + it( 'renders a function', () => { const wrapper = shallow( <Icon icon={ () => <span /> } /> ); @@ -98,6 +105,7 @@ describe( 'Icon', () => { describe.each( [ [ 'dashicon', { icon: 'format-image' } ], + [ 'dashicon element', { icon: <Dashicon icon="format-image" /> } ], [ 'element', { icon: <span /> } ], [ 'svg element', { icon: svg } ], [ 'component', { icon: MyComponent } ], diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 4984cdde2fd36d..a12d2e8baad21c 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -10,6 +10,7 @@ export { default as Spinner } from './spinner'; export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; export { default as BaseControl } from './base-control'; export { default as TextareaControl } from './textarea-control'; +export { default as Button } from './button'; // Higher-Order Components export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing'; diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index cebfc6a91c1826..af74a25e2cee82 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -10,6 +10,7 @@ import { isEmpty } from 'lodash'; import { Dashicon } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies @@ -17,7 +18,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import styles from './styles.scss'; import platformStyles from './cellStyles.scss'; -export default class BottomSheetCell extends Component { +class BottomSheetCell extends Component { constructor( props ) { super( ...arguments ); this.state = { @@ -48,12 +49,15 @@ export default class BottomSheetCell extends Component { editable = true, separatorType, style = {}, + getStylesFromColorScheme, ...valueProps } = this.props; const showValue = value !== undefined; const isValueEditable = editable && onChangeValue !== undefined; - const defaultLabelStyle = showValue || icon !== undefined ? styles.cellLabel : styles.cellLabelCentered; + const cellLabelStyle = getStylesFromColorScheme( styles.cellLabel, styles.cellTextDark ); + const cellLabelCenteredStyle = getStylesFromColorScheme( styles.cellLabelCentered, styles.cellTextDark ); + const defaultLabelStyle = showValue || icon !== undefined ? cellLabelStyle : cellLabelCenteredStyle; const drawSeparator = ( separatorType && separatorType !== 'none' ) || separatorStyle === undefined; const onCellPress = () => { @@ -75,22 +79,26 @@ export default class BottomSheetCell extends Component { }; const separatorStyle = () => { - const leftMarginStyle = { ...styles.cellSeparator, ...platformStyles.separatorMarginLeft }; + //eslint-disable-next-line @wordpress/no-unused-vars-before-return + const defaultSeparatorStyle = this.props.getStylesFromColorScheme( styles.separator, styles.separatorDark ); + const cellSeparatorStyle = this.props.getStylesFromColorScheme( styles.cellSeparator, styles.cellSeparatorDark ); + const leftMarginStyle = { ...cellSeparatorStyle, ...platformStyles.separatorMarginLeft }; switch ( separatorType ) { case 'leftMargin': return leftMarginStyle; case 'fullWidth': - return styles.separator; + return defaultSeparatorStyle; case 'none': return undefined; case undefined: - return showValue ? leftMarginStyle : styles.separator; + return showValue ? leftMarginStyle : defaultSeparatorStyle; } }; const getValueComponent = () => { const styleRTL = I18nManager.isRTL && styles.cellValueRTL; - const finalStyle = { ...styles.cellValue, ...valueStyle, ...styleRTL }; + const cellValueStyle = this.props.getStylesFromColorScheme( styles.cellValue, styles.cellTextDark ); + const finalStyle = { ...cellValueStyle, ...valueStyle, ...styleRTL }; // To be able to show the `middle` ellipsizeMode on editable cells // we show the TextInput just when the user wants to edit the value, @@ -114,7 +122,7 @@ export default class BottomSheetCell extends Component { /> ) : ( <Text - style={ { ...styles.cellValue, ...valueStyle } } + style={ { ...cellValueStyle, ...valueStyle } } numberOfLines={ 1 } ellipsizeMode={ 'middle' } > @@ -142,6 +150,8 @@ export default class BottomSheetCell extends Component { ); }; + const iconStyle = getStylesFromColorScheme( styles.icon, styles.iconDark ); + return ( <TouchableOpacity accessible={ ! this.state.isEditingValue } @@ -159,7 +169,7 @@ export default class BottomSheetCell extends Component { <View style={ styles.cellRowContainer }> { icon && ( <View style={ styles.cellRowContainer }> - <Dashicon icon={ icon } size={ 24 } /> + <Dashicon icon={ icon } size={ 24 } color={ iconStyle.color } /> <View style={ platformStyles.labelIconSeparator } /> </View> ) } @@ -177,3 +187,5 @@ export default class BottomSheetCell extends Component { ); } } + +export default withPreferredColorScheme( BottomSheetCell ); diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 9106709cea25ef..f17284b49e7306 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -9,6 +9,7 @@ import SafeArea from 'react-native-safe-area'; * WordPress dependencies */ import { Component } from '@wordpress/element'; +import { withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies @@ -63,6 +64,7 @@ class BottomSheet extends Component { hideHeader, style = {}, contentStyle = {}, + getStylesFromColorScheme, } = this.props; const panResponder = PanResponder.create( { @@ -118,6 +120,8 @@ class BottomSheet extends Component { }, }; + const backgroundStyle = getStylesFromColorScheme( styles.background, styles.backgroundDark ); + return ( <Modal isVisible={ isVisible } @@ -139,7 +143,7 @@ class BottomSheet extends Component { > <KeyboardAvoidingView behavior={ Platform.OS === 'ios' && 'padding' } - style={ { ...styles.background, borderColor: 'rgba(0, 0, 0, 0.1)', ...style } } + style={ { ...backgroundStyle, borderColor: 'rgba(0, 0, 0, 0.1)', ...style } } keyboardVerticalOffset={ -this.state.safeAreaBottomInset } > <View style={ styles.dragIndicator } /> @@ -160,10 +164,12 @@ function getWidth() { return Math.min( Dimensions.get( 'window' ).width, styles.background.maxWidth ); } -BottomSheet.getWidth = getWidth; -BottomSheet.Button = Button; -BottomSheet.Cell = Cell; -BottomSheet.PickerCell = PickerCell; -BottomSheet.SwitchCell = SwitchCell; +const ThemedBottomSheet = withPreferredColorScheme( BottomSheet ); + +ThemedBottomSheet.getWidth = getWidth; +ThemedBottomSheet.Button = Button; +ThemedBottomSheet.Cell = Cell; +ThemedBottomSheet.PickerCell = PickerCell; +ThemedBottomSheet.SwitchCell = SwitchCell; -export default BottomSheet; +export default ThemedBottomSheet; diff --git a/packages/components/src/mobile/bottom-sheet/styles.native.scss b/packages/components/src/mobile/bottom-sheet/styles.native.scss index 53764ee4fe38f1..8f153715c16705 100644 --- a/packages/components/src/mobile/bottom-sheet/styles.native.scss +++ b/packages/components/src/mobile/bottom-sheet/styles.native.scss @@ -21,6 +21,10 @@ width: 100%; } +.separatorDark { + background-color: $gray-70; +} + .emptyHeaderSpace { height: 14; } @@ -34,6 +38,10 @@ padding-bottom: 0; } +.backgroundDark { + background-color: $background-dark-elevated; +} + .content { padding: 0 16px 0 16px; } @@ -86,6 +94,10 @@ width: 100%; } +.cellSeparatorDark { + background-color: $gray-70; +} + .cellRowContainer { flex-direction: row; align-items: center; @@ -115,6 +127,18 @@ flex: 1; } +.cellTextDark { + color: $white; +} + .cellValueRTL { text-align: left; } + +.icon { + color: #7b9ab1; +} + +.iconDark { + color: #c3c4c7; +} diff --git a/packages/components/src/mobile/html-text-input/index.native.js b/packages/components/src/mobile/html-text-input/index.native.js index e25f7a1af71c4d..7b7c648216ef33 100644 --- a/packages/components/src/mobile/html-text-input/index.native.js +++ b/packages/components/src/mobile/html-text-input/index.native.js @@ -10,7 +10,7 @@ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { parse } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; -import { withInstanceId, compose } from '@wordpress/compose'; +import { withInstanceId, compose, withPreferredColorScheme } from '@wordpress/compose'; /** * Internal dependencies @@ -60,6 +60,9 @@ export class HTMLTextInput extends Component { } render() { + const { getStylesFromColorScheme } = this.props; + const htmlStyle = getStylesFromColorScheme( styles.htmlView, styles.htmlViewDark ); + const placeholderStyle = getStylesFromColorScheme( styles.placeholder, styles.placeholderDark ); return ( <HTMLInputContainer parentHeight={ this.props.parentHeight }> <TextInput @@ -70,6 +73,7 @@ export class HTMLTextInput extends Component { style={ styles.htmlViewTitle } value={ this.props.title } placeholder={ __( 'Add title' ) } + placeholderTextColor={ placeholderStyle.color } onChangeText={ this.props.editTitle } /> <TextInput @@ -77,11 +81,12 @@ export class HTMLTextInput extends Component { accessibilityLabel="html-view-content" textAlignVertical="top" multiline - style={ styles.htmlView } + style={ htmlStyle } value={ this.state.value } onChangeText={ this.edit } onBlur={ this.stopEditing } placeholder={ __( 'Start writing…' ) } + placeholderTextColor={ placeholderStyle.color } scrollEnabled={ HTMLInputContainer.scrollEnabled } /> </HTMLInputContainer> @@ -117,4 +122,5 @@ export default compose( [ }; } ), withInstanceId, + withPreferredColorScheme, ] )( HTMLTextInput ); diff --git a/packages/components/src/mobile/html-text-input/style-common.native.scss b/packages/components/src/mobile/html-text-input/style-common.native.scss index 4db5b985161402..c1ac9f155d4c79 100644 --- a/packages/components/src/mobile/html-text-input/style-common.native.scss +++ b/packages/components/src/mobile/html-text-input/style-common.native.scss @@ -1,6 +1,6 @@ $padding: 8; -$backgroundColor: $white; $htmlFont: $default-monospace-font; +$textColorDark: $white; .keyboardAvoidingView { position: absolute; @@ -13,3 +13,11 @@ $htmlFont: $default-monospace-font; .container { flex: 1; } + +.placeholder { + color: $gray; +} + +.placeholderDark { + color: $gray-50; +} diff --git a/packages/components/src/mobile/html-text-input/style.android.scss b/packages/components/src/mobile/html-text-input/style.android.scss index 10594358722c37..1dca01274d75b5 100644 --- a/packages/components/src/mobile/html-text-input/style.android.scss +++ b/packages/components/src/mobile/html-text-input/style.android.scss @@ -2,7 +2,6 @@ .htmlView { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; @@ -11,7 +10,6 @@ .htmlViewTitle { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; diff --git a/packages/components/src/mobile/html-text-input/style.ios.scss b/packages/components/src/mobile/html-text-input/style.ios.scss index 8b13392b95a9ae..97cf00a7512ff5 100644 --- a/packages/components/src/mobile/html-text-input/style.ios.scss +++ b/packages/components/src/mobile/html-text-input/style.ios.scss @@ -4,15 +4,17 @@ $title-height: 32; .htmlView { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-bottom: $title-height + $padding; } +.htmlViewDark { + color: $textColorDark; +} + .htmlViewTitle { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; diff --git a/packages/components/src/mobile/html-text-input/test/index.native.js b/packages/components/src/mobile/html-text-input/test/index.native.js index 479846d3f6a960..2481e533a4fad8 100644 --- a/packages/components/src/mobile/html-text-input/test/index.native.js +++ b/packages/components/src/mobile/html-text-input/test/index.native.js @@ -33,10 +33,14 @@ const findTitleTextInput = ( wrapper ) => { return findTextInputInWrapper( wrapper, { placeholder } ); }; +const getStylesFromColorScheme = () => { + return { color: 'white' }; +}; + describe( 'HTMLTextInput', () => { it( 'HTMLTextInput renders', () => { const wrapper = shallow( - <HTMLTextInput /> + <HTMLTextInput getStylesFromColorScheme={ getStylesFromColorScheme } /> ); expect( wrapper ).toBeTruthy(); } ); @@ -47,6 +51,7 @@ describe( 'HTMLTextInput', () => { const wrapper = shallow( <HTMLTextInput onChange={ onChange } + getStylesFromColorScheme={ getStylesFromColorScheme } /> ); @@ -71,6 +76,7 @@ describe( 'HTMLTextInput', () => { <HTMLTextInput onPersist={ onPersist } onChange={ jest.fn() } + getStylesFromColorScheme={ getStylesFromColorScheme } /> ); @@ -101,6 +107,7 @@ describe( 'HTMLTextInput', () => { const wrapper = shallow( <HTMLTextInput editTitle={ editTitle } + getStylesFromColorScheme={ getStylesFromColorScheme } /> ); 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 fb9dab39a03f2b..33c56715268971 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 } ) => ( <KeyboardAwareScrollView @@ -17,6 +18,7 @@ export const KeyboardAwareFlatList = ( { keyboardShouldPersistTaps="handled" extraScrollHeight={ extraScrollHeight } extraHeight={ 0 } + enableAutomaticScroll={ autoScroll === undefined ? false : autoScroll } innerRef={ ( ref ) => { 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 b0272e6b5a7b9f..4ee8dbae9b798d 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 e5a64ea140d0d8..95dd5b9856bd7a 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; } @@ -13,12 +15,22 @@ fill: currentColor; } -.unsupported-icon { +.unsupported-icon-light { color: $gray-dark; fill: currentColor; } -.warning-icon { +.unsupported-icon-dark { + color: $white; + fill: currentColor; +} + +.warning-icon-light { color: $gray-dark; fill: currentColor; } + +.warning-icon-dark { + color: $white; + fill: currentColor; +} diff --git a/packages/components/src/toolbar/style.native.scss b/packages/components/src/toolbar/style.native.scss index 1e0d176e275d80..3038ea8e491d8d 100644 --- a/packages/components/src/toolbar/style.native.scss +++ b/packages/components/src/toolbar/style.native.scss @@ -5,3 +5,7 @@ padding-left: 5px; padding-right: 5px; } + +.containerDark { + border-color: #515459; +} diff --git a/packages/components/src/toolbar/toolbar-container.native.js b/packages/components/src/toolbar/toolbar-container.native.js index 887991d2ea1238..33fe77d11db4c0 100644 --- a/packages/components/src/toolbar/toolbar-container.native.js +++ b/packages/components/src/toolbar/toolbar-container.native.js @@ -3,15 +3,20 @@ */ import { View } from 'react-native'; +/** + * WordPress dependencies + */ +import { withPreferredColorScheme } from '@wordpress/compose'; + /** * Internal dependencies */ import styles from './style.scss'; -const ToolbarContainer = ( props ) => ( - <View style={ [ styles.container, props.passedStyle ] }> - { props.children } +const ToolbarContainer = ( { getStylesFromColorScheme, passedStyle, children } ) => ( + <View style={ [ getStylesFromColorScheme( styles.container, styles.containerDark ), passedStyle ] }> + { children } </View> ); -export default ToolbarContainer; +export default withPreferredColorScheme( ToolbarContainer ); diff --git a/packages/compose/src/higher-order/with-preferred-color-scheme/index.native.js b/packages/compose/src/higher-order/with-preferred-color-scheme/index.native.js new file mode 100644 index 00000000000000..3b393c2ecaf5c7 --- /dev/null +++ b/packages/compose/src/higher-order/with-preferred-color-scheme/index.native.js @@ -0,0 +1,32 @@ +/** + * Internal dependencies + */ +import createHigherOrderComponent from '../../utils/create-higher-order-component'; +import usePreferredColorScheme from '../../hooks/use-preferred-color-scheme'; + +const withPreferredColorScheme = createHigherOrderComponent( + ( WrappedComponent ) => ( props ) => { + const colorScheme = usePreferredColorScheme(); + const isDarkMode = colorScheme === 'dark'; + + const getStyles = ( lightStyles, darkStyles ) => { + const finalDarkStyles = { + ...lightStyles, + ...darkStyles, + }; + + return isDarkMode ? finalDarkStyles : lightStyles; + }; + + return ( + <WrappedComponent + preferredColorScheme={ colorScheme } + getStylesFromColorScheme={ getStyles } + { ...props } + /> + ); + }, + 'withPreferredColorScheme' +); + +export default withPreferredColorScheme; diff --git a/packages/compose/src/hooks/use-preferred-color-scheme/index.native.js b/packages/compose/src/hooks/use-preferred-color-scheme/index.native.js new file mode 100644 index 00000000000000..417a9e0bb0e106 --- /dev/null +++ b/packages/compose/src/hooks/use-preferred-color-scheme/index.native.js @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import { useDarkModeContext, eventEmitter } from 'react-native-dark-mode'; + +// Conditional needed to pass UI Tests on CI +if ( eventEmitter.setMaxListeners ) { + eventEmitter.setMaxListeners( 150 ); +} + +/** + * Returns the color scheme value when it changes. Possible values: [ 'light', 'dark' ] + * + * @return {string} return current color scheme. + */ +const usePreferredColorScheme = useDarkModeContext; + +export default usePreferredColorScheme; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js new file mode 100644 index 00000000000000..f410b60094d005 --- /dev/null +++ b/packages/compose/src/index.native.js @@ -0,0 +1,9 @@ + +// Web exports +export * from './index.js'; + +// Higher-order components +export { default as withPreferredColorScheme } from './higher-order/with-preferred-color-scheme'; + +// Hooks +export { default as usePreferredColorScheme } from './hooks/use-preferred-color-scheme'; diff --git a/packages/dom/src/focusable.js b/packages/dom/src/focusable.js index 133b89d3669747..12688755cfe445 100644 --- a/packages/dom/src/focusable.js +++ b/packages/dom/src/focusable.js @@ -76,7 +76,7 @@ function isValidFocusableArea( element ) { export function find( context ) { const elements = context.querySelectorAll( SELECTOR ); - return [ ...elements ].filter( ( element ) => { + return Array.from( elements ).filter( ( element ) => { if ( ! isVisible( element ) ) { return false; } diff --git a/packages/edit-post/src/components/admin-notices/index.js b/packages/edit-post/src/components/admin-notices/index.js index af04efaa9953a3..d39c74c34a0620 100644 --- a/packages/edit-post/src/components/admin-notices/index.js +++ b/packages/edit-post/src/components/admin-notices/index.js @@ -27,7 +27,7 @@ const NOTICE_CLASS_STATUSES = { function getAdminNotices() { // The order is reversed to match expectations of rendered order, since a // NoticesList is itself rendered in reverse order (newest to oldest). - return [ ...document.querySelectorAll( '#wpbody-content > .notice' ) ].reverse(); + return Array.from( document.querySelectorAll( '#wpbody-content > .notice' ) ).reverse(); } /** diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 7c33856a629180..6f87cc7bfc7df5 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -7,7 +7,7 @@ import { ScrollView, View } from 'react-native'; /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { __ } from '@wordpress/i18n'; @@ -30,7 +30,8 @@ function HeaderToolbar( { undo, showInserter, showKeyboardHideButton, - clearSelectedBlock, + getStylesFromColorScheme, + onHideKeyboard, } ) { const scrollViewRef = useRef( null ); const scrollToStart = () => { @@ -38,7 +39,7 @@ function HeaderToolbar( { }; return ( - <View style={ styles.container }> + <View style={ getStylesFromColorScheme( styles.container, styles.containerDark ) }> <ScrollView ref={ scrollViewRef } onContentSizeChange={ scrollToStart } @@ -75,7 +76,7 @@ function HeaderToolbar( { <ToolbarButton title={ __( 'Hide keyboard' ) } icon="keyboard-hide" - onClick={ clearSelectedBlock } + onClick={ onHideKeyboard } extraProps={ { hint: __( 'Tap to hide the keyboard' ) } } /> </Toolbar> @@ -93,10 +94,19 @@ export default compose( [ showInserter: select( 'core/edit-post' ).getEditorMode() === 'visual' && select( 'core/editor' ).getEditorSettings().richEditingEnabled, isTextModeEnabled: select( 'core/edit-post' ).getEditorMode() === 'text', } ) ), - withDispatch( ( dispatch ) => ( { - redo: dispatch( 'core/editor' ).redo, - undo: dispatch( 'core/editor' ).undo, - clearSelectedBlock: dispatch( 'core/block-editor' ).clearSelectedBlock, - } ) ), + withDispatch( ( dispatch ) => { + const { clearSelectedBlock } = dispatch( 'core/block-editor' ); + const { togglePostTitleSelection } = dispatch( 'core/editor' ); + + return { + redo: dispatch( 'core/editor' ).redo, + undo: dispatch( 'core/editor' ).undo, + onHideKeyboard() { + clearSelectedBlock(); + togglePostTitleSelection( false ); + }, + }; + } ), withViewportMatch( { isLargeViewport: 'medium' } ), + withPreferredColorScheme, ] )( HeaderToolbar ); diff --git a/packages/edit-post/src/components/header/header-toolbar/style.native.scss b/packages/edit-post/src/components/header/header-toolbar/style.native.scss index 0aa03b90ed81cc..9210ce5506d620 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.native.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.native.scss @@ -7,6 +7,11 @@ border-top-width: 1px; } +.containerDark { + background-color: $background-dark-elevated; + border-top-color: $background-dark-elevated; +} + .scrollableContent { flex-grow: 1; // Fixes RTL issue on Android. } 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 3f01ba28572d48..5096eaa7c803b5 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" > - <Dashicon + <Icon icon="ellipsis" key="0,0" > - <SVG - aria-hidden={true} - className="dashicon dashicons-ellipsis" - focusable="false" - height={20} - role="img" - viewBox="0 0 20 20" - width={20} - xmlns="http://www.w3.org/2000/svg" + <Dashicon + icon="ellipsis" + size={20} > - <svg - aria-hidden="true" + <SVG + aria-hidden={true} className="dashicon dashicons-ellipsis" focusable="false" height={20} @@ -98,16 +92,27 @@ exports[`MoreMenu should match snapshot 1`] = ` width={20} xmlns="http://www.w3.org/2000/svg" > - <Path - d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + <svg + aria-hidden="true" + className="dashicon dashicons-ellipsis" + focusable="false" + height={20} + role="img" + viewBox="0 0 20 20" + width={20} + xmlns="http://www.w3.org/2000/svg" > - <path + <Path d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" - /> - </Path> - </svg> - </SVG> - </Dashicon> + > + <path + d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + /> + </Path> + </svg> + </SVG> + </Dashicon> + </Icon> </button> </ForwardRef(Button)> </Tooltip> diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 3242628c021b89..44d9a2d51899c4 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -10,8 +10,9 @@ import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge'; */ import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView } from '@wordpress/components'; +import { AutosaveMonitor } from '@wordpress/editor'; /** * Internal dependencies @@ -75,7 +76,7 @@ class Layout extends Component { renderHTML() { return ( - <HTMLTextInput /> + <HTMLTextInput parentHeight={ this.state.rootViewHeight } /> ); } @@ -99,10 +100,13 @@ class Layout extends Component { render() { const { mode, + getStylesFromColorScheme, } = this.props; + const isHtmlView = mode === 'text'; + // add a margin view at the bottom for the header - const marginBottom = Platform.OS === 'android' ? headerToolbarStyles.container.height : 0; + const marginBottom = Platform.OS === 'android' && ! isHtmlView ? headerToolbarStyles.container.height : 0; const toolbarKeyboardAvoidingViewStyle = { ...styles.toolbarKeyboardAvoidingView, @@ -112,18 +116,19 @@ class Layout extends Component { }; return ( - <SafeAreaView style={ styles.container } onLayout={ this.onRootViewLayout }> - <View style={ { flex: 1 } }> - { mode === 'text' ? this.renderHTML() : this.renderVisual() } - </View> - <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } }> + <SafeAreaView style={ getStylesFromColorScheme( styles.container, styles.containerDark ) } onLayout={ this.onRootViewLayout }> + <AutosaveMonitor /> + <View style={ getStylesFromColorScheme( styles.background, styles.backgroundDark ) }> + { isHtmlView ? this.renderHTML() : this.renderVisual() } </View> - <KeyboardAvoidingView - parentHeight={ this.state.rootViewHeight } - style={ toolbarKeyboardAvoidingViewStyle } - > - <Header /> - </KeyboardAvoidingView> + <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } } /> + { ! isHtmlView && ( + <KeyboardAvoidingView + parentHeight={ this.state.rootViewHeight } + style={ toolbarKeyboardAvoidingViewStyle } + > + <Header /> + </KeyboardAvoidingView> ) } </SafeAreaView> ); } @@ -143,4 +148,5 @@ export default compose( [ mode: getEditorMode(), }; } ), + withPreferredColorScheme, ] )( Layout ); diff --git a/packages/edit-post/src/components/layout/style.native.scss b/packages/edit-post/src/components/layout/style.native.scss index e6d7e241bcd0df..7a5026d6664dc9 100644 --- a/packages/edit-post/src/components/layout/style.native.scss +++ b/packages/edit-post/src/components/layout/style.native.scss @@ -5,6 +5,19 @@ background-color: #fff; } +.containerDark { + background-color: $background-dark-elevated; +} + +.background { + flex: 1; + background-color: $white; +} + +.backgroundDark { + background-color: $black; +} + .toolbarKeyboardAvoidingView { position: absolute; bottom: 0; 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 15ca4ed9d451bc..44d7c2902cef4a 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -4,7 +4,7 @@ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { BlockList } from '@wordpress/block-editor'; import { PostTitle } from '@wordpress/editor'; import { ReadableContentView } from '@wordpress/components'; @@ -20,8 +20,9 @@ class VisualEditor extends Component { editTitle, setTitleRef, title, + getStylesFromColorScheme, } = this.props; - + const blockHolderFocusedStyle = getStylesFromColorScheme( styles.blockHolderFocused, styles.blockHolderFocusedDark ); return ( <ReadableContentView> <PostTitle @@ -34,7 +35,7 @@ class VisualEditor extends Component { styles.blockHolderFullBordered : styles.blockHolderSemiBordered } - focusedBorderColor={ styles.blockHolderFocused.borderColor } + focusedBorderColor={ blockHolderFocusedStyle.borderColor } accessibilityLabel="post-title" /> </ReadableContentView> @@ -52,6 +53,7 @@ class VisualEditor extends Component { header={ this.renderHeader() } isFullyBordered={ isFullyBordered } safeAreaBottomInset={ safeAreaBottomInset } + autoScroll={ true } /> ); } @@ -81,4 +83,5 @@ export default compose( [ }, }; } ), + withPreferredColorScheme, ] )( VisualEditor ); diff --git a/packages/edit-post/src/components/visual-editor/style.native.scss b/packages/edit-post/src/components/visual-editor/style.native.scss index 02b49a1515584c..4ade220b5dd9e9 100644 --- a/packages/edit-post/src/components/visual-editor/style.native.scss +++ b/packages/edit-post/src/components/visual-editor/style.native.scss @@ -15,3 +15,7 @@ .blockHolderFocused { border-color: $gray-lighten-30; } + +.blockHolderFocusedDark { + border-color: $gray-70; +} diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 23e0bad6a9e60a..74c2cec2e4b97d 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -3,6 +3,7 @@ */ import memize from 'memize'; import { size, map, without } from 'lodash'; +import { subscribeSetFocusOnTitle } from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -31,6 +32,8 @@ class Editor extends Component { this.getEditorSettings = memize( this.getEditorSettings, { maxSize: 1, } ); + + this.setTitleRef = this.setTitleRef.bind( this ); } getEditorSettings( @@ -66,6 +69,24 @@ class Editor extends Component { return settings; } + componentDidMount() { + this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { + if ( this.postTitleRef ) { + this.postTitleRef.focus(); + } + } ); + } + + componentWillUnmount() { + if ( this.subscriptionParentSetFocusOnTitle ) { + this.subscriptionParentSetFocusOnTitle.remove(); + } + } + + setTitleRef( titleRef ) { + this.postTitleRef = titleRef; + } + render() { const { settings, @@ -97,7 +118,9 @@ class Editor extends Component { // For now, let's assume: serialize( parse( html ) ) !== html raw: serialize( parse( props.initialHtml || '' ) ), }, - type: 'draft', + type: 'post', + status: 'draft', + meta: [], }; return ( diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index cedd6bf8c94b37..a48884c52e9ea0 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -3,6 +3,8 @@ */ import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + /** * WordPress dependencies */ @@ -28,9 +30,14 @@ describe( 'Editor', () => { beforeAll( registerCoreBlocks ); it( 'detects unsupported block and sends hasUnsupportedBlocks true to native', () => { + jest.useFakeTimers(); RNReactNativeGutenbergBridge.editorDidMount = jest.fn(); const appContainer = renderEditorWith( unsupportedBlock ); + // for some reason resetEditorBlocks() is asynchronous when dispatching editEntityRecord + act( () => { + jest.runAllTicks(); + } ); appContainer.unmount(); expect( RNReactNativeGutenbergBridge.editorDidMount ).toHaveBeenCalledTimes( 1 ); diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 69035455d49f13..45156604efe6f4 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -1,5 +1,6 @@ // Post Related Components +export { default as AutosaveMonitor } from './autosave-monitor'; export { default as PostTitle } from './post-title'; export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index ce845b8fb630f6..a47de19cfaa1c3 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { RichText } from '@wordpress/rich-text'; +import { __experimentalRichText as RichText } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withDispatch, withSelect } from '@wordpress/data'; import { withFocusOutside } from '@wordpress/components'; diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index d19da119ef0ba5..7835d9650781c1 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -5,7 +5,6 @@ import RNReactNativeGutenbergBridge, { subscribeParentGetHtml, subscribeParentToggleHTMLMode, subscribeUpdateHtml, - subscribeSetFocusOnTitle, subscribeSetTitle, } from 'react-native-gutenberg-bridge'; @@ -17,19 +16,35 @@ import { parse, serialize, getUnregisteredTypeHandlerName } from '@wordpress/blo import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +const postTypeEntities = [ + { name: 'post', baseURL: '/wp/v2/posts' }, + { name: 'page', baseURL: '/wp/v2/pages' }, + { name: 'attachment', baseURL: '/wp/v2/media' }, + { name: 'wp_block', baseURL: '/wp/v2/blocks' }, +].map( ( postTypeEntity ) => ( { + kind: 'postType', + ...postTypeEntity, + transientEdits: { + blocks: true, + }, + mergedEdits: { + meta: true, + }, +} ) ); + /** * Internal dependencies */ import EditorProvider from './index.js'; class NativeEditorProvider extends Component { - constructor( props ) { + constructor() { super( ...arguments ); // Keep a local reference to `post` to detect changes - this.post = props.post; - - this.setTitleRef = this.setTitleRef.bind( this ); + this.post = this.props.post; + this.props.addEntities( postTypeEntities ); + this.props.receiveEntityRecords( 'postType', this.post.type, this.post ); } componentDidMount() { @@ -48,12 +63,6 @@ class NativeEditorProvider extends Component { this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => { this.updateHtmlAction( payload.html ); } ); - - this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { - if ( this.postTitleRef ) { - this.postTitleRef.focus(); - } - } ); } componentWillUnmount() { @@ -72,10 +81,6 @@ class NativeEditorProvider extends Component { if ( this.subscriptionParentUpdateHtml ) { this.subscriptionParentUpdateHtml.remove(); } - - if ( this.subscriptionParentSetFocusOnTitle ) { - this.subscriptionParentSetFocusOnTitle.remove(); - } } componentDidUpdate( prevProps ) { @@ -87,10 +92,6 @@ class NativeEditorProvider extends Component { } } - setTitleRef( titleRef ) { - this.postTitleRef = titleRef; - } - serializeToNativeAction() { if ( this.props.mode === 'text' ) { this.updateHtmlAction( this.props.getEditedPostContent() ); @@ -169,12 +170,18 @@ export default compose( [ const { switchEditorMode, } = dispatch( 'core/edit-post' ); + const { + addEntities, + receiveEntityRecords, + } = dispatch( 'core' ); return { + addEntities, clearSelectedBlock, editTitle( title ) { editPost( { title } ); }, + receiveEntityRecords, resetEditorBlocksWithoutUndoLevel( blocks ) { resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, diff --git a/packages/editor/src/store/actions.native.js b/packages/editor/src/store/actions.native.js index 51741ac9bc3b44..17733a0e47b408 100644 --- a/packages/editor/src/store/actions.native.js +++ b/packages/editor/src/store/actions.native.js @@ -1,3 +1,7 @@ +/** + * External dependencies + */ +import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; export * from './actions.js'; @@ -14,3 +18,12 @@ export function togglePostTitleSelection( isSelected = true ) { isSelected, }; } + +/** + * Action generator used in signalling that the post should autosave. + * + * @param {Object?} options Extra flags to identify the autosave. + */ +export function* autosave( ) { + RNReactNativeGutenbergBridge.editorDidAutosave(); +} diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index 82b3689a98ead0..a0f0be9c3cb052 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -12,27 +12,29 @@ import { combineReducers } from '@wordpress/data'; * Internal dependencies */ import { - editor, - initialEdits, - currentPost, + postId, + postType, preferences, saving, postLock, + postSavingLock, reusableBlocks, template, - previewLink, - postSavingLock, isReady, editorSettings, } from './reducer.js'; +import { EDITOR_SETTINGS_DEFAULTS } from './defaults.js'; + +EDITOR_SETTINGS_DEFAULTS.autosaveInterval = 0; // This is a way to override default behavior on mobile, and make it ping the native save at each keystroke + export * from './reducer.js'; /** * Reducer returning the post title state. * - * @param {PostTitleState} state Current state. - * @param {Object} action Dispatched action. + * @param {Object} state Current state. + * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ @@ -48,17 +50,15 @@ export const postTitle = combineReducers( { } ); export default optimist( combineReducers( { - editor, - initialEdits, - currentPost, + postId, + postType, + postTitle, preferences, saving, postLock, + postSavingLock, reusableBlocks, template, - previewLink, - postSavingLock, isReady, editorSettings, - postTitle, } ) ); diff --git a/packages/editor/src/store/selectors.native.js b/packages/editor/src/store/selectors.native.js index 8c6ead8e97ba2f..ae84e7b5afe5a1 100644 --- a/packages/editor/src/store/selectors.native.js +++ b/packages/editor/src/store/selectors.native.js @@ -1,3 +1,16 @@ +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + isEditedPostDirty, + isEditedPostSaveable, + hasChangedContent, +} from './selectors.js'; export * from './selectors.js'; @@ -11,3 +24,32 @@ export * from './selectors.js'; export function isPostTitleSelected( state ) { return state.postTitle.isSelected; } + +/** + * Returns true if the post can be autosaved, or false otherwise. + * + * @param {Object} state Global application state. + * @param {Object} autosave A raw autosave object from the REST API. + * + * @return {boolean} Whether the post can be autosaved. + */ +export const isEditedPostAutosaveable = createRegistrySelector( ( ) => function( state ) { + // A post must contain a title, an excerpt, or non-empty content to be valid for autosaving. + if ( ! isEditedPostSaveable( state ) ) { + return false; + } + + // To avoid an expensive content serialization, use the content dirtiness + // flag in place of content field comparison against the known autosave. + // This is not strictly accurate, and relies on a tolerance toward autosave + // request failures for unnecessary saves. + if ( hasChangedContent( state ) ) { + return true; + } + + if ( isEditedPostDirty( state ) ) { + return true; + } + + return false; +} ); diff --git a/packages/format-library/src/link/test/modal.native.js b/packages/format-library/src/link/test/modal.native.js index 03594a4ef1c380..9d84f9f903ffbe 100644 --- a/packages/format-library/src/link/test/modal.native.js +++ b/packages/format-library/src/link/test/modal.native.js @@ -23,7 +23,7 @@ describe( 'LinksUI', () => { onRemove={ onRemove } onClose={ jest.fn() } /> - ).dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI + ).dive().dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI // When @@ -52,8 +52,9 @@ describe( 'LinksUI', () => { // When // Simulate user typing on the URL Cell. - const bottomSheet = wrapper.find( 'BottomSheet' ).first(); - const cell = bottomSheet.find( 'BottomSheetCell' ).first(); + const bottomSheet = wrapper.dive().find( 'BottomSheet' ).first(); + const cell = bottomSheet.dive().find( 'WithPreferredColorScheme(BottomSheetCell)' ).first().dive(); + cell.simulate( 'changeValue', 'wordpress.com' ); // Close the BottomSheet diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index fe705d8ad5b386..206680d3c87668 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -14,7 +14,7 @@ import memize from 'memize'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { compose } from '@wordpress/compose'; +import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; import { childrenBlock } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; @@ -770,25 +770,28 @@ export class RichText extends Component { style, __unstableIsSelected: isSelected, children, + getStylesFromColorScheme, } = this.props; const record = this.getRecord(); const html = this.getHtmlToRender( record, tagName ); - let minHeight = styles[ 'rich-text' ].minHeight; + let minHeight = styles.richText.minHeight; if ( style && style.minHeight ) { minHeight = style.minHeight; } + const placeholderStyle = getStylesFromColorScheme( styles.richTextPlaceholder, styles.richTextPlaceholderDark ); + const { color: defaultPlaceholderTextColor, - } = styles[ 'rich-text-placeholder' ]; + } = placeholderStyle; const { color: defaultColor, textDecorationColor: defaultTextDecorationColor, fontFamily: defaultFontFamily, - } = styles[ 'rich-text' ]; + } = getStylesFromColorScheme( styles.richText, styles.richTextDark ); let selection = null; if ( this.needsSelectionUpdate ) { @@ -817,6 +820,8 @@ export class RichText extends Component { this.firedAfterTextChanged = false; } + const dynamicStyle = getStylesFromColorScheme( style, styles.richTextDark ); + return ( <View> { children && children( { @@ -833,7 +838,7 @@ export class RichText extends Component { } } } style={ { - ...style, + ...dynamicStyle, minHeight: Math.max( minHeight, this.state.height ), } } text={ { text: html, eventCount: this.lastEventCount, selection } } @@ -878,4 +883,5 @@ export default compose( [ withSelect( ( select ) => ( { formatTypes: select( 'core/rich-text' ).getFormatTypes(), } ) ), + withPreferredColorScheme, ] )( RichText ); diff --git a/packages/rich-text/src/component/style.native.scss b/packages/rich-text/src/component/style.native.scss index 6481c415694127..4ed93f7f70239d 100644 --- a/packages/rich-text/src/component/style.native.scss +++ b/packages/rich-text/src/component/style.native.scss @@ -1,11 +1,21 @@ -.rich-text { +.richText { font-family: $default-regular-font; min-height: $min-height-paragraph; color: $gray-900; text-decoration-color: $blue-500; } -.rich-text-placeholder { +.richTextDark { + color: $white; + text-decoration-color: $blue-30; + background-color: $black; +} + +.richTextPlaceholder { color: $gray; } + +.richTextPlaceholderDark { + color: $gray-50; +} diff --git a/packages/rich-text/src/component/test/index.native.js b/packages/rich-text/src/component/test/index.native.js index ec0cbb77195244..6b2bc12f855ffa 100644 --- a/packages/rich-text/src/component/test/index.native.js +++ b/packages/rich-text/src/component/test/index.native.js @@ -8,6 +8,10 @@ import { shallow } from 'enzyme'; */ import { RichText } from '../index'; +const getStylesFromColorScheme = () => { + return { color: 'white' }; +}; + describe( 'RichText Native', () => { let richText; @@ -40,6 +44,7 @@ describe( 'RichText Native', () => { } } formatTypes={ [] } onSelectionChange={ jest.fn() } + getStylesFromColorScheme={ getStylesFromColorScheme } /> ); const event = { diff --git a/test/native/__mocks__/react-native-dark-mode/index.js b/test/native/__mocks__/react-native-dark-mode/index.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/native/__mocks__/styleMock.js b/test/native/__mocks__/styleMock.js index 182af41388dc0a..4e63b494feff94 100644 --- a/test/native/__mocks__/styleMock.js +++ b/test/native/__mocks__/styleMock.js @@ -15,7 +15,7 @@ module.exports = { blockCode: { fontFamily: 'serif', }, - 'rich-text': { + richText: { fontFamily: 'serif', minHeight: 30, }, @@ -66,4 +66,13 @@ module.exports = { iconUploading: { fill: 'gray', }, + placeholder: { + color: 'gray', + }, + richTextPlaceholder: { + color: 'gray', + }, + unsupportedBlockIcon: { + color: 'white', + }, }; diff --git a/test/native/setup.js b/test/native/setup.js index cfd2417a11ad9d..60fe7194c0aa60 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -14,6 +14,7 @@ jest.mock( 'react-native-gutenberg-bridge', () => { subscribeUpdateHtml: jest.fn(), subscribeMediaAppend: jest.fn(), editorDidMount: jest.fn(), + editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), requestMediaPickFromMediaLibrary: jest.fn(), requestMediaPickFromDeviceLibrary: jest.fn(), @@ -21,6 +22,16 @@ jest.mock( 'react-native-gutenberg-bridge', () => { }; } ); +jest.mock( 'react-native-dark-mode', () => { + return { + initialMode: 'light', + eventEmitter: { + on: jest.fn(), + }, + useDarkModeContext: () => 'light', + }; +} ); + jest.mock( 'react-native-modal', () => () => 'Modal' ); jest.mock( 'react-native-hr', () => () => 'Hr' );