diff --git a/packages/block-editor/src/components/block-mobile-toolbar/index.native.js b/packages/block-editor/src/components/block-mobile-toolbar/index.native.js
index 6e3fc8aaa1fdd2..0217a668d94830 100644
--- a/packages/block-editor/src/components/block-mobile-toolbar/index.native.js
+++ b/packages/block-editor/src/components/block-mobile-toolbar/index.native.js
@@ -33,7 +33,10 @@ const BlockMobileToolbar = ( {
-
+
+ { /* Render only one settings icon even if we have more than one fill - need for hooks with controls */ }
+ { ( fills = [ null ] ) => fills[ 0 ] }
+
-
+
+ { ( { currentScreen, extraProps, ...bottomSheetProps } ) => {
+ switch ( currentScreen ) {
+ case colorsUtils.subsheets.color:
+ return (
+
+ );
+ case colorsUtils.subsheets.settings:
+ default:
+ return ;
+ }
+ } }
+
);
}
diff --git a/packages/block-editor/src/components/colors-gradients/panel-color-gradient-settings.native.js b/packages/block-editor/src/components/colors-gradients/panel-color-gradient-settings.native.js
index f4ade7b5487d70..76c2c35ea7f567 100644
--- a/packages/block-editor/src/components/colors-gradients/panel-color-gradient-settings.native.js
+++ b/packages/block-editor/src/components/colors-gradients/panel-color-gradient-settings.native.js
@@ -1,17 +1,51 @@
/**
* WordPress dependencies
*/
-import { PanelBody, UnsupportedFooterControl } from '@wordpress/components';
-import { __ } from '@wordpress/i18n';
+import {
+ ColorControl,
+ BottomSheetConsumer,
+ PanelBody,
+} from '@wordpress/components';
-const PanelColorGradientSettings = () => {
+export default function PanelColorGradientSettings( { settings, title } ) {
return (
-
-
+
+
+ { ( { onReplaceSubsheet } ) =>
+ settings.map(
+ (
+ {
+ onColorChange,
+ colorValue,
+ onGradientChange,
+ gradientValue,
+ label,
+ },
+ index
+ ) => (
+ {
+ onReplaceSubsheet( 'Color', {
+ onColorChange,
+ colorValue: gradientValue || colorValue,
+ gradientValue,
+ onGradientChange,
+ label,
+ } );
+ } }
+ key={ `color-setting-${ label }` }
+ label={ label }
+ color={ gradientValue || colorValue }
+ separatorType={
+ index !== settings.length - 1
+ ? 'fullWidth'
+ : 'none'
+ }
+ />
+ )
+ )
+ }
+
);
-};
-export default PanelColorGradientSettings;
+}
diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js
index f2e9e2ed82c688..00ab1437903245 100644
--- a/packages/block-editor/src/components/index.native.js
+++ b/packages/block-editor/src/components/index.native.js
@@ -33,6 +33,7 @@ export { default as BlockInvalidWarning } from './block-list/block-invalid-warni
export { default as BlockCaption } from './block-caption';
export { default as Caption } from './caption';
export { default as PanelColorSettings } from './panel-color-settings';
+export { default as __experimentalPanelColorGradientSettings } from './colors-gradients/panel-color-gradient-settings';
export { BottomSheetSettings, BlockSettingsButton } from './block-settings';
export { default as VideoPlayer, VIDEO_ASPECT_RATIO } from './video-player';
diff --git a/packages/block-editor/src/components/inspector-controls/index.native.js b/packages/block-editor/src/components/inspector-controls/index.native.js
index 730dfaba5b37d3..49bc55dd5ae6b6 100644
--- a/packages/block-editor/src/components/inspector-controls/index.native.js
+++ b/packages/block-editor/src/components/inspector-controls/index.native.js
@@ -2,12 +2,11 @@
* External dependencies
*/
import React from 'react';
-
+import { View } from 'react-native';
/**
* WordPress dependencies
*/
-import { createSlotFill } from '@wordpress/components';
-
+import { createSlotFill, BottomSheetConsumer } from '@wordpress/components';
/**
* Internal dependencies
*/
@@ -19,7 +18,13 @@ const { Fill, Slot } = createSlotFill( 'InspectorControls' );
const FillWithSettingsButton = ( { children, ...props } ) => {
return (
<>
- { children }
+
+ {
+
+ { () => { children } }
+
+ }
+
{ React.Children.count( children ) > 0 && }
>
);
diff --git a/packages/block-editor/src/hooks/color-panel.native.js b/packages/block-editor/src/hooks/color-panel.native.js
deleted file mode 100644
index 3c505bfd0f15f4..00000000000000
--- a/packages/block-editor/src/hooks/color-panel.native.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function ColorPanel() {
- return null;
-}
diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js
index c196a521b39061..b8f447b67e788a 100644
--- a/packages/block-editor/src/hooks/color.js
+++ b/packages/block-editor/src/hooks/color.js
@@ -11,7 +11,7 @@ import { addFilter } from '@wordpress/hooks';
import { hasBlockSupport, getBlockSupport } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
-import { useRef, useEffect } from '@wordpress/element';
+import { useRef, useEffect, Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -243,7 +243,10 @@ export function ColorEdit( props ) {
return (
- { gradientValue && (
+ { isGradient( backgroundColor ) && (
{
- if ( ! enableContrastChecking ) {
+ if ( isWebPlatform && ! enableContrastChecking ) {
return;
}
@@ -90,7 +92,7 @@ function ColorPanel( { settings, clientId, enableContrastChecking = true } ) {
initialOpen={ false }
settings={ settings }
>
- { enableContrastChecking && (
+ { isWebPlatform && enableContrastChecking && (
defaultGradient.slug === gradient
+ ).gradient
+ );
+ }
return (
- wrapperProps?.style?.backgroundColor ||
+ getColorAndStyleProps( attributes ).style?.backgroundColor ||
// We still need the `backgroundColor.color` to support colors from the color pallete (not custom ones)
backgroundColor.color ||
- styles.fallbackButton.backgroundColor
+ styles.defaultButton.backgroundColor
);
}
getTextColor() {
- const { textColor, wrapperProps } = this.props;
-
+ const { textColor, attributes } = this.props;
return (
- wrapperProps?.style?.color ||
+ getColorAndStyleProps( attributes ).style?.color ||
// We still need the `textColor.color` to support colors from the color pallete (not custom ones)
textColor.color ||
- styles.fallbackButton.color
+ styles.defaultButton.color
);
}
@@ -267,7 +278,7 @@ class ButtonEdit extends Component {
onSetMaxWidth( width ) {
const { maxWidth } = this.state;
const { parentWidth } = this.props;
- const { marginRight: spacing } = styles.button;
+ const { marginRight: spacing } = styles.defaultButton;
const isParentWidthChanged = maxWidth !== parentWidth;
const isWidthChanged = maxWidth !== width;
@@ -398,20 +409,18 @@ class ButtonEdit extends Component {
isButtonFocused,
placeholderTextWidth,
} = this.state;
+ const { paddingTop: spacing, borderWidth } = styles.defaultButton;
if ( parentWidth === 0 ) {
return null;
}
- const borderRadiusValue =
- borderRadius !== undefined
- ? borderRadius
- : styles.button.borderRadius;
+ const borderRadiusValue = Number.isInteger( borderRadius )
+ ? borderRadius
+ : styles.defaultButton.borderRadius;
const outlineBorderRadius =
borderRadiusValue > 0
- ? borderRadiusValue +
- styles.button.paddingTop +
- styles.button.borderWidth
+ ? borderRadiusValue + spacing + borderWidth
: 0;
// To achieve proper expanding and shrinking `RichText` on iOS, there is a need to set a `minWidth`
@@ -430,6 +439,7 @@ class ButtonEdit extends Component {
: placeholder || __( 'Add text…' );
const backgroundColor = this.getBackgroundColor();
+ const textColor = this.getTextColor();
return (
@@ -458,7 +468,7 @@ class ButtonEdit extends Component {
onChange={ this.onChangeText }
style={ {
...richTextStyle.richText,
- color: this.getTextColor(),
+ color: textColor,
} }
textAlign="center"
placeholderTextColor={
@@ -475,11 +485,11 @@ class ButtonEdit extends Component {
this.onToggleButtonFocus( true )
}
__unstableMobileNoFocusOnMount={ ! isSelected }
+ selectionColor={ textColor }
onBlur={ () => {
this.onToggleButtonFocus( false );
this.onSetMaxWidth();
} }
- selectionColor={ this.getTextColor() }
onReplace={ onReplace }
onRemove={ this.onRemove }
onMerge={ mergeBlocks }
@@ -513,6 +523,7 @@ class ButtonEdit extends Component {
/>
+
{ this.getLinkSettings( url, rel, linkTarget, true ) }
-
-
-
);
diff --git a/packages/block-library/src/button/editor.native.scss b/packages/block-library/src/button/editor.native.scss
index 47194070090401..54d25c34eb66a1 100644
--- a/packages/block-library/src/button/editor.native.scss
+++ b/packages/block-library/src/button/editor.native.scss
@@ -25,16 +25,11 @@
bottom: 0;
}
-.button {
- border-width: $border-width;
+.defaultButton {
border-radius: $border-width * 4;
padding: $block-spacing;
- max-width: 580px;
- min-width: 108px;
+ border-width: $border-width;
margin: 2 * $panel-padding;
-}
-
-.fallbackButton {
background-color: $button-fallback-bg;
color: $white;
}
diff --git a/packages/block-library/src/columns/index.js b/packages/block-library/src/columns/index.js
index dec1d8672e6974..92f5f68f154e43 100644
--- a/packages/block-library/src/columns/index.js
+++ b/packages/block-library/src/columns/index.js
@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { columns as icon } from '@wordpress/icons';
-
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
*/
@@ -27,7 +27,7 @@ export const settings = {
align: [ 'wide', 'full' ],
html: false,
lightBlockWrapper: true,
- __experimentalColor: { gradients: true },
+ __experimentalColor: Platform.OS === 'web' && { gradients: true },
},
variations,
example: {
diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js
index d07cf54f7256d8..28fa5ca74b4844 100644
--- a/packages/block-library/src/group/index.js
+++ b/packages/block-library/src/group/index.js
@@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import { group as icon } from '@wordpress/icons';
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -92,7 +93,7 @@ export const settings = {
anchor: true,
html: false,
lightBlockWrapper: true,
- __experimentalColor: { gradients: true },
+ __experimentalColor: Platform.OS === 'web' && { gradients: true },
},
transforms: {
from: [
diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js
index 2b9eac45e956d7..524a0bd7fb7909 100644
--- a/packages/block-library/src/heading/index.js
+++ b/packages/block-library/src/heading/index.js
@@ -8,6 +8,7 @@ import { isEmpty } from 'lodash';
*/
import { heading as icon } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -34,7 +35,7 @@ export const settings = {
anchor: true,
__unstablePasteTextInline: true,
lightBlockWrapper: true,
- __experimentalColor: true,
+ __experimentalColor: Platform.OS === 'web',
__experimentalLineHeight: true,
__experimentalFontSize: true,
},
diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js
index db22f30b70abf0..b50e85eaca7a57 100644
--- a/packages/block-library/src/media-text/index.js
+++ b/packages/block-library/src/media-text/index.js
@@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { mediaAndText as icon } from '@wordpress/icons';
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -25,7 +26,7 @@ export const settings = {
supports: {
align: [ 'wide', 'full' ],
html: false,
- __experimentalColor: { gradients: true },
+ __experimentalColor: Platform.OS === 'web' && { gradients: true },
},
example: {
attributes: {
diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js
index 0e76e74adc338f..e1711a67de97d2 100644
--- a/packages/block-library/src/paragraph/index.js
+++ b/packages/block-library/src/paragraph/index.js
@@ -8,6 +8,7 @@ import { isEmpty } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import { paragraph as icon } from '@wordpress/icons';
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -44,7 +45,7 @@ export const settings = {
className: false,
__unstablePasteTextInline: true,
lightBlockWrapper: true,
- __experimentalColor: true,
+ __experimentalColor: Platform.OS === 'web',
__experimentalLineHeight: true,
__experimentalFontSize: true,
},
diff --git a/packages/components/src/color-control/index.native.js b/packages/components/src/color-control/index.native.js
new file mode 100644
index 00000000000000..97c29a2e3d5af5
--- /dev/null
+++ b/packages/components/src/color-control/index.native.js
@@ -0,0 +1,31 @@
+/**
+ * Internal dependencies
+ */
+import ColorCell from '../mobile/bottom-sheet/color-cell';
+
+function ColorControl( {
+ label,
+ help,
+ instanceId,
+ className,
+ onPress,
+ color,
+ ...props
+} ) {
+ const id = `inspector-color-control-${ instanceId }`;
+
+ return (
+
+ );
+}
+
+export default ColorControl;
diff --git a/packages/components/src/color-indicator/index.native.js b/packages/components/src/color-indicator/index.native.js
new file mode 100644
index 00000000000000..06034baf57cd45
--- /dev/null
+++ b/packages/components/src/color-indicator/index.native.js
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import { View, Animated } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import { Icon, check } from '@wordpress/icons';
+import { LinearGradient } from '@wordpress/components';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+import { colorsUtils } from '../mobile/color-settings/utils';
+
+function SelectedIcon( { opacity } ) {
+ return (
+
+
+
+
+ );
+}
+
+function ColorIndicator( {
+ color,
+ isSelected,
+ withCustomPicker,
+ style,
+ opacity,
+} ) {
+ const { isGradient } = colorsUtils;
+
+ const outlineStyle = usePreferredColorSchemeStyle(
+ styles.outline,
+ styles.outlineDark
+ );
+
+ if ( isGradient( color ) ) {
+ return (
+
+
+ { isSelected && }
+
+ );
+ } else if ( withCustomPicker ) {
+ return (
+
+
+ { color.map( ( gradientValue ) => {
+ return (
+
+ );
+ } ) }
+ { isSelected && }
+
+ );
+ }
+ return (
+
+
+ { isSelected && }
+
+ );
+}
+export default ColorIndicator;
diff --git a/packages/components/src/color-indicator/style.native.scss b/packages/components/src/color-indicator/style.native.scss
new file mode 100644
index 00000000000000..b4ac79720967f4
--- /dev/null
+++ b/packages/components/src/color-indicator/style.native.scss
@@ -0,0 +1,51 @@
+.selected {
+ height: 28px;
+ width: 28px;
+ border-radius: 14px;
+ background-color: $white;
+ justify-content: center;
+ align-items: center;
+}
+
+.circleOption {
+ height: 48px;
+ width: 48px;
+ border-radius: 24px;
+ margin-right: 8px;
+ justify-content: center;
+ align-items: center;
+}
+
+.absolute {
+ position: absolute;
+ margin-right: 0;
+ border-width: 0;
+}
+
+.icon {
+ color: $gray-dark;
+}
+
+.outline {
+ border-color: $light-dim;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: 24px;
+ border-width: $border-width;
+ position: absolute;
+ z-index: 2;
+}
+
+.outlineDark {
+ border-color: $dark-ultra-dim;
+}
+
+.selectedOutline {
+ top: -$border-width;
+ bottom: -$border-width;
+ left: -$border-width;
+ right: -$border-width;
+ border-radius: 14px;
+}
diff --git a/packages/components/src/color-palette/index.native.js b/packages/components/src/color-palette/index.native.js
new file mode 100644
index 00000000000000..4f9f85870604ab
--- /dev/null
+++ b/packages/components/src/color-palette/index.native.js
@@ -0,0 +1,171 @@
+/**
+ * External dependencies
+ */
+import {
+ ScrollView,
+ TouchableWithoutFeedback,
+ View,
+ Animated,
+ Easing,
+} from 'react-native';
+import { map } from 'lodash';
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect, createRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+import ColorIndicator from '../color-indicator';
+import { colorsUtils } from '../mobile/color-settings/utils';
+
+const ANIMATION_DURATION = 200;
+
+function ColorPalette( {
+ setColor,
+ activeColor,
+ isGradientColor,
+ defaultSettings,
+ currentSegment,
+ onCustomPress,
+ shouldEnableBottomSheetScroll,
+} ) {
+ const customSwatchGradients = [
+ 'linear-gradient(120deg, rgba(255,0,0,.8), 0%, rgba(255,255,255,1) 70.71%)',
+ 'linear-gradient(240deg, rgba(0,255,0,.8), 0%, rgba(0,255,0,0) 70.71%)',
+ 'linear-gradient(360deg, rgba(0,0,255,.8), 0%, rgba(0,0,255,0) 70.71%)',
+ ];
+
+ const extendedDefaultColors = [
+ {
+ name: __( 'White' ),
+ slug: 'white',
+ color: '#ffffff',
+ },
+ {
+ name: __( 'Black' ),
+ slug: 'black',
+ color: '#000000',
+ },
+ ...defaultSettings.colors,
+ ];
+
+ const scrollViewRef = createRef();
+
+ const isGradientSegment = currentSegment === colorsUtils.segments[ 1 ];
+
+ const [ scale ] = useState( new Animated.Value( 1 ) );
+ const [ opacity ] = useState( new Animated.Value( 1 ) );
+
+ const defaultColors = map( extendedDefaultColors, 'color' );
+ const defaultGradientColors = map( defaultSettings.gradients, 'gradient' );
+ const colors = isGradientSegment ? defaultGradientColors : defaultColors;
+
+ useEffect( () => {
+ scrollViewRef.current.scrollTo( { x: 0, y: 0 } );
+ }, [ currentSegment ] );
+
+ function isSelectedCustom() {
+ return (
+ ! isGradientColor && activeColor && ! colors.includes( activeColor )
+ );
+ }
+
+ function isSelected( color ) {
+ return ! isSelectedCustom() && activeColor === color;
+ }
+
+ function timingAnimation( property, toValue ) {
+ return Animated.timing( property, {
+ toValue,
+ duration: ANIMATION_DURATION,
+ easing: Easing.ease,
+ } );
+ }
+
+ function performAnimation( color ) {
+ opacity.setValue( isSelected( color ) ? 1 : 0 );
+ scale.setValue( 1 );
+
+ Animated.parallel( [
+ timingAnimation( scale, 2 ),
+ timingAnimation( opacity, 1 ),
+ ] ).start();
+ }
+
+ const scaleInterpolation = scale.interpolate( {
+ inputRange: [ 1, 1.5, 2 ],
+ outputRange: [ 1, 0.7, 1 ],
+ } );
+
+ function onColorPress( color ) {
+ performAnimation( color );
+ setColor( color );
+ }
+
+ const verticalSeparatorStyle = usePreferredColorSchemeStyle(
+ styles.verticalSeparator,
+ styles.verticalSeparatorDark
+ );
+
+ return (
+ shouldEnableBottomSheetScroll( false ) }
+ onScrollEndDrag={ () => shouldEnableBottomSheetScroll( true ) }
+ ref={ scrollViewRef }
+ >
+ { colors.map( ( color ) => {
+ const scaleValue = isSelected( color ) ? scaleInterpolation : 1;
+ return (
+ onColorPress( color ) }
+ key={ `${ color }-${ isSelected( color ) }` }
+ >
+
+
+
+
+ );
+ } ) }
+ { ! isGradientSegment && (
+ <>
+
+
+
+
+
+
+ >
+ ) }
+
+ );
+}
+
+export default ColorPalette;
diff --git a/packages/components/src/color-palette/style.native.scss b/packages/components/src/color-palette/style.native.scss
new file mode 100644
index 00000000000000..76363ef9acb04a
--- /dev/null
+++ b/packages/components/src/color-palette/style.native.scss
@@ -0,0 +1,25 @@
+.contentContainer {
+ flex-direction: row;
+ padding: 0 $panel-padding;
+}
+
+.container {
+ padding-bottom: $panel-padding;
+}
+
+.verticalSeparator {
+ border-width: $border-width / 2;
+ border-color: $light-gray-400;
+ height: 38px;
+ margin-right: $panel-padding / 2;
+ align-self: center;
+}
+
+.verticalSeparatorDark {
+ border-color: $gray-70;
+}
+
+.colorIndicator {
+ margin-top: 12px;
+ margin-bottom: 12px;
+}
diff --git a/packages/components/src/color-picker/index.native.js b/packages/components/src/color-picker/index.native.js
new file mode 100644
index 00000000000000..e95d2b4d770f61
--- /dev/null
+++ b/packages/components/src/color-picker/index.native.js
@@ -0,0 +1,186 @@
+/**
+ * External dependencies
+ */
+import { View, Text, TouchableWithoutFeedback, Platform } from 'react-native';
+import HsvColorPicker from 'react-native-hsv-color-picker';
+import tinycolor from 'tinycolor2';
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { BottomSheet } from '@wordpress/components';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+import { Icon, check, close } from '@wordpress/icons';
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+function ColorPicker( {
+ shouldEnableBottomSheetScroll,
+ shouldDisableBottomSheetMaxHeight,
+ isBottomSheetContentScrolling,
+ setColor,
+ activeColor,
+ isGradientColor,
+ onNavigationBack,
+ onCloseBottomSheet,
+} ) {
+ const isIOS = Platform.OS === 'ios';
+ const hitSlop = { top: 22, bottom: 22, left: 22, right: 22 };
+
+ const [ hue, setHue ] = useState( 0 );
+ const [ sat, setSaturation ] = useState( 0.5 );
+ const [ val, setValue ] = useState( 0.5 );
+ const [ savedColor ] = useState( activeColor );
+
+ const {
+ paddingLeft: spacing,
+ height: pickerHeight,
+ borderRadius,
+ } = styles.picker;
+ const { height: pickerPointerSize } = styles.pickerPointer;
+ const pickerWidth = BottomSheet.getWidth() - 2 * spacing;
+
+ const applyButtonStyle = usePreferredColorSchemeStyle(
+ styles.applyButton,
+ styles.applyButtonDark
+ );
+ const cancelButtonStyle = usePreferredColorSchemeStyle(
+ styles.cancelButton,
+ styles.cancelButtonDark
+ );
+ const colorTextStyle = usePreferredColorSchemeStyle(
+ styles.colorText,
+ styles.colorTextDark
+ );
+ const footerStyle = usePreferredColorSchemeStyle(
+ styles.footer,
+ styles.footerDark
+ );
+
+ const currentColor = tinycolor(
+ `hsv ${ hue } ${ sat } ${ val }`
+ ).toHexString();
+
+ function setHSVFromHex( color ) {
+ const { h, s, v } = tinycolor( color ).toHsv();
+
+ setHue( h );
+ setSaturation( s );
+ setValue( v );
+ }
+
+ useEffect( () => {
+ setColor( currentColor );
+ }, [ currentColor ] );
+
+ useEffect( () => {
+ if ( ! isGradientColor && activeColor ) {
+ setHSVFromHex( activeColor );
+ }
+ setColor( activeColor );
+ shouldDisableBottomSheetMaxHeight( false );
+ onCloseBottomSheet( () => setColor( savedColor ) );
+ }, [] );
+
+ function onHuePickerChange( { hue: h } ) {
+ setHue( h );
+ }
+
+ function onSatValPickerChange( { saturation: s, value: v } ) {
+ setSaturation( s );
+ setValue( v );
+ }
+
+ function onButtonPress( action ) {
+ onNavigationBack();
+ onCloseBottomSheet( null );
+ shouldDisableBottomSheetMaxHeight( true );
+ setColor( action === 'apply' ? currentColor : savedColor );
+ }
+
+ return (
+ <>
+ {
+ shouldEnableBottomSheetScroll( false );
+ } }
+ onSatValPickerDragEnd={ () =>
+ shouldEnableBottomSheetScroll( true )
+ }
+ onHuePickerDragStart={ () =>
+ shouldEnableBottomSheetScroll( false )
+ }
+ onHuePickerDragEnd={ () =>
+ shouldEnableBottomSheetScroll( true )
+ }
+ huePickerBarWidth={ pickerWidth }
+ huePickerBarHeight={ pickerPointerSize / 2 }
+ satValPickerSize={ {
+ width: pickerWidth,
+ height: pickerHeight,
+ } }
+ satValPickerSliderSize={ pickerPointerSize * 2 }
+ satValPickerBorderRadius={ borderRadius }
+ huePickerBorderRadius={ borderRadius }
+ />
+
+ onButtonPress( 'cancel' ) }
+ hitSlop={ hitSlop }
+ >
+
+ { isIOS ? (
+
+ { __( 'Cancel' ) }
+
+ ) : (
+
+ ) }
+
+
+
+ { currentColor.toUpperCase() }
+
+ onButtonPress( 'apply' ) }
+ hitSlop={ hitSlop }
+ >
+
+ { isIOS ? (
+
+ { __( 'Apply' ) }
+
+ ) : (
+
+ ) }
+
+
+
+ >
+ );
+}
+
+export default ColorPicker;
diff --git a/packages/components/src/color-picker/style.native.scss b/packages/components/src/color-picker/style.native.scss
new file mode 100644
index 00000000000000..336e34a17e1d34
--- /dev/null
+++ b/packages/components/src/color-picker/style.native.scss
@@ -0,0 +1,53 @@
+.footer {
+ border-top-width: $border-width;
+ border-color: $light-gray-400;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: $panel-padding;
+ margin-top: $panel-padding;
+ flex-wrap: wrap;
+}
+
+.footerDark {
+ border-color: $gray-70;
+}
+
+.cancelButton {
+ color: #d63638;
+ font-size: 17px;
+}
+
+.cancelButtonDark {
+ color: #f86368;
+}
+
+.applyButton {
+ color: $blue-50;
+ font-size: 17px;
+}
+
+.applyButtonDark {
+ color: $blue-30;
+}
+
+.colorText {
+ font-family: $default-monospace-font;
+ color: $light-primary;
+ font-size: 16px;
+ font-weight: 400;
+}
+
+.colorTextDark {
+ color: $dark-primary;
+}
+
+.picker {
+ padding: $panel-padding;
+ border-radius: $panel-padding / 2;
+ height: 200px;
+}
+
+.pickerPointer {
+ height: 16px;
+}
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index 09818661ab1da6..bec997cb2d9abd 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -37,6 +37,7 @@ export { default as SelectControl } from './select-control';
export { default as RangeControl } from './range-control';
export { default as ResizableBox } from './resizable-box';
export { default as UnsupportedFooterControl } from './unsupported-footer-control';
+export { default as ColorControl } from './color-control';
export { default as QueryControls } from './query-controls';
// Higher-Order Components
@@ -51,6 +52,7 @@ export * from './text';
// Mobile Components
export { default as BottomSheet } from './mobile/bottom-sheet';
+export { BottomSheetConsumer } from './mobile/bottom-sheet/bottom-sheet-context';
export { default as HTMLTextInput } from './mobile/html-text-input';
export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view';
export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list';
@@ -60,6 +62,10 @@ export { default as ReadableContentView } from './mobile/readable-content-view';
export { default as CycleSelectControl } from './mobile/cycle-select-control';
export { default as ImageWithFocalPoint } from './mobile/image-with-focalpoint';
export { default as LinearGradient } from './mobile/linear-gradient';
+export { default as ColorSettings } from './mobile/color-settings';
+
+// Utils
+export { colorsUtils } from './mobile/color-settings/utils';
export {
default as GlobalStylesContext,
diff --git a/packages/components/src/mobile/bottom-sheet/bottom-sheet-context.native.js b/packages/components/src/mobile/bottom-sheet/bottom-sheet-context.native.js
new file mode 100644
index 00000000000000..9252e5daf3ac4e
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet/bottom-sheet-context.native.js
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { Platform, UIManager } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import { createContext } from '@wordpress/element';
+
+// It's needed to set the following flags via UIManager
+// to have `LayoutAnimation` working on Android
+if (
+ Platform.OS === 'android' &&
+ UIManager.setLayoutAnimationEnabledExperimental
+) {
+ UIManager.setLayoutAnimationEnabledExperimental( true );
+}
+
+// Context in BottomSheet is necessary for controlling the
+// transition flow between subsheets and replacing a content inside them
+export const {
+ Provider: BottomSheetProvider,
+ Consumer: BottomSheetConsumer,
+} = createContext( {
+ // Specifies whether content is currently scrolling
+ isBottomSheetContentScrolling: false,
+ // Function called to enable scroll within bottom sheet
+ shouldEnableBottomSheetScroll: () => {},
+ // Function called to disable bottom sheet max height.
+ // E.g. used to extend bottom sheet on full screen in ColorPicker,
+ // which is helpful on small devices with set the largest font/display size.
+ shouldDisableBottomSheetMaxHeight: () => {},
+ // Callback that is called on closing bottom sheet
+ onCloseBottomSheet: () => {},
+ // Android only: Function called to control android hardware back button functionality
+ onHardwareButtonPress: () => {},
+ // Function called to navigate to another subsheet
+ onReplaceSubsheet: () => {},
+ // Object contains extra data passed to the current subsheet
+ extraProps: {},
+ // Specifies the currently active subsheet name
+ currentScreen: undefined,
+} );
diff --git a/packages/components/src/mobile/bottom-sheet/color-cell.native.js b/packages/components/src/mobile/bottom-sheet/color-cell.native.js
new file mode 100644
index 00000000000000..04f8ada26bacd3
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet/color-cell.native.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, chevronRight } from '@wordpress/icons';
+import { ColorIndicator } from '@wordpress/components';
+/**
+ * Internal dependencies
+ */
+import Cell from './cell';
+import styles from './styles.scss';
+
+export default function BottomSheetColorCell( props ) {
+ const { color, ...cellProps } = props;
+
+ return (
+
+ { color && (
+
+ ) }
+
+ |
+ );
+}
diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js
index 826356958c9b71..c1e25671e0c612 100644
--- a/packages/components/src/mobile/bottom-sheet/index.native.js
+++ b/packages/components/src/mobile/bottom-sheet/index.native.js
@@ -10,6 +10,8 @@ import {
ScrollView,
Keyboard,
StatusBar,
+ TouchableHighlight,
+ LayoutAnimation,
} from 'react-native';
import Modal from 'react-native-modal';
import SafeArea from 'react-native-safe-area';
@@ -31,14 +33,32 @@ import CyclePickerCell from './cycle-picker-cell';
import PickerCell from './picker-cell';
import SwitchCell from './switch-cell';
import RangeCell from './range-cell';
+import ColorCell from './color-cell';
import KeyboardAvoidingView from './keyboard-avoiding-view';
+import { BottomSheetProvider } from './bottom-sheet-context';
+
+const ANIMATION_DURATION = 300;
class BottomSheet extends Component {
constructor() {
super( ...arguments );
this.onSafeAreaInsetsUpdate = this.onSafeAreaInsetsUpdate.bind( this );
this.onScroll = this.onScroll.bind( this );
+ this.isScrolling = this.isScrolling.bind( this );
+ this.onShouldEnableScroll = this.onShouldEnableScroll.bind( this );
+ this.onShouldSetBottomSheetMaxHeight = this.onShouldSetBottomSheetMaxHeight.bind(
+ this
+ );
this.onDimensionsChange = this.onDimensionsChange.bind( this );
+ this.onCloseBottomSheet = this.onCloseBottomSheet.bind( this );
+ this.onHandleClosingBottomSheet = this.onHandleClosingBottomSheet.bind(
+ this
+ );
+ this.onHardwareButtonPress = this.onHardwareButtonPress.bind( this );
+ this.onHandleHardwareButtonPress = this.onHandleHardwareButtonPress.bind(
+ this
+ );
+ this.onReplaceSubsheet = this.onReplaceSubsheet.bind( this );
this.keyboardWillShow = this.keyboardWillShow.bind( this );
this.keyboardDidHide = this.keyboardDidHide.bind( this );
@@ -47,6 +67,13 @@ class BottomSheet extends Component {
bounces: false,
maxHeight: 0,
keyboardHeight: 0,
+ scrollEnabled: true,
+ isScrolling: false,
+ onCloseBottomSheet: null,
+ onHardwareButtonPress: null,
+ isMaxHeightSet: true,
+ currentScreen: '',
+ extraProps: {},
};
SafeArea.getSafeAreaInsetsForRootView().then(
@@ -110,6 +137,14 @@ class BottomSheet extends Component {
this.keyboardDidHideListener.remove();
}
+ componentDidUpdate( prevProps ) {
+ const { isVisible } = this.props;
+
+ if ( ! prevProps.isVisible && isVisible ) {
+ this.setState( { currentScreen: '' } );
+ }
+ }
+
onSafeAreaInsetsUpdate( result ) {
const { safeAreaBottomInset } = this.state;
if ( this.safeAreaEventSubscription === null ) {
@@ -127,7 +162,7 @@ class BottomSheet extends Component {
const statusBarHeight =
Platform.OS === 'android' ? StatusBar.currentHeight : 0;
- // `maxHeight` when modal is opened alon with a keyboard
+ // `maxHeight` when modal is opened along with a keyboard
const maxHeightWithOpenKeyboard =
0.95 *
( Dimensions.get( 'window' ).height -
@@ -169,12 +204,67 @@ class BottomSheet extends Component {
onScroll( { nativeEvent } ) {
if ( this.isCloseToTop( nativeEvent ) ) {
this.setState( { bounces: false } );
- }
- if ( this.isCloseToBottom( nativeEvent ) ) {
+ } else if ( this.isCloseToBottom( nativeEvent ) ) {
this.setState( { bounces: true } );
}
}
+ onShouldEnableScroll( value ) {
+ this.setState( { scrollEnabled: value } );
+ }
+
+ onShouldSetBottomSheetMaxHeight( value ) {
+ this.setState( { isMaxHeightSet: value } );
+ }
+
+ isScrolling( value ) {
+ this.setState( { isScrolling: value } );
+ }
+
+ onHandleClosingBottomSheet( action ) {
+ this.setState( { onCloseBottomSheet: action } );
+ }
+
+ onHandleHardwareButtonPress( action ) {
+ this.setState( { onHardwareButtonPress: action } );
+ }
+
+ onCloseBottomSheet() {
+ const { onClose } = this.props;
+ const { onCloseBottomSheet } = this.state;
+ if ( onCloseBottomSheet ) {
+ onCloseBottomSheet();
+ }
+ onClose();
+ }
+
+ onHardwareButtonPress() {
+ const { onClose } = this.props;
+ const { onHardwareButtonPress } = this.state;
+ if ( onHardwareButtonPress ) {
+ return onHardwareButtonPress();
+ }
+ return onClose();
+ }
+
+ onReplaceSubsheet( destination, extraProps, callback ) {
+ LayoutAnimation.configureNext(
+ LayoutAnimation.create(
+ ANIMATION_DURATION,
+ LayoutAnimation.Types.easeInEaseOut,
+ LayoutAnimation.Properties.opacity
+ )
+ );
+
+ this.setState(
+ {
+ currentScreen: destination,
+ extraProps: extraProps || {},
+ },
+ callback
+ );
+ }
+
render() {
const {
title = '',
@@ -185,12 +275,20 @@ class BottomSheet extends Component {
style = {},
contentStyle = {},
getStylesFromColorScheme,
- onClose,
onDismiss,
children,
...rest
} = this.props;
- const { maxHeight, bounces, safeAreaBottomInset } = this.state;
+ const {
+ maxHeight,
+ bounces,
+ safeAreaBottomInset,
+ isScrolling,
+ scrollEnabled,
+ isMaxHeightSet,
+ extraProps,
+ currentScreen,
+ } = this.state;
const panResponder = PanResponder.create( {
onMoveShouldSetPanResponder: ( evt, gestureState ) => {
@@ -231,25 +329,27 @@ class BottomSheet extends Component {
isVisible={ isVisible }
style={ styles.bottomModal }
animationInTiming={ 600 }
- animationOutTiming={ 250 }
+ animationOutTiming={ 300 }
backdropTransitionInTiming={ 50 }
backdropTransitionOutTiming={ 50 }
backdropOpacity={ 0.2 }
- onBackdropPress={ onClose }
- onBackButtonPress={ onClose }
- onSwipe={ onClose }
+ onBackdropPress={ this.onCloseBottomSheet }
+ onBackButtonPress={ this.onHardwareButtonPress }
+ onSwipe={ this.onCloseBottomSheet }
onDismiss={ Platform.OS === 'ios' ? onDismiss : undefined }
onModalHide={
Platform.OS === 'android' ? onDismiss : undefined
}
swipeDirection="down"
onMoveShouldSetResponder={
+ scrollEnabled &&
panResponder.panHandlers.onMoveShouldSetResponder
}
onMoveShouldSetResponderCapture={
+ scrollEnabled &&
panResponder.panHandlers.onMoveShouldSetResponderCapture
}
- onAccessibilityEscape={ onClose }
+ onAccessibilityEscape={ this.onCloseBottomSheet }
{ ...rest }
>
this.isScrolling( true ) }
+ onScrollEndDrag={ () => this.isScrolling( false ) }
scrollEventThrottle={ 16 }
- style={ { maxHeight } }
+ style={ isMaxHeightSet ? { maxHeight } : {} }
contentContainerStyle={ [
styles.content,
hideHeader && styles.emptyHeader,
contentStyle,
] }
+ scrollEnabled={ scrollEnabled }
+ automaticallyAdjustContentInsets={ false }
>
- { children }
+
+
+ <>{ children }>
+
+
@@ -300,5 +423,6 @@ ThemedBottomSheet.CyclePickerCell = CyclePickerCell;
ThemedBottomSheet.PickerCell = PickerCell;
ThemedBottomSheet.SwitchCell = SwitchCell;
ThemedBottomSheet.RangeCell = RangeCell;
+ThemedBottomSheet.ColorCell = ColorCell;
export default ThemedBottomSheet;
diff --git a/packages/components/src/mobile/bottom-sheet/navigation-header.native.js b/packages/components/src/mobile/bottom-sheet/navigation-header.native.js
new file mode 100644
index 00000000000000..dc3974a9140362
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet/navigation-header.native.js
@@ -0,0 +1,74 @@
+/**
+ * External dependencies
+ */
+import { View, TouchableWithoutFeedback, Text, Platform } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, chevronLeft, arrowLeft } from '@wordpress/icons';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+/**
+ * Internal dependencies
+ */
+import styles from './styles.scss';
+
+function BottomSheetNavigationHeader( { leftButtonOnPress, screen } ) {
+ const isIOS = Platform.OS === 'ios';
+
+ const bottomSheetHeaderTitleStyle = usePreferredColorSchemeStyle(
+ styles.bottomSheetHeaderTitle,
+ styles.bottomSheetHeaderTitleDark
+ );
+ const bottomSheetButtonTextStyle = usePreferredColorSchemeStyle(
+ styles.bottomSheetButtonText,
+ styles.bottomSheetButtonTextDark
+ );
+ const chevronLeftStyle = usePreferredColorSchemeStyle(
+ styles.chevronLeftIcon,
+ styles.chevronLeftIconDark
+ );
+ const arrowLeftStyle = usePreferredColorSchemeStyle(
+ styles.arrowLeftIcon,
+ styles.arrowLeftIconDark
+ );
+
+ return (
+
+
+
+ { isIOS ? (
+ <>
+
+
+ { __( 'Back' ) }
+
+ >
+ ) : (
+
+ ) }
+
+
+
+ { screen }
+
+
+
+ );
+}
+
+export default BottomSheetNavigationHeader;
diff --git a/packages/components/src/mobile/bottom-sheet/styles.native.scss b/packages/components/src/mobile/bottom-sheet/styles.native.scss
index 4550fb21d0c52d..9320388f065101 100644
--- a/packages/components/src/mobile/bottom-sheet/styles.native.scss
+++ b/packages/components/src/mobile/bottom-sheet/styles.native.scss
@@ -122,28 +122,28 @@
.cellLabel {
font-size: 17px;
- color: #2e4453;
+ color: $gray-dark;
margin-right: 12px;
flex-shrink: 1;
}
.cellLabelCentered {
font-size: 17px;
- color: #2e4453;
+ color: $gray-dark;
flex: 1;
text-align: center;
}
.cellLabelLeftAlignNoIcon {
font-size: 17px;
- color: #2e4453;
+ color: $gray-dark;
flex: 1;
margin-left: 0;
}
.cellValue {
font-size: 17px;
- color: #2e4453;
+ color: $gray-dark;
text-align: right;
flex: 1;
}
@@ -170,3 +170,70 @@
font-size: $text-editor-font-size;
color: $gray;
}
+
+// Color Cell
+
+.colorCircle {
+ width: 2 * $panel-padding;
+ height: 2 * $panel-padding;
+ border-radius: $panel-padding;
+}
+
+// Navigation Header
+
+.bottomSheetHeader {
+ flex-direction: row;
+ align-items: center;
+ padding-right: $panel-padding;
+ padding-left: $panel-padding;
+ min-height: 44px;
+}
+
+.bottomSheetBackButton {
+ flex-direction: row;
+ align-items: center;
+ flex: 1;
+}
+
+.chevronLeftIcon {
+ color: $blue-50;
+ margin-left: -13px;
+ margin-right: -13px;
+}
+
+.chevronLeftIconDark {
+ color: $blue-30;
+}
+
+.bottomSheetHeaderTitle {
+ color: $light-primary;
+ text-align: center;
+ font-weight: 600;
+ font-size: 16px;
+ flex: 2;
+}
+
+.bottomSheetHeaderTitleDark {
+ color: $dark-primary;
+}
+
+.bottomSheetButtonText {
+ color: $blue-50;
+ font-size: 16px;
+}
+
+.bottomSheetButtonTextDark {
+ color: $blue-30;
+}
+
+.bottomSheetRightSpace {
+ flex: 1;
+}
+
+.arrowLeftIcon {
+ color: $gray-60;
+}
+
+.arrowLeftIconDark {
+ color: $dark-secondary;
+}
diff --git a/packages/components/src/mobile/color-settings/index.native.js b/packages/components/src/mobile/color-settings/index.native.js
new file mode 100644
index 00000000000000..7164a08913477b
--- /dev/null
+++ b/packages/components/src/mobile/color-settings/index.native.js
@@ -0,0 +1,193 @@
+/**
+ * External dependencies
+ */
+import { View, Text, LayoutAnimation } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+/**
+ * Internal dependencies
+ */
+import ColorPicker from '../../color-picker';
+import ColorPalette from '../../color-palette';
+import ColorIndicator from '../../color-indicator';
+import NavigationHeader from '../bottom-sheet/navigation-header';
+import SegmentedControls from '../segmented-control';
+import { colorsUtils } from './utils';
+
+import styles from './style.scss';
+
+function ColorSettings( {
+ label,
+ onColorChange,
+ onGradientChange,
+ colorValue,
+ onReplaceSubsheet,
+ shouldEnableBottomSheetScroll,
+ shouldDisableBottomSheetMaxHeight,
+ isBottomSheetContentScrolling,
+ onCloseBottomSheet,
+ onHardwareButtonPress,
+ defaultSettings,
+} ) {
+ const { segments, subsheets, isGradient } = colorsUtils;
+ const selectedSegmentIndex = isGradient( colorValue ) ? 1 : 0;
+
+ const [ currentValue, setCurrentValue ] = useState( colorValue );
+ const [ isCustomScreen, setIsCustomScreen ] = useState( false );
+ const [ currentSegment, setCurrentSegment ] = useState(
+ segments[ selectedSegmentIndex ]
+ );
+
+ const isSolidSegment = currentSegment === segments[ 0 ];
+
+ const horizontalSeparatorStyle = usePreferredColorSchemeStyle(
+ styles.horizontalSeparator,
+ styles.horizontalSeparatorDark
+ );
+
+ useEffect( () => {
+ onHardwareButtonPress( () => {
+ if ( isCustomScreen ) {
+ onCustomScreenToggle( false );
+ } else {
+ onReplaceSubsheet(
+ subsheets[ 0 ],
+ {},
+ afterHardwareButtonPress()
+ );
+ }
+ } );
+ }, [ isCustomScreen ] );
+
+ useEffect( () => {
+ setCurrentSegment( segments[ selectedSegmentIndex ] );
+ shouldDisableBottomSheetMaxHeight( true );
+ onCloseBottomSheet( null );
+ }, [] );
+
+ function afterHardwareButtonPress() {
+ onHardwareButtonPress( null );
+ shouldDisableBottomSheetMaxHeight( true );
+ }
+
+ function onCustomScreenToggle( shouldShow ) {
+ LayoutAnimation.configureNext(
+ LayoutAnimation.create(
+ 300,
+ LayoutAnimation.Types.easeInEaseOut,
+ LayoutAnimation.Properties.opacity
+ )
+ );
+ setIsCustomScreen( shouldShow );
+ }
+
+ function setColor( color ) {
+ setCurrentValue( color );
+ if ( isSolidSegment && onColorChange && onGradientChange ) {
+ onColorChange( color );
+ onGradientChange( '' );
+ } else if ( isSolidSegment && onColorChange ) {
+ onColorChange( color );
+ } else if ( ! isSolidSegment && onGradientChange ) {
+ onGradientChange( color );
+ }
+ }
+
+ function getFooter() {
+ if ( onGradientChange ) {
+ return (
+ setCurrentSegment( item ) }
+ selectedIndex={ selectedSegmentIndex }
+ addonLeft={
+ currentValue && (
+
+ )
+ }
+ />
+ );
+ }
+ return (
+
+
+ { currentValue && (
+
+ ) }
+
+
+ { __( 'Select a color' ) }
+
+
+
+ );
+ }
+
+ return (
+
+ { isCustomScreen && (
+
+ {
+ onCustomScreenToggle( false );
+ } }
+ onCloseBottomSheet={ onCloseBottomSheet }
+ isBottomSheetContentScrolling={
+ isBottomSheetContentScrolling
+ }
+ />
+
+ ) }
+ { ! isCustomScreen && (
+
+
+ onReplaceSubsheet( subsheets[ 0 ] )
+ }
+ />
+ {
+ onCustomScreenToggle( true );
+ } }
+ shouldEnableBottomSheetScroll={
+ shouldEnableBottomSheetScroll
+ }
+ defaultSettings={ defaultSettings }
+ />
+
+ { getFooter() }
+
+ ) }
+
+ );
+}
+
+export default ColorSettings;
diff --git a/packages/components/src/mobile/color-settings/style.native.scss b/packages/components/src/mobile/color-settings/style.native.scss
new file mode 100644
index 00000000000000..1aa05378a08107
--- /dev/null
+++ b/packages/components/src/mobile/color-settings/style.native.scss
@@ -0,0 +1,40 @@
+
+.horizontalSeparator {
+ border-bottom-width: $border-width;
+ border-color: $light-gray-400;
+}
+
+.horizontalSeparatorDark {
+ border-color: $gray-70;
+}
+
+.colorIndicator {
+ width: 24px;
+ height: 24px;
+}
+
+.footer {
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ align-content: center;
+ padding: 12px $panel-padding;
+}
+
+.colorIndicator {
+ width: 24px;
+ height: 24px;
+}
+
+.selectColorText {
+ text-align: center;
+ color: $gray;
+ font-size: 16px;
+ flex: 2;
+ line-height: 24px;
+}
+
+.flex {
+ flex: 1;
+}
+
diff --git a/packages/components/src/mobile/color-settings/utils.native.js b/packages/components/src/mobile/color-settings/utils.native.js
new file mode 100644
index 00000000000000..1ff4e20cbbb462
--- /dev/null
+++ b/packages/components/src/mobile/color-settings/utils.native.js
@@ -0,0 +1,8 @@
+export const colorsUtils = {
+ subsheets: {
+ settings: 'Settings',
+ color: 'Color',
+ },
+ segments: [ 'Solid', 'Gradient' ],
+ isGradient: ( color ) => color?.includes( 'linear-gradient' ),
+};
diff --git a/packages/components/src/mobile/segmented-control/index.native.js b/packages/components/src/mobile/segmented-control/index.native.js
new file mode 100644
index 00000000000000..e97847e0c997da
--- /dev/null
+++ b/packages/components/src/mobile/segmented-control/index.native.js
@@ -0,0 +1,181 @@
+/**
+ * External dependencies
+ */
+import {
+ View,
+ TouchableWithoutFeedback,
+ Text,
+ Platform,
+ LayoutAnimation,
+ Animated,
+ Easing,
+} from 'react-native';
+import { take, values, map, reduce } from 'lodash';
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+const ANIMATION_DURATION = 200;
+
+const isIOS = Platform.OS === 'ios';
+
+const Segment = ( { isSelected, title, onPress, onLayout } ) => {
+ const isSelectedIOS = isIOS && isSelected;
+
+ const segmentStyle = [ styles.segment, isIOS && styles.segmentIOS ];
+
+ const textStyle = usePreferredColorSchemeStyle(
+ styles.buttonTextDefault,
+ styles.buttonTextDefaultDark
+ );
+ const selectedTextStyle = usePreferredColorSchemeStyle(
+ styles.buttonTextSelected,
+ styles.buttonTextSelectedDark
+ );
+ const shadowStyle = usePreferredColorSchemeStyle( styles.shadowIOS, {} );
+
+ return (
+
+
+
+
+ { title }
+
+
+
+
+ );
+};
+
+const SegmentedControls = ( {
+ segments,
+ segmentHandler,
+ selectedIndex,
+ addonLeft,
+ addonRight,
+} ) => {
+ const selectedSegmentIndex = selectedIndex || 0;
+ const [ activeSegmentIndex, setActiveSegmentIndex ] = useState(
+ selectedSegmentIndex
+ );
+ const [ segmentsDimensions, setSegmentsDimensions ] = useState( {
+ [ activeSegmentIndex ]: { width: 0, height: 0 },
+ } );
+ const [ positionAnimationValue ] = useState( new Animated.Value( 0 ) );
+
+ useEffect( () => {
+ setActiveSegmentIndex( selectedSegmentIndex );
+ segmentHandler( segments[ selectedSegmentIndex ] );
+ }, [] );
+
+ useEffect( () => {
+ positionAnimationValue.setValue(
+ calculateEndValue( activeSegmentIndex )
+ );
+ }, [ segmentsDimensions ] );
+
+ const containerStyle = usePreferredColorSchemeStyle(
+ styles.container,
+ styles.containerDark
+ );
+
+ function performAnimation( index ) {
+ Animated.timing( positionAnimationValue, {
+ toValue: calculateEndValue( index ),
+ duration: ANIMATION_DURATION,
+ easing: Easing.ease,
+ } ).start();
+ }
+
+ function calculateEndValue( index ) {
+ const { paddingLeft: offset } = isIOS
+ ? styles.containerIOS
+ : styles.container;
+ const widths = map( values( segmentsDimensions ), 'width' );
+ const widthsDistance = take( widths, index );
+ const widthsDistanceSum = reduce(
+ widthsDistance,
+ ( sum, n ) => sum + n
+ );
+
+ const endValue = index === 0 ? 0 : widthsDistanceSum;
+ return endValue + offset;
+ }
+
+ function onHandlePress( segment, index ) {
+ LayoutAnimation.configureNext(
+ LayoutAnimation.create(
+ ANIMATION_DURATION,
+ LayoutAnimation.Types.easeInEaseOut,
+ LayoutAnimation.Properties.opacity
+ )
+ );
+ setActiveSegmentIndex( index );
+ segmentHandler( segment );
+ performAnimation( index, segment );
+ }
+
+ function segmentOnLayout( event, index ) {
+ const { width, height } = event.nativeEvent.layout;
+
+ setSegmentsDimensions( {
+ ...segmentsDimensions,
+ [ index ]: { width, height },
+ } );
+ }
+
+ const selectedStyle = usePreferredColorSchemeStyle(
+ styles.selected,
+ styles.selectedDark
+ );
+
+ const width = segmentsDimensions[ activeSegmentIndex ].width;
+ const height = segmentsDimensions[ activeSegmentIndex ].height;
+
+ const outlineStyle = [ styles.outline, isIOS && styles.outlineIOS ];
+
+ return (
+
+ { addonLeft }
+
+ { segments.map( ( segment, index ) => {
+ return (
+ onHandlePress( segment, index ) }
+ isSelected={ activeSegmentIndex === index }
+ key={ index }
+ onLayout={ ( event ) =>
+ segmentOnLayout( event, index )
+ }
+ />
+ );
+ } ) }
+
+
+ { addonRight }
+
+ );
+};
+
+export default SegmentedControls;
diff --git a/packages/components/src/mobile/segmented-control/style.native.scss b/packages/components/src/mobile/segmented-control/style.native.scss
new file mode 100644
index 00000000000000..abacc818804b14
--- /dev/null
+++ b/packages/components/src/mobile/segmented-control/style.native.scss
@@ -0,0 +1,99 @@
+$container-height: 32px;
+$segment-height: 28px;
+$segment-spacing: 2px;
+$border-width-ios: $border-width / 2;
+$border-width-android: $border-width;
+$border-radius-ios: 7px;
+
+.segment {
+ border-radius: $segment-height / 2;
+ padding: 6px $panel-padding;
+ align-items: center;
+ justify-content: center;
+ margin-top: $segment-spacing + $border-width;
+ margin-bottom: $segment-spacing + $border-width;
+ z-index: 2;
+}
+
+.segmentIOS {
+ border-radius: $border-radius-ios;
+ margin-top: $segment-spacing + $border-width-ios;
+ margin-bottom: $segment-spacing + $border-width-ios;
+}
+
+.selected {
+ position: absolute;
+ background-color: $white;
+}
+
+.selectedDark {
+ background-color: $dark-quaternary;
+}
+
+.shadowIOS {
+ box-shadow: 0 0 8px $light-dim;
+ z-index: 2;
+}
+
+.container {
+ min-height: $container-height;
+ background-color: $light-ultra-dim;
+ border-radius: $container-height / 2;
+ align-items: center;
+ flex-direction: row;
+ align-self: center;
+ padding-left: $segment-spacing + $border-width;
+ padding-right: $segment-spacing + $border-width;
+}
+
+.containerDark {
+ background-color: $dark-ultra-dim;
+}
+
+.containerIOS {
+ border-radius: $border-radius-ios + 2 * $border-width-ios;
+ padding-left: $segment-spacing + $border-width-ios;
+ padding-right: $segment-spacing + $border-width-ios;
+}
+
+.outline {
+ position: absolute;
+ min-height: $segment-height - 2 * $border-width-ios;
+ border-width: $border-width;
+ border-radius: $container-height / 2;
+ border-color: $light-ultra-dim;
+}
+
+.outlineIOS {
+ border-radius: $border-radius-ios;
+ border-width: $border-width-ios;
+}
+
+.buttonTextDefault {
+ font-size: 12px;
+ font-weight: 600;
+ text-align: center;
+ color: $light-secondary;
+}
+
+.buttonTextDefaultDark {
+ color: #e0e0e0;
+}
+
+.buttonTextSelected {
+ color: $light-primary;
+}
+
+.buttonTextSelectedDark {
+ color: #f4f4f4;
+}
+
+.row {
+ flex-direction: row;
+ align-items: center;
+ padding: $panel-padding / 2 $panel-padding;
+}
+
+.flex {
+ flex: 1;
+}
diff --git a/packages/components/src/range-control/index.native.js b/packages/components/src/range-control/index.native.js
index 88ec3f870b1b0d..9488fdad160879 100644
--- a/packages/components/src/range-control/index.native.js
+++ b/packages/components/src/range-control/index.native.js
@@ -55,6 +55,7 @@ function RangeControl( {
afterIcon={ afterIcon }
allowReset={ allowReset }
defaultValue={ initialSliderValue }
+ separatorType={ separatorType }
{ ...props }
/>
);
diff --git a/test/native/setup.js b/test/native/setup.js
index 7d656b41eafb42..6f416974b2b17d 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -85,6 +85,10 @@ jest.mock( 'react-native-linear-gradient', () => () => 'LinearGradient', {
virtual: true,
} );
+jest.mock( 'react-native-hsv-color-picker', () => () => 'HsvColorPicker', {
+ virtual: true,
+} );
+
// Overwrite some native module mocks from `react-native` jest preset:
// https://github.com/facebook/react-native/blob/master/jest/setup.js
// to fix issue "TypeError: Cannot read property 'Commands' of undefined"