mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
683b825b32
Summary: Changelog: [Internal] - Update Switch to allow injected implementations ## General understanding of the component The main flow of Switch is pretty straightforward, basically passing the props to the respective native component which uses the platform switch views on Android / iOS The interesting parts of Switch is the fact that it's a controlled component -- meaning that this component sees the JS value prop as the source of truth about the state of this component and any time the native value of the switch is out of sync with the JS value prop, we send a command `setNativeValue` to keep those in sync. The problems I ran into: * Keeping native and JS in sync * Switch skips animation occassionally on iOS simulator ## How we keep native and JS in sync By construction, the native value of the component should be the same as JS value. Then, we know the native value has changed whenever the callback `handleChange` has been fired. **Before** In the handleChange callback, we'd set an [instance variable `lastNativeValue` with the updated value](https://fburl.com/diffusion/nangxzoh) and force update. Then, in `componentDidUpdate`, we'd send the native commands if we determine that `lastNativeValue` and the `value` prop were out of sync. **After** For our modern implementation, we store the value of the switch as reported by `handleChange` and check it against the `props.value` of the switch. If they are out of sync then we will update the native switch via the switch command. **Why is `native` an object?** We need to run the `useLayoutEffect` every time `handleChange` is called independent of the value of the switch. **Why not move the logic of dispatching commands to `handleChange`?**? This would change behavior from old implementation where we would call `onChange` handlers and then re-render. Additionally, the logic to run the native commands were on `componentDidUpdate` which would've run when any prop changed. We can simplify this down to caring only when `props.value` updates. ## Unsolved, existing issue: Switches skip animation occasionally * This occurs both in the modern and old versions of Switch and I've only seen this on iOS simulators. It occurs most frequently in the "events" example where two switches' values are synced and most often the first transition after we navigate to the example surface. I have not been able to reproduce this behavior on device. * Something must be triggering a re-render in the middle of native's animation.. {F564595576} Reviewed By: nadiia, kacieb Differential Revision: D27381306 fbshipit-source-id: 06d13c6fe1ff181443f4b8dd27fb1ac65e071962
260 lines
7.1 KiB
JavaScript
260 lines
7.1 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
* @generate-docs
|
|
*/
|
|
|
|
import Platform from '../../Utilities/Platform';
|
|
import * as React from 'react';
|
|
import StyleSheet from '../../StyleSheet/StyleSheet';
|
|
import SwitchInjection from './SwitchInjection';
|
|
|
|
import AndroidSwitchNativeComponent, {
|
|
Commands as AndroidSwitchCommands,
|
|
} from './AndroidSwitchNativeComponent';
|
|
import SwitchNativeComponent, {
|
|
Commands as SwitchCommands,
|
|
} from './SwitchNativeComponent';
|
|
|
|
import type {ColorValue} from '../../StyleSheet/StyleSheet';
|
|
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
|
|
import type {ViewProps} from '../View/ViewPropTypes';
|
|
|
|
type SwitchChangeEvent = SyntheticEvent<
|
|
$ReadOnly<{|
|
|
value: boolean,
|
|
|}>,
|
|
>;
|
|
|
|
export type Props = $ReadOnly<{|
|
|
...ViewProps,
|
|
|
|
/**
|
|
If true the user won't be able to toggle the switch.
|
|
|
|
@default false
|
|
*/
|
|
disabled?: ?boolean,
|
|
|
|
/**
|
|
The value of the switch. If true the switch will be turned on.
|
|
|
|
@default false
|
|
*/
|
|
value?: ?boolean,
|
|
|
|
/**
|
|
Color of the foreground switch grip. If this is set on iOS, the switch grip will lose its drop shadow.
|
|
*/
|
|
thumbColor?: ?ColorValue,
|
|
|
|
/**
|
|
Custom colors for the switch track.
|
|
|
|
_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`](https://reactnative.dev/docs/switch#ios_backgroundColor).
|
|
*/
|
|
trackColor?: ?$ReadOnly<{|
|
|
false?: ?ColorValue,
|
|
true?: ?ColorValue,
|
|
|}>,
|
|
|
|
/**
|
|
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).
|
|
*/
|
|
ios_backgroundColor?: ?ColorValue,
|
|
|
|
/**
|
|
Invoked 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> | void,
|
|
|
|
/**
|
|
Invoked 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> | void,
|
|
|}>;
|
|
|
|
/**
|
|
Renders a boolean input.
|
|
|
|
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.
|
|
|
|
```SnackPlayer name=Switch
|
|
import React, { useState } from "react";
|
|
import { View, Switch, StyleSheet } from "react-native";
|
|
|
|
const App = () => {
|
|
const [isEnabled, setIsEnabled] = useState(false);
|
|
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Switch
|
|
trackColor={{ false: "#767577", true: "#81b0ff" }}
|
|
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
|
|
ios_backgroundColor="#3e3e3e"
|
|
onValueChange={toggleSwitch}
|
|
value={isEnabled}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
alignItems: "center",
|
|
justifyContent: "center"
|
|
}
|
|
});
|
|
|
|
export default App;
|
|
```
|
|
*/
|
|
class Switch extends React.Component<Props> {
|
|
_nativeSwitchRef: ?React.ElementRef<
|
|
typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent,
|
|
>;
|
|
_lastNativeValue: ?boolean;
|
|
|
|
render(): React.Node {
|
|
const {
|
|
disabled,
|
|
ios_backgroundColor,
|
|
onChange,
|
|
onValueChange,
|
|
style,
|
|
thumbColor,
|
|
trackColor,
|
|
value,
|
|
...props
|
|
} = this.props;
|
|
|
|
const trackColorForFalse = trackColor?.false;
|
|
const trackColorForTrue = trackColor?.true;
|
|
|
|
if (Platform.OS === 'android') {
|
|
const platformProps = {
|
|
enabled: disabled !== true,
|
|
on: value === true,
|
|
style,
|
|
thumbTintColor: thumbColor,
|
|
trackColorForFalse: trackColorForFalse,
|
|
trackColorForTrue: trackColorForTrue,
|
|
trackTintColor: value === true ? trackColorForTrue : trackColorForFalse,
|
|
};
|
|
|
|
return (
|
|
<AndroidSwitchNativeComponent
|
|
{...props}
|
|
{...platformProps}
|
|
accessibilityRole={props.accessibilityRole ?? 'switch'}
|
|
onChange={this._handleChange}
|
|
onResponderTerminationRequest={returnsFalse}
|
|
onStartShouldSetResponder={returnsTrue}
|
|
ref={this._handleSwitchNativeComponentRef}
|
|
/>
|
|
);
|
|
} else {
|
|
const platformProps = {
|
|
disabled,
|
|
onTintColor: trackColorForTrue,
|
|
style: StyleSheet.compose(
|
|
{height: 31, width: 51},
|
|
StyleSheet.compose(
|
|
style,
|
|
ios_backgroundColor == null
|
|
? null
|
|
: {
|
|
backgroundColor: ios_backgroundColor,
|
|
borderRadius: 16,
|
|
},
|
|
),
|
|
),
|
|
thumbTintColor: thumbColor,
|
|
tintColor: trackColorForFalse,
|
|
value: value === true,
|
|
};
|
|
|
|
return (
|
|
<SwitchNativeComponent
|
|
{...props}
|
|
{...platformProps}
|
|
accessibilityRole={props.accessibilityRole ?? 'switch'}
|
|
onChange={this._handleChange}
|
|
onResponderTerminationRequest={returnsFalse}
|
|
onStartShouldSetResponder={returnsTrue}
|
|
ref={this._handleSwitchNativeComponentRef}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
// This is necessary in case native updates the switch and JS decides
|
|
// that the update should be ignored and we should stick with the value
|
|
// that we have in JS.
|
|
const value = this.props.value === true;
|
|
const nativeValue = this._lastNativeValue !== value ? value : null;
|
|
|
|
if (
|
|
nativeValue != null &&
|
|
this._nativeSwitchRef &&
|
|
this._nativeSwitchRef.setNativeProps
|
|
) {
|
|
if (Platform.OS === 'android') {
|
|
AndroidSwitchCommands.setNativeValue(
|
|
this._nativeSwitchRef,
|
|
nativeValue,
|
|
);
|
|
} else {
|
|
SwitchCommands.setValue(this._nativeSwitchRef, nativeValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
_handleChange = (event: SwitchChangeEvent) => {
|
|
if (this.props.onChange != null) {
|
|
this.props.onChange(event);
|
|
}
|
|
|
|
if (this.props.onValueChange != null) {
|
|
this.props.onValueChange(event.nativeEvent.value);
|
|
}
|
|
|
|
this._lastNativeValue = event.nativeEvent.value;
|
|
this.forceUpdate();
|
|
};
|
|
|
|
_handleSwitchNativeComponentRef = (
|
|
ref: ?React.ElementRef<
|
|
typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent,
|
|
>,
|
|
) => {
|
|
this._nativeSwitchRef = ref;
|
|
};
|
|
}
|
|
|
|
const returnsFalse = () => false;
|
|
const returnsTrue = () => true;
|
|
|
|
module.exports = (SwitchInjection.unstable_Switch ??
|
|
Switch: React.AbstractComponent<React.ElementConfig<typeof Switch>>);
|