From f73ebe6b644df05ad6f9fa1f5b3036b7eff6b8ff Mon Sep 17 00:00:00 2001 From: Luke Walczak Date: Fri, 1 Nov 2019 13:57:12 +0100 Subject: [PATCH] [RNMobile] Add mobile Spacer component (#17896) * Revert package-lock.json * Setting attributes for spacer height * Correct the condition for setting maximum value * Small code refactor * Improve Accessibility in range-cell * More accessibility improvements * Small code refactor * Styling spacer refactor * Move logic to RangeCell * Keep Slider along with TextInput within RangeCell * Small cleanup * Fix missing binds * Fix focusing slider on iphoneX when VO is on * Adjust a11y voice over * Refactor pointerEvents when screen reader is on * Announce current value when finished * Improve a11y * Fix a11y on iPhoneX * Update info for translators --- packages/block-library/src/index.native.js | 2 + .../block-library/src/spacer/edit.native.js | 62 ++++++ .../src/spacer/editor.native.scss | 18 ++ .../src/mobile/bottom-sheet/cell.native.js | 33 ++- .../mobile/bottom-sheet/range-cell.native.js | 208 ++++++++++++++---- .../range-cell.native.scss} | 7 - .../src/mobile/slider/index.native.js | 118 ---------- 7 files changed, 283 insertions(+), 165 deletions(-) create mode 100644 packages/block-library/src/spacer/edit.native.js create mode 100644 packages/block-library/src/spacer/editor.native.scss rename packages/components/src/mobile/{slider/styles.scss => bottom-sheet/range-cell.native.scss} (72%) delete mode 100644 packages/components/src/mobile/slider/index.native.js diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 0d06a9df9f11a..c5485580538d1 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 0000000000000..8a638245dde84 --- /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 0000000000000..cc8f941a968b1 --- /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 52ff00b6b5b85..7f44944060a56 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 07b343e9ec078..e3eb50a6e4e21 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 326880b621807..93099e7a8a641 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 111d9987e973d..0000000000000 --- 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;