From 6bff2803d20cba9e6e24bca28666a0fdd800bc73 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 19:32:33 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81popup=20?= =?UTF-8?q?=E9=AB=98=E5=BA=A6=E5=8F=AF=E4=BB=A5=E4=BC=B8=E7=BC=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/demos/h5/demo1.tsx | 37 ++- src/packages/popup/demos/taro/demo1.tsx | 37 ++- src/packages/popup/doc.en-US.md | 9 +- src/packages/popup/doc.md | 9 +- src/packages/popup/doc.taro.md | 9 +- src/packages/popup/doc.zh-TW.md | 9 +- src/packages/popup/popup.scss | 5 - src/packages/popup/popup.taro.tsx | 115 +++++-- src/packages/popup/popup.tsx | 84 +++++- src/packages/range/range.tsx | 381 ++---------------------- src/types/spec/popup/base.ts | 5 + 11 files changed, 298 insertions(+), 402 deletions(-) diff --git a/src/packages/popup/demos/h5/demo1.tsx b/src/packages/popup/demos/h5/demo1.tsx index c39f71b785..677eefd94e 100644 --- a/src/packages/popup/demos/h5/demo1.tsx +++ b/src/packages/popup/demos/h5/demo1.tsx @@ -2,24 +2,53 @@ import React, { useState } from 'react' import { Popup, Cell } from '@nutui/nutui-react' const Demo = () => { - const [showIcon, setShowIcon] = useState(false) + const [showPopup, setShowPopup] = useState(false) + const [showPopupResiable, setShowPopupResiable] = useState(false) return ( <> { - setShowIcon(true) + setShowPopup(true) + }} + /> + { + setShowPopupResiable(true) }} /> { - setShowIcon(false) + setShowPopup(false) + }} + /> + { + setShowPopupResiable(false) + }} + onTouchMove={(height, e, direction) => { + console.log('onTouchMove', height, e, direction) + }} + onTouchStart={(height, e) => { + console.log('onTouchStart', height, e) + }} + onTouchEnd={(height, e) => { + console.log('onTouchEnd', height, e) }} /> diff --git a/src/packages/popup/demos/taro/demo1.tsx b/src/packages/popup/demos/taro/demo1.tsx index 990a6ca016..9a21deb0ae 100644 --- a/src/packages/popup/demos/taro/demo1.tsx +++ b/src/packages/popup/demos/taro/demo1.tsx @@ -2,24 +2,53 @@ import React, { useState } from 'react' import { Popup, Cell } from '@nutui/nutui-react-taro' const Demo = () => { - const [showIcon, setShowIcon] = useState(false) + const [showPopup, setShowPopup] = useState(false) + const [showPopupResiable, setShowPopupResiable] = useState(false) return ( <> { - setShowIcon(true) + setShowPopup(true) + }} + /> + { + setShowPopupResiable(true) }} /> { - setShowIcon(false) + setShowPopup(false) + }} + /> + { + setShowPopupResiable(false) + }} + onTouchMove={(height, e, direction) => { + console.log('onTouchMove', height, e, direction) + }} + onTouchStart={(height, e) => { + console.log('onTouchStart', height, e) + }} + onTouchEnd={(height, e) => { + console.log('onTouchEnd', height, e) }} /> diff --git a/src/packages/popup/doc.en-US.md b/src/packages/popup/doc.en-US.md index 5a70366469..2655f8d1a4 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -87,19 +87,24 @@ import { Popup } from '@nutui/nutui-react' | closeable | whether to show the close button | `boolean` | `false` | | closeIconPosition | close button position | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | Custom Icon | `ReactNode` | `close` | +| resizable | you can resize the height of popup | `boolean` | `false` | +| minHeight | The minHeight of popup | `string` | `26%` | | left | The left of title | `ReactNode` | `-` | | title | The center of title | `ReactNode` | `-` | | description | The subtitle/description | `ReactNode` | `-` | | destroyOnClose | Whether to close after the component is destroyed | `boolean` | `false` | | round | Whether to show rounded corners | `boolean` | `false` | | portal | Mount the specified node | `HTMLElement` \| `(() => HTMLElement)` \| null` | `null` | +| afterShow | afterShow from `Overlay`, Fired when the mask opening animation ends | `event: HTMLElement` | `-` | +| afterClose | afterClose from `Overlay`, Fired when the mask closing animation ends | `event: HTMLElement` | `-` | | onClick | Triggered when the popup is clicked | `event: MouseEvent` | `-` | | onCloseIconClick | Fired when the close icon is clicked | `event: MouseEvent` | `-` | | onOpen | Triggered when the popup is opened | `-` | `-` | | onClose | Fired when the popup is closed | `-` | `-` | -| afterShow | afterShow from `Overlay`, Fired when the mask opening animation ends | `event: HTMLElement` | `-` | -| afterClose | afterClose from `Overlay`, Fired when the mask closing animation ends | `event: HTMLElement` | `-` | | onOverlayClick | Click on the mask to trigger | `event: MouseEvent` | `-` | +| onTouchStart | triggered when starting to touch | `height:string, (event: TouchEvent) => void` | `-` | +| onTouchMove | triggered when starting to move | `(height:string, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | triggered when finishing to touch | `height:string, (event: TouchEvent) => void` | `-` | ## Theming diff --git a/src/packages/popup/doc.md b/src/packages/popup/doc.md index 8c23ec4402..bbdf81af09 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -87,19 +87,24 @@ import { Popup } from '@nutui/nutui-react' | closeable | 是否显示关闭按钮 | `boolean` | `false` | | closeIconPosition | 关闭按钮位置 | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | 自定义 Icon | `ReactNode` | `close` | +| resizable | 上下滑动调整高度 | `boolean` | `false` | +| minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | | description | 子标题/描述部分 | `ReactNode` | `-` | | destroyOnClose | 组件不可见时,卸载内容 | `boolean` | `false` | | round | 是否显示圆角 | `boolean` | `false` | | portal | 指定节点挂载 | `HTMLElement` \| `(() => HTMLElement)` \| null` | `null` | +| afterShow | 继承于`Overlay`, 遮罩打开动画结束时触发 | `event: HTMLElement` | `-` | +| afterClose | 继承于`Overlay`, 遮罩关闭动画结束时触发 | `event: HTMLElement` | `-` | | onClick | 点击弹框时触发 | `event: MouseEvent` | `-` | | onCloseIconClick | 点击关闭图标时触发 | `event: MouseEvent` | `-` | | onOpen | 打开弹框时触发 | `-` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | -| afterShow | 继承于`Overlay`, 遮罩打开动画结束时触发 | `event: HTMLElement` | `-` | -| afterClose | 继承于`Overlay`, 遮罩关闭动画结束时触发 | `event: HTMLElement` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | +| onTouchStart | 开始触碰时触发 | `(height:string, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑动时触发 | `(height:string, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 结束触碰时触发 | `(height:string, event: TouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md index bda19db26e..70b3ab33c3 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -97,19 +97,24 @@ import { Popup } from '@nutui/nutui-react-taro' | closeable | 是否显示关闭按钮 | `boolean` | `false` | | closeIconPosition | 关闭按钮位置 | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | 自定义 Icon | `ReactNode` | `close` | +| resizable | 上下滑动调整高度 | `boolean` | `false` | +| minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | | description | 子标题/描述部分 | `ReactNode` | `-` | | destroyOnClose | 组件不可见时,卸载内容 | `boolean` | `false` | | round | 是否显示圆角 | `boolean` | `false` | | portal | 指定节点挂载 | ``HTMLElement` \| `(() => HTMLElement)` \| null`` | `null` | +| afterShow | 继承于`Overlay`, 遮罩打开动画结束时触发 | `event: HTMLElement` | `-` | +| afterClose | 继承于`Overlay`, 遮罩关闭动画结束时触发 | `event: HTMLElement` | `-` | | onClick | 点击弹框时触发 | `event: MouseEvent` | `-` | | onCloseIconClick | 点击关闭图标时触发 | `event: MouseEvent` | `-` | | onOpen | 打开弹框时触发 | `-` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | -| afterShow | 继承于`Overlay`, 遮罩打开动画结束时触发 | `event: HTMLElement` | `-` | -| afterClose | 继承于`Overlay`, 遮罩关闭动画结束时触发 | `event: HTMLElement` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | +| onTouchStart | 开始触碰时触发 | `(height: string, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑动时触发 | `(height: string, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 结束触碰时触发 | `(height: string, event: TouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/doc.zh-TW.md b/src/packages/popup/doc.zh-TW.md index deac6ce84f..250d24b77c 100644 --- a/src/packages/popup/doc.zh-TW.md +++ b/src/packages/popup/doc.zh-TW.md @@ -87,19 +87,24 @@ import { Popup } from '@nutui/nutui-react' | closeable | 是否顯示關閉按鈕 | `boolean` | `false` | | closeIconPosition | 關閉按鈕位置(top-left,top-right,bottom-left,bottom-right) | `string` | `top-right` | | closeIcon | 自定義 Icon | `ReactNode` | `close` | +| resizable | 上下滑動調整高度 | `boolean` | `false` | +| minHeight | 設定最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | | description | 子標題/描述部分 | `ReactNode` | `-` | | destroyOnClose | 组件不可见时,卸载内容 | `boolean` | `false` | | round | 是否顯示圓角 | `boolean` | `false` | | portal | 指定節點掛載 | `HTMLElement` \| `(() => HTMLElement)` \| null` | `null` | +| afterShow | 继承于`Overlay`, 遮罩打開動畫結束時觸發 | `event: HTMLElement` | `-` | +| afterClose | 继承于`Overlay`, 遮罩關閉動畫結束時觸發 | `event: HTMLElement` | `-` | | onClick | 點擊彈框時觸發 | `event: MouseEvent` | `-` | | onCloseIconClick | 點擊關閉圖標時觸發 | `event: MouseEvent` | `-` | | onOpen | 打開彈框時觸發 | `-` | `-` | | onClose | 關閉彈框時觸發 | `-` | `-` | -| afterShow | 继承于`Overlay`, 遮罩打開動畫結束時觸發 | `event: HTMLElement` | `-` | -| afterClose | 继承于`Overlay`, 遮罩關閉動畫結束時觸發 | `event: HTMLElement` | `-` | | onOverlayClick | 點擊遮罩觸發 | `event: MouseEvent` | `-` | +| onTouchStart | 開始觸碰時觸發 | `(height: string, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑動時觸發 | `(height: string, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 結束觸碰時觸發 | `(height: string, event: TouchEvent) => void` | `-` | ## 主題定制 diff --git a/src/packages/popup/popup.scss b/src/packages/popup/popup.scss index e14b25a059..43c2d714f3 100644 --- a/src/packages/popup/popup.scss +++ b/src/packages/popup/popup.scss @@ -90,11 +90,6 @@ } } - &-bottom, - &-top { - max-height: 87%; - } - &-bottom { bottom: 0; left: 0; diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index 90a33b2858..edcc332338 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -4,17 +4,20 @@ import React, { useEffect, ReactElement, ReactPortal, + useRef, } from 'react' import { createPortal } from 'react-dom' import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' import { Close } from '@nutui/icons-react-taro' import { View, ITouchEvent } from '@tarojs/components' +import { getRectInMultiPlatform } from '@/utils/taro/get-rect' import { defaultOverlayProps } from '@/packages/overlay/overlay.taro' import Overlay from '@/packages/overlay/index.taro' import { useLockScrollTaro } from '@/hooks/taro/use-lock-scoll' import { TaroPopupProps } from '@/types' import { harmony } from '@/utils/taro/platform' +import { pxTransform } from '@/utils/taro/px-transform' const defaultProps: TaroPopupProps = { ...defaultOverlayProps, @@ -29,10 +32,15 @@ const defaultProps: TaroPopupProps = { portal: null, overlay: true, round: false, + resizable: false, + minHeight: '26%', onOpen: () => {}, onClose: () => {}, onOverlayClick: () => true, onCloseIconClick: () => true, + onTouchStart: () => {}, + onTouchMove: () => {}, + onTouchEnd: () => {}, } // 默认1000,参看variables @@ -40,7 +48,10 @@ const _zIndex = 1100 export const Popup: FunctionComponent< Partial & - Omit, 'onClick' | 'title'> + Omit< + React.HTMLAttributes, + 'onClick' | 'title' | 'onTouchStart' | 'onTouchMove' | 'onTouchEnd' + > > = (props) => { const { children, @@ -65,6 +76,8 @@ export const Popup: FunctionComponent< className, destroyOnClose, portal, + resizable, + minHeight, onOpen, onClose, onOverlayClick, @@ -72,21 +85,29 @@ export const Popup: FunctionComponent< afterShow, afterClose, onClick, + onTouchStart, + onTouchMove, + onTouchEnd, } = { ...defaultProps, ...props } - let innerIndex = zIndex || _zIndex const [index, setIndex] = useState(innerIndex) const [innerVisible, setInnerVisible] = useState(visible) const [showChildren, setShowChildren] = useState(true) const [transitionName, setTransitionName] = useState('') - const refObject = useLockScrollTaro(innerVisible && lockScroll) - const classPrefix = 'nut-popup' + const nodeRef = useLockScrollTaro(innerVisible && lockScroll) + + const rootRect = useRef(null) + const touchStartRef = useRef(0) + const touchMoveDistanceRef = useRef(0) + const heightRef = useRef(0) + const isTouching = useRef(false) + const classPrefix = 'nut-popup' const overlayStyles = { ...overlayStyle, } const contentZIndex = harmony() ? index + 1 : index // 解决harmony层级问题 - const popStyles = { zIndex: contentZIndex, ...style } + const popStyles = { zIndex: contentZIndex, minHeight, ...style } const popClassName = classNames( classPrefix, { @@ -98,6 +119,10 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if (resizable && nodeRef.current && heightRef.current) { + nodeRef.current.style.height = `${heightRef.current}px` + } setInnerVisible(true) setIndex(++innerIndex) } @@ -182,26 +207,80 @@ export const Popup: FunctionComponent< } } + const handleTouchStart = async (event: ITouchEvent) => { + if (!resizable || !nodeRef.current) return + // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 + touchStartRef.current = event.touches[0].pageY + // 标记开始滑动 + isTouching.current = true + // 标记当前popup的高度 + const rect = await getRectInMultiPlatform(nodeRef.current) + rootRect.current = rect + heightRef.current = + rootRect.current?.height || nodeRef.current?.offsetHeight || 0 + // console.log( + // 'touchstart', + // touchStartRef.current, + // heightRef.current, + // rootRect.current, + // nodeRef.current?.offsetHeight + // ) + onTouchStart?.(rootRect.current.height, event) + } + + const handleTouchMove = (event: ITouchEvent) => { + if (!resizable || !nodeRef.current || !rootRect.current) return + event.stopPropagation() + + // console.log('向下', rootRect.current.height) + + // move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = + event.touches[0].pageY - touchStartRef.current + // 向下滑动 + if (touchMoveDistanceRef.current > 0 && isTouching.current) { + nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` + onTouchMove?.(nodeRef.current.style.height, event, 'down') + // console.log('向下', nodeRef.current.style.height) + } else { + // 向上滑动 + nodeRef.current.style.height = pxTransform( + heightRef.current - touchMoveDistanceRef.current + ) + onTouchMove?.(nodeRef.current.style.height, event, 'up') + // console.log('向上', nodeRef.current.style.height) + } + } + + const handleTouchEnd = (event: ITouchEvent) => { + if (!resizable || !nodeRef.current || !rootRect.current) return + console.log('touchend', event) + isTouching.current = false + onTouchEnd?.(nodeRef.current.style.height, event) + } + const renderContent = () => { return ( - <> - - {renderTitle()} - {showChildren ? children : null} - - + + {renderTitle()} + {showChildren ? children : null} + ) } const renderPop = () => { return ( {}, onClose: () => {}, onOverlayClick: () => true, onCloseIconClick: () => true, + onTouchStart: () => {}, + onTouchMove: () => {}, + onTouchEnd: () => {}, } // 默认1000,参看variables const _zIndex = 1100 export const Popup: FunctionComponent< - Partial & Omit, 'title'> + Partial & + Omit< + React.HTMLAttributes, + 'title' | 'onTouchStart' | 'onTouchMove' | 'onTouchEnd' + > > = (props) => { const { children, @@ -62,6 +74,8 @@ export const Popup: FunctionComponent< className, destroyOnClose, portal, + resizable, + minHeight, onOpen, onClose, onOverlayClick, @@ -69,6 +83,9 @@ export const Popup: FunctionComponent< afterShow, afterClose, onClick, + onTouchStart, + onTouchMove, + onTouchEnd, } = { ...defaultProps, ...props } const nodeRef = React.useRef(null) let innerIndex = zIndex || _zIndex @@ -77,13 +94,23 @@ export const Popup: FunctionComponent< const [showChildren, setShowChildren] = useState(true) const [transitionName, setTransitionName] = useState('') + const touchStartRef = useRef(0) + const touchMoveDistanceRef = useRef(0) + const heightRef = useRef(0) + const isTouching = useRef(false) + useLockScroll(nodeRef, innerVisible && lockScroll) const classPrefix = 'nut-popup' const overlayStyles = { ...overlayStyle, } - const popStyles = { ...style, zIndex: index } + const popStyles = { + ...style, + zIndex: index, + minHeight, + } + const popClassName = classNames( classPrefix, { @@ -95,6 +122,10 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if (resizable && nodeRef.current && heightRef.current) { + nodeRef.current.style.height = `${heightRef.current}px` + } setInnerVisible(true) setIndex(++innerIndex) } @@ -176,6 +207,51 @@ export const Popup: FunctionComponent< return renderCloseIcon() } } + + const handleTouchStart = (event: TouchEvent) => { + if (!resizable || !nodeRef.current) return + // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 + touchStartRef.current = event.touches[0].pageY + // 标记开始滑动 + isTouching.current = true + // 标记当前popup的高度 + heightRef.current = nodeRef.current?.offsetHeight || 0 + console.log('touchstart', touchStartRef.current, heightRef.current) + onTouchStart?.(nodeRef.current.style.height, event) + } + + const handleTouchMove = (event: TouchEvent) => { + if (!resizable || !nodeRef.current) return + event.stopPropagation() + + // move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = + event.touches[0].pageY - touchStartRef.current + + // console.log( + // 'touchMoveDistanceRef.current', + // touchMoveDistanceRef.current, + // event.touches[0].pageY, + // touchStartRef.current + // ) + // 向下滑动 + if (touchMoveDistanceRef.current > 0) { + nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` + onTouchMove?.(nodeRef.current.style.height, event, 'down') + } else { + // 向上滑动 + nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` + onTouchMove?.(nodeRef.current.style.height, event, 'up') + } + } + + const handleTouchEnd = (event: TouchEvent) => { + if (!resizable || !nodeRef.current) return + console.log('touchend', event) + isTouching.current = false + onTouchEnd?.(nodeRef.current.style.height, event) + } + const renderPop = () => { return ( {renderTitle()} {showChildren && children} diff --git a/src/packages/range/range.tsx b/src/packages/range/range.tsx index a6a623a142..e318d109dc 100644 --- a/src/packages/range/range.tsx +++ b/src/packages/range/range.tsx @@ -2,7 +2,6 @@ import type { TouchEvent } from 'react' import React, { FunctionComponent, useCallback, - useEffect, useMemo, useRef, useState, @@ -22,7 +21,6 @@ const defaultProps = { max: 100, step: 1, vertical: false, - marks: {}, } as WebRangeProps const classPrefix = 'nut-range' @@ -32,13 +30,6 @@ const isSameValue = (newValue: RangeValue, oldValue: RangeValue) => { return JSON.stringify(newValue) === JSON.stringify(oldValue) } -const handleOverlap = (value: number[]) => { - if (value[0] > value[1]) { - return value.slice(0).reverse() - } - return value -} - export const Range: FunctionComponent< Partial & Omit< @@ -50,14 +41,7 @@ export const Range: FunctionComponent< const { className, style, - range, - disabled, - button, vertical, - marks, - minDescription, - maxDescription, - currentDescription, min, max, step, @@ -68,15 +52,9 @@ export const Range: FunctionComponent< onEnd, } = { ...defaultProps, ...props } - const rtlClassPrefix = useMemo( - () => `rtl-${vertical ? verticalClassPrefix : classPrefix}`, - [vertical] - ) - const [buttonIndex, setButtonIndex] = useState(0) const [dragStatus, setDragStatus] = useState('start') const touch = useTouch() - const root = useRef(null) - const [marksList, setMarksList] = useState([]) + const nodeRef = useRef(null) const [startValue, setStartValue] = useState(0) const scope = useMemo(() => { if (max < min || max === min) { @@ -94,33 +72,8 @@ export const Range: FunctionComponent< finalValue: 0, onChange: handleChange, }) - const [exactValue, setExactValue] = useState( - () => value || defaultValue || 0 - ) - const marksRef = useRef<{ [key: string]: any }>({}) - useEffect(() => { - if (marks) { - if (Array.isArray(marks)) { - const list = marks - .sort((a, b) => a.value - b.value) - .filter((point) => point.value >= min && point.value <= max) - setMarksList(list.map((mark) => mark.value)) - list.forEach((mark) => { - marksRef.current[mark.value] = - mark.label !== undefined ? mark.label : mark.value - }) - } else { - const marksKeys = Object.keys(marks) - const list: any = marksKeys - .map(parseFloat) - .sort((a, b) => a - b) - .filter((point) => point >= min && point <= max) - setMarksList(list) - } - } - }, [marks, max, min]) + const classes = classNames(classPrefix, { - [`${classPrefix}-disabled`]: disabled, [verticalClassPrefix]: vertical, }) @@ -131,57 +84,15 @@ export const Range: FunctionComponent< }, className ) - const markClassName = useCallback( - (mark: any) => { - const classPrefix = 'nut-range-mark' - const verticalClassPrefix = 'nut-range-vertical-mark' - let lowerBound = min - let upperBound = max - if (range && Array.isArray(current)) { - lowerBound = current[0] - upperBound = current[1] - } else { - upperBound = current as number - } - const isActive = mark <= upperBound && mark >= lowerBound - const classNames = [ - `${classPrefix}-text-wrapper`, - `${isActive ? `${classPrefix}-text-wrapper-active` : ''}`, - ] - - if (vertical) { - classNames.push(`${verticalClassPrefix}-text-wrapper`) - isActive && - classNames.push(`${verticalClassPrefix}-text-active-wrapper`) - } - - if (rtl) { - classNames.push(`${rtlClassPrefix}-mark-text-wrapper`) - } - - return classNames.join(' ') - }, - [min, max, range, current, vertical, rtl, rtlClassPrefix] - ) - - const isRange = useCallback( - (val: any) => { - return !!range && Array.isArray(val) - }, - [range] - ) const calcMainAxis = useCallback(() => { const modelVal = current as any - return isRange(modelVal) - ? `${((modelVal[1] - modelVal[0]) * 100) / scope}%` - : `${((modelVal - min) * 100) / scope}%` - }, [current, isRange, min, scope]) + return `${((modelVal - min) * 100) / scope}%` + }, [current, min, scope]) const calcOffset = useCallback(() => { - const modelVal = current as any - return isRange(modelVal) ? `${((modelVal[0] - min) * 100) / scope}%` : `0%` - }, [current, isRange, min, scope]) + return `0%` + }, []) const barStyle = useCallback(() => { if (vertical) { @@ -199,32 +110,6 @@ export const Range: FunctionComponent< } }, [calcMainAxis, calcOffset, dragStatus, rtl, vertical]) - const marksStyle = useCallback( - (mark: any) => { - const dir = rtl ? 'right' : 'left' - let style: any = { - [dir]: `${((mark - min) / scope) * 100}%`, - } - if (vertical) { - style = { - top: `${((mark - min) / scope) * 100}%`, - } - } - return style - }, - [min, rtl, scope, vertical] - ) - - const tickClass = useCallback( - (mark: any) => { - if (range && Array.isArray(current)) { - return mark <= current[1] && mark >= current[0] - } - return mark <= current - }, - [current, range] - ) - const format = useCallback( (value: number) => { value = Math.max(+min, Math.min(value, +max)) @@ -235,67 +120,29 @@ export const Range: FunctionComponent< const updateValue = useCallback( (value: any, end?: boolean) => { - if (isRange(value)) { - value = handleOverlap(value).map(format) - } else { - value = format(value) - } + value = format(value) if (!isSameValue(value, current)) { setCurrent(value) } end && onEnd && onEnd(value) }, - [current, format, isRange, onEnd, setCurrent] - ) - - const handleClick = useCallback( - (event: any) => { - if (disabled || !root.current) return - setDragStatus('') - const rect = getRect(root.current) - let delta = event.clientX - rect.left - let total = rect.width - if (vertical) { - delta = event.clientY - rect.top - total = rect.height - } - const value = min + (delta / total) * scope - setExactValue(current) - if (isRange(current)) { - const [left, right] = current as any - const middle = (left + right) / 2 - if (value <= middle) { - updateValue([value, right], true) - } else { - updateValue([left, value], true) - } - } else { - updateValue(value, true) - } - }, - [current, disabled, isRange, min, scope, updateValue, vertical] + [current, format, onEnd, setCurrent] ) const onTouchStart = useCallback( (event: any) => { - if (disabled) return touch.start(event) - setExactValue(current) - if (isRange(current)) { - setStartValue((current as number[]).map(format)) - } else { - setStartValue(format(current as number)) - } + setStartValue(format(current as number)) setDragStatus('start') }, - [current, disabled, format, isRange, touch] + [current, format, touch] ) const onTouchMove = useCallback( (event: TouchEvent) => { event.stopPropagation() - if (disabled || !root.current) { + if (!nodeRef.current) { return } if (dragStatus === 'start') { @@ -303,7 +150,7 @@ export const Range: FunctionComponent< } touch.move(event) setDragStatus('draging') - const rect = getRect(root.current) + const rect = getRect(nodeRef.current) let delta = touch.deltaX.current let total = rect.width let diff = (delta / total) * scope @@ -313,182 +160,18 @@ export const Range: FunctionComponent< total = rect.height diff = (delta / total) * scope } - let newValue - if (isRange(startValue)) { - newValue = (exactValue as number[]).slice() - newValue[buttonIndex] = startValue[buttonIndex] + diff - } else { - newValue = startValue + diff - } - setExactValue(newValue) + const newValue = startValue + diff updateValue(newValue) }, - [ - buttonIndex, - disabled, - dragStatus, - exactValue, - isRange, - onStart, - rtl, - scope, - startValue, - touch, - updateValue, - vertical, - ] + [dragStatus, onStart, rtl, scope, startValue, touch, updateValue, vertical] ) const onTouchEnd = useCallback(() => { - if (disabled) { - return - } if (dragStatus === 'draging') { updateValue(current, true) } setDragStatus('') - }, [current, disabled, dragStatus, updateValue]) - - const curValue = useCallback( - (idx?: number) => { - const modelVal = current as any - const value = typeof idx === 'number' ? modelVal[idx] : modelVal - return value - }, - [current] - ) - - const renderButton = useCallback( - (index?: number) => { - const buttonNumberTransform = vertical - ? 'translate(100%, -50%)' - : 'translate(-50%, -100%)' - - return ( - <> - {button || ( -
- {currentDescription !== null && ( -
- {currentDescription - ? currentDescription(curValue(index)) - : curValue(index)} -
- )} -
- )} - - ) - }, - [button, curValue, currentDescription, rtl, rtlClassPrefix, vertical] - ) - const renderMarks = useCallback(() => { - if (marksList.length <= 0) return null - const markcls = classNames(`${classPrefix}-mark`, { - [`${verticalClassPrefix}-mark`]: vertical, - [`${rtlClassPrefix}-mark`]: rtl, - }) - const textcls = classNames(`${classPrefix}-mark-text`, { - [`${verticalClassPrefix}-mark-text`]: vertical, - }) - return ( -
- {marksList.map((mark: any) => { - return ( - - - {Array.isArray(marks) ? marksRef.current[mark] : marks[mark]} - - - - ) - })} -
- ) - }, [ - markClassName, - marks, - marksList, - marksStyle, - rtl, - rtlClassPrefix, - tickClass, - vertical, - ]) - - const getWrapperTransform = useCallback(() => { - const wrapperTransform = 'translate(-50%, -50%)' - return wrapperTransform - }, []) - - const renderRangeButton = useCallback(() => { - return [0, 1].map((item, index) => { - const isLeft = index === 0 - const suffix = isLeft ? 'left' : 'right' - const transform = 'translate(-50%, -50%)' - const cls = classNames(`${classPrefix}-button-wrapper-${suffix}`, { - [`${verticalClassPrefix}-button-wrapper-${suffix}`]: vertical, - [`${rtlClassPrefix}-button-wrapper-${suffix}`]: rtl, - }) - - return ( -
{ - setButtonIndex(index) - onTouchStart(e) - }} - onTouchMove={onTouchMove} - onTouchEnd={onTouchEnd} - onTouchCancel={onTouchEnd} - onClick={(e) => e.stopPropagation()} - > - {renderButton(index)} -
- ) - }) - }, [ - onTouchEnd, - onTouchMove, - onTouchStart, - renderButton, - vertical, - rtl, - rtlClassPrefix, - ]) + }, [current, dragStatus, updateValue]) const renderSingleButton = useCallback(() => { return ( @@ -498,51 +181,27 @@ export const Range: FunctionComponent< })} style={{ // @ts-ignore - transform: getWrapperTransform(), + transform: 'translate(-50%, -50%)', }} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} onTouchCancel={onTouchEnd} onClick={(e) => e.stopPropagation()} - > - {renderButton()} - + /> ) - }, [ - getWrapperTransform, - onTouchEnd, - onTouchMove, - onTouchStart, - renderButton, - vertical, - ]) - - const renderButtonWrapper = useCallback(() => { - if (range) { - return renderRangeButton() - } - return renderSingleButton() - }, [renderRangeButton, renderSingleButton, range]) + }, [onTouchEnd, onTouchMove, onTouchStart, vertical]) return (
- {minDescription !== null && ( -
{minDescription || min}
- )} -
- {renderMarks()} - +
- {renderButtonWrapper()} + {renderSingleButton()}
- {maxDescription !== null && ( -
{maxDescription || max}
- )}
) } diff --git a/src/types/spec/popup/base.ts b/src/types/spec/popup/base.ts index f3c4421266..0310f778f4 100644 --- a/src/types/spec/popup/base.ts +++ b/src/types/spec/popup/base.ts @@ -21,8 +21,13 @@ export interface BasePopup extends BaseProps, BaseOverlay { destroyOnClose: boolean overlay: boolean round: boolean + resizable: boolean + minHeight: string onOpen: () => void onClose: () => void onOverlayClick: (e: any) => boolean | void onCloseIconClick: (e: any) => boolean | void + onTouchMove: (height: string, e: any, direction: 'up' | 'down') => void + onTouchStart: (height: string, e: any) => void + onTouchEnd: (height: string, e: any) => void } From 0ed45211e46ee8866a6efced0a3e13c558b31f11 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 21:25:27 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/popup.taro.tsx | 49 ++-- src/packages/popup/popup.tsx | 19 +- src/packages/range/range.tsx | 381 ++++++++++++++++++++++++++++-- src/types/spec/popup/base.ts | 6 +- 4 files changed, 399 insertions(+), 56 deletions(-) diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index edcc332338..62f23f76cc 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -11,7 +11,7 @@ import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' import { Close } from '@nutui/icons-react-taro' import { View, ITouchEvent } from '@tarojs/components' -import { getRectInMultiPlatform } from '@/utils/taro/get-rect' +import { getRectInMultiPlatformWithoutCache } from '@/utils/taro/get-rect' import { defaultOverlayProps } from '@/packages/overlay/overlay.taro' import Overlay from '@/packages/overlay/index.taro' import { useLockScrollTaro } from '@/hooks/taro/use-lock-scoll' @@ -214,49 +214,56 @@ export const Popup: FunctionComponent< // 标记开始滑动 isTouching.current = true // 标记当前popup的高度 - const rect = await getRectInMultiPlatform(nodeRef.current) + const rect = await getRectInMultiPlatformWithoutCache(nodeRef.current) rootRect.current = rect heightRef.current = - rootRect.current?.height || nodeRef.current?.offsetHeight || 0 + nodeRef.current?.offsetHeight || rootRect.current?.height || 0 // console.log( // 'touchstart', // touchStartRef.current, - // heightRef.current, + // heightRef.current, // // rootRect.current, - // nodeRef.current?.offsetHeight + // nodeRef.current?.offsetHeight, // + // nodeRef.current.style.height // // ) - onTouchStart?.(rootRect.current.height, event) + onTouchStart?.(heightRef.current, event) } const handleTouchMove = (event: ITouchEvent) => { if (!resizable || !nodeRef.current || !rootRect.current) return event.stopPropagation() - // console.log('向下', rootRect.current.height) - // move过程中,当前的pageY 与 start值比较 touchMoveDistanceRef.current = event.touches[0].pageY - touchStartRef.current - // 向下滑动 - if (touchMoveDistanceRef.current > 0 && isTouching.current) { - nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` - onTouchMove?.(nodeRef.current.style.height, event, 'down') - // console.log('向下', nodeRef.current.style.height) - } else { - // 向上滑动 - nodeRef.current.style.height = pxTransform( - heightRef.current - touchMoveDistanceRef.current - ) - onTouchMove?.(nodeRef.current.style.height, event, 'up') - // console.log('向上', nodeRef.current.style.height) + + const handleMove = () => { + const currentHeight = heightRef.current - touchMoveDistanceRef.current + nodeRef.current.style.height = pxTransform(currentHeight) + if (touchMoveDistanceRef.current > 0 && isTouching.current) { + // 向下滑动 + onTouchMove?.(currentHeight, event, 'down') + // console.log('向下', nodeRef.current.style.height) + } else { + // 向上滑动 + onTouchMove?.(currentHeight, event, 'up') + console.log( + '向上', + heightRef.current, + touchMoveDistanceRef.current, + currentHeight + ) + } } + requestAnimationFrame(handleMove) } const handleTouchEnd = (event: ITouchEvent) => { if (!resizable || !nodeRef.current || !rootRect.current) return console.log('touchend', event) isTouching.current = false - onTouchEnd?.(nodeRef.current.style.height, event) + const currentHeight = heightRef.current - touchMoveDistanceRef.current + onTouchEnd?.(currentHeight, event) } const renderContent = () => { diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index fc5b5a208c..692e34d33b 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -217,7 +217,7 @@ export const Popup: FunctionComponent< // 标记当前popup的高度 heightRef.current = nodeRef.current?.offsetHeight || 0 console.log('touchstart', touchStartRef.current, heightRef.current) - onTouchStart?.(nodeRef.current.style.height, event) + onTouchStart?.(heightRef.current, event) } const handleTouchMove = (event: TouchEvent) => { @@ -228,20 +228,14 @@ export const Popup: FunctionComponent< touchMoveDistanceRef.current = event.touches[0].pageY - touchStartRef.current - // console.log( - // 'touchMoveDistanceRef.current', - // touchMoveDistanceRef.current, - // event.touches[0].pageY, - // touchStartRef.current - // ) + const currentHeight = heightRef.current - touchMoveDistanceRef.current + nodeRef.current.style.height = `${currentHeight}px` // 向下滑动 if (touchMoveDistanceRef.current > 0) { - nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` - onTouchMove?.(nodeRef.current.style.height, event, 'down') + onTouchMove?.(currentHeight, event, 'down') } else { // 向上滑动 - nodeRef.current.style.height = `${heightRef.current - touchMoveDistanceRef.current}px` - onTouchMove?.(nodeRef.current.style.height, event, 'up') + onTouchMove?.(currentHeight, event, 'up') } } @@ -249,7 +243,8 @@ export const Popup: FunctionComponent< if (!resizable || !nodeRef.current) return console.log('touchend', event) isTouching.current = false - onTouchEnd?.(nodeRef.current.style.height, event) + const currentHeight = heightRef.current - touchMoveDistanceRef.current + onTouchEnd?.(currentHeight, event) } const renderPop = () => { diff --git a/src/packages/range/range.tsx b/src/packages/range/range.tsx index e318d109dc..a6a623a142 100644 --- a/src/packages/range/range.tsx +++ b/src/packages/range/range.tsx @@ -2,6 +2,7 @@ import type { TouchEvent } from 'react' import React, { FunctionComponent, useCallback, + useEffect, useMemo, useRef, useState, @@ -21,6 +22,7 @@ const defaultProps = { max: 100, step: 1, vertical: false, + marks: {}, } as WebRangeProps const classPrefix = 'nut-range' @@ -30,6 +32,13 @@ const isSameValue = (newValue: RangeValue, oldValue: RangeValue) => { return JSON.stringify(newValue) === JSON.stringify(oldValue) } +const handleOverlap = (value: number[]) => { + if (value[0] > value[1]) { + return value.slice(0).reverse() + } + return value +} + export const Range: FunctionComponent< Partial & Omit< @@ -41,7 +50,14 @@ export const Range: FunctionComponent< const { className, style, + range, + disabled, + button, vertical, + marks, + minDescription, + maxDescription, + currentDescription, min, max, step, @@ -52,9 +68,15 @@ export const Range: FunctionComponent< onEnd, } = { ...defaultProps, ...props } + const rtlClassPrefix = useMemo( + () => `rtl-${vertical ? verticalClassPrefix : classPrefix}`, + [vertical] + ) + const [buttonIndex, setButtonIndex] = useState(0) const [dragStatus, setDragStatus] = useState('start') const touch = useTouch() - const nodeRef = useRef(null) + const root = useRef(null) + const [marksList, setMarksList] = useState([]) const [startValue, setStartValue] = useState(0) const scope = useMemo(() => { if (max < min || max === min) { @@ -72,8 +94,33 @@ export const Range: FunctionComponent< finalValue: 0, onChange: handleChange, }) - + const [exactValue, setExactValue] = useState( + () => value || defaultValue || 0 + ) + const marksRef = useRef<{ [key: string]: any }>({}) + useEffect(() => { + if (marks) { + if (Array.isArray(marks)) { + const list = marks + .sort((a, b) => a.value - b.value) + .filter((point) => point.value >= min && point.value <= max) + setMarksList(list.map((mark) => mark.value)) + list.forEach((mark) => { + marksRef.current[mark.value] = + mark.label !== undefined ? mark.label : mark.value + }) + } else { + const marksKeys = Object.keys(marks) + const list: any = marksKeys + .map(parseFloat) + .sort((a, b) => a - b) + .filter((point) => point >= min && point <= max) + setMarksList(list) + } + } + }, [marks, max, min]) const classes = classNames(classPrefix, { + [`${classPrefix}-disabled`]: disabled, [verticalClassPrefix]: vertical, }) @@ -84,15 +131,57 @@ export const Range: FunctionComponent< }, className ) + const markClassName = useCallback( + (mark: any) => { + const classPrefix = 'nut-range-mark' + const verticalClassPrefix = 'nut-range-vertical-mark' + let lowerBound = min + let upperBound = max + if (range && Array.isArray(current)) { + lowerBound = current[0] + upperBound = current[1] + } else { + upperBound = current as number + } + const isActive = mark <= upperBound && mark >= lowerBound + const classNames = [ + `${classPrefix}-text-wrapper`, + `${isActive ? `${classPrefix}-text-wrapper-active` : ''}`, + ] + + if (vertical) { + classNames.push(`${verticalClassPrefix}-text-wrapper`) + isActive && + classNames.push(`${verticalClassPrefix}-text-active-wrapper`) + } + + if (rtl) { + classNames.push(`${rtlClassPrefix}-mark-text-wrapper`) + } + + return classNames.join(' ') + }, + [min, max, range, current, vertical, rtl, rtlClassPrefix] + ) + + const isRange = useCallback( + (val: any) => { + return !!range && Array.isArray(val) + }, + [range] + ) const calcMainAxis = useCallback(() => { const modelVal = current as any - return `${((modelVal - min) * 100) / scope}%` - }, [current, min, scope]) + return isRange(modelVal) + ? `${((modelVal[1] - modelVal[0]) * 100) / scope}%` + : `${((modelVal - min) * 100) / scope}%` + }, [current, isRange, min, scope]) const calcOffset = useCallback(() => { - return `0%` - }, []) + const modelVal = current as any + return isRange(modelVal) ? `${((modelVal[0] - min) * 100) / scope}%` : `0%` + }, [current, isRange, min, scope]) const barStyle = useCallback(() => { if (vertical) { @@ -110,6 +199,32 @@ export const Range: FunctionComponent< } }, [calcMainAxis, calcOffset, dragStatus, rtl, vertical]) + const marksStyle = useCallback( + (mark: any) => { + const dir = rtl ? 'right' : 'left' + let style: any = { + [dir]: `${((mark - min) / scope) * 100}%`, + } + if (vertical) { + style = { + top: `${((mark - min) / scope) * 100}%`, + } + } + return style + }, + [min, rtl, scope, vertical] + ) + + const tickClass = useCallback( + (mark: any) => { + if (range && Array.isArray(current)) { + return mark <= current[1] && mark >= current[0] + } + return mark <= current + }, + [current, range] + ) + const format = useCallback( (value: number) => { value = Math.max(+min, Math.min(value, +max)) @@ -120,29 +235,67 @@ export const Range: FunctionComponent< const updateValue = useCallback( (value: any, end?: boolean) => { - value = format(value) + if (isRange(value)) { + value = handleOverlap(value).map(format) + } else { + value = format(value) + } if (!isSameValue(value, current)) { setCurrent(value) } end && onEnd && onEnd(value) }, - [current, format, onEnd, setCurrent] + [current, format, isRange, onEnd, setCurrent] + ) + + const handleClick = useCallback( + (event: any) => { + if (disabled || !root.current) return + setDragStatus('') + const rect = getRect(root.current) + let delta = event.clientX - rect.left + let total = rect.width + if (vertical) { + delta = event.clientY - rect.top + total = rect.height + } + const value = min + (delta / total) * scope + setExactValue(current) + if (isRange(current)) { + const [left, right] = current as any + const middle = (left + right) / 2 + if (value <= middle) { + updateValue([value, right], true) + } else { + updateValue([left, value], true) + } + } else { + updateValue(value, true) + } + }, + [current, disabled, isRange, min, scope, updateValue, vertical] ) const onTouchStart = useCallback( (event: any) => { + if (disabled) return touch.start(event) - setStartValue(format(current as number)) + setExactValue(current) + if (isRange(current)) { + setStartValue((current as number[]).map(format)) + } else { + setStartValue(format(current as number)) + } setDragStatus('start') }, - [current, format, touch] + [current, disabled, format, isRange, touch] ) const onTouchMove = useCallback( (event: TouchEvent) => { event.stopPropagation() - if (!nodeRef.current) { + if (disabled || !root.current) { return } if (dragStatus === 'start') { @@ -150,7 +303,7 @@ export const Range: FunctionComponent< } touch.move(event) setDragStatus('draging') - const rect = getRect(nodeRef.current) + const rect = getRect(root.current) let delta = touch.deltaX.current let total = rect.width let diff = (delta / total) * scope @@ -160,18 +313,182 @@ export const Range: FunctionComponent< total = rect.height diff = (delta / total) * scope } - const newValue = startValue + diff + let newValue + if (isRange(startValue)) { + newValue = (exactValue as number[]).slice() + newValue[buttonIndex] = startValue[buttonIndex] + diff + } else { + newValue = startValue + diff + } + setExactValue(newValue) updateValue(newValue) }, - [dragStatus, onStart, rtl, scope, startValue, touch, updateValue, vertical] + [ + buttonIndex, + disabled, + dragStatus, + exactValue, + isRange, + onStart, + rtl, + scope, + startValue, + touch, + updateValue, + vertical, + ] ) const onTouchEnd = useCallback(() => { + if (disabled) { + return + } if (dragStatus === 'draging') { updateValue(current, true) } setDragStatus('') - }, [current, dragStatus, updateValue]) + }, [current, disabled, dragStatus, updateValue]) + + const curValue = useCallback( + (idx?: number) => { + const modelVal = current as any + const value = typeof idx === 'number' ? modelVal[idx] : modelVal + return value + }, + [current] + ) + + const renderButton = useCallback( + (index?: number) => { + const buttonNumberTransform = vertical + ? 'translate(100%, -50%)' + : 'translate(-50%, -100%)' + + return ( + <> + {button || ( +
+ {currentDescription !== null && ( +
+ {currentDescription + ? currentDescription(curValue(index)) + : curValue(index)} +
+ )} +
+ )} + + ) + }, + [button, curValue, currentDescription, rtl, rtlClassPrefix, vertical] + ) + const renderMarks = useCallback(() => { + if (marksList.length <= 0) return null + const markcls = classNames(`${classPrefix}-mark`, { + [`${verticalClassPrefix}-mark`]: vertical, + [`${rtlClassPrefix}-mark`]: rtl, + }) + const textcls = classNames(`${classPrefix}-mark-text`, { + [`${verticalClassPrefix}-mark-text`]: vertical, + }) + return ( +
+ {marksList.map((mark: any) => { + return ( + + + {Array.isArray(marks) ? marksRef.current[mark] : marks[mark]} + + + + ) + })} +
+ ) + }, [ + markClassName, + marks, + marksList, + marksStyle, + rtl, + rtlClassPrefix, + tickClass, + vertical, + ]) + + const getWrapperTransform = useCallback(() => { + const wrapperTransform = 'translate(-50%, -50%)' + return wrapperTransform + }, []) + + const renderRangeButton = useCallback(() => { + return [0, 1].map((item, index) => { + const isLeft = index === 0 + const suffix = isLeft ? 'left' : 'right' + const transform = 'translate(-50%, -50%)' + const cls = classNames(`${classPrefix}-button-wrapper-${suffix}`, { + [`${verticalClassPrefix}-button-wrapper-${suffix}`]: vertical, + [`${rtlClassPrefix}-button-wrapper-${suffix}`]: rtl, + }) + + return ( +
{ + setButtonIndex(index) + onTouchStart(e) + }} + onTouchMove={onTouchMove} + onTouchEnd={onTouchEnd} + onTouchCancel={onTouchEnd} + onClick={(e) => e.stopPropagation()} + > + {renderButton(index)} +
+ ) + }) + }, [ + onTouchEnd, + onTouchMove, + onTouchStart, + renderButton, + vertical, + rtl, + rtlClassPrefix, + ]) const renderSingleButton = useCallback(() => { return ( @@ -181,27 +498,51 @@ export const Range: FunctionComponent< })} style={{ // @ts-ignore - transform: 'translate(-50%, -50%)', + transform: getWrapperTransform(), }} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} onTouchCancel={onTouchEnd} onClick={(e) => e.stopPropagation()} - /> + > + {renderButton()} +
) - }, [onTouchEnd, onTouchMove, onTouchStart, vertical]) + }, [ + getWrapperTransform, + onTouchEnd, + onTouchMove, + onTouchStart, + renderButton, + vertical, + ]) + + const renderButtonWrapper = useCallback(() => { + if (range) { + return renderRangeButton() + } + return renderSingleButton() + }, [renderRangeButton, renderSingleButton, range]) return (
-
+ {minDescription !== null && ( +
{minDescription || min}
+ )} +
+ {renderMarks()} +
- {renderSingleButton()} + {renderButtonWrapper()}
+ {maxDescription !== null && ( +
{maxDescription || max}
+ )}
) } diff --git a/src/types/spec/popup/base.ts b/src/types/spec/popup/base.ts index 0310f778f4..699d4b9f28 100644 --- a/src/types/spec/popup/base.ts +++ b/src/types/spec/popup/base.ts @@ -27,7 +27,7 @@ export interface BasePopup extends BaseProps, BaseOverlay { onClose: () => void onOverlayClick: (e: any) => boolean | void onCloseIconClick: (e: any) => boolean | void - onTouchMove: (height: string, e: any, direction: 'up' | 'down') => void - onTouchStart: (height: string, e: any) => void - onTouchEnd: (height: string, e: any) => void + onTouchMove: (height: number, e: any, direction: 'up' | 'down') => void + onTouchStart: (height: number, e: any) => void + onTouchEnd: (height: number, e: any) => void } From 5c3c7428ab4e3a3a948391d725ae10d1b5486061 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 21:28:12 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/doc.en-US.md | 6 +++--- src/packages/popup/doc.md | 6 +++--- src/packages/popup/doc.taro.md | 6 +++--- src/packages/popup/doc.zh-TW.md | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/packages/popup/doc.en-US.md b/src/packages/popup/doc.en-US.md index 2655f8d1a4..e34c181115 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -102,9 +102,9 @@ import { Popup } from '@nutui/nutui-react' | onOpen | Triggered when the popup is opened | `-` | `-` | | onClose | Fired when the popup is closed | `-` | `-` | | onOverlayClick | Click on the mask to trigger | `event: MouseEvent` | `-` | -| onTouchStart | triggered when starting to touch | `height:string, (event: TouchEvent) => void` | `-` | -| onTouchMove | triggered when starting to move | `(height:string, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | triggered when finishing to touch | `height:string, (event: TouchEvent) => void` | `-` | +| onTouchStart | triggered when starting to touch | `height: number, (event: TouchEvent) => void` | `-` | +| onTouchMove | triggered when starting to move | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | triggered when finishing to touch | `height: number, (event: TouchEvent) => void` | `-` | ## Theming diff --git a/src/packages/popup/doc.md b/src/packages/popup/doc.md index bbdf81af09..8926045b7e 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -102,9 +102,9 @@ import { Popup } from '@nutui/nutui-react' | onOpen | 打开弹框时触发 | `-` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | -| onTouchStart | 开始触碰时触发 | `(height:string, event: TouchEvent) => void` | `-` | -| onTouchMove | 滑动时触发 | `(height:string, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | 结束触碰时触发 | `(height:string, event: TouchEvent) => void` | `-` | +| onTouchStart | 开始触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑动时触发 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 结束触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md index 70b3ab33c3..a2088d535b 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -112,9 +112,9 @@ import { Popup } from '@nutui/nutui-react-taro' | onOpen | 打开弹框时触发 | `-` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | -| onTouchStart | 开始触碰时触发 | `(height: string, event: TouchEvent) => void` | `-` | -| onTouchMove | 滑动时触发 | `(height: string, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | 结束触碰时触发 | `(height: string, event: TouchEvent) => void` | `-` | +| onTouchStart | 开始触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑动时触发 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 结束触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/doc.zh-TW.md b/src/packages/popup/doc.zh-TW.md index 250d24b77c..15e9782b96 100644 --- a/src/packages/popup/doc.zh-TW.md +++ b/src/packages/popup/doc.zh-TW.md @@ -102,9 +102,9 @@ import { Popup } from '@nutui/nutui-react' | onOpen | 打開彈框時觸發 | `-` | `-` | | onClose | 關閉彈框時觸發 | `-` | `-` | | onOverlayClick | 點擊遮罩觸發 | `event: MouseEvent` | `-` | -| onTouchStart | 開始觸碰時觸發 | `(height: string, event: TouchEvent) => void` | `-` | -| onTouchMove | 滑動時觸發 | `(height: string, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | 結束觸碰時觸發 | `(height: string, event: TouchEvent) => void` | `-` | +| onTouchStart | 開始觸碰時觸發 | `(height: number, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑動時觸發 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 結束觸碰時觸發 | `(height: number, event: TouchEvent) => void` | `-` | ## 主題定制 From 41aa14d80f998ac4198b318bfcc63ebd1f28f616 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 21:37:10 +0800 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20=E9=BB=98=E8=AE=A4=E5=80=BC?= =?UTF-8?q?=E4=B8=8D=E9=9C=80=E8=A6=81,=E8=B5=B0=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/popup.taro.tsx | 2 +- src/packages/popup/popup.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index 62f23f76cc..7017f60922 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -33,7 +33,7 @@ const defaultProps: TaroPopupProps = { overlay: true, round: false, resizable: false, - minHeight: '26%', + minHeight: '', onOpen: () => {}, onClose: () => {}, onOverlayClick: () => true, diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 692e34d33b..13a24903c5 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -31,7 +31,7 @@ const defaultProps: WebPopupProps = { overlay: true, round: false, resizable: false, - minHeight: '26%', + minHeight: '', onOpen: () => {}, onClose: () => {}, onOverlayClick: () => true, From dd2694bb37b9a4c94ed709a09215cc74b9c568a7 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 21:47:43 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E9=99=90=E5=88=B6=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/demos/h5/demo2.tsx | 1 + src/packages/popup/doc.en-US.md | 2 +- src/packages/popup/doc.md | 2 +- src/packages/popup/doc.taro.md | 2 +- src/packages/popup/doc.zh-TW.md | 2 +- src/packages/popup/popup.taro.tsx | 25 +++++++++++++++++++++---- src/packages/popup/popup.tsx | 13 +++++++++---- 7 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/packages/popup/demos/h5/demo2.tsx b/src/packages/popup/demos/h5/demo2.tsx index 4718b46b68..e2b4856b4b 100644 --- a/src/packages/popup/demos/h5/demo2.tsx +++ b/src/packages/popup/demos/h5/demo2.tsx @@ -44,6 +44,7 @@ const Demo2 = () => { visible={showTop} destroyOnClose position="top" + resizable onClose={() => { setShowTop(false) }} diff --git a/src/packages/popup/doc.en-US.md b/src/packages/popup/doc.en-US.md index e34c181115..3a14de22fd 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -87,7 +87,7 @@ import { Popup } from '@nutui/nutui-react' | closeable | whether to show the close button | `boolean` | `false` | | closeIconPosition | close button position | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | Custom Icon | `ReactNode` | `close` | -| resizable | you can resize the height of popup | `boolean` | `false` | +| resizable | you can resize the height of popup, just used by position from bottom | `boolean` | `false` | | minHeight | The minHeight of popup | `string` | `26%` | | left | The left of title | `ReactNode` | `-` | | title | The center of title | `ReactNode` | `-` | diff --git a/src/packages/popup/doc.md b/src/packages/popup/doc.md index 8926045b7e..bba0872f00 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -87,7 +87,7 @@ import { Popup } from '@nutui/nutui-react' | closeable | 是否显示关闭按钮 | `boolean` | `false` | | closeIconPosition | 关闭按钮位置 | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | 自定义 Icon | `ReactNode` | `close` | -| resizable | 上下滑动调整高度 | `boolean` | `false` | +| resizable | 上下滑动调整高度,当前只支持从底部弹出 | `boolean` | `false` | | minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md index a2088d535b..fbbcc7aed2 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -97,7 +97,7 @@ import { Popup } from '@nutui/nutui-react-taro' | closeable | 是否显示关闭按钮 | `boolean` | `false` | | closeIconPosition | 关闭按钮位置 | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | 自定义 Icon | `ReactNode` | `close` | -| resizable | 上下滑动调整高度 | `boolean` | `false` | +| resizable | 上下滑动调整高度,当前只支持从底部弹出 | `boolean` | `false` | | minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | diff --git a/src/packages/popup/doc.zh-TW.md b/src/packages/popup/doc.zh-TW.md index 15e9782b96..71dcb2df35 100644 --- a/src/packages/popup/doc.zh-TW.md +++ b/src/packages/popup/doc.zh-TW.md @@ -87,7 +87,7 @@ import { Popup } from '@nutui/nutui-react' | closeable | 是否顯示關閉按鈕 | `boolean` | `false` | | closeIconPosition | 關閉按鈕位置(top-left,top-right,bottom-left,bottom-right) | `string` | `top-right` | | closeIcon | 自定義 Icon | `ReactNode` | `close` | -| resizable | 上下滑動調整高度 | `boolean` | `false` | +| resizable | 上下滑動調整高度,目前只支援從底部彈出 | `boolean` | `false` | | minHeight | 設定最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index 7017f60922..b136ecfedc 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -120,7 +120,12 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { // 当高度改变后,再次打开时,将高度置为初始高度 - if (resizable && nodeRef.current && heightRef.current) { + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { nodeRef.current.style.height = `${heightRef.current}px` } setInnerVisible(true) @@ -208,7 +213,7 @@ export const Popup: FunctionComponent< } const handleTouchStart = async (event: ITouchEvent) => { - if (!resizable || !nodeRef.current) return + if (position !== 'bottom' || !resizable || !nodeRef.current) return // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 touchStartRef.current = event.touches[0].pageY // 标记开始滑动 @@ -230,7 +235,13 @@ export const Popup: FunctionComponent< } const handleTouchMove = (event: ITouchEvent) => { - if (!resizable || !nodeRef.current || !rootRect.current) return + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return event.stopPropagation() // move过程中,当前的pageY 与 start值比较 @@ -259,7 +270,13 @@ export const Popup: FunctionComponent< } const handleTouchEnd = (event: ITouchEvent) => { - if (!resizable || !nodeRef.current || !rootRect.current) return + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return console.log('touchend', event) isTouching.current = false const currentHeight = heightRef.current - touchMoveDistanceRef.current diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 13a24903c5..c3430ddc88 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -123,7 +123,12 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { // 当高度改变后,再次打开时,将高度置为初始高度 - if (resizable && nodeRef.current && heightRef.current) { + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { nodeRef.current.style.height = `${heightRef.current}px` } setInnerVisible(true) @@ -209,7 +214,7 @@ export const Popup: FunctionComponent< } const handleTouchStart = (event: TouchEvent) => { - if (!resizable || !nodeRef.current) return + if (position !== 'bottom' || !resizable || !nodeRef.current) return // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 touchStartRef.current = event.touches[0].pageY // 标记开始滑动 @@ -221,7 +226,7 @@ export const Popup: FunctionComponent< } const handleTouchMove = (event: TouchEvent) => { - if (!resizable || !nodeRef.current) return + if (position !== 'bottom' || !resizable || !nodeRef.current) return event.stopPropagation() // move过程中,当前的pageY 与 start值比较 @@ -240,7 +245,7 @@ export const Popup: FunctionComponent< } const handleTouchEnd = (event: TouchEvent) => { - if (!resizable || !nodeRef.current) return + if (position !== 'bottom' || !resizable || !nodeRef.current) return console.log('touchend', event) isTouching.current = false const currentHeight = heightRef.current - touchMoveDistanceRef.current From 13eea4e182f37ce32c41ee6b934c98d9c1996d03 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 21:52:47 +0800 Subject: [PATCH 06/11] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sites-react/doc/docs/react/migrate-from-v2.en-US.md | 7 ++++++- src/sites/sites-react/doc/docs/react/migrate-from-v2.md | 7 ++++++- .../sites-react/doc/docs/taro/migrate-from-v2.en-US.md | 7 ++++++- src/sites/sites-react/doc/docs/taro/migrate-from-v2.md | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md b/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md index 1a26c36665..0ec52c5709 100644 --- a/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md +++ b/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md @@ -93,7 +93,12 @@ If your project uses these components, please read the documentation carefully a [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- Added the resizable property for scrolling up and down when the bottom popup is active. +- Added the minHeight property for setting the minimum height, which can be used with resizable. +- Added the onTouchStart, onTouchMove, and onTouchEnd methods. ### Layout diff --git a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md index 38894f2da0..65d66178ad 100644 --- a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md @@ -93,7 +93,12 @@ plugins: [ [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- 新增属性 resizable,用于底部弹出时,可上下滑动 +- 新增属性 minHeight,用于设置最小高度,可搭配 resizable 使用 +- 新增 onTouchStart、onTouchMove、onTouchEnd 方法 ### 布局组件 diff --git a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md index 4ffb56f497..7ca17fa1cf 100644 --- a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md +++ b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md @@ -93,7 +93,12 @@ If your project uses these components, please read the documentation carefully a [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- Added the resizable property for scrolling up and down when the bottom popup is active. +- Added the minHeight property for setting the minimum height, which can be used with resizable. +- Added the onTouchStart, onTouchMove, and onTouchEnd methods. ### Layout diff --git a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md index 3a6bbfc126..2fca0c62cf 100644 --- a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md @@ -93,7 +93,12 @@ plugins: [ [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- 新增属性 resizable,用于底部弹出时,可上下滑动 +- 新增属性 minHeight,用于设置最小高度,可搭配 resizable 使用 +- 新增 onTouchStart、onTouchMove、onTouchEnd 方法 ### 布局组件 From fa4d1b0a5aae210c3f32184499c02ec554bb2344 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 10:53:38 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=E9=80=82=E9=85=8D=E9=B8=BF?= =?UTF-8?q?=E8=92=99,=E5=88=9D=E5=A7=8B=E5=80=BC=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/doc.en-US.md | 10 ++--- src/packages/popup/doc.md | 2 +- src/packages/popup/doc.taro.md | 6 +-- src/packages/popup/popup.taro.tsx | 70 ++++++++++++++++--------------- src/packages/popup/popup.tsx | 2 - 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/packages/popup/doc.en-US.md b/src/packages/popup/doc.en-US.md index 3a14de22fd..ef9afa14d2 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -87,8 +87,8 @@ import { Popup } from '@nutui/nutui-react' | closeable | whether to show the close button | `boolean` | `false` | | closeIconPosition | close button position | `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top-right` | | closeIcon | Custom Icon | `ReactNode` | `close` | -| resizable | you can resize the height of popup, just used by position from bottom | `boolean` | `false` | -| minHeight | The minHeight of popup | `string` | `26%` | +| resizable | Enable vertical resizing of the popup | `boolean` | `false` | +| minHeight | Minimum height of the popup | `string` | `26%` | | left | The left of title | `ReactNode` | `-` | | title | The center of title | `ReactNode` | `-` | | description | The subtitle/description | `ReactNode` | `-` | @@ -102,9 +102,9 @@ import { Popup } from '@nutui/nutui-react' | onOpen | Triggered when the popup is opened | `-` | `-` | | onClose | Fired when the popup is closed | `-` | `-` | | onOverlayClick | Click on the mask to trigger | `event: MouseEvent` | `-` | -| onTouchStart | triggered when starting to touch | `height: number, (event: TouchEvent) => void` | `-` | -| onTouchMove | triggered when starting to move | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | triggered when finishing to touch | `height: number, (event: TouchEvent) => void` | `-` | +| onTouchStart | triggered when starting to touch | `(height: number, event: TouchEvent) => void` | `-` | +| onTouchMove | triggered while moving | `(height: number, event: TouchEvent, direction: 'up' \| 'down') => void` | `-` | +| onTouchEnd | triggered when finishing to touch | `(height: number, event: TouchEvent) => void` | `-` | ## Theming diff --git a/src/packages/popup/doc.md b/src/packages/popup/doc.md index bba0872f00..df903651b4 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -103,7 +103,7 @@ import { Popup } from '@nutui/nutui-react' | onClose | 关闭弹框时触发 | `-` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | | onTouchStart | 开始触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | -| onTouchMove | 滑动时触发 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchMove | 滑动时触发 | `(height: number, event: TouchEvent, direction: 'up' \| 'down') => void` | `-` | | onTouchEnd | 结束触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md index fbbcc7aed2..341c881a3e 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -112,9 +112,9 @@ import { Popup } from '@nutui/nutui-react-taro' | onOpen | 打开弹框时触发 | `-` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | | onOverlayClick | 点击遮罩触发 | `event: MouseEvent` | `-` | -| onTouchStart | 开始触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | -| onTouchMove | 滑动时触发 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | -| onTouchEnd | 结束触碰时触发 | `(height: number, event: TouchEvent) => void` | `-` | +| onTouchStart | 开始触碰时触发 | `(height: number, event: ITouchEvent) => void` | `-` | +| onTouchMove | 滑动时触发 | `(height: number, event: ITouchEvent, direction: 'up' \| 'down') => void` | `-` | +| onTouchEnd | 结束触碰时触发 | `(height: number, event: ITouchEvent) => void` | `-` | ## 主题定制 diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index b136ecfedc..a3ae488840 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -10,7 +10,8 @@ import { createPortal } from 'react-dom' import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' import { Close } from '@nutui/icons-react-taro' -import { View, ITouchEvent } from '@tarojs/components' +import { View } from '@tarojs/components' +import type { ITouchEvent, CommonEventFunction } from '@tarojs/components' import { getRectInMultiPlatformWithoutCache } from '@/utils/taro/get-rect' import { defaultOverlayProps } from '@/packages/overlay/overlay.taro' import Overlay from '@/packages/overlay/index.taro' @@ -94,12 +95,15 @@ export const Popup: FunctionComponent< const [innerVisible, setInnerVisible] = useState(visible) const [showChildren, setShowChildren] = useState(true) const [transitionName, setTransitionName] = useState('') - const nodeRef = useLockScrollTaro(innerVisible && lockScroll) + const nodeRef = useLockScrollTaro( + innerVisible && lockScroll + ) as React.MutableRefObject const rootRect = useRef(null) const touchStartRef = useRef(0) const touchMoveDistanceRef = useRef(0) const heightRef = useRef(0) + const defaultHeightRef = useRef(0) const isTouching = useRef(false) const classPrefix = 'nut-popup' @@ -116,6 +120,14 @@ export const Popup: FunctionComponent< }, className ) + const [popupHeight, setPopupHeight] = useState('') + const resizeStyles = () => { + if (popupHeight !== '') { + return { + height: popupHeight, + } + } + } const open = () => { if (!innerVisible) { @@ -126,7 +138,7 @@ export const Popup: FunctionComponent< nodeRef.current && heightRef.current ) { - nodeRef.current.style.height = `${heightRef.current}px` + setPopupHeight(pxTransform(defaultHeightRef.current)) } setInnerVisible(true) setIndex(++innerIndex) @@ -212,10 +224,11 @@ export const Popup: FunctionComponent< } } - const handleTouchStart = async (event: ITouchEvent) => { + const handleTouchStart: CommonEventFunction = async (event) => { if (position !== 'bottom' || !resizable || !nodeRef.current) return + const e = event as ITouchEvent // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 - touchStartRef.current = event.touches[0].pageY + touchStartRef.current = e.touches[0].pageY // 标记开始滑动 isTouching.current = true // 标记当前popup的高度 @@ -223,18 +236,11 @@ export const Popup: FunctionComponent< rootRect.current = rect heightRef.current = nodeRef.current?.offsetHeight || rootRect.current?.height || 0 - // console.log( - // 'touchstart', - // touchStartRef.current, - // heightRef.current, // - // rootRect.current, - // nodeRef.current?.offsetHeight, // - // nodeRef.current.style.height // - // ) - onTouchStart?.(heightRef.current, event) + if (!defaultHeightRef.current) defaultHeightRef.current = heightRef.current + onTouchStart?.(heightRef.current, e) } - const handleTouchMove = (event: ITouchEvent) => { + const handleTouchMove: CommonEventFunction = (event) => { if ( position !== 'bottom' || !resizable || @@ -242,34 +248,28 @@ export const Popup: FunctionComponent< !rootRect.current ) return - event.stopPropagation() - // move过程中,当前的pageY 与 start值比较 - touchMoveDistanceRef.current = - event.touches[0].pageY - touchStartRef.current + const e = event as ITouchEvent + e.stopPropagation() + + // 计算位移:move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = e.touches[0].pageY - touchStartRef.current const handleMove = () => { const currentHeight = heightRef.current - touchMoveDistanceRef.current - nodeRef.current.style.height = pxTransform(currentHeight) + setPopupHeight(pxTransform(currentHeight)) if (touchMoveDistanceRef.current > 0 && isTouching.current) { // 向下滑动 - onTouchMove?.(currentHeight, event, 'down') - // console.log('向下', nodeRef.current.style.height) + onTouchMove?.(currentHeight, e, 'down') } else { // 向上滑动 - onTouchMove?.(currentHeight, event, 'up') - console.log( - '向上', - heightRef.current, - touchMoveDistanceRef.current, - currentHeight - ) + onTouchMove?.(currentHeight, e, 'up') } } requestAnimationFrame(handleMove) } - const handleTouchEnd = (event: ITouchEvent) => { + const handleTouchEnd: CommonEventFunction = (event) => { if ( position !== 'bottom' || !resizable || @@ -277,17 +277,21 @@ export const Popup: FunctionComponent< !rootRect.current ) return - console.log('touchend', event) + const e = event as ITouchEvent isTouching.current = false const currentHeight = heightRef.current - touchMoveDistanceRef.current - onTouchEnd?.(currentHeight, event) + onTouchEnd?.(currentHeight, e) } const renderContent = () => { return ( ) => { if (position !== 'bottom' || !resizable || !nodeRef.current) return - console.log('touchend', event) isTouching.current = false const currentHeight = heightRef.current - touchMoveDistanceRef.current onTouchEnd?.(currentHeight, event) From 1664272d3e327fe6f3858b11b7a2841d36ea3899 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 11:36:25 +0800 Subject: [PATCH 08/11] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/__tests__/popup.spec.tsx | 91 +++++++++++++++------ 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/src/packages/popup/__tests__/popup.spec.tsx b/src/packages/popup/__tests__/popup.spec.tsx index 9f60141578..61c224027c 100644 --- a/src/packages/popup/__tests__/popup.spec.tsx +++ b/src/packages/popup/__tests__/popup.spec.tsx @@ -1,34 +1,22 @@ import * as React from 'react' -import { render, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import { Popup } from '../popup' -test('should change z-index when using z-index prop', () => { - const { container } = render() - const element = container.querySelector('.nut-popup') as HTMLElement - expect(element.style.zIndex).toEqual('99') +test('renders without crashing', () => { + render(Test Content) + expect(screen.getByText('Test Content')).toBeInTheDocument() }) -test('prop overlay-class test', async () => { - const { container } = render() - const overlay = container.querySelector('.nut-overlay') as HTMLElement - expect(overlay).toHaveClass('testclas') -}) +test('opens and closes correctly', () => { + const { rerender } = render(Test Content) -test('prop overlay-style test', async () => { - const { container } = render( - - ) - const overlay = container.querySelector('.nut-overlay') as HTMLElement - expect(overlay).toHaveStyle({ - color: 'red', - }) -}) + // Initially, it should not be visible + expect(screen.queryByText('Test Content')).not.toBeInTheDocument() -test('should lock scroll when showed', async () => { - const { rerender } = render() - rerender() - expect(document.body.classList.contains('nut-overflow-hidden')).toBe(true) + // Rerender with visible true + rerender(Test Content) + expect(screen.getByText('Test Content')).toBeInTheDocument() }) test('should not render overlay when overlay prop is false', () => { @@ -91,6 +79,20 @@ test('pop description', () => { expect(title).toHaveTextContent('副标题') }) +test('pop minHeight', () => { + const { container } = render( + + ) + const node = container.querySelector('.nut-popup') as HTMLElement + expect(node).toHaveStyle({ minHeight: '30%' }) +}) + +test('pop resizable', () => { + const { container } = render() + const node = container.querySelector('.nut-popup') as HTMLElement + // expect(node).toHaveStyle({ minHeight: '30%' }) +}) + test('should render close icon when using closeable prop', () => { const { container } = render() const closeIcon = container.querySelector( @@ -145,10 +147,14 @@ test('event click-title-right icon and keep overlay test ', () => { expect(overlay2).toBeNull() }) -test('should emit open event when prop visible is set to true', () => { +test('should emit open event when prop visible is set to true', async () => { const onOpen = vi.fn() const { rerender } = render() - rerender() + rerender( + + test + + ) expect(onOpen).toBeCalled() }) @@ -171,3 +177,38 @@ test('pop destroyOnClose', () => { fireEvent.click(overlay) expect(onClose).toBeCalled() }) + +test('handles touch events correctly', () => { + const handleTouchStart = vi.fn() + const handleTouchMove = vi.fn() + const handleTouchEnd = vi.fn() + + render( + + Test Content + + ) + + const popup = document.body.querySelector('.nut-popup') as HTMLElement + + // Simulate touch events + fireEvent.touchStart(popup, { touches: [{ pageY: 400 }] }) + expect(handleTouchStart).toHaveBeenCalled() + + fireEvent.touchMove(popup, { touches: [{ pageY: 50 }] }) + expect(handleTouchMove).toHaveBeenCalled() + + fireEvent.touchMove(popup, { touches: [{ pageY: 450 }] }) + expect(handleTouchMove).toHaveBeenCalled() + + fireEvent.touchEnd(popup) + expect(handleTouchEnd).toHaveBeenCalled() +}) From d0c3921f995f8995bb80ef045d7b2b5d4282fc04 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 11:43:10 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0h5=20=E7=9A=84?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/popup.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 913fd20e34..8a0f58b84b 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -97,6 +97,8 @@ export const Popup: FunctionComponent< const touchStartRef = useRef(0) const touchMoveDistanceRef = useRef(0) const heightRef = useRef(0) + // 首次可调整时记录的默认高度 + const defaultHeightRef = useRef(0) const isTouching = useRef(false) useLockScroll(nodeRef, innerVisible && lockScroll) @@ -129,7 +131,7 @@ export const Popup: FunctionComponent< nodeRef.current && heightRef.current ) { - nodeRef.current.style.height = `${heightRef.current}px` + nodeRef.current.style.height = `${defaultHeightRef.current}px` } setInnerVisible(true) setIndex(++innerIndex) @@ -221,6 +223,7 @@ export const Popup: FunctionComponent< isTouching.current = true // 标记当前popup的高度 heightRef.current = nodeRef.current?.offsetHeight || 0 + if (!defaultHeightRef.current) defaultHeightRef.current = heightRef.current onTouchStart?.(heightRef.current, event) } From f333fd5884961e482b4a57d5d89af195dfcdf24e Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 12:01:19 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E4=B8=8B=E9=99=90=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/popup.taro.tsx | 18 ++++++++++++++++-- src/packages/popup/popup.tsx | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index a3ae488840..33b8ab1199 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -256,7 +256,14 @@ export const Popup: FunctionComponent< touchMoveDistanceRef.current = e.touches[0].pageY - touchStartRef.current const handleMove = () => { - const currentHeight = heightRef.current - touchMoveDistanceRef.current + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) setPopupHeight(pxTransform(currentHeight)) if (touchMoveDistanceRef.current > 0 && isTouching.current) { // 向下滑动 @@ -279,7 +286,14 @@ export const Popup: FunctionComponent< return const e = event as ITouchEvent isTouching.current = false - const currentHeight = heightRef.current - touchMoveDistanceRef.current + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) onTouchEnd?.(currentHeight, e) } diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 8a0f58b84b..6b467c9910 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -235,7 +235,15 @@ export const Popup: FunctionComponent< touchMoveDistanceRef.current = event.touches[0].pageY - touchStartRef.current - const currentHeight = heightRef.current - touchMoveDistanceRef.current + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) + nodeRef.current.style.height = `${currentHeight}px` // 向下滑动 if (touchMoveDistanceRef.current > 0) { @@ -249,7 +257,14 @@ export const Popup: FunctionComponent< const handleTouchEnd = (event: TouchEvent) => { if (position !== 'bottom' || !resizable || !nodeRef.current) return isTouching.current = false - const currentHeight = heightRef.current - touchMoveDistanceRef.current + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) onTouchEnd?.(currentHeight, event) } From 6c515f4e65e695815f96514dd7fa79f42124b1c3 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 12:08:14 +0800 Subject: [PATCH 11/11] =?UTF-8?q?test:=20fix=20=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/__tests__/popup.spec.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/packages/popup/__tests__/popup.spec.tsx b/src/packages/popup/__tests__/popup.spec.tsx index 61c224027c..d414415387 100644 --- a/src/packages/popup/__tests__/popup.spec.tsx +++ b/src/packages/popup/__tests__/popup.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' import { Popup } from '../popup' @@ -87,12 +87,6 @@ test('pop minHeight', () => { expect(node).toHaveStyle({ minHeight: '30%' }) }) -test('pop resizable', () => { - const { container } = render() - const node = container.querySelector('.nut-popup') as HTMLElement - // expect(node).toHaveStyle({ minHeight: '30%' }) -}) - test('should render close icon when using closeable prop', () => { const { container } = render() const closeIcon = container.querySelector( @@ -155,7 +149,7 @@ test('should emit open event when prop visible is set to true', async () => { test ) - expect(onOpen).toBeCalled() + await waitFor(() => expect(onOpen).toBeCalled()) }) test('event click-overlay test', async () => {