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 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", diff --git a/src/packages/input/input.taro.tsx b/src/packages/input/input.taro.tsx index d88240e831..76d8edd90e 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 @@ -183,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} 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..6fe5660e34 100644 --- a/src/packages/popup/doc.en-US.md +++ b/src/packages/popup/doc.en-US.md @@ -87,19 +87,25 @@ 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` | `-` | +| 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` | | 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..db4f0a5aa4 100644 --- a/src/packages/popup/doc.md +++ b/src/packages/popup/doc.md @@ -87,19 +87,25 @@ 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` | `-` | +| top | 顶部占位 | `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..d003789411 100644 --- a/src/packages/popup/doc.taro.md +++ b/src/packages/popup/doc.taro.md @@ -97,19 +97,25 @@ 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` | `-` | +| top | 頂部佔位 | `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 a3650326f6..913d49ce95 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, @@ -57,6 +69,7 @@ export const Popup: FunctionComponent< closeIcon, left, title, + top, description, style, transition, @@ -65,6 +78,8 @@ export const Popup: FunctionComponent< className, destroyOnClose, portal, + resizable, + minHeight, onOpen, onClose, onOverlayClick, @@ -72,21 +87,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 +121,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 +225,118 @@ 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 renderPop = () => { return ( + {top} {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..b76b3ba0a8 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, @@ -54,6 +66,7 @@ export const Popup: FunctionComponent< closeIcon, left, title, + top, description, style, transition, @@ -62,6 +75,8 @@ export const Popup: FunctionComponent< className, destroyOnClose, portal, + resizable, + minHeight, onOpen, onClose, onOverlayClick, @@ -69,6 +84,9 @@ export const Popup: FunctionComponent< afterShow, afterClose, onClick, + onTouchStart, + onTouchMove, + onTouchEnd, } = { ...defaultProps, ...props } const nodeRef = React.useRef(null) let innerIndex = zIndex || _zIndex @@ -77,13 +95,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 +125,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 +215,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 ( + {top} {renderTitle()} {showChildren && children} 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, 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..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 @@ -93,7 +93,13 @@ 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 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 38894f2da0..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 @@ -93,7 +93,13 @@ plugins: [ [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- 新增属性 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 4ffb56f497..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 @@ -93,7 +93,13 @@ 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 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 3a6bbfc126..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 @@ -93,7 +93,13 @@ plugins: [ [//]: # '#### Icon' [//]: # '#### Image' [//]: # '#### Overlay' -[//]: # '#### Popup' + +#### Popup + +- 新增属性 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 f3c4421266..38d6d4e3ae 100644 --- a/src/types/spec/popup/base.ts +++ b/src/types/spec/popup/base.ts @@ -17,12 +17,18 @@ export interface BasePopup extends BaseProps, BaseOverlay { closeIcon: ReactNode left?: ReactNode title?: ReactNode + top?: ReactNode description?: ReactNode 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 }