From 6ef9e821a8f88eac7bf93275965fe9f90311a4e4 Mon Sep 17 00:00:00 2001 From: shadow351 <69882128+shadow351@users.noreply.github.com> Date: Mon, 15 Feb 2021 10:49:11 -0600 Subject: [PATCH] [RNMobile] Add Cover Block media settings (#25810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Render media within settings panel Initial pass at rendering media. Potentially includes unnecessary focus styles and media upload callback handlers. * Display media edit button Currently, `isSelected` must be true for the edit button to render. * Add "Edit focal point" bottom sheet cell * Replace unused bottom cell value Leveraging `customActionButton` allows for rendering the cell child icon, rather than relying upon an empty value. * Enable navigation to edit focal point sheet * [wip] Add focal point settings component * [wip] Add native focal point picker * Create foundation for focal point picker UI Remove unused code. Set up required navigation structure to pass route parameters. * Remove duplicative effect This same code is executed a few lines prior. * Add focal point picker media and range controls Render media and range control within focal point picker. * Remove unnecessary minHeight prop When editing the focal point picker, we need to render the image with its full aspect ratio, rather than restricting the height. * Add focal point picker handle * Position focal point handle based upon state * [wip] Add drag handler for focal point picker * Fix lint warning * Update focal point picker blue Previous color appears to have been removed from the source. * Fix bad merge conflict resolution * Fix various lint warnings * Add TODO note * Change default position for focal point range cells * Add TapGestureHandler to focal point picker * Round value for RangeControl text input Avoid rendering floats within the `RangeControl` text input. * Add focal point hint to Cover Block media preview * Simplify RangeControl props Remove unnecessary props. * Clear focal point when clearing media * Add Fixed background toggle * Display button to attach media when no media is attached * Fix beginning drag atop focal point picker drag handle Previously, if you began your drag directly atop the drag handle, the position would not be updated as you dragged. Instead, the parent would manage the pan gesture and attempt to scroll the bottom sheet content. * Render media preview within fixed dimensions To avoid images with extreme aspect ratios creating large amounts of scrolling, we limit the dimensions of the media preview. This prevents the min-height value from being applied to this element and animating multiple locations on the screen, in the bottom sheet and in the page content. * Leverage full-height sub-sheet for focal point picker Better manage media display and scroll management through a larger focal point editing sheet. * Sync pan gesture values to sliders `@react-native-communitye/slider` is an uncontrolled component, so we must increment a key to sync the pan gesture values to the sliders. https://git.io/JTe4A * Display percentage suffix for sliders * Improve focal point picker styles * Reduce max-height for media preview * Limit max-height for focal point picker media * Replace RNGH with RN PanResponder to fix Android modals Even though work has been done, it would appear that RNGH doesn’t work on Android within modals. - https://git.io/JTkgM - https://git.io/JTkgS - https://git.io/JTkg9 - https://bit.ly/3nHbpVl * Improve PanResponder implementation Reduce the frequency the position state is updated to improve performance. * Abstract FocalPoint into separate file Address PR feedback. * Revert "Leverage full-height sub-sheet for focal point picker" This reverts commit c0b75779ab51ee38a1f56b1e405efaa3296e5d83. Allowing scrolling of the full-height bottom sheet content for landscape orientation proved to be difficult. Now that we set a max-height for the focal point picker media, a full-height bottom sheet may not be all that necessary. * Add drag handle tooltip * Hoist tooltip visibility control into focal point picker Because the focal point picker needs a `PanResponder` atop the entire image, it makes more sense to allow it to manage the touch input required to toggle the tooltip visibility. * Prevent collapsed placeholder element while image size is fetched To prevent the dynamically sized image element from collapsing, this displays the element at the parent's full width until we know the aspect ratio of the image. * Revert "Revert "Leverage full-height sub-sheet for focal point picker"" This reverts commit d537371f1e83704793d6a323df4a4ef0e25cec99. * Fix odd layout jumping when incrementing slider key When incrementing the slider key for the focal point picker, a strange jump would occur between initial mount and second mount. The jump appeared to be related to rendering the "value component" in `BottomSheet.Cell`. Replacing the empty `value` with the `leftAlign` label fixes the issue and appears to create no regression. - https://git.io/JTmWU * Revert "Revert "Revert "Leverage full-height sub-sheet for focal point picker""" This reverts commit 1ad1e2662b32d4cb583af80f5060367e3909aa02. * Limit fixed background setting to images The web interface disallows setting fixed backgrounds for media types of video. * Duplicate styles needed for focal point settings * Add dynamic focal point to media preview * Ensure focal point coordinates are never empty Avoid error thrown from referencing empty focal point. * Disable focal point hint for fixed background media * Render media preview for videos * Fix video media preview sizing * Improve video media preview sizing Add border to match image media previews. Ensure element does not collapse while video dimensions are loaded. * Add video support to focal point picker * Simplify prop type logic Co-authored-by: Gerardo Pacheco * Address review feedback * Refactor Tooltip to dismiss on tap Refactor to align closer to existing tooltip component so that the tip dismisses on tap anywhere, rather than on focal point handle drag. * Add focal point picker tooltip flag * Improve placeholder media to prevent layout jumps * Tooltip overlay avoids blocking child context interactivity Leverage the responder capture callbacks to dismiss the tooltip, but promote the overlay to the controlling responder. * Improve tooltip styles Reorganize styles to clarify magic numbers. * Add guard for undefined prop callback * Fix focal point picker drag handle jump Replace `setOffset` with `extractOffset` as it accomplishes the intended goal directly. It also helps avoid a jump that occurs when the tooltip is dismissing while interacting with the drag handle. * Support left aligned tooltips * Page template picker shares tooltip Leverage a shared tooltip between page template picker and focal point picker. * Expand Tooltip positioning flexibility * Support right aligned tooltips * Tapping tooltip label dismisses tooltip * Update focal point picker tooltip usage * Fix disallowed syntax throwing lint errors * Abstract Cover controls to reduce file size Relocate the controls sections into a new file to reduce the size of the Cover edit file. * Fix lint warning * Replace potential circular dependency with type check utility The previous approach likely was a circular dependency causing the unit test failures. This change leverages the file type check utility used by the existing web focal point picker. * Fix erroneously flush media preview Add bottom margin to media preview to avoid odd styling. * Prevent focal point hint overlapping edit media button The `Image` component renders an edit button. By adding a `children` prop, we can now render the focal point hint between the image media and the edit button, ensuring that the edit button is always accessible. The `Video` component does not render an edit button and therefore does not need to render the focal point hint as a child. * Replace HoC with Hook Address code review feedback. * Revert "Update focal point picker tooltip usage" This reverts commit d2b9fb8e6addefc29762c62188ded8fd0a537769. * Revert "Page template picker shares tooltip" This reverts commit 633b1d50e3409d9b7ae83c77b26524517760bc6b. * Leverage UnitControl for focal point picker Replace custom suffix for `RangeControl` with newly build `UnitControl`. This adds the ability to disable the unit picker when there is only a single unit provided to `UnitControl`, as switching between an option of one unit provides little value. Disabling the picker also matches the web experience. * Avoid scrolling focal point picker with full-height bottom sheet Leverage entire device height to avoid the need to scroll the focal point picker bottom sheet. * Remove unnecessary bottom sheet methods These were originally copied from the `ColorPickerSettings`, but do not appear to be applicable for this focal point picker implementation. * Ensure full-height bottom sheet does not linger Without explicitly disabling the full-screen height bottom sheet when the bottom sheet is closed, it will remaining enabled. This results in erroneously apply full-height styles to all bottom sheets. The existing `useFocusEffect` callback is only invoked when the back/cancel button is tapped, but not when dismissing the bottom sheet via swiping or tapping the overlay. * Fix bad merge conflict resolution The page template picker tooltip was erroneously re-added during a merge conflict resolution. * Remove lingering, unused code * Fix negative and lost values when dragging outside bounds Previously, if the slider/drag handle was dragged outside of the edge of the range/image, the value would either be set to a negative value or ignored altogether. This ensures that negative values are not set and that instead are set to zero. * Add comments for additional context * Avoid unnecessary re-renders with useCallback Address code review feedback that we should avoid these arrow functions within the component return. * Display tooltip within demo editor * Remove unnecessary useCallback usage This code was not providing any performance gains, as the children components are not memoized. * Avoid importing react directly Fix lint errors. * Remove short-sighted fix for full-height bottom sheet Originally added to address full-height styles lingering when swiping-to-dismiss a bottom sheet, it's now recognized this fix does not address the root issue. Additional details are captured in a new issue. https://github.com/WordPress/gutenberg/issues/28173 * Remove unused import * Fix broken min-height increment The incorrect `key` caused the `UnitControl` to be unnecessarily re-mounted, which cleared out the interval that incremented the value when the user held their finger down on the increment/decrement button. This mistake was likely the result of a bad merge conflict, as it was already fixed previously. https://github.com/WordPress/gutenberg/pull/27005 * Remove erroneously committed configuration file * Mobile - Wip - Picker and BottomSheet * Add MediaUpload within bottom sheet context react-native-modal does not support opening multiple modals on iOS. In order to dismiss the bottom sheet prior to opening the media picker modal we must provide the MediaUpload access to the bottom sheet context to register a callback to open the second modal once the first has closed. Additionally, this newly added MediaUpload cannot be used for the ImageEditingButton because the "Replace" functionality of this button involves opening _two_ pickers. The first one would dismiss bottom sheet, thus removing access to this new MediaUpload picker. This results in the second picker no longer having access to the media upload callback. Because of this, the ImageEditingButton continues to use the pre-existing MediaUpload in the parent Cover edit component. * Compute relative coordinates in PanResponder release Ideally, the x and y coords 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 * Capture Podfile.lock changes * Capture Podfile.lock changes * Revert "Capture Podfile.lock changes" This reverts commit 203635216d4b30c65ad25a51a23b57154a8a3def and 67f18a13e8121d4b4d15d75553945aa62e6f7b5e. * Apply consistent media background Share media background color for inspector controls and focal point picker. * Remove highlight border from focal point media Add prop to allow disabling the blue border applied to images when they are "selected" for editing. * Disable focal point picker when fixed background enabled We desire that the focal point cannot be changed when fixed background is enabled. This is because the focal point coordinates have no impact in this context. This also matches the behavior for the web editor. * Remove extra padding atop and beneath focal point picker settings * Update focal point hint styles Increase focal point hint contrast and balance sizing. * Use two digit strings for coordinates Ensure the coordinate values are strings with two digits after the decimal, rather than floats. This helps ensure the data match between web and native. * Fix style lint error * Reduce opacity of disabled focal point navigation button Lower opacity to communicate when the edit focal point navigation button is disabled while fix background is toggled on. * Remove bottom margin beneath media preview * Allow focal point cross-hair to overflow horizontal bounds Avoiding cutting off the cross-hair improves its visibility when it is situated along the edge of the media preview. * Allow long press action for bottom cell This supports use cases like long pressing edit an image. * Add edit image accessibility label * Improve accessibility of edit image button Previously, it was impossible to select the edit image button, as it was rendered beneath the image itself. * Reorder Cover block media settings sections * Add clear media button to inspector controls Cover image edit button Align with image edit button found within the canvas. * Improve double modal handling for iOS This allows dismissing the bottom sheet _after_ the second picker, rather than before it. This is helpful for the scenario where a user cancels the first picker. Now the bottom sheet will still be open in that scenario. * Increase a11y label accuracy The related menu also provides the ability to clear the media. * Improve accessibility of add media button Previously, voice over would announce nothing when selecting this button, as it did not have a label or hint. * Pass string through a11n tooling Avoid a static English string. * Improve focal point settings media styles Address styling oddities for images and videos within the focal point settings. Namely, Android would render videos with the incorrect width, overflowing the parent container. * Avoid invisible, paused videos on Android Rendering `react-native-video` with `paused` set to `true` causes the video to not render visibly on Android only. This is presumably related to other issues regarding `paused`. https://git.io/Jt6Dr By seeking the video to 0, the video correctly renders on Android. * Prevent image from overflowing width * Add CHANGELOG note Co-authored-by: David Calhoun Co-authored-by: Gerardo Pacheco --- .../block-settings/container.native.js | 8 + .../components/media-upload/index.native.js | 5 + .../src/cover/controls.native.js | 303 ++++++++++++++++++ .../block-library/src/cover/edit.native.js | 170 ++++------ .../src/cover/focal-point-settings.native.js | 53 +++ packages/block-library/src/cover/shared.js | 4 + .../block-library/src/cover/style.native.scss | 50 ++- .../focal-point-picker/focal-point.native.js | 30 ++ .../src/focal-point-picker/index.native.js | 279 ++++++++++++++++ .../src/focal-point-picker/style.scss | 56 ++++ .../tooltip/index.native.js | 151 +++++++++ .../tooltip/style.native.scss | 42 +++ packages/components/src/index.native.js | 2 + .../src/mobile/bottom-sheet/cell.native.js | 2 + .../src/mobile/bottom-sheet/index.native.js | 18 +- .../mobile/bottom-sheet/range-cell.native.js | 2 +- .../focal-point-settings/index.native.js | 78 +++++ .../focal-point-settings/styles.native.scss | 3 + .../image/image-editing-button.native.js | 14 +- .../src/mobile/image/index.native.js | 48 +-- .../src/mobile/media-edit/index.native.js | 1 + .../components/src/mobile/picker/index.ios.js | 37 ++- .../src/unit-control/index.native.js | 51 +-- packages/primitives/src/svg/index.native.js | 7 +- .../GutenbergBridgeJS2Parent.java | 9 + .../RNReactNativeGutenbergBridgeModule.java | 20 ++ .../WPAndroidGlue/WPAndroidGlueCode.java | 19 +- packages/react-native-bridge/index.js | 12 + .../ios/GutenbergBridgeDelegate.swift | 7 + .../ios/RNReactNativeGutenbergBridge.m | 2 + .../ios/RNReactNativeGutenbergBridge.swift | 20 +- packages/react-native-editor/CHANGELOG.md | 1 + .../java/com/gutenberg/MainApplication.java | 9 + .../GutenbergViewController.swift | 28 +- 34 files changed, 1356 insertions(+), 185 deletions(-) create mode 100644 packages/block-library/src/cover/controls.native.js create mode 100644 packages/block-library/src/cover/focal-point-settings.native.js create mode 100644 packages/components/src/focal-point-picker/focal-point.native.js create mode 100644 packages/components/src/focal-point-picker/index.native.js create mode 100644 packages/components/src/focal-point-picker/style.scss create mode 100644 packages/components/src/focal-point-picker/tooltip/index.native.js create mode 100644 packages/components/src/focal-point-picker/tooltip/style.native.scss create mode 100644 packages/components/src/mobile/focal-point-settings/index.native.js create mode 100644 packages/components/src/mobile/focal-point-settings/styles.native.scss 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 3bbf64ac87d416..4ceb4fab58778c 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 00000000000000..cf536cd2d32e9c --- /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 e1d05426fcf5e6..f003ea7b6d800b 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, @@ -62,17 +58,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', @@ -82,8 +77,6 @@ const INNER_BLOCKS_TEMPLATE = [ }, ], ]; -const COVER_MAX_HEIGHT = 1000; -const COVER_DEFAULT_HEIGHT = 300; const Cover = ( { attributes, @@ -113,8 +106,6 @@ const Cover = ( { false ); - const CONTAINER_HEIGHT = minHeight || COVER_DEFAULT_HEIGHT; - useEffect( () => { // sync with local media store mediaUploadSync(); @@ -196,19 +187,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 ); @@ -230,7 +208,12 @@ const Cover = ( { }; const onClearMedia = useCallback( () => { - setAttributes( { id: undefined, url: undefined } ); + setAttributes( { + focalPoint: undefined, + hasParallax: undefined, + id: undefined, + url: undefined, + } ); closeSettingsBottomSheet(); }, [ closeSettingsBottomSheet ] ); @@ -293,8 +276,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 = ( @@ -546,32 +480,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 a80ce7d28c8701..18ac6076be922f 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 33fdadffd68228..c132ae60f4f1d6 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 00000000000000..a5a7a92c298ec1 --- /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 00000000000000..f48701a57a4825 --- /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 00000000000000..826f1f88d82d92 --- /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 00000000000000..8938233a0baf46 --- /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 00000000000000..427dcece517586 --- /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 0fee6fc26bad96..45c47fbb6764c5 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 16dc36a12efdfe..adf40c4d818815 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 c937aad9773e43..63b7ea41f1436a 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 00000000000000..99b41f9c6f1421 --- /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 14314f1c24a707..fccc4fa2bfe334 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 0dbdb5753101d3..3116426526cba3 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 1e2ab90cd0b9ee..71728dfc73844b 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 b813c5803e8ecf..bd99917ec40426 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 c2e7e0d69b7b6c..ada1bc77af7731 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 4d3bf26acf1acd..a8f6c36fef1160 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 adccb294973685..f529f173d7e8fb 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 eb1732de4616da..d1bd905ce85404 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 87d3129ef50659..fe658bfd8e1fa6 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 8bf3742ce1634b..1c4ee514d148a2 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 f054e3a1b09bb0..de7f97c25c92ad 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 36b0af8ca8f071..0981b73bcbbbf5 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 4b5211b49eca02..927f477fcd8a46 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 05af11e8a66df8..096bb7821627b1 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 9e87b98f69c6cb..a4d01e34feb84e 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 175ad6dbebe63c..2dafaeebb02b82 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()