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 && (
+
+ ) }
+ { ! displayPlaceholder && (
+
+
+
+
+ ) }
+
+
+
+
+
+
+ );
+}
+
+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()