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