未验证 提交 c995270a 编写于 作者: L love_forever 提交者: GitHub

feat: popup增加指定节点挂载与多层堆叠功能 (#197)

上级 e9c482db
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correct close icon when using close-icon prop 1`] = `"<i class=\\" nutui-iconfont nut-icon nutui-icon nut-icon-success \\" style=\\"font-size: 12px; width: 12px; height: 12px;\\"></i>"`;
import * as React from 'react'
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Popup } from '../popup'
function sleep(delay = 0): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, delay)
})
}
test('should change z-index when using z-index prop', () => {
const { container } = render(
<>
<Popup visible zIndex={99} />
</>
)
const element = container.querySelector('.nut-popup') as HTMLElement
expect(element.style.zIndex).toEqual('99')
})
test('should change animation duration when using duration prop', () => {
const { container } = render(
<>
<Popup visible duration={12} />
</>
)
const overlay = container.querySelector('.nut-overlay') as HTMLElement
expect(overlay.style.animationDuration).toEqual('12s')
})
test('prop overlay-class test', async () => {
const { container } = render(
<>
<Popup visible overlayClass="testclas" />
</>
)
const overlay = container.querySelector('.nut-overlay') as HTMLElement
expect(overlay).toHaveClass('testclas')
})
test('prop overlay-style test', async () => {
const { container } = render(
<>
<Popup visible overlayStyle={{ color: 'red' }} />
</>
)
const overlay = container.querySelector('.nut-overlay') as HTMLElement
expect(overlay).toHaveStyle({
color: 'red',
})
})
test('should lock scroll when showed', async () => {
const { rerender } = render(<Popup visible={false} />)
rerender(<Popup visible />)
expect(document.body.classList.contains('nut-overflow-hidden')).toBe(true)
})
test('should not render overlay when overlay prop is false', () => {
const { container } = render(
<>
<Popup visible overlay={false} />
</>
)
const overlay = container.querySelectorAll('.nut-overlay') as NodeListOf<Node>
expect(overlay.length).toBe(0)
})
test('prop close-on-click-overlay test', async () => {
const { container } = render(
<>
<Popup visible closeOnClickOverlay={false} />
</>
)
fireEvent.click(container)
const overlay = container.querySelector('.nut-overlay') as HTMLElement
expect(overlay.style.display).toEqual('')
})
test('pop from top', () => {
const { container } = render(
<>
<Popup visible position="top" />
</>
)
const pop = container.querySelector('.popup-top') as HTMLElement
expect(pop).toBeTruthy()
})
test('pop from bottom', () => {
const { container } = render(
<>
<Popup visible position="bottom" />
</>
)
const pop = container.querySelector('.popup-bottom') as HTMLElement
expect(pop).toBeTruthy()
})
test('pop from left', () => {
const { container } = render(
<>
<Popup visible position="left" />
</>
)
const pop = container.querySelector('.popup-left') as HTMLElement
expect(pop).toBeTruthy()
})
test('pop from right', () => {
const { container } = render(
<>
<Popup visible position="right" />
</>
)
const pop = container.querySelector('.popup-right') as HTMLElement
expect(pop).toBeTruthy()
})
test('should render close icon when using closeable prop', () => {
const { container } = render(
<>
<Popup visible closeable />
</>
)
const closeIcon = container.querySelector(
'.nutui-popup__close-icon'
) as HTMLElement
expect(closeIcon).toBeTruthy()
})
test('should render correct close icon when using close-icon prop', () => {
const { container } = render(
<>
<Popup visible closeable closeIcon="success" />
</>
)
const closeIcon = container.querySelector(
'.nutui-popup__close-icon'
) as HTMLElement
expect(closeIcon.innerHTML).toMatchSnapshot()
})
test('should have "van-popup--round" class when setting the round prop', () => {
const { container } = render(
<>
<Popup visible round />
</>
)
const round = container.querySelector('.round') as HTMLElement
expect(round).toBeTruthy()
})
test('should allow to using teleport prop', () => {
render(
<>
<Popup visible />
</>
)
expect(document.body.querySelector('.nut-popup')).toBeTruthy()
})
test('event click test', async () => {
const { container } = render(
<>
<Popup visible closeOnClickOverlay />
</>
)
const overlay = container.querySelector('.nut-overlay') as Element
await fireEvent.click(overlay)
expect(overlay).toHaveClass('hidden-render')
})
test('event click-close-icon test', () => {
const onClickCloseIcon = jest.fn()
const { container } = render(
<>
<Popup visible closeable onClickCloseIcon={onClickCloseIcon} />
</>
)
const closeIcon = container.querySelector(
'.nutui-popup__close-icon'
) as HTMLElement
const overlay = container.querySelector('.nut-overlay') as Element
fireEvent.click(closeIcon)
expect(onClickCloseIcon).toBeCalled()
expect(overlay).toHaveClass('hidden-render')
})
test('should emit open event when prop visible is set to true', () => {
const onOpen = jest.fn()
const { rerender } = render(
<>
<Popup visible={false} onOpen={onOpen} />
</>
)
rerender(
<>
<Popup visible onOpen={onOpen} />
</>
)
expect(onOpen).toBeCalled()
})
test('event click-overlay test', async () => {
const onClickOverlay = jest.fn()
const { container } = render(
<>
<Popup visible onClickOverlay={onClickOverlay} />
</>
)
const overlay = container.querySelector('.nut-overlay') as Element
fireEvent.click(overlay)
expect(onClickOverlay).toBeCalled()
})
......@@ -17,6 +17,8 @@ interface T {
a52bef0c: string
d04fcbda: string
'0aaad620': string
ea3d02f2: string
c9e6df49: string
}
const PopupDemo = () => {
......@@ -34,6 +36,8 @@ const PopupDemo = () => {
a52bef0c: '图标位置',
d04fcbda: '自定义图标',
'0aaad620': '圆角弹框',
ea3d02f2: '指定节点挂载',
c9e6df49: '多层堆叠',
},
'zh-TW': {
ce5c5446: '基礎類型',
......@@ -48,6 +52,8 @@ const PopupDemo = () => {
a52bef0c: '圖標位置',
d04fcbda: '自定義圖標',
'0aaad620': '圓角彈框',
ea3d02f2: '指定節點掛載',
c9e6df49: '多層堆疊',
},
'en-US': {
ce5c5446: 'base type',
......@@ -62,6 +68,8 @@ const PopupDemo = () => {
a52bef0c: 'Icon position',
d04fcbda: 'custom icon',
'0aaad620': 'Rounded popup',
ea3d02f2: 'Mount the specified node',
c9e6df49: 'multi-layer stacking',
},
})
......@@ -74,6 +82,9 @@ const PopupDemo = () => {
const [showIconPosition, setShowIconPosition] = useState(false)
const [showIconDefine, setShowIconDefine] = useState(false)
const [showBottomRound, setShowBottomRound] = useState(false)
const [showMountNode, setShowMountNode] = useState(false)
const [showMutiple, setShowMutiple] = useState(false)
const [showMutipleInner, setShowMutipleInner] = useState(false)
return (
<>
......@@ -228,6 +239,65 @@ const PopupDemo = () => {
setShowBottomRound(false)
}}
/>
<h2>{translated.ea3d02f2}</h2>
<Cell
title={translated.ea3d02f2}
isLink
onClick={() => {
setShowMountNode(true)
}}
/>
<Popup
visible={showMountNode}
style={{ padding: '30px 50px' }}
teleport={document.body}
onClose={() => {
setShowMountNode(false)
}}
>
body
</Popup>
<h2>{translated.c9e6df49}</h2>
<Cell
title={translated.c9e6df49}
isLink
onClick={() => {
setShowMutiple(true)
}}
/>
<Popup
visible={showMutiple}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutiple(false)
}}
>
<span
onClick={() => {
setShowMutipleInner(true)
}}
>
Click It
</span>
</Popup>
<Popup
visible={showMutipleInner}
style={{ padding: '30px 50px' }}
overlayStyle={{ backgroundColor: 'transparent' }}
onClose={() => {
setShowMutipleInner(false)
}}
>
<span
onClick={() => {
setShowMutipleInner(false)
}}
>
close
</span>
</Popup>
</div>
</>
)
......
......@@ -116,6 +116,68 @@ export default App;
```
:::
### Mount the specified node
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMountNode, setShowMountNode] = useState(false);
return (
<>
<Cell title="Mount the specified node" isLink onClick={() => { setShowMountNode(true) }}/>
<Popup visible={showMountNode} style={{ padding: '30px 50px' }} teleport={ document.body } onClose={() => { setShowMountNode(false) }}>
body
</Popup>
</>
);
};
export default App;
```
:::
### multi-layer stacking
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMutiple, setShowMutiple] = useState(false)
const [showMutipleInner, setShowMutipleInner] = useState(false)
return (
<>
<Cell title="multi-layer stacking" isLink onClick={() => { setShowMutiple(true) }}/>
<Popup
visible={showMutiple}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutiple(false)
}}
>
<span onClick={ () => { setShowMutipleInner(true) } }>Click It</span>
</Popup>
<Popup
visible={showMutipleInner}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutipleInner(false)
}}
>
<span onClick={ () => { setShowMutipleInner(false) } }>close</span>
</Popup>
</>
);
};
export default App;
```
:::
## API
### Props
......@@ -139,6 +201,7 @@ export default App;
| closeIcon | Custom Icon | String | `"close"` |
| destroyOnClose | Whether to close after the component is destroyed | Boolean | `true` |
| round | Whether to show rounded corners | Boolean | `false` |
| teleport | Mount the specified node | HTMLElement、(() => HTMLElement) 、null | `null` |
### Events
......
......@@ -116,6 +116,68 @@ export default App;
```
:::
### 指定节点挂载
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMountNode, setShowMountNode] = useState(false);
return (
<>
<Cell title="指定节点挂载" isLink onClick={() => { setShowMountNode(true) }}/>
<Popup visible={showMountNode} style={{ padding: '30px 50px' }} teleport={ document.body } onClose={() => { setShowMountNode(false) }}>
body
</Popup>
</>
);
};
export default App;
```
:::
### 多层堆叠
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMutiple, setShowMutiple] = useState(false)
const [showMutipleInner, setShowMutipleInner] = useState(false)
return (
<>
<Cell title="多层堆叠" isLink onClick={() => { setShowMutiple(true) }}/>
<Popup
visible={showMutiple}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutiple(false)
}}
>
<span onClick={ () => { setShowMutipleInner(true) } }>Click It</span>
</Popup>
<Popup
visible={showMutipleInner}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutipleInner(false)
}}
>
<span onClick={ () => { setShowMutipleInner(false) } }>close</span>
</Popup>
</>
);
};
export default App;
```
:::
## API
### Props
......@@ -139,6 +201,7 @@ export default App;
| closeIcon | 自定义 Icon | String | `"close"` |
| destroyOnClose | 组件销毁后是否关闭 | Boolean | `true` |
| round | 是否显示圆角 | Boolean | `false` |
| teleport | 指定节点挂载 | HTMLElement、(() => HTMLElement) 、null | `null` |
### Events
......
......@@ -116,6 +116,68 @@ export default App;
```
:::
### 指定節點掛載
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMountNode, setShowMountNode] = useState(false);
return (
<>
<Cell title="指定節點掛載" isLink onClick={() => { setShowMountNode(true) }}/>
<Popup visible={showMountNode} style={{ padding: '30px 50px' }} teleport={ document.body } onClose={() => { setShowMountNode(false) }}>
body
</Popup>
</>
);
};
export default App;
```
:::
### 多層堆疊
:::demo
```tsx
import React, { useState } from "react";
import { Popup, Cell } from '@nutui/nutui-react';
const App = () => {
const [showMutiple, setShowMutiple] = useState(false)
const [showMutipleInner, setShowMutipleInner] = useState(false)
return (
<>
<Cell title="多層堆疊" isLink onClick={() => { setShowMutiple(true) }}/>
<Popup
visible={showMutiple}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutiple(false)
}}
>
<span onClick={ () => { setShowMutipleInner(true) } }>Click It</span>
</Popup>
<Popup
visible={showMutipleInner}
style={{ padding: '30px 50px' }}
onClose={() => {
setShowMutipleInner(false)
}}
>
<span onClick={ () => { setShowMutipleInner(false) } }>close</span>
</Popup>
</>
);
};
export default App;
```
:::
## API
### Props
......@@ -139,6 +201,7 @@ export default App;
| closeIcon | 自定義 Icon | String | `"close"` |
| destroyOnClose | 組件銷毀後是否關閉 | Boolean | `true` |
| round | 是否顯示圓角 | Boolean | `false` |
| teleport | 指定節點掛載 | HTMLElement、(() => HTMLElement) 、null | `null` |
### Events
......
......@@ -4,7 +4,10 @@ import React, {
useEffect,
MouseEventHandler,
MouseEvent,
ReactElement,
ReactPortal,
} from 'react'
import { createPortal } from 'react-dom'
import { CSSTransition } from 'react-transition-group'
import classNames from 'classnames'
import { EnterHandler, ExitHandler } from 'react-transition-group/Transition'
......@@ -13,6 +16,8 @@ import Icon from '@/packages/icon'
import Overlay from '@/packages/overlay'
import bem from '@/utils/bem'
type Teleport = HTMLElement | (() => HTMLElement) | null
interface PopupProps extends OverlayProps {
position: string
transition: string
......@@ -22,7 +27,7 @@ interface PopupProps extends OverlayProps {
closeIconPosition: string
closeIcon: string
destroyOnClose: boolean
// teleport: string | HTMLElement
teleport: Teleport
overlay: boolean
round: boolean
onOpen: () => void
......@@ -43,7 +48,7 @@ const defaultProps = {
closeIconPosition: 'top-right',
closeIcon: 'close',
destroyOnClose: true,
// teleport: 'body',
teleport: null,
overlay: true,
round: false,
onOpen: () => {},
......@@ -64,8 +69,10 @@ export const Popup: FunctionComponent<
const {
children,
visible,
overlay,
closeOnClickOverlay,
overlayStyle,
overlayClass,
zIndex,
lockScroll,
duration,
......@@ -79,6 +86,7 @@ export const Popup: FunctionComponent<
popClass,
className,
destroyOnClose,
teleport,
onOpen,
onClose,
onClickOverlay,
......@@ -127,9 +135,6 @@ export const Popup: FunctionComponent<
const open = () => {
if (!innerVisible) {
// if(zIndex !== undefined) {
// _zIndex = +zIndex;
// }
setInnerVisible(true)
setIndex(++_zIndex)
}
......@@ -181,26 +186,23 @@ export const Popup: FunctionComponent<
onClosed && onClosed(e)
}
useEffect(() => {
visible && open()
!visible && close()
}, [visible])
const resolveContainer = (getContainer: Teleport | undefined) => {
const container =
typeof getContainer === 'function' ? getContainer() : getContainer
return container || document.body
}
useEffect(() => {
setTransitionName(transition || `popup-slide-${position}`)
}, [position])
const renderToContainer = (getContainer: Teleport, node: ReactElement) => {
if (getContainer) {
const container = resolveContainer(getContainer)
return createPortal(node, container) as ReactPortal
}
return (
<>
<Overlay
style={overlayStyles}
visible={innerVisible}
closeOnClickOverlay={closeOnClickOverlay}
zIndex={zIndex}
lockScroll={lockScroll}
duration={duration}
onClick={onHandleClickOverlay}
/>
return node
}
const renderPop = () => {
return (
<CSSTransition
classNames={transitionName}
unmountOnExit
......@@ -220,8 +222,43 @@ export const Popup: FunctionComponent<
)}
</div>
</CSSTransition>
</>
)
)
}
const renderNode = () => {
return (
<>
{overlay ? (
<>
<Overlay
style={overlayStyles}
overlayClass={overlayClass}
visible={innerVisible}
closeOnClickOverlay={closeOnClickOverlay}
zIndex={zIndex}
lockScroll={lockScroll}
duration={duration}
onClick={onHandleClickOverlay}
/>
{renderPop()}
</>
) : (
<>{renderPop()}</>
)}
</>
)
}
useEffect(() => {
visible && open()
!visible && close()
}, [visible])
useEffect(() => {
setTransitionName(transition || `popup-slide-${position}`)
}, [position])
return <>{renderToContainer(teleport as Teleport, renderNode())}</>
}
Popup.defaultProps = defaultProps
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册