From 7d440c74f4edd1bdb64c4fc378b8b9d9a926d1cb Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 4 Feb 2020 11:44:10 -0800 Subject: [PATCH] [change] modernize Switch Rewrite Switch to use function components and hooks. Rewrite the tests to replace enzyme with testing-library. --- .../exports/Switch/__tests__/index-test.js | 36 ++-- .../src/exports/Switch/index.js | 204 +++++++++--------- 2 files changed, 126 insertions(+), 114 deletions(-) diff --git a/packages/react-native-web/src/exports/Switch/__tests__/index-test.js b/packages/react-native-web/src/exports/Switch/__tests__/index-test.js index af390699..0435cfb9 100644 --- a/packages/react-native-web/src/exports/Switch/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/Switch/__tests__/index-test.js @@ -1,54 +1,58 @@ /* eslint-env jasmine, jest */ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; import Switch from '..'; -const checkboxSelector = 'input[type="checkbox"]'; +function findCheckbox(container) { + return container.firstChild.querySelector('input'); +} describe('components/Switch', () => { test('accessibilityLabel is applied to native checkbox', () => { - const component = shallow(); - expect(component.find(checkboxSelector).prop('aria-label')).toBe('switch'); + const { container } = render(); + expect(findCheckbox(container).getAttribute('aria-label')).toBe('switch'); }); describe('disabled', () => { test('when "false" a default checkbox is rendered', () => { - const component = shallow(); - expect(component.find(checkboxSelector).prop('disabled')).toBe(undefined); + const { container } = render(); + expect(findCheckbox(container).disabled).toBe(false); }); test('when "true" a disabled checkbox is rendered', () => { - const component = shallow(); - expect(component.find(checkboxSelector).prop('disabled')).toBe(true); + const { container } = render(); + expect(findCheckbox(container).disabled).toBe(true); }); }); describe('onValueChange', () => { test('when value is "false" it receives "true"', () => { const onValueChange = jest.fn(); - const component = shallow(); - component.find('input').simulate('change', { nativeEvent: { target: { checked: true } } }); + const { container } = render(); + const checkbox = findCheckbox(container); + checkbox.click(); // Needed to get ReactDOM to trigger 'change' event expect(onValueChange).toHaveBeenCalledWith(true); }); test('when value is "true" it receives "false"', () => { const onValueChange = jest.fn(); - const component = shallow(); - component.find('input').simulate('change', { nativeEvent: { target: { checked: false } } }); + const { container } = render(); + const checkbox = findCheckbox(container); + checkbox.click(); // Needed to get ReactDOM to trigger 'change' event expect(onValueChange).toHaveBeenCalledWith(false); }); }); describe('value', () => { test('when "false" an unchecked checkbox is rendered', () => { - const component = shallow(); - expect(component.find(checkboxSelector).prop('checked')).toBe(false); + const { container } = render(); + expect(findCheckbox(container).checked).toBe(false); }); test('when "true" a checked checkbox is rendered', () => { - const component = shallow(); - expect(component.find(checkboxSelector).prop('checked')).toBe(true); + const { container } = render(); + expect(findCheckbox(container).checked).toBe(true); }); }); }); diff --git a/packages/react-native-web/src/exports/Switch/index.js b/packages/react-native-web/src/exports/Switch/index.js index 1a84a2be..f25c3226 100644 --- a/packages/react-native-web/src/exports/Switch/index.js +++ b/packages/react-native-web/src/exports/Switch/index.js @@ -10,13 +10,12 @@ import type { ColorValue } from '../../types'; import type { ViewProps } from '../View'; -import applyNativeMethods from '../../modules/applyNativeMethods'; import createElement from '../createElement'; import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue'; import StyleSheet from '../StyleSheet'; import UIManager from '../UIManager'; import View from '../View'; -import React from 'react'; +import React, { forwardRef, useImperativeHandle, useRef } from 'react'; type SwitchProps = { ...ViewProps, @@ -33,110 +32,119 @@ const emptyObject = {}; const thumbDefaultBoxShadow = '0px 1px 3px rgba(0,0,0,0.5)'; const thumbFocusedBoxShadow = `${thumbDefaultBoxShadow}, 0 0 0 10px rgba(0,0,0,0.1)`; -class Switch extends React.Component { - _checkboxElement: HTMLInputElement; - _thumbElement: View; +const Switch = forwardRef((props, ref) => { + const { + accessibilityLabel, + activeThumbColor = '#009688', + activeTrackColor = '#A3D3CF', + disabled = false, + onValueChange, + style = emptyObject, + thumbColor = '#FAFAFA', + trackColor = '#939393', + value = false, + ...other + } = props; - static displayName = 'Switch'; + const checkboxRef = useRef(null); + const thumbRef = useRef(null); - blur() { - UIManager.blur(this._checkboxElement); + useImperativeHandle( + ref, + () => { + return { + blur() { + UIManager.blur(checkboxRef.current); + }, + focus() { + UIManager.focus(checkboxRef.current); + } + }; + }, + [checkboxRef] + ); + + function handleChange(event: Object) { + if (onValueChange != null) { + onValueChange(event.nativeEvent.target.checked); + } } - focus() { - UIManager.focus(this._checkboxElement); - } - - render() { - const { - accessibilityLabel, - activeThumbColor = '#009688', - activeTrackColor = '#A3D3CF', - disabled = false, - onValueChange, // eslint-disable-line - style = emptyObject, - thumbColor = '#FAFAFA', - trackColor = '#939393', - value = false, - ...other - } = this.props; - - const { height: styleHeight, width: styleWidth } = StyleSheet.flatten(style); - const height = styleHeight || 20; - const minWidth = multiplyStyleLengthValue(height, 2); - const width = styleWidth > minWidth ? styleWidth : minWidth; - const trackBorderRadius = multiplyStyleLengthValue(height, 0.5); - const trackCurrentColor = value - ? (trackColor != null && typeof trackColor === 'object' && trackColor.true) || - activeTrackColor - : (trackColor != null && typeof trackColor === 'object' && trackColor.false) || trackColor; - const thumbCurrentColor = value ? activeThumbColor : thumbColor; - const thumbHeight = height; - const thumbWidth = thumbHeight; - - const rootStyle = [styles.root, style, disabled && styles.cursorDefault, { height, width }]; - - const trackStyle = [ - styles.track, - { - backgroundColor: disabled ? '#D5D5D5' : trackCurrentColor, - borderRadius: trackBorderRadius - } - ]; - - const thumbStyle = [ - styles.thumb, - value && styles.thumbActive, - { - backgroundColor: disabled ? '#BDBDBD' : thumbCurrentColor, - height: thumbHeight, - marginStart: value ? multiplyStyleLengthValue(thumbWidth, -1) : 0, - width: thumbWidth - } - ]; - - const nativeControl = createElement('input', { - accessibilityLabel, - checked: value, - disabled: disabled, - onBlur: this._handleFocusState, - onChange: this._handleChange, - onFocus: this._handleFocusState, - ref: this._setCheckboxRef, - style: [styles.nativeControl, styles.cursorInherit], - type: 'checkbox' - }); - - return ( - - - - {nativeControl} - - ); - } - - _handleChange = (event: Object) => { - const { onValueChange } = this.props; - onValueChange && onValueChange(event.nativeEvent.target.checked); - }; - - _handleFocusState = (event: Object) => { + function handleFocusState(event: Object) { const isFocused = event.nativeEvent.type === 'focus'; const boxShadow = isFocused ? thumbFocusedBoxShadow : thumbDefaultBoxShadow; - if (this._thumbElement) { - this._thumbElement.setNativeProps({ style: { boxShadow } }); + if (thumbRef.current != null) { + thumbRef.current.setNativeProps({ style: { boxShadow } }); } - }; + } - _setCheckboxRef = element => { - this._checkboxElement = element; - }; + const { height: styleHeight, width: styleWidth } = StyleSheet.flatten(style); + const height = styleHeight || 20; + const minWidth = multiplyStyleLengthValue(height, 2); + const width = styleWidth > minWidth ? styleWidth : minWidth; + const trackBorderRadius = multiplyStyleLengthValue(height, 0.5); + const trackCurrentColor = (function() { + if (value === true) { + if (trackColor != null && typeof trackColor === 'object') { + return trackColor.true; + } else { + return activeTrackColor; + } + } else { + if (trackColor != null && typeof trackColor === 'object') { + return trackColor.false; + } else { + return trackColor; + } + } + })(); + const thumbCurrentColor = value ? activeThumbColor : thumbColor; + const thumbHeight = height; + const thumbWidth = thumbHeight; - _setThumbRef = element => { - this._thumbElement = element; - }; -} + const rootStyle = [styles.root, style, disabled && styles.cursorDefault, { height, width }]; + + const trackStyle = [ + styles.track, + { + backgroundColor: disabled ? '#D5D5D5' : trackCurrentColor, + borderRadius: trackBorderRadius + } + ]; + + const thumbStyle = [ + styles.thumb, + value && styles.thumbActive, + { + backgroundColor: disabled ? '#BDBDBD' : thumbCurrentColor, + height: thumbHeight, + marginStart: value ? multiplyStyleLengthValue(thumbWidth, -1) : 0, + width: thumbWidth + } + ]; + + const nativeControl = createElement('input', { + accessibilityLabel, + checked: value, + disabled: disabled, + onBlur: handleFocusState, + onChange: handleChange, + onFocus: handleFocusState, + ref: checkboxRef, + style: [styles.nativeControl, styles.cursorInherit], + type: 'checkbox' + }); + + return ( + + + + {nativeControl} + + ); +}); + +Switch.displayName = 'Switch'; const styles = StyleSheet.create({ root: { @@ -177,4 +185,4 @@ const styles = StyleSheet.create({ } }); -export default applyNativeMethods(Switch); +export default Switch;