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"