diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js
index 0d06a9df9f11af..c5485580538d18 100644
--- a/packages/block-library/src/index.native.js
+++ b/packages/block-library/src/index.native.js
@@ -144,6 +144,8 @@ export const registerCoreBlocks = () => {
mediaText,
// eslint-disable-next-line no-undef
!! __DEV__ ? group : null,
+ // eslint-disable-next-line no-undef
+ !! __DEV__ ? spacer : null,
].forEach( registerBlock );
setDefaultBlockName( paragraph.name );
diff --git a/packages/block-library/src/spacer/edit.native.js b/packages/block-library/src/spacer/edit.native.js
new file mode 100644
index 00000000000000..8a638245dde845
--- /dev/null
+++ b/packages/block-library/src/spacer/edit.native.js
@@ -0,0 +1,62 @@
+
+/**
+ * External dependencies
+ */
+import { View } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import {
+ PanelBody,
+ BottomSheet,
+} from '@wordpress/components';
+import { withPreferredColorScheme } from '@wordpress/compose';
+import { useState, useEffect } from '@wordpress/element';
+import {
+ InspectorControls,
+} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import styles from './editor.scss';
+
+const minSpacerHeight = 20;
+const maxSpacerHeight = 500;
+
+const SpacerEdit = ( { isSelected, attributes, setAttributes, getStylesFromColorScheme } ) => {
+ const { height } = attributes;
+ const [ sliderSpacerMaxHeight, setSpacerMaxHeight ] = useState( height );
+
+ // Height defined on the web can be higher than
+ // `maxSpacerHeight`, so there is a need to `setSpacerMaxHeight`
+ // after the initial render.
+ useEffect( () => {
+ setSpacerMaxHeight( height > maxSpacerHeight ? height * 2 : maxSpacerHeight );
+ }, [] );
+
+ const defaultStyle = getStylesFromColorScheme( styles.staticSpacer, styles.staticDarkSpacer );
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default withPreferredColorScheme( SpacerEdit );
diff --git a/packages/block-library/src/spacer/editor.native.scss b/packages/block-library/src/spacer/editor.native.scss
new file mode 100644
index 00000000000000..cc8f941a968b18
--- /dev/null
+++ b/packages/block-library/src/spacer/editor.native.scss
@@ -0,0 +1,18 @@
+.staticSpacer {
+ height: 20px;
+ background-color: transparent;
+ border: $border-width dashed $light-gray-500;
+ border-radius: 1px;
+}
+
+.staticDarkSpacer {
+ border: $border-width dashed rgba($color: $light-gray-500, $alpha: 0.3);
+}
+
+.selectedSpacer {
+ border: $border-width * 2 solid $blue-30;
+}
+
+.rangeCellContainer {
+ padding-bottom: 16px;
+}
diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js
index 52ff00b6b5b856..7f44944060a56e 100644
--- a/packages/components/src/mobile/bottom-sheet/cell.native.js
+++ b/packages/components/src/mobile/bottom-sheet/cell.native.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { TouchableOpacity, Text, View, TextInput, I18nManager } from 'react-native';
+import { TouchableOpacity, Text, View, TextInput, I18nManager, AccessibilityInfo } from 'react-native';
import { isEmpty } from 'lodash';
/**
@@ -23,7 +23,10 @@ class BottomSheetCell extends Component {
super( ...arguments );
this.state = {
isEditingValue: props.autoFocus || false,
+ isScreenReaderEnabled: false,
};
+
+ this.handleScreenReaderToggled = this.handleScreenReaderToggled.bind( this );
}
componentDidUpdate() {
@@ -32,8 +35,31 @@ class BottomSheetCell extends Component {
}
}
+ componentDidMount() {
+ AccessibilityInfo.addEventListener(
+ 'screenReaderChanged',
+ this.handleScreenReaderToggled,
+ );
+
+ AccessibilityInfo.isScreenReaderEnabled().then( ( isScreenReaderEnabled ) => {
+ this.setState( { isScreenReaderEnabled } );
+ } );
+ }
+
+ componentWillUnmount() {
+ AccessibilityInfo.removeEventListener(
+ 'screenReaderChanged',
+ this.handleScreenReaderToggled,
+ );
+ }
+
+ handleScreenReaderToggled( isScreenReaderEnabled ) {
+ this.setState( { isScreenReaderEnabled } );
+ }
+
render() {
const {
+ accessible,
accessibilityLabel,
accessibilityHint,
accessibilityRole,
@@ -157,10 +183,11 @@ class BottomSheetCell extends Component {
};
const iconStyle = getStylesFromColorScheme( styles.icon, styles.iconDark );
+ const containerPointerEvents = this.state.isScreenReaderEnabled && accessible ? 'none' : 'auto';
return (
) }
-
+
{ icon && (
diff --git a/packages/components/src/mobile/bottom-sheet/range-cell.native.js b/packages/components/src/mobile/bottom-sheet/range-cell.native.js
index 07b343e9ec0781..e3eb50a6e4e216 100644
--- a/packages/components/src/mobile/bottom-sheet/range-cell.native.js
+++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.js
@@ -1,46 +1,180 @@
/**
* External dependencies
*/
-import { Platform } from 'react-native';
+import { Platform, AccessibilityInfo, findNodeHandle, TextInput, Slider } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { _x, __, sprintf } from '@wordpress/i18n';
+import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import Cell from './cell';
-import Slider from '../slider';
-
-export default function BottomSheetRangeCell( props ) {
- const {
- value,
- defaultValue,
- onChangeValue,
- minimumValue = 0,
- maximumValue = 10,
- disabled,
- step = 1,
- minimumTrackTintColor = '#00669b',
- maximumTrackTintColor = Platform.OS === 'ios' ? '#e9eff3' : '#909090',
- thumbTintColor = Platform.OS === 'ios' ? '#fff' : '#00669b',
- ...cellProps
- } = props;
-
- return (
- |
-
- |
- );
+import styles from './range-cell.scss';
+
+class BottomSheetRangeCell extends Component {
+ constructor( props ) {
+ super( props );
+ this.handleToggleFocus = this.handleToggleFocus.bind( this );
+ this.handleChange = this.handleChange.bind( this );
+ this.handleValueSave = this.handleValueSave.bind( this );
+ this.handleReset = this.handleReset.bind( this );
+ this.onChangeValue = this.onChangeValue.bind( this );
+ this.onCellPress = this.onCellPress.bind( this );
+
+ const initialValue = this.validateInput( props.value || props.defaultValue || props.minimumValue );
+
+ this.state = { accessible: true, sliderValue: initialValue, initialValue, hasFocus: false };
+ }
+
+ componentDidUpdate( ) {
+ const reset = this.props.value === null;
+ if ( reset ) {
+ this.handleReset();
+ }
+ }
+
+ componentWillUnmount() {
+ this.handleToggleFocus();
+ }
+
+ handleChange( text ) {
+ if ( ! isNaN( Number( text ) ) ) {
+ this.setState( { sliderValue: text } );
+ this.announceCurrentValue( text );
+ }
+ }
+
+ handleReset() {
+ this.handleValueSave( this.props.defaultValue || this.state.initialValue );
+ }
+
+ handleToggleFocus( validateInput = true ) {
+ const newState = { hasFocus: ! this.state.hasFocus };
+
+ if ( validateInput ) {
+ const sliderValue = this.validateInput( this.state.sliderValue );
+ this.handleValueSave( sliderValue );
+ }
+
+ this.setState( newState );
+ }
+
+ validateInput( text ) {
+ const { minimumValue, maximumValue } = this.props;
+ if ( ! text ) {
+ return minimumValue;
+ }
+ if ( typeof text === 'number' ) {
+ return Math.min( Math.max( text, minimumValue ), maximumValue );
+ }
+ return Math.min( Math.max( text.replace( /[^0-9]/g, '' ).replace( /^0+(?=\d)/, '' ), minimumValue ), maximumValue );
+ }
+
+ handleValueSave( text ) {
+ if ( ! isNaN( Number( text ) ) ) {
+ this.onChangeValue( text );
+ this.setState( { sliderValue: text } );
+ this.announceCurrentValue( text );
+ }
+ }
+
+ onChangeValue( initialValue ) {
+ const { minimumValue, maximumValue, setAttributes, attribute } = this.props;
+
+ let sliderValue = initialValue;
+ if ( sliderValue < minimumValue ) {
+ sliderValue = minimumValue;
+ } else if ( sliderValue > maximumValue ) {
+ sliderValue = maximumValue;
+ }
+ setAttributes( {
+ [ attribute ]: sliderValue,
+ } );
+ }
+
+ onCellPress() {
+ this.setState( { accessible: false } );
+ if ( this.sliderRef ) {
+ const reactTag = findNodeHandle( this.sliderRef );
+ AccessibilityInfo.setAccessibilityFocus( reactTag );
+ }
+ }
+
+ announceCurrentValue( value ) {
+ const announcement = sprintf( __( 'Current value is %s' ), value );
+ AccessibilityInfo.announceForAccessibility( announcement );
+ }
+
+ render() {
+ const {
+ value,
+ defaultValue,
+ minimumValue = 0,
+ maximumValue = 10,
+ disabled,
+ step = 1,
+ minimumTrackTintColor = '#00669b',
+ maximumTrackTintColor = Platform.OS === 'ios' ? '#e9eff3' : '#909090',
+ thumbTintColor = Platform.OS === 'android' && '#00669b',
+ ...cellProps
+ } = this.props;
+
+ const { hasFocus, sliderValue, accessible } = this.state;
+
+ const accessibilityLabel =
+ sprintf(
+ /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: Current value. */
+ _x( '%1$s. Current value is %2$s', 'Slider for picking a number inside a range' ),
+ cellProps.label, value
+ );
+
+ return (
+
+ {
+ this.sliderRef = slider;
+ } }
+ style={ styles.slider }
+ accessibilityRole={ 'adjustable' }
+ />
+
+ |
+ );
+ }
}
+
+export default BottomSheetRangeCell;
diff --git a/packages/components/src/mobile/slider/styles.scss b/packages/components/src/mobile/bottom-sheet/range-cell.native.scss
similarity index 72%
rename from packages/components/src/mobile/slider/styles.scss
rename to packages/components/src/mobile/bottom-sheet/range-cell.native.scss
index 326880b621807a..93099e7a8a6415 100644
--- a/packages/components/src/mobile/slider/styles.scss
+++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.scss
@@ -1,10 +1,3 @@
-.sliderContainer {
- flex: 1;
- flex-direction: row;
- align-content: center;
- justify-content: space-evenly;
-}
-
.slider {
flex-grow: 1;
}
diff --git a/packages/components/src/mobile/slider/index.native.js b/packages/components/src/mobile/slider/index.native.js
deleted file mode 100644
index 111d9987e973db..00000000000000
--- a/packages/components/src/mobile/slider/index.native.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * External dependencies
- */
-import { Slider as RNSlider, TextInput, View } from 'react-native';
-
-/**
- * WordPress dependencies
- */
-import { Component } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import styles from './styles.scss';
-
-class Slider extends Component {
- constructor( props ) {
- super( props );
- this.handleToggleFocus = this.handleToggleFocus.bind( this );
- this.handleChange = this.handleChange.bind( this );
- this.handleValueSave = this.handleValueSave.bind( this );
- this.handleReset = this.handleReset.bind( this );
-
- const initialValue = this.validateInput( props.value || props.defaultValue || props.minimumValue );
-
- this.state = { hasFocus: false, initialValue, sliderValue: initialValue };
- }
-
- componentDidUpdate( ) {
- const reset = this.props.value === null;
- if ( reset ) {
- this.handleReset();
- }
- }
-
- handleToggleFocus( validateInput = true ) {
- const newState = { hasFocus: ! this.state.hasFocus };
-
- if ( validateInput ) {
- const sliderValue = this.validateInput( this.state.sliderValue );
- this.handleValueSave( sliderValue );
- }
-
- this.setState( newState );
- }
-
- validateInput( text ) {
- const { minimumValue, maximumValue } = this.props;
- if ( ! text ) {
- return minimumValue;
- }
- if ( typeof text === 'number' ) {
- return Math.min( Math.max( text, minimumValue ), maximumValue );
- }
- return Math.min( Math.max( text.replace( /[^0-9]/g, '' ).replace( /^0+(?=\d)/, '' ), minimumValue ), maximumValue );
- }
-
- handleChange( text ) {
- if ( ! isNaN( Number( text ) ) ) {
- this.setState( { sliderValue: text } );
- }
- }
-
- handleValueSave( text ) {
- if ( ! isNaN( Number( text ) ) ) {
- if ( this.props.onChangeValue ) {
- this.props.onChangeValue( text );
- }
- this.setState( { sliderValue: text } );
- }
- }
-
- handleReset() {
- this.handleValueSave( this.props.defaultValue || this.state.initialValue );
- }
-
- render() {
- const {
- minimumValue,
- maximumValue,
- disabled,
- step,
- minimumTrackTintColor,
- maximumTrackTintColor,
- thumbTintColor,
- } = this.props;
-
- const { hasFocus, sliderValue } = this.state;
-
- return (
-
-
-
-
- );
- }
-}
-
-export default Slider;