diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index de7449cd..940c7eab 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -26,6 +26,9 @@ toc: false - 🆕 Support (`precision` `filter` ) new props - ⚡️ Deprecated (`mode`)prop; date format by [Day.js](https://day.js.org/docs/en/parse/string-format) - ❗️Delete **ImagePicker** and remove dependence `@react-native-camera-roll/camera-roll` +- **Switch** + - fix: `checked` prop support controlled mode [#1325](https://github.com/ant-design/ant-design-mobile-rn/issues/1325) + - feat: `onChange` prop when the Promise is returned, the loading status will be displayed automatically ### 5.0.5 `2023-11-08` diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 26dce033..4557ee01 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -15,7 +15,7 @@ toc: false --- ### 5.1.0 -`2024-02-20` +`2024-02-23` - 重构 **Picker** & **PickerView** - 🔥 重构开发并移除 `@react-native-picker/picker` 依赖 - 💄 基于 `ScrollView {snapToInterval}` 开发并支持`web`端 @@ -26,6 +26,9 @@ toc: false - 🆕 新增 (`precision` `filter` ) 属性支持 - ⚡️ 废弃(`mode`)属性;时间格式引用[Day.js](https://day.js.org/docs/zh-CN/parse/string-format) - ❗️删除 **ImagePicker** 并移除 `@react-native-camera-roll/camera-roll` 依赖 +- **Switch** + - fix: `checked`属性支持全受控模式 [#1325](https://github.com/ant-design/ant-design-mobile-rn/issues/1325) + - feat: `onChange`属性当返回 Promise 时,会自动显示加载状态 ### 5.0.5 `2023-11-08` diff --git a/components/_util/hooks/useAnimations.tsx b/components/_util/hooks/useAnimations.tsx index b48d974a..3189c538 100644 --- a/components/_util/hooks/useAnimations.tsx +++ b/components/_util/hooks/useAnimations.tsx @@ -45,6 +45,7 @@ export function useAnimatedTiming(): [Animated.Value, animateType] { useNativeDriver, }).start() }, + // @ts-ignore [animatedValue], ) var [animatedValue] = useAnimate({ diff --git a/components/_util/isPromise.ts b/components/_util/isPromise.ts new file mode 100644 index 00000000..d7013d76 --- /dev/null +++ b/components/_util/isPromise.ts @@ -0,0 +1,5 @@ +export function isPromise(obj: unknown): obj is Promise { + return ( + !!obj && typeof obj === 'object' && typeof (obj as any).then === 'function' + ) +} diff --git a/components/switch/PropsType.tsx b/components/switch/PropsType.tsx index 1709ea1a..c75252e3 100644 --- a/components/switch/PropsType.tsx +++ b/components/switch/PropsType.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react' import { ColorValue } from 'react-native' @@ -15,6 +14,9 @@ export interface SwitchPropsType { title?: string checkedChildren?: string | React.ReactNode unCheckedChildren?: string | React.ReactNode - onChange?: (checked: boolean) => void + onChange?: (checked: boolean) => void | Promise + /** + * @deprecated + */ onPress?: (checked: boolean) => void } diff --git a/components/switch/Switch.tsx b/components/switch/Switch.tsx index 88bf4053..d88f844b 100644 --- a/components/switch/Switch.tsx +++ b/components/switch/Switch.tsx @@ -1,13 +1,20 @@ import classNames from 'classnames' import useMergedState from 'rc-util/lib/hooks/useMergedState' import * as React from 'react' -import { Animated, Easing, StyleProp, View, ViewStyle } from 'react-native' +import { + Animated, + Easing, + Pressable, + StyleProp, + View, + ViewStyle, +} from 'react-native' +import devWarning from '../_util/devWarning' +import { useAnimatedTiming } from '../_util/hooks/useAnimations' +import { isPromise } from '../_util/isPromise' import RNActivityIndicator from '../activity-indicator' -import ButtonWave from '../button/ButtonWave' import { WithTheme, WithThemeStyles } from '../style' import AntmView from '../view/index' -import devWarning from '../_util/devWarning' -import { useAnimatedTiming } from '../_util/hooks/useAnimations' import { SwitchPropsType } from './PropsType' import SwitchStyles, { SwitchStyle } from './style/index' @@ -21,6 +28,7 @@ export interface SwitchProps const AnimatedView = Animated.createAnimatedComponent(AntmView) const AntmSwitch = ({ prefixCls = 'switch', + style, checked, defaultChecked, disabled, @@ -40,29 +48,38 @@ const AntmSwitch = ({ 'Switch', '`value` is not a valid prop, do you mean `checked`?', ) - // Compatible with old code : checked without onChange was alse onControlled - const checkedRef = React.useRef() - if (checkedRef.current === undefined) { - checkedRef.current = checked ?? defaultChecked - } const [innerChecked, setInnerChecked] = useMergedState(false, { - value: checkedRef.current, + value: checked, defaultValue: defaultChecked, + onChange: onChange, }) + const [innerLoading, setInnerLoading] = useMergedState(false, { + value: loading, + }) + + const PADDING = 11 // switch旁白最低宽度 + const TRACK_PADDING = 5 // switch轨道按压变形宽度 + const BORDER_WIDTH = 2 // switch轨道边框宽度 + + // switch height measure + const [itemHeight, setHeight] = React.useState(31) + const wrapperMeasure = React.useCallback((e) => { + setHeight(e.nativeEvent.layout.height) + }, []) //disabled when loading - disabled = disabled || loading + disabled = disabled || innerLoading // animate1 const [animatedValue, animate] = useAnimatedTiming() const transitionMargin = { marginLeft: animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [25, 7], + outputRange: [itemHeight - BORDER_WIDTH, BORDER_WIDTH], }), marginRight: animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [7, 25], + outputRange: [BORDER_WIDTH, itemHeight - BORDER_WIDTH], }), } @@ -71,48 +88,59 @@ const AntmSwitch = ({ const transitionWidth = { width: animatedValue2.interpolate({ inputRange: [0, 1], - outputRange: [22, 28], + outputRange: [ + itemHeight - BORDER_WIDTH * 2, + itemHeight - BORDER_WIDTH * 2 + TRACK_PADDING, + ], }), left: !innerChecked ? animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [0, 10], + outputRange: [BORDER_WIDTH, PADDING], }) : undefined, right: innerChecked ? animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [10, 0], + outputRange: [PADDING, BORDER_WIDTH], }) : 0, } //initial animate React.useEffect(() => { - if (checkedRef.current) { + if (innerChecked) { animate({}) animate2({ toValue: 0 }) } else { animate({ toValue: 0 }) } - }, [animate, animate2, checkedRef]) + }, [animate, animate2, innerChecked, itemHeight]) - function triggerChange(newChecked: boolean) { + async function triggerChange(newChecked: boolean) { if (!disabled) { - checkedRef.current = newChecked setInnerChecked(newChecked) - onChange?.(newChecked) + const result = onChange?.(newChecked) + if (isPromise(result)) { + setInnerLoading(true) + try { + await result + setInnerLoading(false) + } catch (e) { + setInnerLoading(false) + throw e + } + } return newChecked } return innerChecked } - function onInternalClick() { - const ret = triggerChange(!innerChecked) + async function onInternalClick() { + const ret = await triggerChange(!innerChecked) // [Legacy] trigger onClick with value onPress?.(ret) - animate({ toValue: ret ? 1 : 0 }) } function onPressIn() { @@ -134,6 +162,7 @@ const AntmSwitch = ({ }) .split(' ') .map((a) => styles[a]) + .concat([style]) const ant_switch_inner = classNames(`${prefixCls}_inner`, { [`${prefixCls}_inner_checked`]: innerChecked, @@ -149,6 +178,13 @@ const AntmSwitch = ({ }) .split(' ') .map((a) => styles[a]) + .concat([ + { + width: itemHeight - BORDER_WIDTH * 2, + height: itemHeight - BORDER_WIDTH * 2, + borderRadius: itemHeight - BORDER_WIDTH * 2, + }, + ]) // color props const Color = innerChecked @@ -170,51 +206,44 @@ const AntmSwitch = ({ const accessibilityState = { checked: innerChecked, disabled, - busy: loading, + busy: innerLoading, } return ( - - - - - {loading && ( - - )} - - - {innerChecked ? checkedChildren : unCheckedChildren} - - - - + {...restProps} + disabled={disabled} + onPressIn={onPressIn} + onPressOut={onPressOut} + onPress={onInternalClick}> + + + {innerLoading && ( + + )} + + + {innerChecked ? checkedChildren : unCheckedChildren} + + + ) }} diff --git a/components/switch/__tests__/__snapshots__/demo.test.js.snap b/components/switch/__tests__/__snapshots__/demo.test.js.snap index fc8c9bfd..6d6f09cc 100644 --- a/components/switch/__tests__/__snapshots__/demo.test.js.snap +++ b/components/switch/__tests__/__snapshots__/demo.test.js.snap @@ -101,103 +101,90 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": false, - "disabled": undefined, + "disabled": false, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > - - + - + } + /> @@ -319,107 +306,94 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": false, "disabled": true, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > - - + - + } + /> @@ -626,105 +600,92 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": true, - "disabled": undefined, + "disabled": false, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > - - + - 开启 - - + } + > + 开 + @@ -800,105 +761,92 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": false, - "disabled": undefined, + "disabled": false, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > - - + - 0 - - + } + > + 0 + @@ -974,119 +922,106 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": true, - "disabled": undefined, + "disabled": false, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > - + - + - -  - - +  + @@ -1214,125 +1149,111 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` "disabled": true, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > + - - - - + - + @@ -1413,125 +1334,111 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` "disabled": true, } } - style={ - Array [ - Object { - "alignItems": "center", - "borderColor": "transparent", - "borderRadius": 20, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "minHeight": 25, - "minWidth": 50, - "overflow": "hidden", - "padding": 0, - "position": "relative", - }, - Object { - "padding": 1, - }, - ] - } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} > + - - - - + - + @@ -1653,107 +1560,299 @@ exports[`renders ./components/switch/demo/basic.tsx correctly 1`] = ` accessibilityRole="switch" accessibilityState={ Object { - "busy": undefined, + "busy": false, "checked": true, - "disabled": undefined, + "disabled": false, } } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + > + + + + + + + + + + + + + + + 异步 + + + + + + + - - + + + - + - + - + } + /> diff --git a/components/switch/demo/basic.tsx b/components/switch/demo/basic.tsx index a3d1b1b8..c803266d 100644 --- a/components/switch/demo/basic.tsx +++ b/components/switch/demo/basic.tsx @@ -7,6 +7,7 @@ export default class SwitchExample extends React.Component { super(props) this.state = { disabled: true, + checked: false, } } @@ -15,6 +16,20 @@ export default class SwitchExample extends React.Component { disabled: !this.state.disabled, }) } + + sleep1s = () => { + return new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + } + + onChangeAsync = async (val: boolean) => { + await this.sleep1s() + this.setState({ + checked: val, + }) + } + render() { return ( @@ -36,8 +51,8 @@ export default class SwitchExample extends React.Component { } @@ -66,6 +81,17 @@ export default class SwitchExample extends React.Component { color="red" + + + }> + onChange 返回 Promise + + ) } diff --git a/components/switch/index.en-US.md b/components/switch/index.en-US.md index 10c96be6..29e1833a 100644 --- a/components/switch/index.en-US.md +++ b/components/switch/index.en-US.md @@ -7,14 +7,17 @@ title: Switch Select between two status, e.g. Select On or Off. ### Rules -- Used in `List` only. -- There is no need to add extra text to describe the value of `Switch` . +- This is a **controlled component** that requires an `onChange` callback that updates the `checked` prop in order for the component to reflect user actions. ## API Properties | Descrition | Type | Default -----------|------------|------|-------- | checked | Whether is checked by default | Boolean | false | +| defaultChecked | Whether to open initially | Boolean | false | | disabled | whether is disabled | Boolean | false | +| loading | Loading status | Boolean | false | +| onChange | The callback function when changing, when the Promise is returned, the loading status will be displayed automatically | `(val: boolean) => void \| Promise` | - | | color | Background color when the switch is turned on. | String | #4dd865 | -| onChange | The callback function that is triggered when the selected state changes. | (checked: bool): void | - | +| checkedChildren | Selected content | ReactNode | - | +| unCheckedChildren | Non-selected content | ReactNode | - | \ No newline at end of file diff --git a/components/switch/index.zh-CN.md b/components/switch/index.zh-CN.md index d6327d0a..e4997856 100644 --- a/components/switch/index.zh-CN.md +++ b/components/switch/index.zh-CN.md @@ -8,18 +8,17 @@ subtitle: 滑动开关 在两个互斥对象进行选择,eg:选择开或关。 ### 规则 -- 只在 List 中使用。 -- 避免增加额外的文案来描述当前 Switch 的值。 +- 这是一个“受控组件”。你必须使用`onChange`回调来更新`checked`属性以响应用户的操作。 ## API 属性 | 说明 | 类型 | 默认值 ----|-----|------|------ | checked | 是否默认选中 | Boolean | false | -| checkedChildren | 选中时的内容 | String \| ReactNode | 无 | +| defaultChecked | 初始是否打开 | Boolean | false | | disabled | 是否不可修改 | Boolean | false | -| loading | 加载中的开关 -| unCheckedChildren | 非选中时的内容 | String \| ReactNode | 无 | -| onChange | change 事件触发的回调函数 | (checked: bool): void | 无 | -| color | 开关打开后的颜色 | String | #4dd865 | -| onPress | click事件触发的回调函数,当switch为disabled时,入参的值始终是默认传入的checked值。 | (checked: bool): void | 无 | +| loading | 加载中的开关 | Boolean | false | +| onChange | 变化时的回调函数,当返回 Promise 时,会自动显示加载状态 | `(val: boolean) => void \| Promise` | 无 | +| color | 开关打开后的颜色 | String | `#4dd865` | +| checkedChildren | 选中时的内容 | ReactNode | 无 | +| unCheckedChildren | 非选中时的内容 | ReactNode | 无 | \ No newline at end of file diff --git a/components/switch/style/index.tsx b/components/switch/style/index.tsx index 3f95edae..923b5580 100644 --- a/components/switch/style/index.tsx +++ b/components/switch/style/index.tsx @@ -20,36 +20,48 @@ export default (theme: Theme) => StyleSheet.create({ switch: { position: 'relative', - minWidth: 50, - minHeight: 25, + width: 55, + height: 31, display: 'flex', flexDirection: 'row', alignItems: 'center', padding: 0, - borderRadius: 20, - borderWidth: 1, - borderColor: 'transparent', - overflow: 'hidden', + borderRadius: 31, }, // handle switch_handle: { position: 'absolute', - width: 22, - height: 22, - borderRadius: 999, + width: 27, + height: 27, + borderRadius: 27, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', backgroundColor: '#ffffff', + shadowColor: 'rgb(0, 35, 11)', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.2, + shadowRadius: 10, + elevation: 10, }, // inner switch_inner: { color: '#fff', fontSize: 12, + flex: 1, + textAlign: 'center', + alignItems: 'center', + justifyContent: 'center', }, switch_inner_checked: { marginLeft: 7, - marginRight: 25, + marginRight: 27, }, switch_inner_unchecked: { - marginLeft: 25, + marginLeft: 27, marginRight: 7, }, // checked