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
}
| | | |