From 0234e9c822c558fe7adbcbb08a9bc2912dda2819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 14:58:40 +0800 Subject: [PATCH 01/24] feat: Modal add useModal(#1383) --- components/modal/AlertContainer.tsx | 5 +- components/modal/Modal.tsx | 122 +++++++++---------- components/modal/ModalView.tsx | 52 +++++--- components/modal/OperationContainer.tsx | 6 +- components/modal/PromptContainer.tsx | 11 +- components/modal/PropsType.tsx | 32 +++-- components/modal/index.en-US.md | 133 +++++++++++++++++++-- components/modal/index.tsx | 4 +- components/modal/index.zh-CN.md | 124 ++++++++++++++++++-- components/modal/useModal/index.tsx | 150 ++++++++++++++++++++++++ 10 files changed, 516 insertions(+), 123 deletions(-) create mode 100644 components/modal/useModal/index.tsx diff --git a/components/modal/AlertContainer.tsx b/components/modal/AlertContainer.tsx index f3e110c4f..2068b33f5 100644 --- a/components/modal/AlertContainer.tsx +++ b/components/modal/AlertContainer.tsx @@ -1,9 +1,9 @@ import React, { isValidElement } from 'react' import { ScrollView, Text, TextStyle } from 'react-native' import Modal from './Modal' -import { Action, CallbackOnBackHandler } from './PropsType' +import { Action, CallbackOnBackHandler, ModalPropsType } from './PropsType' -export interface AlertContainerProps { +export interface AlertContainerProps extends Pick { title: React.ReactNode content: React.ReactNode actions: Action[] @@ -68,6 +68,7 @@ export default class AlertContainer extends React.Component< title={title} visible={this.state.visible} footer={footer} + modalType={this.props.modalType} onAnimationEnd={onAnimationEnd} onRequestClose={this.onBackAndroid} bodyStyle={{ diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index 6739ca30c..9765b8c64 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -2,33 +2,24 @@ import React from 'react' import { KeyboardAvoidingView, Platform, - StyleProp, Text, - TextStyle, TouchableHighlight, TouchableWithoutFeedback, View, - ViewStyle, } from 'react-native' import { getComponentLocale } from '../_util/getLocale' import { LocaleContext } from '../locale-provider' -import { WithTheme, WithThemeStyles } from '../style' +import { WithTheme } from '../style' import RCModal from './ModalView' -import { CallbackOnBackHandler, ModalPropsType } from './PropsType' +import { ModalPropsType } from './PropsType' import alert from './alert' import zh_CN from './locale/zh_CN' import operation from './operation' import prompt from './prompt' -import modalStyles, { ModalStyle } from './style/index' +import modalStyles from './style/index' +import useModal from './useModal' -export interface ModalProps - extends ModalPropsType, - WithThemeStyles { - style?: StyleProp - bodyStyle?: StyleProp - onRequestClose?: CallbackOnBackHandler - children?: React.ReactNode -} +export interface ModalProps extends ModalPropsType {} class AntmModal extends React.Component { static defaultProps = { @@ -48,6 +39,7 @@ class AntmModal extends React.Component { static alert: typeof alert static operation: typeof operation static prompt: typeof prompt + static useModal: typeof useModal static contextType = LocaleContext @@ -67,6 +59,7 @@ class AntmModal extends React.Component { bodyStyle, onAnimationEnd, onRequestClose, + modalType, } = this.props // tslint:disable-next-line:variable-name @@ -158,31 +151,30 @@ class AntmModal extends React.Component { ) : null return ( - - - - - {title ? ( - {title} - ) : null} - {children} - {footerDom} - {closableDom} - - - - + + + + {title ? ( + {title} + ) : null} + {children} + {footerDom} + {closableDom} + + + ) } if (popup) { @@ -194,39 +186,37 @@ class AntmModal extends React.Component { animType = 'slide-down' } return ( - - - {children} - - + + {children} + ) } if (animType === 'slide') { animType = undefined } return ( - - - {children} - - + + {children} + ) }} diff --git a/components/modal/ModalView.tsx b/components/modal/ModalView.tsx index 00cbdf360..9a9219ddc 100644 --- a/components/modal/ModalView.tsx +++ b/components/modal/ModalView.tsx @@ -4,6 +4,7 @@ import { BackHandler, Dimensions, Easing, + Modal as NativeModal, StyleProp, StyleSheet, TouchableWithoutFeedback, @@ -11,7 +12,7 @@ import { ViewStyle, } from 'react-native' import Portal from '../portal' -import { CallbackOnBackHandler } from './PropsType' +import { ModalPropsType } from './PropsType' const styles = StyleSheet.create({ wrap: { @@ -36,19 +37,23 @@ const styles = StyleSheet.create({ const screen = Dimensions.get('window') -export interface IModalPropTypes { - wrapStyle?: StyleProp +export interface IModalPropTypes + extends Pick< + ModalPropsType, + | 'animateAppear' + | 'children' + | 'maskClosable' + | 'modalType' + | 'onAnimationEnd' + | 'onClose' + | 'onRequestClose' + | 'visible' + > { + animationDuration?: number + animationType: 'none' | 'fade' | 'slide-up' | 'slide-down' maskStyle?: StyleProp style?: {} - children?: React.ReactNode - animationType: 'none' | 'fade' | 'slide-up' | 'slide-down' - animationDuration?: number - visible: boolean - maskClosable?: boolean - animateAppear?: boolean - onClose?: () => void // onDismiss - onAnimationEnd?: (visible: boolean) => void // onShow - onRequestClose?: CallbackOnBackHandler + wrapStyle?: StyleProp } export default class RCModal extends React.Component { @@ -62,6 +67,7 @@ export default class RCModal extends React.Component { maskClosable: true, onClose() {}, onAnimationEnd(_visible: boolean) {}, + modalType: 'portal', } as IModalPropTypes animMask: any @@ -234,8 +240,26 @@ export default class RCModal extends React.Component { }, } + let Modal = Portal as React.ElementType + switch (props.modalType) { + case 'modal': + Modal = (p) => + React.createElement(NativeModal, { + visible: true, + transparent: true, + ...p, + }) + break + case 'view': + Modal = (p) => + React.createElement(View, { + style: StyleSheet.absoluteFill, + ...p, + }) + } + return ( - + { {this.props.children} - + ) } } diff --git a/components/modal/OperationContainer.tsx b/components/modal/OperationContainer.tsx index 00badbc63..048f11b0a 100644 --- a/components/modal/OperationContainer.tsx +++ b/components/modal/OperationContainer.tsx @@ -2,10 +2,11 @@ import React from 'react' import { TextStyle } from 'react-native' import { WithTheme } from '../style' import Modal from './Modal' -import { Action, CallbackOnBackHandler } from './PropsType' +import { Action, CallbackOnBackHandler, ModalPropsType } from './PropsType' import modalStyle from './style/index' -export interface OperationContainerProps { +export interface OperationContainerProps + extends Pick { actions: Action[] onAnimationEnd?: (visible: boolean) => void onBackHandler?: CallbackOnBackHandler @@ -69,6 +70,7 @@ export default class OperationContainer extends React.Component< transparent maskClosable visible={this.state.visible} + modalType={this.props.modalType} onClose={this.onClose} onAnimationEnd={onAnimationEnd} onRequestClose={this.onBackAndroid} diff --git a/components/modal/PromptContainer.tsx b/components/modal/PromptContainer.tsx index e91f57647..918d9e41d 100644 --- a/components/modal/PromptContainer.tsx +++ b/components/modal/PromptContainer.tsx @@ -4,11 +4,17 @@ import { getComponentLocale } from '../_util/getLocale' import { LocaleContext } from '../locale-provider' import { WithTheme, WithThemeStyles } from '../style' import Modal from './Modal' -import { CallbackOnBackHandler, CallbackOrActions } from './PropsType' +import { + CallbackOnBackHandler, + CallbackOrActions, + ModalPropsType, +} from './PropsType' import zh_CN from './locale/zh_CN' import promptStyles, { PromptStyle } from './style/prompt' -export interface PropmptContainerProps extends WithThemeStyles { +export interface PropmptContainerProps + extends WithThemeStyles, + Pick { title: React.ReactNode message?: React.ReactNode type?: 'default' | 'login-password' | 'secure-text' @@ -147,6 +153,7 @@ export default class PropmptContainer extends React.Component< title={title} visible={this.state.visible} footer={footer} + modalType={this.props.modalType} onAnimationEnd={onAnimationEnd} onRequestClose={this.onBackAndroid}> {message ? {message} : null} diff --git a/components/modal/PropsType.tsx b/components/modal/PropsType.tsx index 99104b6ff..b7f9fbfa3 100644 --- a/components/modal/PropsType.tsx +++ b/components/modal/PropsType.tsx @@ -1,20 +1,28 @@ import React from 'react' -import { TextStyle } from 'react-native' -export interface ModalPropsType { - title?: React.ReactNode - visible: boolean - maskClosable?: boolean - closable?: boolean - footer?: Action[] - onClose?: () => void - transparent?: boolean - popup?: boolean +import { StyleProp, TextStyle, ViewStyle } from 'react-native' +import { ModalStyle } from './style/index' + +export interface ModalPropsType { + animateAppear?: boolean animated?: boolean + animationType?: 'none' | 'fade' | 'slide-up' | 'slide-down' | 'slide' + bodyStyle?: StyleProp + children?: React.ReactNode + closable?: boolean + footer?: Action[] locale?: object - animationType?: any + maskClosable?: boolean + modalType?: 'portal' | 'modal' | 'view' onAnimationEnd?: (visible: boolean) => void - animateAppear?: boolean + onClose?: () => void operation?: boolean + onRequestClose?: CallbackOnBackHandler + popup?: boolean + style?: StyleProp + styles?: Partial + title?: React.ReactNode + transparent?: boolean + visible: boolean } export interface Action { diff --git a/components/modal/index.en-US.md b/components/modal/index.en-US.md index c2ee1e388..f6d632ae5 100644 --- a/components/modal/index.en-US.md +++ b/components/modal/index.en-US.md @@ -16,18 +16,70 @@ Use to show important information for the system, and ask for user feedback. eg: ### Modal -Properties | Descrition | Type | Default ------------|------------|------|-------- -| visible | Determine whether a modal dialog is visible or not | Boolean | false | -| closable | Determine whether a close (x) button is visible or not | Boolean | false | -| maskClosable | Determine whether to close the modal dialog when clicked mask of it | Boolean | true | -| onClose | Callback for clicking close icon x or mask | (): void | - | -| transparent | transparent mode or full screen mode | Boolean | false | -| popup | popup mode | Boolean | false | -| animationType | Options: 'fade' / 'slide' | String | fade | -| title | title | React.Element | - | -| footer | footer content | Array [{text, onPress}] | [] | -| onRequestClose | The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV. Returns `true` to prevent `BackHandler` events when modal is open.| (): boolean | false | +Properties | Descrition | Type | Default | Version +-----------|------------|------|---------|---------| +| visible | Determine whether a modal dialog is visible or not | Boolean | false | | +| closable | Determine whether a close (x) button is visible or not | Boolean | false | | +| maskClosable | Determine whether to close the modal dialog when clicked mask of it | Boolean | true | | +| onClose | Callback for clicking close icon x or mask | (): void | - | | +| transparent | transparent mode or full screen mode | Boolean | false | | +| popup | popup mode | Boolean | false | | +| animationType | Options: 'fade' / 'slide' | String | |fade | +| modalType | 弹窗的类型,
为`'portal'`时则从``根节点插入(默认),
为`'modal'`时则同[`react-native/Modal`](https://reactnative.dev/docs/modal)(用于获取当前context),
为`'view'`时则同`react-native/View`(用于弹窗中嵌套弹窗) | `'portal'` | `'modal'` | `'view'` | `'portal'` | `5.2.4` | +| title | title | React.Element | - | | +| footer | footer content | Array [{text, onPress}] | [] | | +| onRequestClose | The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV. Returns `true` to prevent `BackHandler` events when modal is open.| (): boolean | false | | +| style | style same as `styles.innerContainer` | `ViewStyle` | - | | +| styles | Semantic DOM style | [ModalStyle](#modalstyle-interface) | - | | + +### ModalStyle interface + +```typescript +interface ModalStyle { + container: ViewStyle // Set `z-index` + wrap: ViewStyle // Set modal flex layout: `{justifyContent: 'center',alignItems: 'center'}` + innerContainer: ViewStyle // modal content view, default: `{ widh:286 }` + + // modal content fields + footer: ViewStyle + header: TextStyle + body: ViewStyle + closeWrap: ViewStyle + close: TextStyle + buttonGroupH: ViewStyle + buttonGroupV: ViewStyle + buttonWrapH: ViewStyle + buttonWrapV: ViewStyle + buttonText: TextStyle + + // popup + popupContainer: ViewStyle + popupSlideUp: ViewStyle + popupSlideDown: ViewStyle + // operation + operationContainer: ViewStyle + operationBody: ViewStyle + buttonTextOperation: TextStyle +} +``` + +### Modal.useModal() + +When you need using Context, you can use `contextHolder` which created by `Modal.useModal` to insert into children. Modal created by hooks will get all the context where `contextHolder` are. Created `modal` has the same creating function with [`Modal.method`](#static-method)(Static method). + +```jsx +const [modal, contextHolder] = Modal.useModal(); + +React.useEffect(() => { + modal.alert( + // ... + ); +}, []); + +return {contextHolder}; +``` + +## Static method ### Modal.alert(title, message, actions?) @@ -78,4 +130,61 @@ function App() { Portal.remove(key) } } +``` +When using `Modal.useModal`, use the `modal.remove(key)` method: +```jsx +import { Modal, Portal } from '@ant-design/react-native' +import { useRef } from 'react' + +function App() { + const [modal, contextHolder] = Modal.useModal(); + const key = useRef() + + const onOpen = () => { + key.current = modal.alert({}) + } + + const onClose = () => { + // close the modal.alert + modal.remove(key) + } + + return ( + <> + ... + {contextHolder} + + ) +} +``` + +### Why I can not access context,redux,useRouter in `` or `Modal.xxx`? + +Rendering `` or calling Modal methods directly is dynamically inserted into the `` root node through `Portal.add` by default. At this time, its context is different from the context of the current code, so the context information cannot be obtained. + +When you need context info,
+**1.** you can use `Modal.useModal` to get `modal` instance and `contextHolder` node. And put it in your children: +```tsx +const [modal, contextHolder] = Modal.useModal(); + +// then call modal.confirm instead of Modal.confirm + +return ( + + {/* contextHolder is in Context1, which means modal will get context of Context1 */} + {contextHolder} + + {/* contextHolder is out of Context2, which means modal will not get context of Context2 */} + + +); +``` + +**Note**: You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection. + +**2.** When using ``, by setting `modalType='modal'`, the **native Modal component** will be used internally to maintain the context: +```tsx + + ... + ``` \ No newline at end of file diff --git a/components/modal/index.tsx b/components/modal/index.tsx index 47aba96d9..b9969a0c9 100644 --- a/components/modal/index.tsx +++ b/components/modal/index.tsx @@ -1,10 +1,12 @@ import Modal from './Modal' import alert from './alert' -import prompt from './prompt' import operation from './operation' +import prompt from './prompt' +import useModal from './useModal/index' Modal.alert = alert Modal.prompt = prompt Modal.operation = operation +Modal.useModal = useModal export default Modal diff --git a/components/modal/index.zh-CN.md b/components/modal/index.zh-CN.md index cdcea5246..ed33a0fa1 100644 --- a/components/modal/index.zh-CN.md +++ b/components/modal/index.zh-CN.md @@ -17,18 +17,69 @@ subtitle: 对话框 ### Modal -属性 | 说明 | 类型 | 默认值 -----|-----|------|------ -| visible | 对话框是否可见 | Boolean | false | -| closable | 是否显示关闭按钮 | Boolean | false | -| maskClosable | 点击蒙层是否允许关闭 | Boolean | true | -| onClose | 点击 x 或 mask 回调 | (): void | 无 | -| transparent | 是否背景透明 | Boolean | false | -| popup | 是否弹窗模式 | Boolean | false | -| animationType | 可选: 'fade' / 'slide' | String | fade | -| title | 标题 | React.Element | 无 | -| footer | 底部内容 | Array [{text, onPress}] | [] | -| onRequestClose | `onRequestClose`回调会在用户按下 Android 设备上的后退按键或是 Apple TV 上的菜单键时触发。返回true时会在 modal 处于开启状态时阻止`BackHandler`事件。| (): boolean | false | +属性 | 说明 | 类型 | 默认值 | 版本 | +----|-----|------|-------|------| +| visible | 对话框是否可见 | Boolean | false | | +| closable | 是否显示关闭按钮 | Boolean | false | | +| maskClosable | 点击蒙层是否允许关闭 | Boolean | true | | +| onClose | 点击 x 或 mask 回调 | (): void | 无 | | +| transparent | 是否背景透明 | Boolean | false | | +| popup | 是否弹窗模式 | Boolean | false | | +| animationType | 可选: 'fade' / 'slide' | String | fade | | +| modalType | 弹窗的类型,
为`'portal'`时则从``根节点插入(默认),
为`'modal'`时则同[`react-native/Modal`](https://reactnative.dev/docs/modal)(用于获取当前context),
为`'view'`时则同`react-native/View`(用于弹窗中嵌套弹窗) | `'portal'` | `'modal'` | `'view'` | `'portal'` | `5.2.4` | +| title | 标题 | React.Element | 无 | | +| footer | 底部内容 | Array [{text, onPress}] | [] | | +| onRequestClose | `onRequestClose`回调会在用户按下 Android 设备上的后退按键或是 Apple TV 上的菜单键时触发。返回true时会在 modal 处于开启状态时阻止`BackHandler`事件。| (): boolean | false | | +| style | 样式,同`styles.innerContainer` | `ViewStyle` | - | | +| styles | 语义化结构 style | [ModalStyle](#modalstyle-语义化样式) | - | | + +### ModalStyle 语义化样式 + +```typescript +interface ModalStyle { + container: ViewStyle // 设置了`z-index` + wrap: ViewStyle // 设置了modal的flex布局,默认: `{justifyContent: 'center',alignItems: 'center'}` + innerContainer: ViewStyle // modal主要内容区域, 默认:`{ width:286 }` + + // modal内容组成的各个元素样式 + footer: ViewStyle + header: TextStyle + body: ViewStyle + closeWrap: ViewStyle + close: TextStyle + buttonGroupH: ViewStyle + buttonGroupV: ViewStyle + buttonWrapH: ViewStyle + buttonWrapV: ViewStyle + buttonText: TextStyle + + // popup + popupContainer: ViewStyle + popupSlideUp: ViewStyle + popupSlideDown: ViewStyle + + // operation + operationContainer: ViewStyle + operationBody: ViewStyle + buttonTextOperation: TextStyle +} +``` + +### Modal.useModal() + +`5.2.4`新增。当你需要使用 Context 时,可以通过 `Modal.useModal` 创建一个 `contextHolder` 插入子节点中。通过 hooks 创建的临时 Modal 将会得到 `contextHolder` 所在位置的所有上下文。创建的 `modal` 对象拥有与 [`Modal.method`](#静态方法) 静态方法相同的创建通知方法。 + +```jsx +const [modal, contextHolder] = Modal.useModal(); + +React.useEffect(() => { + modal.confirm( + // ... + ); +}, []); + +return {contextHolder}; +``` ## 静态方法 ### Modal.alert(title, message, actions?, platform?) @@ -80,4 +131,53 @@ function App() { Portal.remove(key) } } +``` +使用 `Modal.useModal` 时则借助 `modal.remove(key)` 方法: +```jsx +import { Modal, Portal } from '@ant-design/react-native' +import { useRef } from 'react' + +function App() { + const [modal, contextHolder] = Modal.useModal(); + const key = useRef() + + const onOpen = () => { + key.current = modal.alert({}) + } + + const onClose = () => { + // 关闭modal.alert + modal.remove(key) + } +} +``` + +### 为什么 Modal 不能获取当前 context、redux、useRouter 的内容? + +渲染 `` 或直接调用 Modal 方法,默认是通过 `Portal.add` 动态插入到 `` 根节点的,此时其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。 + +当你需要 context 信息时,
+1.通过 `Modal.useModal` 方法会返回 `modal` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可: + +```tsx +const [modal, contextHolder] = Modal.useModal(); + +return ( + + {/* contextHolder 在 Context1 内,它可以获得 Context1 的 context */} + {contextHolder} + + {/* contextHolder 在 Context2 外,因而不会获得 Context2 的 context */} + + +); +``` + +**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。 + +2.使用``时,则通过设置 `modalType='modal'` 内部会改为使用 **原生Modal组件** 来保持 context : +```tsx + + ... + ``` \ No newline at end of file diff --git a/components/modal/useModal/index.tsx b/components/modal/useModal/index.tsx new file mode 100644 index 000000000..45f79e339 --- /dev/null +++ b/components/modal/useModal/index.tsx @@ -0,0 +1,150 @@ +import React, { useCallback } from 'react' +import { Modal, View } from 'react-native' +import AlertContainer from '../AlertContainer' +import OperationContainer from '../OperationContainer' +import PropmptContainer from '../PromptContainer' +import alert from '../alert' +import operation from '../operation' +import prompt from '../prompt' + +const StaticMethod = { alert, prompt, operation } +export type HookAPI = typeof StaticMethod & { remove: (key: number) => void } + +let nextKey = 20000 + +function useModal(): readonly [ + instance: HookAPI, + contextHolder?: React.ReactElement, +] { + // ========================== Effect ========================== + const [actionQueue, setActionQueue] = React.useState< + { + key: number + el: React.ReactElement + }[] + >([]) + + const remove = useCallback((key: number) => { + setActionQueue((prev) => prev.filter((p) => p.key !== key)) + }, []) + + const ElementsHolder = React.useMemo( + () => + actionQueue.length ? ( + + + {actionQueue.map((a) => a.el)} + + + ) : undefined, + [actionQueue], + ) + + // =========================== Hook =========================== + const fns = React.useMemo( + () => ({ + alert: (...args: Parameters) => { + const [title, content, actions = [{ text: '确定' }], onBackHandler] = + args + + const key = nextKey++ + setActionQueue((prev) => [ + ...prev, + { + key, + el: ( + { + if (!visible) { + remove(key) + } + }} + onBackHandler={onBackHandler} + modalType="view" + /> + ), + }, + ]) + return key + }, + prompt: (...args: Parameters) => { + const [ + title, + message, + callbackOrActions, + type = 'default', + defaultValue = '', + placeholders = ['', ''], + onBackHandler, + ] = args + + if (!callbackOrActions) { + console.error('Must specify callbackOrActions') + return + } + + const key = nextKey++ + setActionQueue((prev) => [ + ...prev, + { + key, + el: ( + { + if (!visible) { + remove(key) + } + }} + placeholders={placeholders} + onBackHandler={onBackHandler} + modalType="view" + /> + ), + }, + ]) + return key + }, + operation: (...args: Parameters) => { + const [actions, onBackHandler] = args + + const key = nextKey++ + setActionQueue((prev) => [ + ...prev, + { + key, + el: ( + 0 ? actions : [{ text: '确定' }]} + onAnimationEnd={(visible: boolean) => { + if (!visible) { + remove(key) + } + }} + onBackHandler={onBackHandler} + modalType="view" + /> + ), + }, + ]) + return key + }, + remove, + }), + [remove], + ) + + return [fns, ElementsHolder] as const +} + +export default useModal From 30c38d4707390b6fdb73148e9bc405198f0729e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 14:59:34 +0800 Subject: [PATCH 02/24] feat: Picker support modalType(#1383) --- components/picker/Picker.tsx | 4 +++- components/picker/Popup.tsx | 2 ++ components/picker/PopupPickerTypes.tsx | 3 ++- components/picker/index.en-US.md | 26 ++++++++++++++++++++++++++ components/picker/index.zh-CN.md | 26 ++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/components/picker/Picker.tsx b/components/picker/Picker.tsx index 8136563f6..278270e41 100644 --- a/components/picker/Picker.tsx +++ b/components/picker/Picker.tsx @@ -59,6 +59,7 @@ const RMCPicker = forwardRef((props, ref) => { innerValue, columns, handleSelect, + modalType, ...restProps } = props @@ -190,7 +191,8 @@ const RMCPicker = forwardRef((props, ref) => { onDismiss={handleDismiss} onClose={actions.close} okButtonProps={okButtonProps} - dismissButtonProps={dismissButtonProps}> + dismissButtonProps={dismissButtonProps} + modalType={modalType}> {/* TODO: 组件卸载是在visible更新fasle之后,需要前置 */} {/* 否则会无效执行onPickerChange */} {innerVisible && ( diff --git a/components/picker/Popup.tsx b/components/picker/Popup.tsx index c732d608c..af3334eb6 100644 --- a/components/picker/Popup.tsx +++ b/components/picker/Popup.tsx @@ -15,6 +15,7 @@ const PopupPicker = memo((props: PopupPickerProps) => { onOk, onClose, children, + modalType, } = props const titleEl = @@ -41,6 +42,7 @@ const PopupPicker = memo((props: PopupPickerProps) => { return ( -} +} & Pick diff --git a/components/picker/index.en-US.md b/components/picker/index.en-US.md index fb9ffe566..2789b6f78 100644 --- a/components/picker/index.en-US.md +++ b/components/picker/index.en-US.md @@ -112,3 +112,29 @@ Properties | Descrition | Type ### Ref Same as PickerActions + + +## FAQ + +### Why is the Picker hidden when it popup in the native Modal? + +By default, `` is dynamically inserted into the `` root node via `Portal.add`, and the zIndex level of the native Modal is above everything, including its root node. + +So if you must use the `` component in the native Modal, you can set `modalType='modal'` to keep it at the same level as the native Modal. + +```tsx +import {Modal} from 'react-native'; +import {Picker} from '@ant-design/react-native'; + + + ... + + + 省市选择 + + + +``` \ No newline at end of file diff --git a/components/picker/index.zh-CN.md b/components/picker/index.zh-CN.md index ec63670a4..c493d0844 100644 --- a/components/picker/index.zh-CN.md +++ b/components/picker/index.zh-CN.md @@ -112,3 +112,29 @@ interface PickerStyle extends Partial { ### Ref 同 PickerActions + + +## FAQ + +### 为什么在原生 Modal 中弹出 Picker 会被遮住? + +默认情况下,``是通过 `Portal.add` 动态插入到 `` 根节点的,而原生 Modal 的zIndex层级高于一切,包括它的根节点。 + +所以如果一定要在原生 Modal 中同时使用 `` 组件,可以通过设置 `modalType='modal'` 来保持和原生 Modal 同一层级。 + +```tsx +import {Modal} from 'react-native'; +import {Picker} from '@ant-design/react-native'; + + + ... + + + 省市选择 + + + +``` \ No newline at end of file From c6b7b97c6775bf657d69ae63fe0b73349ea68ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 15:02:31 +0800 Subject: [PATCH 03/24] feat: Carousel support onScrollAnimationEnd --- components/carousel/index.en-US.md | 29 +++++++++++++++-------------- components/carousel/index.tsx | 8 ++++++-- components/carousel/index.zh-CN.md | 29 +++++++++++++++-------------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/components/carousel/index.en-US.md b/components/carousel/index.en-US.md index 949a9e41f..00252db0e 100644 --- a/components/carousel/index.en-US.md +++ b/components/carousel/index.en-US.md @@ -6,20 +6,21 @@ title: Carousel ## API -Properties | Descrition | Type | Default ------------|------------|------|-------- -| afterChange | callback to be called after a slide is changed | (current: number) => void | | -| autoplay | autoplay mode active | Boolean | false | -| autoplayInterval | interval for autoplay iteration | Number | 3000 | -| dots | whether to display the indication dots | Boolean | true | -| dotStyle | style of dots | ViewStyle | | -| dotActiveStyle | style of active dot | ViewStyle | | -| infinite | whether is infinite loop | Boolean | false | -| pageStyle | style of the carousel page | ViewStyle | | -| pagination | A generator function which could be used to customized pagination. | (props) => ReactNode | | -| selectedIndex | current selected index | number | 0 | -| style | ScrollView style
(`tips: Recommended setting, the overall carousel size is determined by the container scrollview and not the inner page`) | ViewStyle | | -| vertical | controls the pagination display direction. | Boolean | false | +Properties | Descrition | Type | Default | Version +-----------|------------|------|---------|---------- +| afterChange | callback to be called after a slide is changed | (current: number) => void | | | +| autoplay | autoplay mode active | Boolean | false | | +| autoplayInterval | interval for autoplay iteration | Number | 3000 | | +| dots | whether to display the indication dots | Boolean | true | | +| dotStyle | style of dots | ViewStyle | | | +| dotActiveStyle | style of active dot | ViewStyle | | | +| infinite | whether is infinite loop | Boolean | false | | +| pageStyle | style of the carousel page | ViewStyle | | | +| pagination | A generator function which could be used to customized pagination. | (props) => ReactNode | | | +| selectedIndex | current selected index | number | 0 | | +| style | ScrollView style
(`tips: Recommended setting, the overall carousel size is determined by the container scrollview and not the inner page`) | ViewStyle | | | +| vertical | controls the pagination display direction. | Boolean | false | | +| onScrollAnimationEnd | Called when a scrolling animation ends. | ()=>void | | `5.2.4` | The rest of the props of Carousel are exactly the same as the react-native [ScrollView](https://reactnative.dev/docs/scrollview.html); diff --git a/components/carousel/index.tsx b/components/carousel/index.tsx index b52f0ca60..80aae9401 100644 --- a/components/carousel/index.tsx +++ b/components/carousel/index.tsx @@ -217,6 +217,9 @@ class Carousel extends React.PureComponent { if (isScrollAnimationEnd) { this.updateIndex(currentOffset) this.autoplay() + if (this.props.onScrollAnimationEnd) { + this.props.onScrollAnimationEnd() + } } } @@ -249,7 +252,7 @@ class Carousel extends React.PureComponent { }, () => { // web & android - this.scrollview?.current?.scrollTo({ ...offset, animated: true }) + this.scrollview?.current?.scrollTo({ ...offset, animated: false }) this.autoplay() }, ) @@ -440,7 +443,8 @@ class Carousel extends React.PureComponent { onScrollEndDrag={this.onScrollEndDrag} onScroll={this.onScroll} onTouchStart={this.onTouchStartForWeb} - onTouchEnd={this.onTouchEndForWeb}> + onTouchEnd={this.onTouchEndForWeb} + onScrollAnimationEnd={undefined}> {pages} ) diff --git a/components/carousel/index.zh-CN.md b/components/carousel/index.zh-CN.md index 0f1f6a177..201f14916 100644 --- a/components/carousel/index.zh-CN.md +++ b/components/carousel/index.zh-CN.md @@ -9,20 +9,21 @@ subtitle: 走马灯 ## API -属性 | 说明 | 类型 | 默认值 -----|-----|------|------ -| afterChange | 切换面板后的回调函数 | (current: number) => void | 无 | -| autoplay | 是否自动切换 | Boolean | false | -| autoplayInterval | 自动切换的时间间隔 | Number | 3000 | -| dots | 是否显示面板指示点 | Boolean | true | -| dotStyle | 指示点样式 | ViewStyle | 无 | -| dotActiveStyle | 当前激活的指示点样式 | ViewStyle | 无 | -| infinite | 是否循环播放 | Boolean | false | -| pageStyle | 轮播页内样式 | ViewStyle | 无 | -| pagination | 自定义 pagination | (props) => ReactNode | | -| selectedIndex | 手动设置当前显示的索引 | number | 0 | -| style | 轮播容器样式
(建议设置,整体轮播大小由容器决定非页内决定) | ViewStyle | 无 | -| vertical | 垂直显示 | Boolean | false | +属性 | 说明 | 类型 | 默认值 | 版本 +----|-----|------|-------|----- +| afterChange | 切换面板后的回调函数 | (current: number) => void | 无 | | +| autoplay | 是否自动切换 | Boolean | false | | +| autoplayInterval | 自动切换的时间间隔 | Number | 3000 | | +| dots | 是否显示面板指示点 | Boolean | true | | +| dotStyle | 指示点样式 | ViewStyle | 无 | | +| dotActiveStyle | 当前激活的指示点样式 | ViewStyle | 无 | | +| infinite | 是否循环播放 | Boolean | false | | +| pageStyle | 轮播页内样式 | ViewStyle | 无 | | +| pagination | 自定义 pagination | (props) => ReactNode | | | +| selectedIndex | 手动设置当前显示的索引 | number | 0 | | +| style | 轮播容器样式
(建议设置,整体轮播大小由容器决定非页内决定) | ViewStyle | 无 | | +| vertical | 垂直显示 | Boolean | false | | +| onScrollAnimationEnd | 当滚动动画结束时调用 | ()=>void | 无 | `5.2.4` | Carousel 的其他属性和 react-native 内置组件[ScrollView](https://reactnative.dev/docs/scrollview.html) 一致;
From cdeb1f2d4d75216254ab6af8788c0c6f480609d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 15:03:51 +0800 Subject: [PATCH 04/24] fix:[Form] fix Require cycle --- components/form/FormItem/ItemHolder.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/form/FormItem/ItemHolder.tsx b/components/form/FormItem/ItemHolder.tsx index d1ecd3b36..763a9a41a 100644 --- a/components/form/FormItem/ItemHolder.tsx +++ b/components/form/FormItem/ItemHolder.tsx @@ -2,8 +2,9 @@ import type { Meta } from 'rc-field-form/lib/interface' import React, { useMemo } from 'react' import { StyleSheet } from 'react-native' import { FormItemProps } from '.' -import { List, View } from '../..' +import List from '../../list/index' import { useTheme } from '../../style' +import View from '../../view/index' import ErrorList from '../ErrorList' import FormItemLabel from '../FormItemLabel' import type { ReportMetaChange } from '../context' From c27af56716bc997b955100cc7c1b930334d67cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 15:10:13 +0800 Subject: [PATCH 05/24] feat:[typescript] export all component types --- components/flex/Flex.tsx | 3 +- components/index.tsx | 2 + components/list/List.tsx | 15 +++---- components/tab-bar/PropsType.tsx | 19 ++++++++- components/tab-bar/TabBarItem.tsx | 29 ++----------- components/tab-bar/index.tsx | 8 ++-- components/types.ts | 67 +++++++++++++++++++++++++++++++ components/view/index.tsx | 2 +- 8 files changed, 105 insertions(+), 40 deletions(-) create mode 100644 components/types.ts diff --git a/components/flex/Flex.tsx b/components/flex/Flex.tsx index 31ca36d2c..e33e3e3c3 100644 --- a/components/flex/Flex.tsx +++ b/components/flex/Flex.tsx @@ -3,11 +3,12 @@ import { StyleProp, TouchableWithoutFeedback, View, + ViewProps, ViewStyle, } from 'react-native' import { FlexPropsType } from './PropsType' -export interface FlexProps extends FlexPropsType { +export interface FlexProps extends FlexPropsType, ViewProps { onPress?: () => void onLongPress?: () => void onPressIn?: () => void diff --git a/components/index.tsx b/components/index.tsx index fe4055b25..cf0e94dc0 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -60,3 +60,5 @@ export class ImagePicker {} * @deprecated */ export class SegmentedControl {} + +export type * from './types.ts' diff --git a/components/list/List.tsx b/components/list/List.tsx index fe3e214ca..38b116ed9 100644 --- a/components/list/List.tsx +++ b/components/list/List.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { StyleProp, Text, View, ViewStyle } from 'react-native' +import { StyleProp, Text, View, ViewProps, ViewStyle } from 'react-native' import { useTheme } from '../style' import { ListPropsType } from './PropsType' import listStyles, { ListStyle } from './style/index' @@ -9,10 +9,10 @@ export interface ListProps extends ListPropsType { style?: StyleProp } -const InternalList: React.ForwardRefRenderFunction = ( - props, - ref, -) => { +const InternalList: React.ForwardRefRenderFunction< + View, + ListProps & ViewProps +> = (props, ref) => { const { children, style, renderHeader, renderFooter, styles, ...restProps } = props @@ -55,8 +55,9 @@ const InternalList: React.ForwardRefRenderFunction = ( ) } -const List = React.forwardRef(InternalList) as (( - props: React.PropsWithChildren & React.RefAttributes, +const List = React.forwardRef(InternalList) as (( + props: React.PropsWithChildren & + React.RefAttributes, ) => React.ReactElement) & Pick diff --git a/components/tab-bar/PropsType.tsx b/components/tab-bar/PropsType.tsx index bbbdd4b2c..3edc1320b 100644 --- a/components/tab-bar/PropsType.tsx +++ b/components/tab-bar/PropsType.tsx @@ -1,5 +1,13 @@ import React from 'react' -import { ImageRequireSource, ImageURISource } from 'react-native' +import { + ImageRequireSource, + ImageStyle, + ImageURISource, + StyleProp, + ViewStyle, +} from 'react-native' +import TabBarItem from './TabBarItem' +import { TabBarStyle } from './style' export interface TabBarProps { barTintColor?: string @@ -7,6 +15,8 @@ export interface TabBarProps { unselectedTintColor?: string animated?: boolean swipeable?: boolean + styles?: TabBarStyle + children: React.ReactElement[] | React.ReactElement } export type TabBarIcon = | ImageURISource @@ -20,4 +30,11 @@ export interface TabBarItemProps { icon?: TabBarIcon selectedIcon?: TabBarIcon title: string + tintColor?: string + unselectedTintColor?: string + iconStyle?: StyleProp + renderAsOriginal?: boolean + style?: StyleProp + styles?: TabBarStyle + children?: React.ReactNode } diff --git a/components/tab-bar/TabBarItem.tsx b/components/tab-bar/TabBarItem.tsx index ea7d5e582..0d757dad6 100644 --- a/components/tab-bar/TabBarItem.tsx +++ b/components/tab-bar/TabBarItem.tsx @@ -1,30 +1,8 @@ import React, { isValidElement } from 'react' -import { - Image, - ImageStyle, - StyleProp, - Text, - TouchableWithoutFeedback, - View, -} from 'react-native' +import { Image, Text, TouchableWithoutFeedback, View } from 'react-native' import Icon, { IconProps } from '../icon' -import { TabBarIcon } from './PropsType' -import TabBarItemStyles from './style' +import { TabBarItemProps } from './PropsType' -export interface TabBarItemProps { - badge?: string | number - onPress?: () => void - selected?: boolean - icon?: TabBarIcon - selectedIcon?: TabBarIcon - title: string - tintColor?: string - unselectedTintColor?: string - iconStyle?: StyleProp - renderAsOriginal?: boolean - styles?: ReturnType - children?: React.ReactNode -} export default class TabBarItem extends React.Component { static defaultProps = { onPress() {}, @@ -40,6 +18,7 @@ export default class TabBarItem extends React.Component { onPress, badge, iconStyle, + style, } = this.props const styles = this.props.styles! const itemSelectedStyle = selected ? styles.barItemSelected : null @@ -62,7 +41,7 @@ export default class TabBarItem extends React.Component { (source as any).type.displayName === 'Icon' return ( - + {source === null ? null : isValidElement(source) ? ( isIcon ? ( diff --git a/components/tab-bar/index.tsx b/components/tab-bar/index.tsx index 9101001ce..527290cd1 100644 --- a/components/tab-bar/index.tsx +++ b/components/tab-bar/index.tsx @@ -2,12 +2,10 @@ import React from 'react' import { SafeAreaView, View } from 'react-native' import { WithTheme } from '../style' import { TabBarProps } from './PropsType' -import TabBarStyles, { TabBarStyle } from './style/index' import TabBarItem from './TabBarItem' -export interface TabBarNativeProps extends TabBarProps { - styles?: TabBarStyle - children: React.ReactElement[] | React.ReactElement -} +import TabBarStyles from './style/index' + +export interface TabBarNativeProps extends TabBarProps {} class TabBar extends React.Component { static defaultProps = { diff --git a/components/types.ts b/components/types.ts new file mode 100644 index 000000000..337f3f6cd --- /dev/null +++ b/components/types.ts @@ -0,0 +1,67 @@ +export type { TextProps } from 'react-native' +export type { AccordionNativeProps as AccordionProps } from './accordion/index' +export type { ActivityIndicatorNativeProps as ActivityIndicatorProps } from './activity-indicator/index' +export type { BadgeProps } from './badge/index' +export type { ButtonProps } from './button/index' +export type { CardNativeProps as CardProps } from './card/index' +export type { CarouselForwardedRef, CarouselProps } from './carousel/PropsType' +export type { + CheckboxForwardedRef, + CheckboxItemProps, + CheckboxProps, +} from './checkbox/PropsType' +export type { CollapsePanelProps, CollapseProps } from './collapse/PropsType' +export type { DatePickerViewProps } from './date-picker-view/date-picker-view' +export type { DatePickerProps } from './date-picker/date-picker' +export type { DrawerNativeProps as DrawerProps } from './drawer/index' +export type { FlexProps } from './flex/Flex' +export type { FlexItemProps } from './flex/FlexItem' +export type { + FormInstance, + FormItemProps, + FormListFieldData, + FormListOperation, + FormListProps, + FormProps, + Rule as FormRule, +} from './form/index' +export type { GridProps } from './grid/index' +export type { IconProps } from './icon/index' +export type { InputProps, TextAreaProps } from './input/PropsType' +export type { ListViewProps } from './list-view/index' +export type { BriefProps } from './list/Brief' +export type { ListProps } from './list/List' +export type { ListItemProps } from './list/ListItem' +export type { ModalProps } from './modal/Modal' +export type { MarqueeProps, NoticeBarProps } from './notice-bar/PropsType' +export type { PaginationNativeProps as PaginationProps } from './pagination/index' +export type { PickerViewProps } from './picker-view/index' +export type { PickerProps } from './picker/index' +export type { PopoverProps } from './popover/index' +export type { PortalProps } from './portal/portal' +export type { ProgressProps } from './progress/index' +export type { ProviderProps } from './provider/index' +export type { + RadioGroupProps, + RadioItemProps, + RadioProps, +} from './radio/PropsType' +export type { ResultNativeProps as ResultProps } from './result/index' +export type { SearchBarProps } from './search-bar/index' +export type { SliderProps } from './slider/PropsType' +export type { StepperProps } from './stepper/PropsType' +export type { StepsProps } from './steps/index' +export type { + SwipeActionProps, + SwipeoutButtonProps, +} from './swipe-action/PropsType' +export type { SwitchProps } from './switch/Switch' +export type { TabBarItemProps, TabBarProps } from './tab-bar/PropsType' +export type { TabsProps } from './tabs/Tabs' +export type { TagNativeProps as TagProps } from './tag/index' +export type { TextareaItemProps } from './textarea-item/index' +export type { ToastProps } from './toast/index' +export type { TooltipProps } from './tooltip/PropsType' +export type { ViewInterface as ViewProps } from './view/index' +export type { WhiteSpaceProps } from './white-space/index' +export type { WingBlankProps } from './wing-blank/index' diff --git a/components/view/index.tsx b/components/view/index.tsx index 188897003..e9d336ea5 100644 --- a/components/view/index.tsx +++ b/components/view/index.tsx @@ -9,7 +9,7 @@ import { ViewStyle, } from 'react-native' -interface ViewInterface extends ViewProps, TextProps { +export interface ViewInterface extends ViewProps, TextProps { children?: React.ReactNode | React.ReactText style?: StyleProp | StyleProp } From ee778615e03118bb576f9d8c7bb973ec5f39637f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 20:32:40 +0800 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20Slider=20add=20disabledStep?= =?UTF-8?q?=E3=80=81onSlidingStart=E3=80=81onSlidingComplete=20prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/slider/PropsType.tsx | 5 +- components/slider/demo/basic.tsx | 56 ++++- components/slider/index.en-US.md | 8 +- components/slider/index.zh-CN.md | 7 +- components/slider/slider.tsx | 398 ++++++++++++++++++++---------- components/slider/style/index.tsx | 5 +- components/slider/thumb.tsx | 38 +-- components/slider/ticks.tsx | 10 +- 8 files changed, 359 insertions(+), 168 deletions(-) diff --git a/components/slider/PropsType.tsx b/components/slider/PropsType.tsx index ff162fc0d..f6cb86769 100644 --- a/components/slider/PropsType.tsx +++ b/components/slider/PropsType.tsx @@ -16,11 +16,14 @@ export type BaseSliderProps = { marks?: SliderMarks ticks?: boolean disabled?: boolean + disabledStep?: boolean icon?: ReactNode popover?: boolean | ((value: number) => ReactNode) residentPopover?: boolean onChange?: (value: SliderValue) => void - onAfterChange?: (value: SliderValue) => void + onAfterChange?: (value: SliderValue, index: number) => void + onSlidingStart?: (value: SliderValue, index: number) => void + onSlidingComplete?: (value: SliderValue, index: number) => void style?: StyleProp styles?: Partial } diff --git a/components/slider/demo/basic.tsx b/components/slider/demo/basic.tsx index facf3be1d..cad0760c3 100644 --- a/components/slider/demo/basic.tsx +++ b/components/slider/demo/basic.tsx @@ -1,14 +1,19 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { ScrollView } from 'react-native' -import { List, Slider, Toast } from '../../' +import { List, Slider, Switch, Toast } from '../../' export default function StepperExample() { + useEffect(() => { + Toast.config({ stackable: false }) + }, []) + + const [disabledStep, setDisabledStep] = useState(false) const marks = { 0: 0, - 20: 20, + // 20: 20, 40: 40, - 60: 60, + 60: '', 80: 80, 100: 100, } @@ -25,19 +30,54 @@ export default function StepperExample() { return ( - + + { + setDisabledStep(val) + }} + /> + }> + Disabled Step + + 是否禁用步距;禁用后`onChange`将返回带有小数点的值 + + + + true} + onTouchStart={(e) => e.stopPropagation()}> - + + console.log('onSlidingStart', { val, index }) + } + onSlidingComplete={(val, index) => + console.log('onSlidingComplete', { val, index }) + } + /> - + - + diff --git a/components/slider/index.en-US.md b/components/slider/index.en-US.md index f47f2af33..204534dc1 100644 --- a/components/slider/index.en-US.md +++ b/components/slider/index.en-US.md @@ -17,12 +17,15 @@ A Slider component for selecting particular value in range, eg: controls the dis | --- | --- | --- | --- | --- | | defaultValue | Default value | `number` \|
`[number, number]` | `range ? [0, 0] : 0` | | | disabled | Whether disabled | `boolean` | `false` | | +| disabledStep | Whether disabled step; if `true`, `onChange` will return a value with a decimal point | `boolean` | `false` | `5.2.4` | | icon | The icon of slider | `ReactNode` | - | | | marks | Tick marks | `{ [key: number]: React.ReactNode }` | - | `5.2.1` | | max | Max value | `number` | `100` | | | min | Min value | `number` | `0` | | | onAfterChange | Consistent with the trigger timing of `touchend`, pass the current value as a parameter | `(value: number | [number, number]) => void` | - | | | onChange | Triggered when the slider is dragged, and the current dragged value is passed in as a parameter | `(value: number | [number, number]) => void` | - | | +| onSlidingStart | Callback that is called when the user picks up the slider.
The initial value is passed as an argument to the callback handler. | `(value: number | [number, number], index: number) => void` | - | `5.2.4` | +| onSlidingComplete | Callback that is called when the user releases the slider, regardless if the value has changed.
The current value is passed as an argument to the callback handler. | `(value: number | [number, number], index: number) => void` | - | `5.2.4` | | popover | Whether to display the popover when dragging. Support passing in function to customize the rendering content. | `boolean | ((value: number) => ReactNode)` | `false` | `5.2.1` | | residentPopover | Whether the `popover` is permanently displayed , this attribute takes effect when `popover` exists | `boolean ` | `false` | `5.2.1` | | range | Whether it is a double sliders | `boolean` | `false` | `5.2.1` | @@ -38,8 +41,11 @@ A Slider component for selecting particular value in range, eg: controls the dis ```typescript interface SliderStyle { - slider: ViewStyle // Same as `style` + // Same as `style` + slider: ViewStyle // belongs to PanGesture area when `range=false` disabled: ViewStyle + + // Track trackContianer: ViewStyle // track container track: ViewStyle // track line fill: ViewStyle // Filled portion of the track line diff --git a/components/slider/index.zh-CN.md b/components/slider/index.zh-CN.md index 3b67bfebc..e28ce9e53 100644 --- a/components/slider/index.zh-CN.md +++ b/components/slider/index.zh-CN.md @@ -18,12 +18,15 @@ version: update | --- | --- | --- | --- | --- | | defaultValue | 默认值 | `number` \|
`[number, number]` | `range ? [0, 0] : 0` | | | disabled | 是否禁用 | `boolean` | `false` | | +| disabledStep | 是否禁用步距;禁用后`onChange`将返回带有小数点的值 | `boolean` | `false` | `5.2.4` | | icon | 滑块的图标 | `ReactNode` | - | | | marks | 刻度标记 | `{ [key: number]: React.ReactNode }` | - | `5.2.1` | | max | 最大值 | `number` | `100` | | | min | 最小值 | `number` | `0` | | | onAfterChange | 与 `touchend` 触发时机一致,把当前值作为参数传入 | `(value: number | [number, number]) => void` | - | | | onChange | 拖拽滑块时触发,并把当前拖拽的值作为参数传入 | `(value: number | [number, number]) => void` | - | | +| onSlidingStart | 当用户拿起滑块时调用的回调。
初始值作为参数传递给回调处理程序。 | `(value: number | [number, number], index: number) => void` | - | `5.2.4` | +| onSlidingComplete | 当用户释放滑块时调用的回调,无论值是否已更改。
当前值作为参数传递给回调处理程序。 | `(value: number | [number, number], index: number) => void` | - | `5.2.4` | | popover | 是否在拖动时显示悬浮提示,支持传入函数自定义渲染内容 | `boolean | ((value: number) => ReactNode)` | `false` | `5.2.1` | | residentPopover | `popover` 是否常驻显示,`popover` 存在时生效 | `boolean ` | `false` | `5.2.1` | | range | 是否为双滑块 | `boolean` | `false` | `5.2.1` | @@ -39,8 +42,10 @@ version: update ```typescript interface SliderStyle { - slider: ViewStyle // 同 style + slider: ViewStyle // 同 style,`range=false`时属于PanGesture区域 disabled: ViewStyle + + // 轨道 trackContianer: ViewStyle // 轨道容器 track: ViewStyle // 轨道线 fill: ViewStyle // 轨道填充部分 diff --git a/components/slider/slider.tsx b/components/slider/slider.tsx index 458d539ea..da14436a3 100644 --- a/components/slider/slider.tsx +++ b/components/slider/slider.tsx @@ -1,12 +1,20 @@ import getMiniDecimal, { toFixed } from '@rc-component/mini-decimal' import useMergedState from 'rc-util/lib/hooks/useMergedState' -import React, { useContext, useMemo, useRef, useState } from 'react' +import React, { + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react' +import { LayoutChangeEvent, LayoutRectangle, View } from 'react-native' import { - GestureResponderEvent, - LayoutChangeEvent, - LayoutRectangle, - View, -} from 'react-native' + Gesture, + GestureDetector, + GestureStateChangeEvent, + TapGestureHandlerEventPayload, +} from 'react-native-gesture-handler' +import { runOnJS } from 'react-native-reanimated' import devWarning from '../_util/devWarning' import HapticsContext from '../provider/HapticsContext' import { useTheme } from '../style' @@ -28,21 +36,26 @@ export function Slider( const { defaultValue, disabled = false, + disabledStep = false, icon, marks, max = 100, min = 0, onAfterChange, onChange, + onSlidingStart, + onSlidingComplete, popover, residentPopover, range, - step = 1, style, styles, ticks, } = props as BaseSliderProps & { range: boolean } + // step越小滑动刷新率fps越高 + const step = props.step || 1 + const ss = useTheme({ styles, themeStyles: SliderStyles, @@ -61,27 +74,26 @@ export function Slider( return getMiniDecimal(fixedStr).toNumber() } - function reverseValue(value: [number, number]) { - const mergedDecimalLen = Math.max( - getDecimalLen(step), - getDecimalLen(value[0]), - getDecimalLen(value[1]), - ) - return ( - range - ? value.map((v) => alignValue(v, mergedDecimalLen)) - : alignValue(value[1], mergedDecimalLen) - ) as SliderValue - } + const reverseValue = useCallback( + (value: [number, number]) => { + const mergedDecimalLen = Math.max( + getDecimalLen(step), + getDecimalLen(value[0]), + getDecimalLen(value[1]), + ) + return ( + range + ? value.map((v) => alignValue(v, mergedDecimalLen)) + : alignValue(value[1], mergedDecimalLen) + ) as SliderValue + }, + [range, step], + ) function getDecimalLen(n: number) { return (`${n}`.split('.')[1] || '').length } - function onAfterChangeRange(value: [number, number]) { - onAfterChange?.(reverseValue(value)) - } - let propsValue = props.value as SliderValue if (range && typeof props.value === 'number') { devWarning( @@ -97,15 +109,18 @@ export function Slider( ) const sliderValue = sortValue(convertValue(rawValue)) - function setSliderValue(value: [number, number]) { - const next = sortValue(value) + const setSliderValue = useCallback( + (value: [number, number]) => { + const next = sortValue(value) - const current = sliderValue - if (next[0] === current[0] && next[1] === current[1]) { - return - } - setRawValue(reverseValue(next)) - } + const current = sliderValue + if (next[0] === current[0] && next[1] === current[1]) { + return + } + setRawValue(reverseValue(next)) + }, + [reverseValue, setRawValue, sliderValue], + ) const fillSize = `${(100 * (sliderValue[1] - sliderValue[0])) / (max - min)}%` const fillStart = `${(100 * (sliderValue[0] - min)) / (max - min)}%` @@ -131,66 +146,161 @@ export function Slider( return [] }, [marks, ticks, step, min, max]) - function getValueByPosition(position: number) { - const newPosition = position < min ? min : position > max ? max : position - - let value = min - - // 显示了刻度点,就只能移动到点上 - if (pointList.length) { - value = nearest(pointList, newPosition) - } else { - // 使用 MiniDecimal 避免精度问题 - const cell = Math.round((newPosition - min) / step) - const nextVal = getMiniDecimal(cell).multi(step) - value = getMiniDecimal(min).add(nextVal.toString()).toNumber() - } - return value - } - const [trackLayout, setTrackLayout] = useState() const onTrackLayout = (e: LayoutChangeEvent) => { setTrackLayout(e.nativeEvent.layout) } + const getValueByPosition = useCallback( + (position: number, last: boolean) => { + const newPosition = position < min ? min : position > max ? max : position + + // 禁用步距 + if (!last && !range && disabledStep) { + return newPosition + } + + let value = min + // 显示了刻度点,就只能移动到点上 + if (pointList.length && !disabledStep) { + value = nearest(pointList, newPosition) + } else { + // 使用 MiniDecimal 避免精度问题 + const cell = Math.round((newPosition - min) / step) + const nextVal = getMiniDecimal(cell).multi(step) + value = getMiniDecimal(min).add(nextVal.toString()).toNumber() + } + return value + }, + [disabledStep, max, min, pointList, range, step], + ) + + const [isSliding, setSliding] = useState(false) + const onSlidingStartI = useCallback( + (index = 0) => { + onSlidingStart?.(rawValue, index) + setSliding(true) + }, + [onSlidingStart, rawValue], + ) + const onSlidingCompleteI = useCallback( + (val, index) => { + onSlidingComplete?.(val, index) + setSliding(false) + }, + [onSlidingComplete], + ) + + const onAfterChangeRange = useCallback( + (value: [number, number], index?: number) => { + const val = reverseValue(value) + const i = (range && index) || 0 + onAfterChange?.(val, i) + onSlidingCompleteI(val, i) + }, + [onAfterChange, onSlidingCompleteI, range, reverseValue], + ) + const onHaptics = useContext(HapticsContext) - const onTrackClick = (event: GestureResponderEvent) => { - event.stopPropagation() - if (disabled) { - return - } - if (!trackLayout) { - return - } - const sliderOffsetLeft = trackLayout.x - const position = - ((event.nativeEvent.locationX - sliderOffsetLeft) / - Math.ceil(trackLayout.width)) * - (max - min) + - min - const targetValue = getValueByPosition(position) - let nextSliderValue: [number, number] - if (range) { - // 移动的滑块采用就近原则 - if ( - Math.abs(targetValue - sliderValue[0]) > - Math.abs(targetValue - sliderValue[1]) - ) { - nextSliderValue = [sliderValue[0], targetValue] + + // on trackContainer tap + const onTrackClick = useCallback( + (event: GestureStateChangeEvent) => { + if (!trackLayout) { + return + } + + const position = + (event.x / Math.ceil(trackLayout.width)) * (max - min) + min + const targetValue = getValueByPosition(position, true) + let nextSliderValue: [number, number] + if (range) { + // 移动的滑块采用就近原则 + if ( + Math.abs(targetValue - sliderValue[0]) > + Math.abs(targetValue - sliderValue[1]) + ) { + nextSliderValue = [sliderValue[0], targetValue] + } else { + nextSliderValue = [targetValue, sliderValue[1]] + } } else { - nextSliderValue = [targetValue, sliderValue[1]] + nextSliderValue = [min, targetValue] } - } else { - nextSliderValue = [min, targetValue] - } - setSliderValue(nextSliderValue) - onAfterChangeRange(nextSliderValue) - if (!ticks) { - onHaptics('slider') - } - } + setSliderValue(nextSliderValue) + if (!ticks) { + onHaptics('slider') + } + }, + [ + getValueByPosition, + max, + min, + onHaptics, + range, + setSliderValue, + sliderValue, + ticks, + trackLayout, + ], + ) + // on thumb pan gesture const valueBeforeDragRef = useRef<[number, number]>() + const onDrag = useCallback( + (index: number, locationX: number, last: boolean) => { + if (!trackLayout) { + return + } + // 是百分比位置,而非x坐标 + const position = + (locationX / Math.ceil(trackLayout.width)) * (max - min) + min + + const val = getValueByPosition(position, last) + if (!valueBeforeDragRef.current) { + valueBeforeDragRef.current = [...sliderValue] + } + valueBeforeDragRef.current[index] = val + const next = sortValue([...valueBeforeDragRef.current]) + setSliderValue(next) + if (last) { + valueBeforeDragRef.current = undefined + onAfterChangeRange(next, index) + } + }, + [ + getValueByPosition, + max, + min, + onAfterChangeRange, + setSliderValue, + sliderValue, + trackLayout, + ], + ) + + // on trackContainer pan gesture + const valueBeforeSlideRef = useRef() + const onSlide = useCallback( + (translationX: number, last: boolean) => { + if (!trackLayout) { + return + } + + if (typeof valueBeforeSlideRef.current === 'undefined') { + valueBeforeSlideRef.current = + ((rawValue as number) / max) * trackLayout.width + } + + // 复用 + onDrag(1, translationX + valueBeforeSlideRef.current, last) + + if (last) { + valueBeforeSlideRef.current = undefined + } + }, + [max, onDrag, rawValue, trackLayout], + ) const renderThumb = (index: number) => { return ( @@ -200,79 +310,93 @@ export function Slider( trackLayout={trackLayout} min={min} max={max} - disabled={disabled} + disabled={disabled || !range} + isSliding={isSliding} icon={icon} popover={!!popover} residentPopover={!!residentPopover} - onDrag={(locationX, last) => { - if (!trackLayout) { - return - } - const sliderOffsetLeft = trackLayout.x - const position = - ((locationX - sliderOffsetLeft) / Math.ceil(trackLayout.width)) * - (max - min) + - min - const val = getValueByPosition(position) - if (!valueBeforeDragRef.current) { - valueBeforeDragRef.current = [...sliderValue] - } - valueBeforeDragRef.current[index] = val - const next = sortValue([...valueBeforeDragRef.current]) - setSliderValue(next) - if (last) { - valueBeforeDragRef.current = undefined - onAfterChangeRange(next) - } - }} + onDrag={onDrag.bind(this, index)} + onSlidingStart={onSlidingStartI.bind(this, index)} style={index === 0 ? { position: 'absolute' } : {}} styles={ss} /> ) } + const gesture = React.useMemo(() => { + const pan = Gesture.Pan() + .enabled(!disabled && !range) + .onBegin(() => runOnJS(onSlidingStartI)()) + .onUpdate((e) => { + // 这里的e.x和e.absoluteX是指手势点的位置,而不是thumb的位置 + runOnJS(onSlide)(e.translationX, false) + }) + .onEnd((e) => { + runOnJS(onSlide)(e.translationX, true) + }) + .onFinalize((_e, success) => { + !success && runOnJS(onAfterChangeRange)(sliderValue) + }) + + // 点击 + const tap = Gesture.Tap() + .enabled(!disabled) + .onFinalize((e, success) => { + success && runOnJS(onTrackClick)(e) + }) + + return Gesture.Race(pan, tap) + }, [ + disabled, + onAfterChangeRange, + onSlide, + onSlidingStartI, + onTrackClick, + range, + sliderValue, + ]) + return ( - - true}> - - - {/* 刻度 */} - {ticks && ( - + + + + + {/* 刻度 */} + {ticks && ( + + )} + {range && renderThumb(0)} + {renderThumb(1)} + + + {/* 刻度下的标记 */} + {marks && ( + )} - {range && renderThumb(0)} - {renderThumb(1)} - {/* 刻度下的标记 */} - {marks && ( - - )} - + ) } diff --git a/components/slider/style/index.tsx b/components/slider/style/index.tsx index 395b69019..5ab2fea37 100644 --- a/components/slider/style/index.tsx +++ b/components/slider/style/index.tsx @@ -4,6 +4,8 @@ import { Theme } from '../../style' export interface SliderStyle { slider: ViewStyle disabled: ViewStyle + + // 轨道 trackContianer: ViewStyle track: ViewStyle fill: ViewStyle @@ -54,7 +56,7 @@ export default (theme: Theme) => }, thumb: { - zIndex: 2, + zIndex: 3, }, ticks: { @@ -62,6 +64,7 @@ export default (theme: Theme) => width: '100%', height: 3, backgroundColor: 'transparent', + zIndex: 2, }, tick: { position: 'absolute', diff --git a/components/slider/thumb.tsx b/components/slider/thumb.tsx index 67eccb20b..791e63c6f 100644 --- a/components/slider/thumb.tsx +++ b/components/slider/thumb.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react' -import React, { useContext, useState } from 'react' +import React, { useMemo, useState } from 'react' import { LayoutChangeEvent, LayoutRectangle, @@ -9,7 +9,6 @@ import { } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import Animated, { runOnJS, useAnimatedStyle } from 'react-native-reanimated' -import HapticsContext from '../provider/HapticsContext' import Tooltip from '../tooltip' import { SliderStyle } from './style' import { ThumbIcon } from './thumb-icon' @@ -19,8 +18,10 @@ type ThumbProps = { min: number max: number disabled: boolean + isSliding: boolean trackLayout?: LayoutRectangle onDrag: (value: number, last?: boolean) => void + onSlidingStart: () => void icon?: ReactNode popover: boolean | ((value: number) => ReactNode) residentPopover: boolean @@ -38,8 +39,10 @@ const Thumb: FC = (props) => { icon, residentPopover, onDrag, + onSlidingStart, style, styles, + isSliding, } = props const [thumbLayout, setThumbLayout] = useState() @@ -60,21 +63,22 @@ const Thumb: FC = (props) => { }, [max, min, thumbLayout?.width, trackLayout?.width, value]) const [dragging, setDragging] = useState(false) - const onHaptics = useContext(HapticsContext) - const gesture = Gesture.Pan() - .enabled(!disabled) - .onBegin(() => runOnJS(onHaptics)('slider')) - .onUpdate((e) => { - !dragging && runOnJS(setDragging)(true) - runOnJS(onDrag)(e.absoluteX - (thumbLayout?.width || 0)) - }) - .onEnd((e) => { - runOnJS(onDrag)(e.absoluteX - (thumbLayout?.width || 0), true) - }) - .onFinalize(() => { - runOnJS(setDragging)(false) - }) + const gesture = useMemo( + () => + Gesture.Pan() + .enabled(!disabled) + .onBegin(() => runOnJS(onSlidingStart)()) + .onUpdate((e) => { + !dragging && runOnJS(setDragging)(true) + runOnJS(onDrag)(e.absoluteX - (thumbLayout?.width || 0)) + }) + .onFinalize((e) => { + runOnJS(setDragging)(false) + runOnJS(onDrag)(e.absoluteX - (thumbLayout?.width || 0), true) + }), + [disabled, dragging, onDrag, onSlidingStart, thumbLayout?.width], + ) const renderPopoverContent = typeof props.popover === 'function' @@ -95,7 +99,7 @@ const Thumb: FC = (props) => { {thumbElement} diff --git a/components/slider/ticks.tsx b/components/slider/ticks.tsx index cdbc4080d..47a8a947f 100644 --- a/components/slider/ticks.tsx +++ b/components/slider/ticks.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import React, { useContext, useEffect, useRef } from 'react' +import React, { useContext, useEffect, useMemo, useRef } from 'react' import { View } from 'react-native' import HapticsContext from '../provider/HapticsContext' import { SliderStyle } from './style' @@ -23,13 +23,19 @@ const Ticks: FC = ({ }) => { const onHaptics = useContext(HapticsContext) const didmountRef = useRef(false) + const activeLength = useMemo( + () => + points.filter((point) => point <= upperBound && point >= lowerBound) + .length, + [lowerBound, points, upperBound], + ) useEffect(() => { if (didmountRef.current) { onHaptics('slider') } else { didmountRef.current = true } - }, [upperBound, lowerBound, onHaptics]) + }, [activeLength, onHaptics]) const range = max - min const elements = points.map((point) => { From 91c89eb3db67c9332dfb633f13cacd2d267b124a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Mon, 7 Oct 2024 20:33:09 +0800 Subject: [PATCH 07/24] fix: Tooltip safe floatingStyles --- components/list/List.tsx | 2 +- components/modal/demo/basic.tsx | 38 +++++++++++++++++++++++++++++++-- components/tooltip/Tooltip.tsx | 9 +++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/components/list/List.tsx b/components/list/List.tsx index 38b116ed9..6779b9a9d 100644 --- a/components/list/List.tsx +++ b/components/list/List.tsx @@ -4,7 +4,7 @@ import { useTheme } from '../style' import { ListPropsType } from './PropsType' import listStyles, { ListStyle } from './style/index' -export interface ListProps extends ListPropsType { +export interface ListProps extends ListPropsType, ViewProps { styles?: Partial style?: StyleProp } diff --git a/components/modal/demo/basic.tsx b/components/modal/demo/basic.tsx index 803396192..e95102678 100644 --- a/components/modal/demo/basic.tsx +++ b/components/modal/demo/basic.tsx @@ -1,7 +1,15 @@ /* tslint:disable:no-console */ import React from 'react' import { ScrollView, Text, View } from 'react-native' -import { Button, Modal, Toast, WhiteSpace, WingBlank } from '../../' +import { + Button, + List, + Modal, + Switch, + Toast, + WhiteSpace, + WingBlank, +} from '../../' export default class BasicModalExample extends React.Component { constructor(props: any) { @@ -10,6 +18,7 @@ export default class BasicModalExample extends React.Component { visible: false, visible1: false, visible2: false, + modalType: 'portal', } } @@ -121,6 +130,25 @@ export default class BasicModalExample extends React.Component { ] return ( + + { + this.setState({ modalType: val ? 'modal' : 'portal' }) + }} + checkedChildren="modal" + unCheckedChildren="portal" + /> + }> + 切换modalType + + `modalType='modal'`时将调用原生Modal{' '} + + +