From 965adee109b86cc36f39a713237ade9592f13dad Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Tue, 31 Jul 2018 20:54:02 -0700 Subject: [PATCH] RN: Revamp Switch Component Summary: Revamps the Switch API with the goal of increasing the pit of success: - Introduce `trackColor` which encourages callers configuring the color to set colors for both cases. - Introduce `ios_backgroundColor` which allows customizing the iOS-only background fill color. - Deprecate `tintColor` because it is not obvious that this is for the `false` case. - Deprecate `onTintColor` because the prop is named unconventionally like a callback. - Renamed `thumbTintColor` to `thumbColor`. This revision also cleans up the `Switch` component in the following ways: - More precise Flow types for native components. - Inline iOS-specific style (so that the code gets stripped on Android). - Minor documentaiton cleanup. After this commit, all deprecated props will continue working. Next, I plan to introduce warnings. Eventually (e.g. in a couple releases), we can drop support for the deprecated props. Reviewed By: TheSavior Differential Revision: D9081343 fbshipit-source-id: c5eb949047dd7a0ffa72621839999d38e58cada8 --- Libraries/Components/Switch/Switch.js | 244 +++++++++++++++++--------- Libraries/Types/CoreEventTypes.js | 6 + 2 files changed, 171 insertions(+), 79 deletions(-) diff --git a/Libraries/Components/Switch/Switch.js b/Libraries/Components/Switch/Switch.js index c67adb866..c585c7d6f 100644 --- a/Libraries/Components/Switch/Switch.js +++ b/Libraries/Components/Switch/Switch.js @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow + * @format */ 'use strict'; @@ -15,149 +15,235 @@ const React = require('React'); const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); -const nullthrows = require('fbjs/lib/nullthrows'); const requireNativeComponent = require('requireNativeComponent'); +import type {SwitchChangeEvent} from 'CoreEventTypes'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; export type Props = $ReadOnly<{| ...ViewProps, - /** - * The value of the switch. If true the switch will be turned on. - * Default value is false. - */ - value?: ?boolean, /** - * If true the user won't be able to toggle the switch. - * Default value is false. + * Whether the switch is disabled. Defaults to false. */ disabled?: ?boolean, /** - * Switch change handler. + * Boolean value of the switch. Defaults to false. + */ + value?: ?boolean, + + /** + * Custom color for the switch thumb. + */ + thumbColor?: ?ColorValue, + + /** + * Custom colors for the switch track. * - * Invoked with the event when the value changes. For getting the value - * the switch was changed to use onValueChange instead. + * NOTE: On iOS when the switch value is false, the track shrinks into the + * border. If you want to change the color of the background exposed by the + * shrunken track, use `ios_backgroundColor`. */ - onChange?: ?Function, + trackColor?: ?$ReadOnly<{| + false?: ?ColorValue, + true?: ?ColorValue, + |}>, /** - * Invoked with the new value when the value changes. + * On iOS, custom color for the background. This background color can be seen + * either when the switch value is false or when the switch is disabled (and + * the switch is translucent). */ - onValueChange?: ?Function, + ios_backgroundColor?: ?ColorValue, /** - * Used to locate this view in end-to-end tests. + * Called when the user tries to change the value of the switch. + * + * Receives the change event as an argument. If you want to only receive the + * new value, use `onValueChange` instead. + */ + onChange?: ?(event: SwitchChangeEvent) => Promise | void, + + /** + * Called when the user tries to change the value of the switch. + * + * Receives the new value as an argument. If you want to instead receive an + * event, use `onChange`. + */ + onValueChange?: ?(value: boolean) => Promise | void, + + /** + * Identifier used to find this view in tests. */ testID?: ?string, /** - * Border color on iOS and background color on Android when the switch is turned off. + * @deprecated See `thumbColor`. + */ + thumbTintColor?: ?ColorValue, + + /** + * @deprecated See `trackColor.false`. */ tintColor?: ?ColorValue, /** - * Background color when the switch is turned on. + * @deprecated See `trackColor.true`. */ onTintColor?: ?ColorValue, +|}>; - /** - * Color of the foreground switch grip. - */ - thumbTintColor?: ?ColorValue, +// @see ReactSwitchManager.java +type NativeAndroidProps = $ReadOnly<{| + ...ViewProps, + enabled?: ?boolean, + on?: ?boolean, + onChange?: ?(event: SwitchChangeEvent) => mixed, + thumbTintColor?: ?string, + trackTintColor?: ?string, +|}>; + +// @see RCTSwitchManager.m +type NativeIOSProps = $ReadOnly<{| + ...ViewProps, + disabled?: ?boolean, + onChange?: ?(event: SwitchChangeEvent) => mixed, + onTintColor?: ?string, + thumbTintColor?: ?string, + tintColor?: ?string, + value?: ?boolean, |}>; type NativeSwitchType = Class< ReactNative.NativeComponent< $ReadOnly<{| - ...Props, - enabled?: ?boolean, - on?: ?boolean, - trackTintColor?: ?ColorValue, + ...NativeAndroidProps, + ...NativeIOSProps, |}>, >, >; -const RCTSwitch: NativeSwitchType = +const NativeSwitch: NativeSwitchType = Platform.OS === 'android' ? (requireNativeComponent('AndroidSwitch'): any) : (requireNativeComponent('RCTSwitch'): any); /** - * Renders a boolean input. + * A visual toggle between two mutually exclusive states. * * This is a controlled component that requires an `onValueChange` callback that * updates the `value` prop in order for the component to reflect user actions. - * If the `value` prop is not updated, the component will continue to render - * the supplied `value` prop instead of the expected result of any user actions. - * - * @keyword checkbox - * @keyword toggle + * If the `value` prop is not updated, the component will continue to render the + * supplied `value` prop instead of the expected result of any user actions. */ class Switch extends React.Component { - static defaultProps = { - value: false, - disabled: false, - }; - - _rctSwitch: ?React.ElementRef = null; - - _onChange = (event: Object) => { - if (Platform.OS === 'android') { - nullthrows(this._rctSwitch).setNativeProps({on: this.props.value}); - } else { - nullthrows(this._rctSwitch).setNativeProps({value: this.props.value}); - } - - this.props.onChange && this.props.onChange(event); - this.props.onValueChange && - this.props.onValueChange(event.nativeEvent.value); - }; + _nativeSwitchRef: ?React.ElementRef; render() { - const props = { - ...this.props, - onStartShouldSetResponder: () => true, - onResponderTerminationRequest: () => false, - }; + const { + disabled, + ios_backgroundColor, + onChange, + onTintColor, + onValueChange, + style, + testID, + thumbColor, + thumbTintColor, + tintColor, + trackColor, + value, + ...props + } = this.props; + + // Support deprecated color props. + let _thumbColor = thumbColor; + let _trackColorForFalse = trackColor?.false; + let _trackColorForTrue = trackColor?.true; + + // TODO: Add a warning when used. + if (thumbTintColor != null) { + _thumbColor = thumbTintColor; + } + if (tintColor != null) { + _trackColorForFalse = tintColor; + } + if (onTintColor != null) { + _trackColorForTrue = onTintColor; + } const platformProps = Platform.OS === 'android' - ? { - enabled: !this.props.disabled, - on: this.props.value, - style: this.props.style, - trackTintColor: this.props.value - ? this.props.onTintColor - : this.props.tintColor, - } - : { + ? ({ + enabled: disabled !== true, + on: value === true, + style, + thumbTintColor: _thumbColor, + trackTintColor: + value === true ? _trackColorForTrue : _trackColorForFalse, + }: NativeAndroidProps) + : ({ + disabled, + onTintColor: _trackColorForTrue, style: StyleSheet.compose( - styles.rctSwitchIOS, - this.props.style, + {height: 31, width: 51}, + StyleSheet.compose( + style, + ios_backgroundColor == null + ? null + : { + backgroundColor: ios_backgroundColor, + borderRadius: 16, + }, + ), ), - }; + thumbTintColor: _thumbColor, + tintColor: _trackColorForFalse, + value: value === true, + }: NativeIOSProps); return ( - { - this._rctSwitch = ref; - }} - onChange={this._onChange} + onChange={this._handleChange} + onResponderTerminationRequest={returnsFalse} + onStartShouldSetResponder={returnsTrue} + ref={this._handleNativeSwitchRef} /> ); } + + _handleChange = (event: SwitchChangeEvent) => { + if (this._nativeSwitchRef == null) { + return; + } + + // Force value of native switch in order to control it. + const value = this.props.value === true; + if (Platform.OS === 'android') { + this._nativeSwitchRef.setNativeProps({on: value}); + } else { + this._nativeSwitchRef.setNativeProps({value}); + } + + if (this.props.onChange != null) { + this.props.onChange(event); + } + + if (this.props.onValueChange != null) { + this.props.onValueChange(event.nativeEvent.value); + } + }; + + _handleNativeSwitchRef = (ref: ?React.ElementRef) => { + this._nativeSwitchRef = ref; + }; } -const styles = StyleSheet.create({ - rctSwitchIOS: { - height: 31, - width: 51, - }, -}); +const returnsFalse = () => false; +const returnsTrue = () => true; module.exports = Switch; diff --git a/Libraries/Types/CoreEventTypes.js b/Libraries/Types/CoreEventTypes.js index 0714dc231..58a0cc5ed 100644 --- a/Libraries/Types/CoreEventTypes.js +++ b/Libraries/Types/CoreEventTypes.js @@ -80,3 +80,9 @@ export type ScrollEvent = SyntheticEvent< zoomScale: number, |}>, >; + +export type SwitchChangeEvent = SyntheticEvent< + $ReadOnly<{| + value: boolean, + |}>, +>;