From bb3056c5f8385cacd48e305cc84f27959c0db3cc Mon Sep 17 00:00:00 2001 From: "Alex.hxy" <1872591453@qq.com> Date: Fri, 15 Aug 2025 20:04:42 +0800 Subject: [PATCH 1/9] chore(release): v3.0.18 --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6dfa81cdb..b138a67332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v3.0.18 + +`2025-08-15` + +- 🏡 chore: 升级icon库 (#3330) +- 🏡 chore: 发布taro下的样式按需插件 +- 📖 docs: 更新介绍部分内容 (#3324) +- :sparkles: feat: Ellipsis校验越界不走缓存配置 (#3329) +- :sparkles: feat(price): 支持自定义颜色&数据原样输出 (#3328) +- :sparkles: feat(notify): 支持promise调用notice (#3319) +- :bug: fix(noticebar): 适配鸿蒙样式修复 (#3332) +- :bug: Fix icons svg (#3331) + # v3.0.17 `2025-08-01` diff --git a/package.json b/package.json index 917c12119f..1611ee2f26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nutui/nutui-react-taro", - "version": "3.0.17", + "version": "3.0.18", "style": "dist/style.css", "main": "dist/nutui.react.umd.js", "module": "dist/es/packages/nutui.react.build.js", From e6e678f8932dd7013e24e2c224c82d10f463a667 Mon Sep 17 00:00:00 2001 From: YONGQI Date: Tue, 19 Aug 2025 10:04:40 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(swiper):=20duration=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=20(#3337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 修复属性透传 * feat: 直接删除duration --- src/packages/swiper/swiper.taro.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/packages/swiper/swiper.taro.tsx b/src/packages/swiper/swiper.taro.tsx index bfdd05f8b0..10f01f7715 100644 --- a/src/packages/swiper/swiper.taro.tsx +++ b/src/packages/swiper/swiper.taro.tsx @@ -41,7 +41,6 @@ export const Swiper = React.forwardRef( circular, autoPlay, autoplay, - duration, vertical, direction, defaultValue, From 6331fc3446c2337378baa6a5add0a91a224ded1d Mon Sep 17 00:00:00 2001 From: RyanCW <75795462+Ryan-CW-Code@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:51:09 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(range):=20taro=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=B8=B2=E6=9F=93useReady=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=20(#3297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(range): taro环境异步渲染useReady不会触发 * refactor: 将组件中使用的useReady替换为useLayoutEffect --- .../avatarcropper/avatarcropper.taro.tsx | 8 +- src/packages/lottie/lottiemp.taro.tsx | 25 ++++--- src/packages/range/range.taro.tsx | 16 ++-- src/packages/rate/rate.taro.tsx | 11 +-- src/packages/swipe/swipe.taro.tsx | 74 ++++++++++--------- 5 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/packages/avatarcropper/avatarcropper.taro.tsx b/src/packages/avatarcropper/avatarcropper.taro.tsx index ee184339f9..f227f259b2 100644 --- a/src/packages/avatarcropper/avatarcropper.taro.tsx +++ b/src/packages/avatarcropper/avatarcropper.taro.tsx @@ -4,8 +4,9 @@ import React, { useMemo, useCallback, FunctionComponent, + useLayoutEffect, } from 'react' -import Taro, { useReady, createSelectorQuery } from '@tarojs/taro' +import Taro, { createSelectorQuery } from '@tarojs/taro' import classNames from 'classnames' import { Canvas, CommonEventFunction, View } from '@tarojs/components' import { getWindowInfo } from '@/utils/taro/get-system-info' @@ -137,7 +138,7 @@ export const AvatarCropper: FunctionComponent< cropperCanvasContext: null, }) - useReady(() => { + useLayoutEffect(() => { if (showAlipayCanvas2D) { const { canvasId } = canvasAll createSelectorQuery() @@ -149,7 +150,7 @@ export const AvatarCropper: FunctionComponent< }) .exec() } - }) + }, [showAlipayCanvas2D, state.displayHeight, state.displayWidth]) useEffect(() => { setCanvasAll({ @@ -693,4 +694,5 @@ export const AvatarCropper: FunctionComponent< ) } + AvatarCropper.displayName = 'NutAvatarCropper' diff --git a/src/packages/lottie/lottiemp.taro.tsx b/src/packages/lottie/lottiemp.taro.tsx index 5dd321c581..82790d79e8 100644 --- a/src/packages/lottie/lottiemp.taro.tsx +++ b/src/packages/lottie/lottiemp.taro.tsx @@ -1,5 +1,5 @@ -import React, { useImperativeHandle, useRef } from 'react' -import { createSelectorQuery, getEnv, useReady, useUnload } from '@tarojs/taro' +import React, { useImperativeHandle, useLayoutEffect, useRef } from 'react' +import { createSelectorQuery, getEnv, useUnload } from '@tarojs/taro' import lottie from '@nutui/lottie-miniprogram' import { getWindowInfo } from '@/utils/taro/get-system-info' import { useUuid } from '@/hooks/use-uuid' @@ -20,15 +20,10 @@ export const Lottie = React.forwardRef((props: TaroLottieProps, ref: any) => { speed = 1, dpr = true, } = props - const setSpeed = () => { - if (animation.current) { - animation.current.setSpeed(Math.abs(speed)) - animation.current.setDirection(speed > 0 ? 1 : -1) - } - } + useImperativeHandle(ref, () => animation.current || {}) const pixelRatio = useRef(getWindowInfo().pixelRatio) - useReady(() => { + useLayoutEffect(() => { createSelectorQuery() .select(`#${id}`) .fields( @@ -62,9 +57,14 @@ export const Lottie = React.forwardRef((props: TaroLottieProps, ref: any) => { context, }, }) - onComplete && + if (onComplete) { animation.current.addEventListener('complete', onComplete) - setSpeed() + } + + if (animation.current) { + animation.current.setSpeed(Math.abs(speed)) + animation.current.setDirection(speed > 0 ? 1 : -1) + } inited.current = true } catch (error) { console.error(error) @@ -72,7 +72,8 @@ export const Lottie = React.forwardRef((props: TaroLottieProps, ref: any) => { } ) .exec() - }) + }, [autoPlay, dpr, id, loop, onComplete, source, speed, style]) + useUnload(() => { onComplete && animation.current && diff --git a/src/packages/range/range.taro.tsx b/src/packages/range/range.taro.tsx index d2f7e0e34c..54a36dcfbf 100644 --- a/src/packages/range/range.taro.tsx +++ b/src/packages/range/range.taro.tsx @@ -2,13 +2,13 @@ import React, { FunctionComponent, useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, } from 'react' import classNames from 'classnames' import { Text, View } from '@tarojs/components' -import { useReady, nextTick } from '@tarojs/taro' import { pxTransform } from '@/utils/taro/px-transform' import { useTouch } from '@/hooks/use-touch' import { ComponentDefaults } from '@/utils/typings' @@ -286,17 +286,13 @@ export const Range: FunctionComponent< [innerValue, disabled, isRange, min, scope, updateValue, vertical] ) - useReady(() => { - const getRootRect = async () => { - if (root.current) { - const rect = await getRectInMultiPlatform(root.current) + useLayoutEffect(() => { + if (root.current) { + getRectInMultiPlatform(root.current).then((rect) => { rootRect.current = rect - } + }) } - nextTick(() => { - getRootRect() - }) - }) + }, []) const onTouchStart = useCallback( (event: any) => { diff --git a/src/packages/rate/rate.taro.tsx b/src/packages/rate/rate.taro.tsx index e2f30008f5..000c68f296 100644 --- a/src/packages/rate/rate.taro.tsx +++ b/src/packages/rate/rate.taro.tsx @@ -1,13 +1,14 @@ import React, { FunctionComponent, ReactElement, + useCallback, useEffect, + useLayoutEffect, useRef, useState, } from 'react' import classNames from 'classnames' import { StarFill } from '@nutui/icons-react-taro' -import { useReady } from '@tarojs/taro' import { ITouchEvent, Text, View } from '@tarojs/components' import { ComponentDefaults } from '@/utils/typings' import { usePropsValue } from '@/hooks/use-props-value' @@ -131,7 +132,7 @@ export const Rate: FunctionComponent> = (props) => { } } - const updateRects = () => { + const updateRects = useCallback(() => { for (let index = 0; index < refs.length; index++) { const item = refs[index] if (item) { @@ -140,11 +141,11 @@ export const Rate: FunctionComponent> = (props) => { }) } } - } + }, [refs]) - useReady(() => { + useLayoutEffect(() => { updateRects() - }) + }, [updateRects]) const handleTouchStart = (e: any) => { if (!touchable || readOnly || disabled) { diff --git a/src/packages/swipe/swipe.taro.tsx b/src/packages/swipe/swipe.taro.tsx index ec6b22f692..8d5dc887e7 100644 --- a/src/packages/swipe/swipe.taro.tsx +++ b/src/packages/swipe/swipe.taro.tsx @@ -1,15 +1,16 @@ import React, { forwardRef, MouseEvent, + useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useRef, useState, } from 'react' import classNames from 'classnames' import { ITouchEvent, View } from '@tarojs/components' import { BaseEventOrig } from '@tarojs/components/types/common' -import { nextTick, useReady } from '@tarojs/taro' import { useTouch } from '@/hooks/use-touch' import { getRectInMultiPlatform } from '@/utils/taro/get-rect' import { ComponentDefaults } from '@/utils/typings' @@ -45,8 +46,43 @@ export const Swipe = forwardRef< const leftId = `swipe-left-${uid}` const rightId = `swipe-right-${uid}` + const { children, className, style } = { ...defaultProps, ...props } + + const root: any = useRef() + const opened = useRef(false) + const lockClick = useRef(false) + const startOffset = useRef(0) + + const [state, setState] = useState({ + offset: 0, + dragging: false, + }) + + const [actionWidth, updateState] = useRefState({ + left: 0, + right: 0, + }) + const setActionWidth = useCallback( + (fn: any) => { + const res = fn() + if (res.left !== undefined) { + updateState({ + ...actionWidth.current, + left: res.left, + }) + } + if (res.right !== undefined) { + updateState({ + ...actionWidth.current, + right: res.right, + }) + } + }, + [actionWidth, updateState] + ) + // 获取元素的时候要在页面 onReady 后,需要参考小程序的事件周期 - useReady(() => { + useLayoutEffect(() => { const getWidth = async () => { if (leftWrapper.current) { const leftRect = await getRectInMultiPlatform( @@ -64,40 +100,10 @@ export const Swipe = forwardRef< setActionWidth((v: any) => ({ ...v, right: rightRect.width })) } } - nextTick(() => getWidth()) - }) - const { children, className, style } = { ...defaultProps, ...props } + getWidth() + }, [leftId, rightId, setActionWidth]) - const root: any = useRef() - const opened = useRef(false) - const lockClick = useRef(false) - const startOffset = useRef(0) - - const [state, setState] = useState({ - offset: 0, - dragging: false, - }) - - const [actionWidth, updateState] = useRefState({ - left: 0, - right: 0, - }) - const setActionWidth = (fn: any) => { - const res = fn() - if (res.left !== undefined) { - updateState({ - ...actionWidth.current, - left: res.left, - }) - } - if (res.right !== undefined) { - updateState({ - ...actionWidth.current, - right: res.right, - }) - } - } const wrapperStyle = { transform: `translate(${state.offset}${!harmony() ? 'px' : ''}, 0)`, transitionDuration: state.dragging ? '0s' : '.6s', From 94c507317534542647d1f11d2a2dc977571088e4 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 10:03:47 +0800 Subject: [PATCH 4/9] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e950d421e..119140d5e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nutui/nutui-react-taro", - "version": "3.0.18-cpp.beta.0", + "version": "3.0.18-cpp", "style": "dist/style.css", "main": "dist/nutui.react.umd.js", "module": "dist/es/packages/nutui.react.build.js", From 5dfd20f96b8a8b7cbc3c70d22258b04a5c8dc05e Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Wed, 20 Aug 2025 23:30:41 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat(popup):=20=E5=A2=9E=E5=8A=A0=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=BB=91=E5=8A=A8=E4=BF=AE=E6=94=B9=E9=AB=98=E5=BA=A6?= 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/popup.scss | 8 +- src/packages/popup/popup.taro.tsx | 178 ++++++++++++++++++++---- src/packages/popup/popup.tsx | 84 ++++++++++- src/types/spec/popup/base.ts | 5 + 6 files changed, 305 insertions(+), 44 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/popup.scss b/src/packages/popup/popup.scss index e14b25a059..fcc14d99ba 100644 --- a/src/packages/popup/popup.scss +++ b/src/packages/popup/popup.scss @@ -90,10 +90,10 @@ } } - &-bottom, - &-top { - max-height: 87%; - } + // &-bottom, + // &-top { + // max-height: 87%; + // } &-bottom { bottom: 0; diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index a3650326f6..9653672f9a 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 { 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' 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: '', 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,30 @@ 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 defaultHeightRef = 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, { @@ -95,9 +117,26 @@ export const Popup: FunctionComponent< }, className ) + const [popupHeight, setPopupHeight] = useState('') + const resizeStyles = () => { + if (popupHeight !== '') { + return { + height: popupHeight, + } + } + } const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { + setPopupHeight(pxTransform(defaultHeightRef.current)) + } setInnerVisible(true) setIndex(++innerIndex) } @@ -182,43 +221,122 @@ export const Popup: FunctionComponent< } } + const handleTouchStart = async (event: ITouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 + touchStartRef.current = event.touches[0].pageY + // 标记开始滑动 + isTouching.current = true + // 标记当前popup的高度 + const rect = await getRectInMultiPlatformWithoutCache(nodeRef.current) + rootRect.current = rect + heightRef.current = + // @ts-ignore + nodeRef.current?.offsetHeight || rootRect.current?.height || 0 + if (!defaultHeightRef.current) defaultHeightRef.current = heightRef.current + // console.log( + // '====> touchstart', + // touchStartRef.current, + // heightRef.current, // + // rootRect.current, + // // @ts-ignore + // nodeRef?.current?.offsetHeight, // + // // @ts-ignore + // nodeRef?.current?.style?.height // + // ) + onTouchStart?.(heightRef.current, event) + } + + const handleTouchMove = (event: ITouchEvent) => { + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return + event.stopPropagation() + + // move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = + event.touches[0].pageY - touchStartRef.current + + const handleMove = () => { + const currentHeight = heightRef.current - touchMoveDistanceRef.current + setPopupHeight(pxTransform(currentHeight)) + if (touchMoveDistanceRef.current > 0 && isTouching.current) { + // 向下滑动 + onTouchMove?.(currentHeight, event, 'down') + // console.log('=====> 向下', pxTransform(currentHeight)) + } else { + // 向上滑动 + onTouchMove?.(currentHeight, event, 'up') + // console.log( + // '=====> 向上', + // heightRef.current, + // touchMoveDistanceRef.current, + // currentHeight + // ) + } + } + requestAnimationFrame(handleMove) + } + + const handleTouchEnd = (event: ITouchEvent) => { + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return + isTouching.current = false + const currentHeight = heightRef.current - touchMoveDistanceRef.current + onTouchEnd?.(currentHeight, event) + } + const renderPop = () => { return ( {renderTitle()} {showChildren ? children : null} ) - // return ( - // - // - // {renderTitle()} - // {showChildren ? children : null} - // - // - // ) } + // const renderPop = () => { + // return ( + // + // {renderContent()} + // + // ) + // } const renderNode = () => { return ( diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 1328c667d5..c3430ddc88 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -4,7 +4,10 @@ import React, { ReactPortal, useEffect, useState, + useRef, } from 'react' +import type { TouchEvent } from 'react' + import { createPortal } from 'react-dom' import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' @@ -27,17 +30,26 @@ const defaultProps: WebPopupProps = { portal: null, overlay: true, round: false, + resizable: false, + minHeight: '', onOpen: () => {}, 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,15 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { + nodeRef.current.style.height = `${heightRef.current}px` + } setInnerVisible(true) setIndex(++innerIndex) } @@ -176,6 +212,46 @@ export const Popup: FunctionComponent< return renderCloseIcon() } } + + const handleTouchStart = (event: TouchEvent) => { + if (position !== 'bottom' || !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?.(heightRef.current, event) + } + + const handleTouchMove = (event: TouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + event.stopPropagation() + + // move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = + event.touches[0].pageY - touchStartRef.current + + const currentHeight = heightRef.current - touchMoveDistanceRef.current + nodeRef.current.style.height = `${currentHeight}px` + // 向下滑动 + if (touchMoveDistanceRef.current > 0) { + onTouchMove?.(currentHeight, event, 'down') + } else { + // 向上滑动 + onTouchMove?.(currentHeight, event, 'up') + } + } + + const handleTouchEnd = (event: TouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + console.log('touchend', event) + isTouching.current = false + const currentHeight = heightRef.current - touchMoveDistanceRef.current + onTouchEnd?.(currentHeight, event) + } + const renderPop = () => { return ( {renderTitle()} {showChildren && children} diff --git a/src/types/spec/popup/base.ts b/src/types/spec/popup/base.ts index f3c4421266..699d4b9f28 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: number, e: any, direction: 'up' | 'down') => void + onTouchStart: (height: number, e: any) => void + onTouchEnd: (height: number, e: any) => void } From b8c3fd2b6968f6976dd291f1c1b023d63a778525 Mon Sep 17 00:00:00 2001 From: YONGQI Date: Thu, 21 Aug 2025 10:17:28 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix(input):=20=E5=85=BC=E5=AE=B9h5=E5=92=8C?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E8=8E=B7=E5=8F=96=E5=8E=9F=E7=94=9F?= =?UTF-8?q?input=E6=A0=87=E7=AD=BE=20(#3341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: inputRef获取真实input-dom * feat: 兼容小程序和h5获取input标签 * feat: 取消?问好,出错直接抛出 --- src/packages/input/input.taro.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/packages/input/input.taro.tsx b/src/packages/input/input.taro.tsx index d88240e831..d27c75e450 100644 --- a/src/packages/input/input.taro.tsx +++ b/src/packages/input/input.taro.tsx @@ -77,16 +77,28 @@ export const Input = forwardRef((props: Partial, ref) => { const inputRef = useRef(null) const [active, setActive] = useState(false) + // 兼容H5和小程序获取原生input标签 + const getNativeInput = () => { + if (Taro.getEnv() === 'WEB') { + const taroInputCoreEl = inputRef.current as HTMLElement + const inputEl = taroInputCoreEl.querySelector('input') + return inputEl + } + return inputRef.current + } + useImperativeHandle(ref, () => { return { clear: () => { setValue('') }, focus: () => { - inputRef.current?.focus() + const nativeInput = getNativeInput() + nativeInput?.focus() }, blur: () => { - inputRef.current?.blur() + const nativeInput = getNativeInput() + nativeInput?.blur() }, get nativeElement() { return inputRef.current From 3dbc7bc3d92ffdbae0a24d31431f52853f502f07 Mon Sep 17 00:00:00 2001 From: xiaoyatong <84436086+xiaoyatong@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:27:44 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(Popup):=20=E6=96=B0=E5=A2=9E=E5=BC=B9?= =?UTF-8?q?=E5=B1=82=E5=8F=AF=E4=B8=8A=E4=B8=8B=E6=BB=91=E5=8A=A8=20(#3340?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 支持popup 高度可以伸缩 * feat: 适配小程序 * feat: 修改文档 * fix: 默认值不需要,走样式 * feat: 增加使用的限制条件 * docs: 增加文档 * fix: 适配鸿蒙,初始值重置修改 * test: 添加单测 * fix: 增加h5 的初始值 * fix: 增加高度下限约束 * test: fix 单测 --- src/packages/popup/__tests__/popup.spec.tsx | 87 +++++++--- src/packages/popup/demos/h5/demo1.tsx | 37 +++- src/packages/popup/demos/h5/demo2.tsx | 1 + 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 | 159 +++++++++++++++--- src/packages/popup/popup.tsx | 100 ++++++++++- .../doc/docs/react/migrate-from-v2.en-US.md | 7 +- .../doc/docs/react/migrate-from-v2.md | 7 +- .../doc/docs/taro/migrate-from-v2.en-US.md | 7 +- .../doc/docs/taro/migrate-from-v2.md | 7 +- src/types/spec/popup/base.ts | 5 + 16 files changed, 423 insertions(+), 72 deletions(-) diff --git a/src/packages/popup/__tests__/popup.spec.tsx b/src/packages/popup/__tests__/popup.spec.tsx index 9f60141578..d414415387 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, waitFor } 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,14 @@ 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('should render close icon when using closeable prop', () => { const { container } = render() const closeIcon = container.querySelector( @@ -145,11 +141,15 @@ 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() - expect(onOpen).toBeCalled() + rerender( + + test + + ) + await waitFor(() => expect(onOpen).toBeCalled()) }) test('event click-overlay test', async () => { @@ -171,3 +171,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() +}) 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/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/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..ef9afa14d2 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 | 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` | `-` | | 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: 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 8c23ec4402..df903651b4 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: number, event: TouchEvent) => 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 bda19db26e..341c881a3e 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: 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/doc.zh-TW.md b/src/packages/popup/doc.zh-TW.md index deac6ce84f..71dcb2df35 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: number, event: TouchEvent) => void` | `-` | +| onTouchMove | 滑動時觸發 | `(height: number, event: TouchEvent, 'up' \| 'down') => void` | `-` | +| onTouchEnd | 結束觸碰時觸發 | `(height: number, 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..33b8ab1199 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -4,17 +4,21 @@ 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 { 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' 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 +33,15 @@ const defaultProps: TaroPopupProps = { portal: null, overlay: true, round: false, + resizable: false, + minHeight: '', onOpen: () => {}, onClose: () => {}, onOverlayClick: () => true, onCloseIconClick: () => true, + onTouchStart: () => {}, + onTouchMove: () => {}, + onTouchEnd: () => {}, } // 默认1000,参看variables @@ -40,7 +49,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 +77,8 @@ export const Popup: FunctionComponent< className, destroyOnClose, portal, + resizable, + minHeight, onOpen, onClose, onOverlayClick, @@ -72,21 +86,32 @@ 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 + ) 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' 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, { @@ -95,9 +120,26 @@ export const Popup: FunctionComponent< }, className ) + const [popupHeight, setPopupHeight] = useState('') + const resizeStyles = () => { + if (popupHeight !== '') { + return { + height: popupHeight, + } + } + } const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { + setPopupHeight(pxTransform(defaultHeightRef.current)) + } setInnerVisible(true) setIndex(++innerIndex) } @@ -182,26 +224,105 @@ export const Popup: FunctionComponent< } } + const handleTouchStart: CommonEventFunction = async (event) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + const e = event as ITouchEvent + // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 + touchStartRef.current = e.touches[0].pageY + // 标记开始滑动 + isTouching.current = true + // 标记当前popup的高度 + const rect = await getRectInMultiPlatformWithoutCache(nodeRef.current) + rootRect.current = rect + heightRef.current = + nodeRef.current?.offsetHeight || rootRect.current?.height || 0 + if (!defaultHeightRef.current) defaultHeightRef.current = heightRef.current + onTouchStart?.(heightRef.current, e) + } + + const handleTouchMove: CommonEventFunction = (event) => { + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return + + const e = event as ITouchEvent + e.stopPropagation() + + // 计算位移:move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = e.touches[0].pageY - touchStartRef.current + + const handleMove = () => { + 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) { + // 向下滑动 + onTouchMove?.(currentHeight, e, 'down') + } else { + // 向上滑动 + onTouchMove?.(currentHeight, e, 'up') + } + } + requestAnimationFrame(handleMove) + } + + const handleTouchEnd: CommonEventFunction = (event) => { + if ( + position !== 'bottom' || + !resizable || + !nodeRef.current || + !rootRect.current + ) + return + const e = event as ITouchEvent + isTouching.current = false + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) + onTouchEnd?.(currentHeight, e) + } + 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,25 @@ 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 defaultHeightRef = 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 +124,15 @@ export const Popup: FunctionComponent< const open = () => { if (!innerVisible) { + // 当高度改变后,再次打开时,将高度置为初始高度 + if ( + position === 'bottom' && + resizable && + nodeRef.current && + heightRef.current + ) { + nodeRef.current.style.height = `${defaultHeightRef.current}px` + } setInnerVisible(true) setIndex(++innerIndex) } @@ -176,6 +214,60 @@ export const Popup: FunctionComponent< return renderCloseIcon() } } + + const handleTouchStart = (event: TouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + // 开始touch,记录下touch的pageY,用以判断是向上滑动还是向下滑动 + touchStartRef.current = event.touches[0].pageY + // 标记开始滑动 + isTouching.current = true + // 标记当前popup的高度 + heightRef.current = nodeRef.current?.offsetHeight || 0 + if (!defaultHeightRef.current) defaultHeightRef.current = heightRef.current + onTouchStart?.(heightRef.current, event) + } + + const handleTouchMove = (event: TouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + event.stopPropagation() + + // move过程中,当前的pageY 与 start值比较 + touchMoveDistanceRef.current = + event.touches[0].pageY - touchStartRef.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) { + onTouchMove?.(currentHeight, event, 'down') + } else { + // 向上滑动 + onTouchMove?.(currentHeight, event, 'up') + } + } + + const handleTouchEnd = (event: TouchEvent) => { + if (position !== 'bottom' || !resizable || !nodeRef.current) return + isTouching.current = false + const min = + typeof minHeight === 'number' + ? minHeight + : parseInt(String(minHeight || 0), 10) || 0 + const currentHeight = Math.max( + min, + heightRef.current - touchMoveDistanceRef.current + ) + onTouchEnd?.(currentHeight, event) + } + const renderPop = () => { return ( {renderTitle()} {showChildren && children} 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 方法 ### 布局组件 diff --git a/src/types/spec/popup/base.ts b/src/types/spec/popup/base.ts index f3c4421266..699d4b9f28 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: number, e: any, direction: 'up' | 'down') => void + onTouchStart: (height: number, e: any) => void + onTouchEnd: (height: number, e: any) => void } From e47d0d45282db3acb94c401f74c5e9a2de2c38ad Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 16:34:25 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat(popup):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E9=A1=B6=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/popup/doc.en-US.md | 1 + src/packages/popup/doc.md | 1 + src/packages/popup/doc.taro.md | 1 + src/packages/popup/popup.taro.tsx | 2 ++ src/packages/popup/popup.tsx | 2 ++ src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md | 1 + src/sites/sites-react/doc/docs/react/migrate-from-v2.md | 1 + src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md | 1 + src/sites/sites-react/doc/docs/taro/migrate-from-v2.md | 1 + src/types/spec/popup/base.ts | 1 + 10 files changed, 12 insertions(+) diff --git a/src/packages/popup/doc.en-US.md b/src/packages/popup/doc.en-US.md index ef9afa14d2..6fe5660e34 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -91,6 +91,7 @@ import { Popup } from '@nutui/nutui-react' | minHeight | Minimum height of the popup | `string` | `26%` | | left | The left of title | `ReactNode` | `-` | | title | The center of title | `ReactNode` | `-` | +| top | The top of popup | `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` | diff --git a/src/packages/popup/doc.md b/src/packages/popup/doc.md index df903651b4..db4f0a5aa4 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -91,6 +91,7 @@ import { Popup } from '@nutui/nutui-react' | minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | +| top | 顶部占位 | `ReactNode` | `-` | | description | 子标题/描述部分 | `ReactNode` | `-` | | destroyOnClose | 组件不可见时,卸载内容 | `boolean` | `false` | | round | 是否显示圆角 | `boolean` | `false` | diff --git a/src/packages/popup/doc.taro.md b/src/packages/popup/doc.taro.md index 341c881a3e..d003789411 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -101,6 +101,7 @@ import { Popup } from '@nutui/nutui-react-taro' | minHeight | 设置最小高度 | `string` | `26%` | | left | 标题左侧部分 | `ReactNode` | `-` | | title | 标题中间部分 | `ReactNode` | `-` | +| top | 頂部佔位 | `ReactNode` | `-` | | description | 子标题/描述部分 | `ReactNode` | `-` | | destroyOnClose | 组件不可见时,卸载内容 | `boolean` | `false` | | round | 是否显示圆角 | `boolean` | `false` | diff --git a/src/packages/popup/popup.taro.tsx b/src/packages/popup/popup.taro.tsx index 9157bbc1ee..913d49ce95 100644 --- a/src/packages/popup/popup.taro.tsx +++ b/src/packages/popup/popup.taro.tsx @@ -69,6 +69,7 @@ export const Popup: FunctionComponent< closeIcon, left, title, + top, description, style, transition, @@ -314,6 +315,7 @@ export const Popup: FunctionComponent< onTouchEnd={handleTouchEnd} onTouchCancel={handleTouchEnd} > + {top} {renderTitle()} {showChildren ? children : null} diff --git a/src/packages/popup/popup.tsx b/src/packages/popup/popup.tsx index 6b467c9910..b76b3ba0a8 100644 --- a/src/packages/popup/popup.tsx +++ b/src/packages/popup/popup.tsx @@ -66,6 +66,7 @@ export const Popup: FunctionComponent< closeIcon, left, title, + top, description, style, transition, @@ -290,6 +291,7 @@ export const Popup: FunctionComponent< onTouchEnd={handleTouchEnd} onTouchCancel={handleTouchEnd} > + {top} {renderTitle()} {showChildren && children} 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 0ec52c5709..f4707ab365 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 @@ -98,6 +98,7 @@ If your project uses these components, please read the documentation carefully a - 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 a new attribute top to display user-defined content above the title. - 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 65d66178ad..4e44be83ad 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 @@ -98,6 +98,7 @@ plugins: [ - 新增属性 resizable,用于底部弹出时,可上下滑动 - 新增属性 minHeight,用于设置最小高度,可搭配 resizable 使用 +- 新增属性 top,用于在title上侧展示用户自定义内容 - 新增 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 7ca17fa1cf..02ac582a79 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 @@ -98,6 +98,7 @@ If your project uses these components, please read the documentation carefully a - 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 a new attribute top to display user-defined content above the title. - 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 2fca0c62cf..b1aae2581b 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 @@ -98,6 +98,7 @@ plugins: [ - 新增属性 resizable,用于底部弹出时,可上下滑动 - 新增属性 minHeight,用于设置最小高度,可搭配 resizable 使用 +- 新增属性 top,用于在title上侧展示用户自定义内容 - 新增 onTouchStart、onTouchMove、onTouchEnd 方法 ### 布局组件 diff --git a/src/types/spec/popup/base.ts b/src/types/spec/popup/base.ts index 699d4b9f28..38d6d4e3ae 100644 --- a/src/types/spec/popup/base.ts +++ b/src/types/spec/popup/base.ts @@ -17,6 +17,7 @@ export interface BasePopup extends BaseProps, BaseOverlay { closeIcon: ReactNode left?: ReactNode title?: ReactNode + top?: ReactNode description?: ReactNode destroyOnClose: boolean overlay: boolean From e7a4b1f32ec1f3ecc8e26a9236e17f4cec7db47c Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Thu, 21 Aug 2025 16:35:30 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(input):=20taro=20=E4=B8=8B=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E5=8F=AF=E4=BB=A5=E7=82=B9=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/input/input.taro.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/input/input.taro.tsx b/src/packages/input/input.taro.tsx index d27c75e450..76d8edd90e 100644 --- a/src/packages/input/input.taro.tsx +++ b/src/packages/input/input.taro.tsx @@ -195,7 +195,7 @@ export const Input = forwardRef((props: Partial, ref) => { placeholder === undefined ? locale.placeholder : placeholder } placeholderClass={`${classPrefix}-placeholder`} - disabled={disabled || readOnly} + disabled={disabled} value={value} focus={autoFocus || focus} confirmType={confirmType}