Skip to content

Commit

Permalink
[RNMobile] Add mobile Spacer component (#17896)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lukewalczak authored and pinarol committed Nov 1, 2019
1 parent afdd6df commit f73ebe6
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 165 deletions.
2 changes: 2 additions & 0 deletions packages/block-library/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
62 changes: 62 additions & 0 deletions packages/block-library/src/spacer/edit.native.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={ [ defaultStyle, isSelected && styles.selectedSpacer, { height } ] }>
<InspectorControls>
<PanelBody title={ __( 'Spacer Settings' ) } >
<BottomSheet.RangeCell
icon={ 'admin-settings' }
label={ __( 'Height in pixels' ) }
minimumValue={ minSpacerHeight }
maximumValue={ sliderSpacerMaxHeight }
separatorType={ 'none' }
value={ height }
attribute="height"
setAttributes={ setAttributes }
style={ styles.rangeCellContainer }
/>
</PanelBody>
</InspectorControls>
</View>
);
};

export default withPreferredColorScheme( SpacerEdit );
18 changes: 18 additions & 0 deletions packages/block-library/src/spacer/editor.native.scss
Original file line number Diff line number Diff line change
@@ -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;
}
33 changes: 30 additions & 3 deletions packages/components/src/mobile/bottom-sheet/cell.native.js
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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() {
Expand All @@ -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,
Expand Down Expand Up @@ -157,10 +183,11 @@ class BottomSheetCell extends Component {
};

const iconStyle = getStylesFromColorScheme( styles.icon, styles.iconDark );
const containerPointerEvents = this.state.isScreenReaderEnabled && accessible ? 'none' : 'auto';

return (
<TouchableOpacity
accessible={ ! this.state.isEditingValue }
accessible={ accessible !== undefined ? accessible : ! this.state.isEditingValue }
accessibilityLabel={ getAccessibilityLabel() }
accessibilityRole={ accessibilityRole || 'button' }
accessibilityHint={ isValueEditable ?
Expand All @@ -174,7 +201,7 @@ class BottomSheetCell extends Component {
{ drawTopSeparator && (
<View style={ separatorStyle() } />
) }
<View style={ styles.cellContainer }>
<View style={ styles.cellContainer } pointerEvents={ containerPointerEvents }>
<View style={ styles.cellRowContainer }>
{ icon && (
<View style={ styles.cellRowContainer }>
Expand Down
208 changes: 171 additions & 37 deletions packages/components/src/mobile/bottom-sheet/range-cell.native.js
Original file line number Diff line number Diff line change
@@ -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 (
<Cell
editable={ false }
{ ...cellProps }
>
<Slider
value={ value }
defaultValue={ defaultValue }
disabled={ disabled }
step={ step }
minimumValue={ minimumValue }
maximumValue={ maximumValue }
minimumTrackTintColor={ minimumTrackTintColor }
maximumTrackTintColor={ maximumTrackTintColor }
thumbTintColor={ thumbTintColor }
onChangeValue={ onChangeValue }
/>
</Cell>
);
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 (
<Cell
{ ...cellProps }
accessibilityRole={ 'none' }
editable={ false }
accessible={ accessible }
onPress={ this.onCellPress }
accessibilityLabel={ accessibilityLabel }
accessibilityHint={
/* translators: accessibility text (hint for focusing a slider) */
__( 'Double tap to change the value using slider' )
}
>
<Slider
value={ this.validateInput( sliderValue ) }
defaultValue={ defaultValue }
disabled={ disabled }
step={ step }
minimumValue={ minimumValue }
maximumValue={ maximumValue }
minimumTrackTintColor={ minimumTrackTintColor }
maximumTrackTintColor={ maximumTrackTintColor }
thumbTintColor={ thumbTintColor }
onValueChange={ this.handleChange }
onSlidingComplete={ this.handleValueSave }
ref={ ( slider ) => {
this.sliderRef = slider;
} }
style={ styles.slider }
accessibilityRole={ 'adjustable' }
/>
<TextInput
style={ [ styles.sliderTextInput, hasFocus ? styles.isSelected : {} ] }
onChangeText={ this.handleChange }
onFocus={ this.handleToggleFocus }
onBlur={ this.handleToggleFocus }
keyboardType="number-pad"
returnKeyType="done"
value={ `${ sliderValue }` }
/>
</Cell>
);
}
}

export default BottomSheetRangeCell;
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
.sliderContainer {
flex: 1;
flex-direction: row;
align-content: center;
justify-content: space-evenly;
}

.slider {
flex-grow: 1;
}
Expand Down
Loading

0 comments on commit f73ebe6

Please sign in to comment.