diff --git a/packages/block-editor/src/components/block-settings/container.native.js b/packages/block-editor/src/components/block-settings/container.native.js index 3bbf64ac87d41..4ceb4fab58778 100644 --- a/packages/block-editor/src/components/block-settings/container.native.js +++ b/packages/block-editor/src/components/block-settings/container.native.js @@ -5,6 +5,7 @@ import { InspectorControls } from '@wordpress/block-editor'; import { BottomSheet, ColorSettings, + FocalPointSettings, LinkPickerScreen, } from '@wordpress/components'; import { compose } from '@wordpress/compose'; @@ -18,6 +19,7 @@ import { store as blockEditorStore } from '../../store'; export const blockSettingsScreens = { settings: 'Settings', color: 'Color', + focalPoint: 'FocalPoint', linkPicker: 'linkPicker', }; @@ -47,6 +49,12 @@ function BottomSheetSettings( { > + + + { return { ...option, + requiresModal: true, types: allowedTypes, id: option.value, }; @@ -60,6 +61,7 @@ export class MediaUpload extends Component { id: mediaSources.deviceCamera, // ID is the value sent to native value: mediaSources.deviceCamera + '-IMAGE', // This is needed to diferenciate image-camera from video-camera sources. label: __( 'Take a Photo' ), + requiresModal: true, types: [ MEDIA_TYPE_IMAGE ], icon: capturePhoto, }; @@ -68,6 +70,7 @@ export class MediaUpload extends Component { id: mediaSources.deviceCamera, value: mediaSources.deviceCamera, label: __( 'Take a Video' ), + requiresModal: true, types: [ MEDIA_TYPE_VIDEO ], icon: captureVideo, }; @@ -76,6 +79,7 @@ export class MediaUpload extends Component { id: mediaSources.deviceLibrary, value: mediaSources.deviceLibrary, label: __( 'Choose from device' ), + requiresModal: true, types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ], icon: image, }; @@ -84,6 +88,7 @@ export class MediaUpload extends Component { id: mediaSources.siteMediaLibrary, value: mediaSources.siteMediaLibrary, label: __( 'WordPress Media Library' ), + requiresModal: true, types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, diff --git a/packages/block-library/src/cover/controls.native.js b/packages/block-library/src/cover/controls.native.js new file mode 100644 index 0000000000000..cf536cd2d32e9 --- /dev/null +++ b/packages/block-library/src/cover/controls.native.js @@ -0,0 +1,303 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; +import Video from 'react-native-video'; + +/** + * WordPress dependencies + */ +import { + Image, + Icon, + IMAGE_DEFAULT_FOCAL_POINT, + PanelBody, + RangeControl, + UnitControl, + TextControl, + BottomSheet, + ToggleControl, +} from '@wordpress/components'; +import { plus } from '@wordpress/icons'; +import { useState, useCallback, useRef } from '@wordpress/element'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { InspectorControls, MediaUpload } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; +import OverlayColorSettings from './overlay-color-settings'; +import FocalPointSettings from './focal-point-settings'; +import { + ALLOWED_MEDIA_TYPES, + COVER_MIN_HEIGHT, + COVER_MAX_HEIGHT, + COVER_DEFAULT_HEIGHT, + CSS_UNITS, + IMAGE_BACKGROUND_TYPE, + VIDEO_BACKGROUND_TYPE, +} from './shared'; + +function Controls( { + attributes, + didUploadFail, + hasOnlyColorBackground, + isUploadInProgress, + onClearMedia, + onSelectMedia, + setAttributes, +} ) { + const { + backgroundType, + dimRatio, + hasParallax, + focalPoint, + minHeight, + minHeightUnit = 'px', + url, + } = attributes; + const CONTAINER_HEIGHT = minHeight || COVER_DEFAULT_HEIGHT; + const onHeightChange = useCallback( + ( value ) => { + if ( minHeight || value !== COVER_DEFAULT_HEIGHT ) { + setAttributes( { minHeight: value } ); + } + }, + [ minHeight ] + ); + + const onOpacityChange = useCallback( ( value ) => { + setAttributes( { dimRatio: value } ); + }, [] ); + + const onChangeUnit = useCallback( ( nextUnit ) => { + setAttributes( { + minHeightUnit: nextUnit, + minHeight: + nextUnit === 'px' + ? Math.max( CONTAINER_HEIGHT, COVER_MIN_HEIGHT ) + : CONTAINER_HEIGHT, + } ); + }, [] ); + + const [ displayPlaceholder, setDisplayPlaceholder ] = useState( true ); + + function setFocalPoint( value ) { + setAttributes( { focalPoint: value } ); + } + + const toggleParallax = () => { + setAttributes( { + hasParallax: ! hasParallax, + ...( ! hasParallax + ? { focalPoint: undefined } + : { focalPoint: IMAGE_DEFAULT_FOCAL_POINT } ), + } ); + }; + + const addMediaButtonStyle = usePreferredColorSchemeStyle( + styles.addMediaButton, + styles.addMediaButtonDark + ); + + function focalPointPosition( { x, y } = IMAGE_DEFAULT_FOCAL_POINT ) { + return { + left: `${ ( hasParallax ? 0.5 : x ) * 100 }%`, + top: `${ ( hasParallax ? 0.5 : y ) * 100 }%`, + }; + } + + const [ videoNaturalSize, setVideoNaturalSize ] = useState( null ); + const videoRef = useRef( null ); + + const mediaBackground = usePreferredColorSchemeStyle( + styles.mediaBackground, + styles.mediaBackgroundDark + ); + const imagePreviewStyles = [ + displayPlaceholder && styles.imagePlaceholder, + ]; + const videoPreviewStyles = [ + { + aspectRatio: + videoNaturalSize && + videoNaturalSize.width / videoNaturalSize.height, + // Hide Video component since it has black background while loading the source + opacity: displayPlaceholder ? 0 : 1, + }, + styles.video, + displayPlaceholder && styles.imagePlaceholder, + ]; + + const focalPointHint = ! hasParallax && ! displayPlaceholder && ( + + ); + + const renderMediaSection = ( { + open: openMediaOptions, + getMediaOptions, + } ) => ( + <> + { getMediaOptions() } + { url ? ( + <> + + + { IMAGE_BACKGROUND_TYPE === backgroundType && ( + { + setDisplayPlaceholder( false ); + } } + onSelectMediaUploadOption={ onSelectMedia } + openMediaOptions={ openMediaOptions } + url={ url } + height="100%" + style={ imagePreviewStyles } + width={ styles.image.width } + /> + ) } + { VIDEO_BACKGROUND_TYPE === backgroundType && ( + + + + { IMAGE_BACKGROUND_TYPE === backgroundType && ( + + ) } + + + ) : ( + + ) } + + ); + + return ( + + + + + + + + { url ? ( + + + + ) : null } + + + + + + ); +} + +export default Controls; diff --git a/packages/block-library/src/cover/edit.native.js b/packages/block-library/src/cover/edit.native.js index b4d48d334a5ee..0b71e2d3963df 100644 --- a/packages/block-library/src/cover/edit.native.js +++ b/packages/block-library/src/cover/edit.native.js @@ -6,6 +6,7 @@ import { TouchableWithoutFeedback, InteractionManager, AccessibilityInfo, + Platform, } from 'react-native'; import Video from 'react-native-video'; @@ -24,10 +25,6 @@ import { Image, ImageEditingButton, IMAGE_DEFAULT_FOCAL_POINT, - PanelBody, - RangeControl, - UnitControl, - TextControl, ToolbarButton, ToolbarGroup, Gradient, @@ -41,7 +38,6 @@ import { InnerBlocks, InspectorControls, MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, MediaPlaceholder, MediaUpload, MediaUploadProgress, @@ -61,17 +57,16 @@ import { getProtocol } from '@wordpress/url'; import styles from './style.scss'; import { attributesFromMedia, - COVER_MIN_HEIGHT, + ALLOWED_MEDIA_TYPES, IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE, - CSS_UNITS, + COVER_DEFAULT_HEIGHT, } from './shared'; -import OverlayColorSettings from './overlay-color-settings'; +import Controls from './controls'; /** * Constants */ -const ALLOWED_MEDIA_TYPES = [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ]; const INNER_BLOCKS_TEMPLATE = [ [ 'core/paragraph', @@ -81,8 +76,6 @@ const INNER_BLOCKS_TEMPLATE = [ }, ], ]; -const COVER_MAX_HEIGHT = 1000; -const COVER_DEFAULT_HEIGHT = 300; const Cover = ( { attributes, @@ -112,8 +105,6 @@ const Cover = ( { false ); - const CONTAINER_HEIGHT = minHeight || COVER_DEFAULT_HEIGHT; - useEffect( () => { // sync with local media store mediaUploadSync(); @@ -195,19 +186,6 @@ const Cover = ( { onSelect( media ); }; - const onHeightChange = useCallback( - ( value ) => { - if ( minHeight || value !== COVER_DEFAULT_HEIGHT ) { - setAttributes( { minHeight: value } ); - } - }, - [ minHeight ] - ); - - const onOpacityChange = useCallback( ( value ) => { - setAttributes( { dimRatio: value } ); - }, [] ); - const onMediaPressed = () => { if ( isUploadInProgress ) { requestImageUploadCancelDialog( id ); @@ -229,7 +207,12 @@ const Cover = ( { }; const onClearMedia = useCallback( () => { - setAttributes( { id: undefined, url: undefined } ); + setAttributes( { + focalPoint: undefined, + hasParallax: undefined, + id: undefined, + url: undefined, + } ); closeSettingsBottomSheet(); }, [ closeSettingsBottomSheet ] ); @@ -292,8 +275,18 @@ const Cover = ( { ); + const accessibilityHint = + Platform.OS === 'ios' + ? __( 'Double tap to open Action Sheet to add image or video' ) + : __( 'Double tap to open Bottom Sheet to add image or video' ); + const addMediaButton = () => ( - + ); - const onChangeUnit = useCallback( ( nextUnit ) => { - setAttributes( { - minHeightUnit: nextUnit, - minHeight: - nextUnit === 'px' - ? Math.max( CONTAINER_HEIGHT, COVER_MIN_HEIGHT ) - : CONTAINER_HEIGHT, - } ); - }, [] ); - const onBottomSheetClosed = useCallback( () => { InteractionManager.runAfterInteractions( () => { setCustomColorPickerShowing( false ); } ); }, [] ); - const controls = ( - - - { url ? ( - - - - ) : null } - - - - { url ? ( - - - - ) : null } - - ); - const colorPickerControls = ( @@ -545,32 +479,17 @@ const Cover = ( { return ( - { isSelected && controls } - - { isImage && - url && - openMediaOptionsRef.current && - isParentSelected && - ! isUploadInProgress && - ! didUploadFail && ( - - - - ) } + { isSelected && ( + + ) } + { isImage && + url && + openMediaOptionsRef.current && + isParentSelected && + ! isUploadInProgress && + ! didUploadFail && ( + + + + ) } + { shouldShowFailure && ( { + navigation.navigate( blockSettingsScreens.focalPoint, { + focalPoint, + onFocalPointChange, + url, + } ); + } } + > + { /* + * Wrapper View element used around Icon as workaround for SVG opacity + * issue: https://git.io/JtuXD + */ } + + + + + ); +} + +export default FocalPointSettings; diff --git a/packages/block-library/src/cover/shared.js b/packages/block-library/src/cover/shared.js index a80ce7d28c870..18ac6076be922 100644 --- a/packages/block-library/src/cover/shared.js +++ b/packages/block-library/src/cover/shared.js @@ -4,6 +4,7 @@ import { getBlobTypeByURL, isBlobURL } from '@wordpress/blob'; import { __ } from '@wordpress/i18n'; import { Platform } from '@wordpress/element'; +import { MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/block-editor'; const POSITION_CLASSNAMES = { 'top left': 'is-position-top-left', @@ -21,9 +22,12 @@ const POSITION_CLASSNAMES = { export const IMAGE_BACKGROUND_TYPE = 'image'; export const VIDEO_BACKGROUND_TYPE = 'video'; export const COVER_MIN_HEIGHT = 50; +export const COVER_MAX_HEIGHT = 1000; +export const COVER_DEFAULT_HEIGHT = 300; export function backgroundImageStyles( url ) { return url ? { backgroundImage: `url(${ url })` } : {}; } +export const ALLOWED_MEDIA_TYPES = [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ]; const isWeb = Platform.OS === 'web'; diff --git a/packages/block-library/src/cover/style.native.scss b/packages/block-library/src/cover/style.native.scss index 33fdadffd6822..c132ae60f4f1d 100644 --- a/packages/block-library/src/cover/style.native.scss +++ b/packages/block-library/src/cover/style.native.scss @@ -55,12 +55,12 @@ position: absolute; } -.backgroundSolid { - background-color: $gray-lighten-30; +.mediaBackground { + background-color: $light-ultra-dim; } -.backgroundSolidDark { - background-color: $background-dark-secondary; +.mediaBackgroundDark { + background-color: $dark-ultra-dim; } .uploadFailedContainer { @@ -117,6 +117,14 @@ left: 7px; } +.addMediaButton { + color: $blue-50; +} + +.addMediaButtonDark { + color: $blue-30; +} + .clearMediaButton { color: $alert-red; } @@ -140,6 +148,40 @@ width: 100%; } +.mediaPreview { + flex-direction: row; + justify-content: center; +} + +.mediaInner { + max-height: 150px; + position: relative; +} + +.imagePlaceholder { + min-width: 100%; +} + +.video { + height: 100%; + max-width: 100%; +} + +.focalPointHint { + box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.5); + fill: $white; + left: 50%; + margin: -16px 0 0 -16px; + opacity: 0.8; + position: absolute; + top: 50%; + width: 32px; +} + +.dimmedActionButton { + opacity: 0.45; +} + .colorPaletteWrapper { min-height: 50px; } diff --git a/packages/components/src/focal-point-picker/focal-point.native.js b/packages/components/src/focal-point-picker/focal-point.native.js new file mode 100644 index 0000000000000..a5a7a92c298ec --- /dev/null +++ b/packages/components/src/focal-point-picker/focal-point.native.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/primitives'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +export default function FocalPoint( { height, style, width } ) { + return ( + + + + + ); +} diff --git a/packages/components/src/focal-point-picker/index.native.js b/packages/components/src/focal-point-picker/index.native.js new file mode 100644 index 0000000000000..f48701a57a482 --- /dev/null +++ b/packages/components/src/focal-point-picker/index.native.js @@ -0,0 +1,279 @@ +/** + * External dependencies + */ +import { Animated, PanResponder, View } from 'react-native'; +import Video from 'react-native-video'; +import { clamp } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + requestFocalPointPickerTooltipShown, + setFocalPointPickerTooltipShown, +} from '@wordpress/react-native-bridge'; +import { __ } from '@wordpress/i18n'; +import { Image, UnitControl } from '@wordpress/components'; +import { useRef, useState, useMemo, useEffect } from '@wordpress/element'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import FocalPoint from './focal-point'; +import Tooltip from './tooltip'; +import styles from './style.scss'; +import { isVideoType } from './utils'; + +const MIN_POSITION_VALUE = 0; +const MAX_POSITION_VALUE = 100; +const FOCAL_POINT_UNITS = [ { default: '50', label: '%', value: '%' } ]; + +function FocalPointPicker( props ) { + const { focalPoint, onChange, shouldEnableBottomSheetScroll, url } = props; + + const isVideo = isVideoType( url ); + + const [ containerSize, setContainerSize ] = useState( null ); + const [ sliderKey, setSliderKey ] = useState( 0 ); + const [ displayPlaceholder, setDisplayPlaceholder ] = useState( true ); + const [ videoNaturalSize, setVideoNaturalSize ] = useState( null ); + const [ tooltipVisible, setTooltipVisible ] = useState( false ); + + let locationPageOffsetX = useRef().current; + let locationPageOffsetY = useRef().current; + const videoRef = useRef( null ); + + useEffect( () => { + requestFocalPointPickerTooltipShown( ( tooltipShown ) => { + if ( ! tooltipShown ) { + setTooltipVisible( true ); + setFocalPointPickerTooltipShown( true ); + } + } ); + }, [] ); + + // Animated coordinates for drag handle + const pan = useRef( new Animated.ValueXY() ).current; + + /** + * Set drag handle position anytime focal point coordinates change. + * E.g. initial render, dragging range sliders. + */ + useEffect( () => { + if ( containerSize ) { + pan.setValue( { + x: focalPoint.x * containerSize.width, + y: focalPoint.y * containerSize.height, + } ); + } + }, [ focalPoint, containerSize ] ); + + // Pan responder to manage drag handle interactivity + const panResponder = useMemo( + () => + PanResponder.create( { + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + + onPanResponderGrant: ( event ) => { + shouldEnableBottomSheetScroll( false ); + const { + locationX: x, + locationY: y, + pageX, + pageY, + } = event.nativeEvent; + locationPageOffsetX = pageX - x; + locationPageOffsetY = pageY - y; + pan.setValue( { x, y } ); // Set cursor to tap location + pan.extractOffset(); // Set offset to current value + }, + // Move cursor to match delta drag + onPanResponderMove: Animated.event( [ + null, + { dx: pan.x, dy: pan.y }, + ] ), + onPanResponderRelease: ( event ) => { + shouldEnableBottomSheetScroll( true ); + pan.flattenOffset(); // Flatten offset into value + const { pageX, pageY } = event.nativeEvent; + // Ideally, x and y below are merely locationX and locationY from the + // nativeEvent. However, we are required to compute these relative + // coordinates to workaround a bug affecting Android's PanResponder. + // Specifically, dragging the handle outside the bounds of the image + // results in inaccurate locationX and locationY coordinates to be + // reported. https://git.io/JtWmi + const x = pageX - locationPageOffsetX; + const y = pageY - locationPageOffsetY; + onChange( { + x: clamp( x / containerSize?.width, 0, 1 ).toFixed( 2 ), + y: clamp( y / containerSize?.height, 0, 1 ).toFixed( + 2 + ), + } ); + // Slider (child of RangeCell) is uncontrolled, so we must increment a + // key to re-mount and sync the pan gesture values to the sliders + // https://git.io/JTe4A + setSliderKey( ( prevState ) => prevState + 1 ); + }, + } ), + [ containerSize ] + ); + + const mediaBackground = usePreferredColorSchemeStyle( + styles.mediaBackground, + styles.mediaBackgroundDark + ); + const imagePreviewStyles = [ + displayPlaceholder && styles.mediaPlaceholder, + styles.image, + ]; + const videoPreviewStyles = [ + { + aspectRatio: + videoNaturalSize && + videoNaturalSize.width / videoNaturalSize.height, + // Hide Video component since it has black background while loading the source + opacity: displayPlaceholder ? 0 : 1, + }, + styles.video, + displayPlaceholder && styles.mediaPlaceholder, + ]; + const focalPointGroupStyles = [ + styles.focalPointGroup, + { + transform: [ + { + translateX: pan.x.interpolate( { + inputRange: [ 0, containerSize?.width || 0 ], + outputRange: [ 0, containerSize?.width || 0 ], + extrapolate: 'clamp', + } ), + }, + { + translateY: pan.y.interpolate( { + inputRange: [ 0, containerSize?.height || 0 ], + outputRange: [ 0, containerSize?.height || 0 ], + extrapolate: 'clamp', + } ), + }, + ], + }, + ]; + const FOCAL_POINT_SIZE = 50; + const focalPointStyles = [ + styles.focalPoint, + { + height: FOCAL_POINT_SIZE, + marginLeft: -( FOCAL_POINT_SIZE / 2 ), + marginTop: -( FOCAL_POINT_SIZE / 2 ), + width: FOCAL_POINT_SIZE, + }, + ]; + + const onTooltipPress = () => setTooltipVisible( false ); + const onMediaLayout = ( event ) => { + const { height, width } = event.nativeEvent.layout; + + if ( + width !== 0 && + height !== 0 && + ( containerSize?.width !== width || + containerSize?.height !== height ) + ) { + setContainerSize( { width, height } ); + } + }; + const onImageDataLoad = () => setDisplayPlaceholder( false ); + const onVideoLoad = ( event ) => { + const { height, width } = event.naturalSize; + setVideoNaturalSize( { height, width } ); + setDisplayPlaceholder( false ); + // Avoid invisible, paused video on Android, presumably related to + // https://git.io/Jt6Dr + videoRef?.current.seek( 0 ); + }; + const onXCoordinateChange = ( x ) => + onChange( { x: ( x / 100 ).toFixed( 2 ) } ); + const onYCoordinateChange = ( y ) => + onChange( { y: ( y / 100 ).toFixed( 2 ) } ); + + return ( + + + + + { ! isVideo && ( + + ) } + { isVideo && ( + + + + + + + ); +} + +export default FocalPointPicker; diff --git a/packages/components/src/focal-point-picker/style.scss b/packages/components/src/focal-point-picker/style.scss new file mode 100644 index 0000000000000..826f1f88d82d9 --- /dev/null +++ b/packages/components/src/focal-point-picker/style.scss @@ -0,0 +1,56 @@ +.container { + padding: 0 16px; +} + +.mediaBackground { + background-color: $light-ultra-dim; +} + +.mediaBackgroundDark { + background-color: $dark-ultra-dim; +} + +.media { + flex-direction: row; + justify-content: center; +} + +.mediaContainer { + position: relative; + height: 100%; + max-height: 200px; +} + +.mediaPlaceholder { + min-width: 100%; +} + +.image { + max-width: 100%; +} + +.video { + height: 100%; + max-width: 100%; +} + +.focalPointGroup { + left: 0; + position: absolute; + top: 0; +} + +.focalPoint { + position: absolute; + top: 0; + left: 0; + opacity: 0.8; +} + +.focalPointIconPathOutline { + fill: $white; +} + +.focalPointIconPathFill { + fill: $blue-wordpress; +} diff --git a/packages/components/src/focal-point-picker/tooltip/index.native.js b/packages/components/src/focal-point-picker/tooltip/index.native.js new file mode 100644 index 0000000000000..8938233a0baf4 --- /dev/null +++ b/packages/components/src/focal-point-picker/tooltip/index.native.js @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import { Animated, Easing, PanResponder, Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { + createContext, + useEffect, + useRef, + useState, + useContext, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +const TooltipContext = createContext(); + +function Tooltip( { children, onPress, style, visible } ) { + const panResponder = useRef( + PanResponder.create( { + /** + * To allow dimissing the tooltip on press while also avoiding blocking + * interactivity within the child context, we place this `onPress` side + * effect within the `onStartShouldSetPanResponderCapture` callback. + * + * This is a bit unorthodox, but may be the simplest approach to achieving + * this outcome. This is effectively a gesture responder that never + * becomes the controlling responder. https://bit.ly/2J3ugKF + */ + onStartShouldSetPanResponderCapture: () => { + if ( onPress ) { + onPress(); + } + return false; + }, + } ) + ).current; + + return ( + + + { children } + + + ); +} + +function Label( { align, text, xOffset, yOffset } ) { + const animationValue = useRef( new Animated.Value( 0 ) ).current; + const [ dimensions, setDimensions ] = useState( null ); + const visible = useContext( TooltipContext ); + + if ( typeof visible === 'undefined' ) { + throw new Error( + 'Tooltip.Label cannot be rendered outside of the Tooltip component' + ); + } + + useEffect( () => { + startAnimation(); + }, [ visible ] ); + + const startAnimation = () => { + Animated.timing( animationValue, { + toValue: visible ? 1 : 0, + duration: visible ? 300 : 150, + useNativeDriver: true, + delay: visible ? 500 : 0, + easing: Easing.out( Easing.quad ), + } ).start(); + }; + + // Transforms rely upon onLayout to enable custom offsets additions + let tooltipTransforms; + if ( dimensions ) { + tooltipTransforms = [ + { + translateX: + ( align === 'center' ? -dimensions.width / 2 : 0 ) + + xOffset, + }, + { translateY: -dimensions.height + yOffset }, + ]; + } + + const tooltipStyles = [ + styles.tooltip, + { + shadowColor: styles.tooltipShadow.color, + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 2, + elevation: 2, + transform: tooltipTransforms, + }, + align === 'left' && styles.tooltipLeftAlign, + ]; + const arrowStyles = [ + styles.arrow, + align === 'left' && styles.arrowLeftAlign, + ]; + + return ( + + { + const { height, width } = nativeEvent.layout; + setDimensions( { height, width } ); + } } + style={ tooltipStyles } + > + { text } + + + + ); +} + +Label.defaultProps = { + align: 'center', + xOffset: 0, + yOffset: 0, +}; + +Tooltip.Label = Label; + +export default Tooltip; diff --git a/packages/components/src/focal-point-picker/tooltip/style.native.scss b/packages/components/src/focal-point-picker/tooltip/style.native.scss new file mode 100644 index 0000000000000..427dcece51758 --- /dev/null +++ b/packages/components/src/focal-point-picker/tooltip/style.native.scss @@ -0,0 +1,42 @@ +$tooltipColor: #121212; +$tooltipArrowSize: 6px; +$tooltipArrowOffset: -($tooltipArrowSize - 1); + +.tooltip { + background-color: $tooltipColor; + border-radius: 4px; + left: 50%; + position: absolute; + top: $tooltipArrowOffset; + z-index: 1; +} + +.tooltipLeftAlign { + left: 0; +} + +.tooltipShadow { + color: $black; +} + +.text { + color: $white; + padding: 12px 16px; +} + +.arrow { + border-color: $tooltipColor transparent transparent transparent; + border-style: solid; + border-width: $tooltipArrowSize $tooltipArrowSize 0 $tooltipArrowSize; + bottom: $tooltipArrowOffset; + height: 0; + left: 50%; + margin-left: -$tooltipArrowSize; + position: absolute; + top: 100%; + width: 0; +} + +.arrowLeftAlign { + left: 16px; +} diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 0fee6fc26bad9..45c47fbb6764c 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -14,6 +14,7 @@ export { default as ColorPicker } from './color-picker'; export { default as Dashicon } from './dashicon'; export { default as Dropdown } from './dropdown'; export { default as DropdownMenu } from './dropdown-menu'; +export { default as FocalPointPicker } from './focal-point-picker'; export { default as Toolbar } from './toolbar'; export { default as ToolbarButton } from './toolbar-button'; export { default as __experimentalToolbarContext } from './toolbar-context'; @@ -73,6 +74,7 @@ export { default as ReadableContentView } from './mobile/readable-content-view'; export { default as CycleSelectControl } from './mobile/cycle-select-control'; export { default as Gradient } from './mobile/gradient'; export { default as ColorSettings } from './mobile/color-settings'; +export { default as FocalPointSettings } from './mobile/focal-point-settings'; export { LinkPicker } from './mobile/link-picker'; export { default as LinkPickerScreen } from './mobile/link-picker/link-picker-screen'; export { default as LinkSettings } from './mobile/link-settings'; diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index 16dc36a12efdf..adf40c4d81881 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -91,6 +91,7 @@ class BottomSheetCell extends Component { disabled = false, activeOpacity, onPress, + onLongPress, label, value, valuePlaceholder = '', @@ -301,6 +302,7 @@ class BottomSheetCell extends Component { disabled={ disabled } activeOpacity={ opacity } onPress={ onCellPress } + onLongPress={ onLongPress } style={ [ styles.clipToBounds, style ] } borderless={ borderless } > diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index c937aad9773e4..63b7ea41f1436 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -48,6 +48,7 @@ class BottomSheet extends Component { this.onScroll = this.onScroll.bind( this ); this.isScrolling = this.isScrolling.bind( this ); this.onShouldEnableScroll = this.onShouldEnableScroll.bind( this ); + this.onDismiss = this.onDismiss.bind( this ); this.onShouldSetBottomSheetMaxHeight = this.onShouldSetBottomSheetMaxHeight.bind( this ); @@ -211,6 +212,16 @@ class BottomSheet extends Component { } } + onDismiss() { + const { onDismiss } = this.props; + + if ( onDismiss ) { + onDismiss(); + } + + this.onCloseBottomSheet(); + } + onShouldEnableScroll( value ) { this.setState( { scrollEnabled: value } ); } @@ -236,6 +247,7 @@ class BottomSheet extends Component { const { handleClosingBottomSheet } = this.state; if ( handleClosingBottomSheet ) { handleClosingBottomSheet(); + this.onHandleClosingBottomSheet( null ); } if ( onClose ) { onClose(); @@ -283,7 +295,6 @@ class BottomSheet extends Component { style = {}, contentStyle = {}, getStylesFromColorScheme, - onDismiss, children, withHeaderSeparator = false, hasNavigation, @@ -368,6 +379,7 @@ class BottomSheet extends Component { { withHeaderSeparator && } ); + return ( { + const navigation = useNavigation(); + + function onButtonPress( action ) { + navigation.goBack(); + if ( action === 'apply' ) { + onFocalPointChange( draftFocalPoint ); + } + } + + const [ draftFocalPoint, setDraftFocalPoint ] = useState( focalPoint ); + function setPosition( coordinates ) { + setDraftFocalPoint( ( prevState ) => ( { + ...prevState, + ...coordinates, + } ) ); + } + + return ( + + onButtonPress( 'cancel' ) } + applyButtonOnPress={ () => onButtonPress( 'apply' ) } + isFullscreen + /> + + + ); + } +); + +function FocalPointSettings( props ) { + const route = useRoute(); + const { shouldEnableBottomSheetScroll } = useContext( BottomSheetContext ); + + return ( + + ); +} + +export default FocalPointSettings; diff --git a/packages/components/src/mobile/focal-point-settings/styles.native.scss b/packages/components/src/mobile/focal-point-settings/styles.native.scss new file mode 100644 index 0000000000000..99b41f9c6f142 --- /dev/null +++ b/packages/components/src/mobile/focal-point-settings/styles.native.scss @@ -0,0 +1,3 @@ +.safearea { + height: 100%; +} diff --git a/packages/components/src/mobile/image/image-editing-button.native.js b/packages/components/src/mobile/image/image-editing-button.native.js index 14314f1c24a70..fccc4fa2bfe33 100644 --- a/packages/components/src/mobile/image/image-editing-button.native.js +++ b/packages/components/src/mobile/image/image-editing-button.native.js @@ -18,8 +18,12 @@ import styles from './style.scss'; const accessibilityHint = Platform.OS === 'ios' - ? __( 'Double tap to open Action Sheet to edit or replace the image' ) - : __( 'Double tap to open Bottom Sheet to edit or replace the image' ); + ? __( + 'Double tap to open Action Sheet to edit, replace, or clear the image' + ) + : __( + 'Double tap to open Bottom Sheet to edit, replace, or clear the image' + ); const ImageEditingButton = ( { onSelectMediaUploadOption, @@ -34,10 +38,10 @@ const ImageEditingButton = ( { openReplaceMediaOptions={ openMediaOptions } render={ ( { open, mediaOptions } ) => ( diff --git a/packages/components/src/mobile/image/index.native.js b/packages/components/src/mobile/image/index.native.js index 0dbdb5753101d..3116426526cba 100644 --- a/packages/components/src/mobile/image/index.native.js +++ b/packages/components/src/mobile/image/index.native.js @@ -34,10 +34,12 @@ const ImageComponent = ( { editButton = true, focalPoint, height: imageHeight, + highlightSelected = true, isSelected, isUploadFailed, isUploadInProgress, mediaPickerOptions, + onImageDataLoad, onSelectMediaUploadOption, openMediaOptions, resizeMode, @@ -45,6 +47,7 @@ const ImageComponent = ( { retryIcon, url, shapeStyle, + style, width: imageWidth, } ) => { const [ imageData, setImageData ] = useState( null ); @@ -53,11 +56,15 @@ const ImageComponent = ( { useEffect( () => { if ( url ) { Image.getSize( url, ( imgWidth, imgHeight ) => { - setImageData( { + const metaData = { aspectRatio: imgWidth / imgHeight, width: imgWidth, height: imgHeight, - } ); + }; + setImageData( metaData ); + if ( onImageDataLoad ) { + onImageDataLoad( metaData ); + } } ); } }, [ url ] ); @@ -166,6 +173,7 @@ const ImageComponent = ( { // to disappear when an aligned image can't be downloaded // https://github.com/wordpress-mobile/gutenberg-mobile/issues/1592 imageData && align && { alignItems: align }, + style, ] } onLayout={ onContainerLayout } > @@ -178,14 +186,16 @@ const ImageComponent = ( { key={ url } style={ imageContainerStyles } > - { isSelected && ! ( isUploadInProgress || isUploadFailed ) && ( - - ) } + { isSelected && + highlightSelected && + ! ( isUploadInProgress || isUploadFailed ) && ( + + ) } { ! imageData ? ( @@ -229,16 +239,16 @@ const ImageComponent = ( { ) } - - { editButton && isSelected && ! isUploadInProgress && ( - - ) } + + { editButton && isSelected && ! isUploadInProgress && ( + + ) } ); }; diff --git a/packages/components/src/mobile/media-edit/index.native.js b/packages/components/src/mobile/media-edit/index.native.js index 1e2ab90cd0b9e..71728dfc73844 100644 --- a/packages/components/src/mobile/media-edit/index.native.js +++ b/packages/components/src/mobile/media-edit/index.native.js @@ -22,6 +22,7 @@ const editOption = { id: MEDIA_EDITOR, value: MEDIA_EDITOR, label: __( 'Edit' ), + requiresModal: true, types: [ MEDIA_TYPE_IMAGE ], }; diff --git a/packages/components/src/mobile/picker/index.ios.js b/packages/components/src/mobile/picker/index.ios.js index b813c5803e8ec..bd99917ec4042 100644 --- a/packages/components/src/mobile/picker/index.ios.js +++ b/packages/components/src/mobile/picker/index.ios.js @@ -7,7 +7,9 @@ import { ActionSheetIOS } from 'react-native'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; +import { Component, forwardRef, useContext } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { BottomSheetContext } from '@wordpress/components'; class Picker extends Component { presentPicker() { @@ -18,6 +20,9 @@ class Picker extends Component { destructiveButtonIndex, disabledButtonIndices, getAnchor, + isBottomSheetOpened, + closeBottomSheet, + onHandleClosingBottomSheet, } = this.props; const labels = options.map( ( { label } ) => label ); const fullOptions = [ __( 'Cancel' ) ].concat( labels ); @@ -36,7 +41,15 @@ class Picker extends Component { return; } const selected = options[ buttonIndex - 1 ]; - onChange( selected.value ); + + if ( selected.requiresModal && isBottomSheetOpened ) { + onHandleClosingBottomSheet( () => { + onChange( selected.value ); + } ); + closeBottomSheet(); + } else { + onChange( selected.value ); + } } ); } @@ -46,4 +59,22 @@ class Picker extends Component { } } -export default Picker; +const PickerComponent = forwardRef( ( props, ref ) => { + const isBottomSheetOpened = useSelect( ( select ) => + select( 'core/edit-post' ).isEditorSidebarOpened() + ); + const { closeGeneralSidebar } = useDispatch( 'core/edit-post' ); + const { onHandleClosingBottomSheet } = useContext( BottomSheetContext ); + + return ( + + ); +} ); + +export default PickerComponent; diff --git a/packages/components/src/unit-control/index.native.js b/packages/components/src/unit-control/index.native.js index c2e7e0d69b7b6..ada1bc77af773 100644 --- a/packages/components/src/unit-control/index.native.js +++ b/packages/components/src/unit-control/index.native.js @@ -16,7 +16,8 @@ import RangeCell from '../mobile/bottom-sheet/range-cell'; import StepperCell from '../mobile/bottom-sheet/stepper-cell'; import Picker from '../mobile/picker'; import styles from './style.scss'; -import { CSS_UNITS } from './utils'; +import { CSS_UNITS, hasUnits } from './utils'; + /** * WordPress dependencies */ @@ -68,18 +69,26 @@ function UnitControl( { : __( 'Double tap to open Bottom Sheet with available options' ); const renderUnitButton = useMemo( () => { - return ( - - - { unit } - - + const unitButton = ( + + { unit } + ); + + if ( hasUnits( units ) ) { + return ( + + { unitButton } + + ); + } + + return unitButton; }, [ onPickerPresent, accessibilityLabel, @@ -100,14 +109,16 @@ function UnitControl( { return ( { renderUnitButton } - + { hasUnits( units ) ? ( + + ) : null } ); }, [ pickerRef, units, onUnitChange, getAnchor ] ); diff --git a/packages/primitives/src/svg/index.native.js b/packages/primitives/src/svg/index.native.js index 4d3bf26acf1ac..a8f6c36fef116 100644 --- a/packages/primitives/src/svg/index.native.js +++ b/packages/primitives/src/svg/index.native.js @@ -44,10 +44,15 @@ export const SVG = ( { const defaultStyle = isPressed ? styles[ 'is-pressed' ] : styles[ 'components-toolbar__control-' + colorScheme ]; + const propStyle = Array.isArray( props.style ) + ? props.style.reduce( ( acc, el ) => { + return { ...acc, ...el }; + }, {} ) + : props.style; const styleValues = Object.assign( {}, defaultStyle, - props.style, + propStyle, ...stylesFromClasses ); diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index adccb29497368..f529f173d7e8f 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -48,6 +48,10 @@ interface ReplaceUnsupportedBlockCallback { void replaceUnsupportedBlock(String content, String blockId); } + interface FocalPointPickerTooltipShownCallback { + void onRequestFocalPointPickerTooltipShown(boolean tooltipShown); + } + // Ref: https://github.com/facebook/react-native/blob/master/Libraries/polyfills/console.js#L376 enum LogLevel { TRACE(0), @@ -172,4 +176,9 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void requestMediaFilesSaveCancelDialog(ReadableArray mediaFiles); void mediaFilesBlockReplaceSync(ReadableArray mediaFiles, String blockId); + + void setFocalPointPickerTooltipShown(boolean tooltipShown); + + void requestFocalPointPickerTooltipShown(FocalPointPickerTooltipShownCallback focalPointPickerTooltipShownCallback); + } diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index eb1732de4616d..d1bd905ce8540 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -21,6 +21,7 @@ import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.GutenbergUserEvent; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaType; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.OtherMediaOptionsReceivedCallback; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FocalPointPickerTooltipShownCallback; import org.wordpress.mobile.WPAndroidGlue.DeferredEventEmitter; import org.wordpress.mobile.WPAndroidGlue.MediaOption; @@ -335,6 +336,25 @@ public void showXpostSuggestions(Promise promise) { mGutenbergBridgeJS2Parent.onShowXpostSuggestions(promise::resolve); } + @ReactMethod + public void setFocalPointPickerTooltipShown(boolean tooltipShown) { + mGutenbergBridgeJS2Parent.setFocalPointPickerTooltipShown(tooltipShown); + } + + @ReactMethod + public void requestFocalPointPickerTooltipShown(final Callback jsCallback) { + FocalPointPickerTooltipShownCallback focalPointPickerTooltipShownCallback = requestFocalPointPickerTooltipShownCallback(jsCallback); + mGutenbergBridgeJS2Parent.requestFocalPointPickerTooltipShown(focalPointPickerTooltipShownCallback); + } + + private FocalPointPickerTooltipShownCallback requestFocalPointPickerTooltipShownCallback(final Callback jsCallback) { + return new FocalPointPickerTooltipShownCallback() { + @Override public void onRequestFocalPointPickerTooltipShown(boolean tooltipShown) { + jsCallback.invoke(tooltipShown); + } + }; + } + private GutenbergBridgeJS2Parent.MediaSelectedCallback getNewMediaSelectedCallback(final Boolean allowMultipleSelection, final Callback jsCallback) { return new GutenbergBridgeJS2Parent.MediaSelectedCallback() { @Override diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 87d3129ef5065..fe658bfd8e1fa 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -93,6 +93,7 @@ public class WPAndroidGlueCode { private OnGutenbergDidSendButtonPressedActionListener mOnGutenbergDidSendButtonPressedActionListener; private ReplaceUnsupportedBlockCallback mReplaceUnsupportedBlockCallback; private OnMediaFilesCollectionBasedBlockEditorListener mOnMediaFilesCollectionBasedBlockEditorListener; + private OnFocalPointPickerTooltipShownEventListener mOnFocalPointPickerTooltipShownListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -199,6 +200,11 @@ public interface OnGutenbergDidSendButtonPressedActionListener { void gutenbergDidSendButtonPressedAction(String buttonType); } + public interface OnFocalPointPickerTooltipShownEventListener { + void onSetFocalPointPickerTooltipShown(boolean tooltipShown); + boolean onRequestFocalPointPickerTooltipShown(); + } + public interface OnContentInfoReceivedListener { void onContentInfoFailed(); void onEditorNotReady(); @@ -467,6 +473,16 @@ public void mediaFilesBlockReplaceSync(ReadableArray mediaFiles, String blockId) ); } + @Override + public void setFocalPointPickerTooltipShown(boolean showTooltip) { + mOnFocalPointPickerTooltipShownListener.onSetFocalPointPickerTooltipShown(showTooltip); + } + + @Override + public void requestFocalPointPickerTooltipShown(FocalPointPickerTooltipShownCallback focalPointPickerTooltipShownCallback) { + boolean tooltipShown = mOnFocalPointPickerTooltipShownListener.onRequestFocalPointPickerTooltipShown(); + focalPointPickerTooltipShownCallback.onRequestFocalPointPickerTooltipShown(tooltipShown); + } }, mIsDarkMode); return Arrays.asList( @@ -543,6 +559,7 @@ public void attachToContainer(ViewGroup viewGroup, OnGutenbergDidSendButtonPressedActionListener onGutenbergDidSendButtonPressedActionListener, ShowSuggestionsUtil showSuggestionsUtil, OnMediaFilesCollectionBasedBlockEditorListener onMediaFilesCollectionBasedBlockEditorListener, + OnFocalPointPickerTooltipShownEventListener onFocalPointPickerTooltipListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -560,6 +577,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnGutenbergDidSendButtonPressedActionListener = onGutenbergDidSendButtonPressedActionListener; mShowSuggestionsUtil = showSuggestionsUtil; mOnMediaFilesCollectionBasedBlockEditorListener = onMediaFilesCollectionBasedBlockEditorListener; + mOnFocalPointPickerTooltipShownListener = onFocalPointPickerTooltipListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -970,4 +988,3 @@ public void updateCapabilities(GutenbergProps gutenbergProps) { mDeferredEventEmitter.updateCapabilities(gutenbergProps); } } - diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 8bf3742ce1634..1c4ee514d148a 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -360,4 +360,16 @@ export function mediaFilesBlockReplaceSync( mediaFiles, blockClientId ) { ); } +export function requestFocalPointPickerTooltipShown( callback ) { + return RNReactNativeGutenbergBridge.requestFocalPointPickerTooltipShown( + callback + ); +} + +export function setFocalPointPickerTooltipShown( tooltipShown ) { + return RNReactNativeGutenbergBridge.setFocalPointPickerTooltipShown( + tooltipShown + ); +} + export default RNReactNativeGutenbergBridge; diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index f054e3a1b09bb..de7f97c25c92a 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -230,6 +230,13 @@ public protocol GutenbergBridgeDelegate: class { /// - Parameter callback: Completion handler to be called with an xpost or an error func gutenbergDidRequestXpost(callback: @escaping (Swift.Result) -> Void) + /// Tells the delegate that the editor requested to show the tooltip + func gutenbergDidRequestFocalPointPickerTooltipShown() -> Bool + + /// Tells the delegate that the editor requested to set the tooltip's visibility + /// - Parameter tooltipShown: Tooltip's visibility value + func gutenbergDidRequestSetFocalPointPickerTooltipShown(_ tooltipShown: Bool) + func gutenbergDidSendButtonPressedAction(_ buttonType: Gutenberg.ActionButtonType) // Media Collection diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index 36b0af8ca8f07..0981b73bcbbbf 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -27,6 +27,8 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(requestMediaFilesUploadCancelDialog:(NSArray *)mediaFiles) RCT_EXTERN_METHOD(requestMediaFilesSaveCancelDialog:(NSArray *)mediaFiles) RCT_EXTERN_METHOD(onCancelUploadForMediaCollection:(NSArray *)mediaFiles) +RCT_EXTERN_METHOD(requestFocalPointPickerTooltipShown:(RCTResponseSenderBlock)callback) +RCT_EXTERN_METHOD(setFocalPointPickerTooltipShown:(BOOL)tooltipShown) RCT_EXTERN_METHOD(actionButtonPressed:(NSString *)buttonType) RCT_EXTERN_METHOD(mediaSaveSync) RCT_EXTERN_METHOD(mediaFilesBlockReplaceSync) diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 4b5211b49eca0..927f477fcd8a4 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -24,11 +24,11 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { @objc func provideToNative_Html(_ html: String, title: String, changed: Bool, contentInfo: [String:Int]) { DispatchQueue.main.async { - let info = ContentInfo.decode(from: contentInfo) + let info = ContentInfo.decode(from: contentInfo) self.delegate?.gutenbergDidProvideHTML(title: title, html: html, changed: changed, contentInfo: info) } } - + @objc func requestMediaPickFrom(_ source: String, filter: [String]?, allowMultipleSelection: Bool, callback: @escaping RCTResponseSenderBlock) { let mediaSource = getMediaSource(withId: source) @@ -93,7 +93,7 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { guard let mediaInfo = mediaInfo else { callback(nil) return - } + } callback([mediaInfo.id as Any, mediaInfo.url as Any]) }) } @@ -259,7 +259,7 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { sendEvent(withName: event.rawValue, body: body) } } - + @objc func logUserEvent(_ event: String, properties: [AnyHashable: Any]?) { guard let logEvent = GutenbergUserEvent(event: event, properties: properties) else { return } @@ -275,7 +275,7 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { case .failure(let error): rejecter(error.domain, "\(error.code)", error) } - }) + }) } @objc @@ -325,6 +325,16 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { self.delegate?.gutenbergDidRequestMediaSaveSync() } } + } + + @objc + func requestFocalPointPickerTooltipShown(_ callback: @escaping RCTResponseSenderBlock) { + callback([self.delegate?.gutenbergDidRequestFocalPointPickerTooltipShown() ?? false]) + } + + @objc + func setFocalPointPickerTooltipShown(_ tooltipShown: Bool) { + self.delegate?.gutenbergDidRequestSetFocalPointPickerTooltipShown(tooltipShown) } @objc diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 05af11e8a66df..096bb7821627b 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased * [**] Make inserter long-press options "add to beginning" and "add to end" always available. [#28610] +* [**] Add support for setting Cover block focal point. [#25810] ## 1.46.0 * [***] New Block: Audio [#27401, #27467, #28594] diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index 9e87b98f69c6c..a4d01e34feb84 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -152,6 +152,15 @@ public void requestMediaEditor(MediaSelectedCallback mediaSelectedCallback, Stri public void logUserEvent(GutenbergUserEvent gutenbergUserEvent, ReadableMap eventProperties) { } + @Override + public void setFocalPointPickerTooltipShown(boolean tooltipShown) { + } + + @Override + public void requestFocalPointPickerTooltipShown(FocalPointPickerTooltipShownCallback focalPointPickerTooltipShownCallback) { + focalPointPickerTooltipShownCallback.onRequestFocalPointPickerTooltipShown(false); + } + @Override public void editorDidEmitLog(String message, LogLevel logLevel) { switch (logLevel) { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 175ad6dbebe63..2dafaeebb02b8 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -40,12 +40,12 @@ class GutenbergViewController: UIViewController { @objc func saveButtonPressed(sender: UIBarButtonItem) { gutenberg.requestHTML() } - + func registerLongPressGestureRecognizer() { longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) view.addGestureRecognizer(longPressGesture) } - + @objc func handleLongPress() { NotificationCenter.default.post(Notification(name: MediaUploadCoordinator.failUpload )) } @@ -209,7 +209,7 @@ extension GutenbergViewController: GutenbergBridgeDelegate { print("Gutenberg requested media editor for " + mediaUrl.absoluteString) callback([MediaInfo(id: 1, url: "https://cldup.com/Fz-ASbo2s3.jpg", type: "image")]) } - + func gutenbergDidLogUserEvent(_ event: GutenbergUserEvent) { print("Gutenberg loged user event") } @@ -251,6 +251,14 @@ extension GutenbergViewController: GutenbergBridgeDelegate { func gutenbergDidRequestMediaFilesSaveCancelDialog(_ mediaFiles: [String]) { print(#function) + } + + func gutenbergDidRequestFocalPointPickerTooltipShown() -> Bool { + return false; + } + + func gutenbergDidRequestSetFocalPointPickerTooltipShown(_ tooltipShown: Bool) { + print("Gutenberg requested setting tooltip flag") } } @@ -282,15 +290,15 @@ extension GutenbergViewController: GutenbergBridgeDataSource { func gutenbergLocale() -> String? { return Locale.preferredLanguages.first ?? "en" } - + func gutenbergTranslations() -> [String : [String]]? { return nil } - + func gutenbergInitialContent() -> String? { return nil } - + func gutenbergInitialTitle() -> String? { return nil } @@ -364,7 +372,7 @@ extension GutenbergViewController { present(alert, animated: true) } - + var toggleHTMLModeAction: UIAlertAction { return UIAlertAction( title: htmlMode ? "Switch To Visual" : "Switch to HTML", @@ -373,7 +381,7 @@ extension GutenbergViewController { self.toggleHTMLMode(action) }) } - + var updateHtmlAction: UIAlertAction { return UIAlertAction( title: "Update HTML", @@ -402,7 +410,7 @@ extension GutenbergViewController { self.gutenberg.updateCapabilities() }) } - + func alertWithTextInput(using handler: ((String?) -> Void)?) -> UIAlertController { let alert = UIAlertController(title: "Enter HTML", message: nil, preferredStyle: .alert) alert.addTextField() @@ -413,7 +421,7 @@ extension GutenbergViewController { alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) return alert } - + func toggleHTMLMode(_ action: UIAlertAction) { htmlMode = !htmlMode gutenberg.toggleHTMLMode()