-
Notifications
You must be signed in to change notification settings - Fork 205
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add drag panel & popover component
- Loading branch information
Showing
3 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import React, { useState } from 'react'; | ||
import { Box, Text, styled } from 'coral-system'; | ||
import { Popover, PopoverProps, IconFont } from '../lib/esm'; | ||
import Draggable from 'react-draggable'; | ||
import { CloseOutlined } from '@ant-design/icons'; | ||
import { noop } from '@music163/tango-helpers'; | ||
|
||
const CloseIcon = styled(CloseOutlined)` | ||
cursor: pointer; | ||
margin-left: 10px; | ||
padding: 2px; | ||
font-size: 13px; | ||
&:hover { | ||
color: var(--tango-colors-text1); | ||
background-color: var(--tango-colors-line1); | ||
border-radius: 4px; | ||
} | ||
`; | ||
|
||
interface DragPanelProps extends Omit<PopoverProps, 'overlay'> { | ||
// 标题 | ||
title?: React.ReactNode | string; | ||
// 内容 | ||
body?: React.ReactNode | string; | ||
// 底部 | ||
footer?: ((close: () => void) => React.ReactNode) | React.ReactNode | string; | ||
// 宽度 | ||
width?: number | string; | ||
// 右上角区域 | ||
extra?: React.ReactNode | string; | ||
children?: React.ReactNode; | ||
} | ||
|
||
export function DragPanel({ | ||
title, | ||
footer, | ||
body, | ||
children, | ||
width = 330, | ||
extra, | ||
onOpenChange = noop, | ||
...props | ||
}: DragPanelProps) { | ||
const [open, setOpen] = useState(false); | ||
|
||
const footerNode = | ||
typeof footer === 'function' | ||
? footer(() => { | ||
setOpen(false); | ||
onOpenChange(false); | ||
}) | ||
: footer; | ||
|
||
return ( | ||
<Popover | ||
open={open} | ||
onOpenChange={(innerOpen) => { | ||
setOpen(innerOpen); | ||
onOpenChange(innerOpen); | ||
}} | ||
overlay={ | ||
<Draggable handle=".selection-drag-bar"> | ||
<Box | ||
bg="#FFF" | ||
borderRadius="m" | ||
boxShadow="lowDown" | ||
border="solid" | ||
borderColor="line2" | ||
overflow="hidden" | ||
width={width} | ||
> | ||
{/* 头部区域 */} | ||
<Box | ||
px="l" | ||
py="m" | ||
className="selection-drag-bar" | ||
borderBottom="1px solid var(--tango-colors-line2)" | ||
cursor="move" | ||
display="flex" | ||
justifyContent="space-between" | ||
> | ||
<Box fontSize="12px" color="text2"> | ||
<IconFont type="icon-applications" /> | ||
<Text marginLeft={'5px'}>{title}</Text> | ||
</Box> | ||
<Box color="text2" fontSize="12px" display="flex" alignItems="center"> | ||
{extra} | ||
<CloseIcon | ||
onClick={() => { | ||
setOpen(false); | ||
onOpenChange(false); | ||
}} | ||
/> | ||
</Box> | ||
</Box> | ||
{/* 主体区域 */} | ||
{body} | ||
{/* 底部 */} | ||
{footer && ( | ||
<Box | ||
px="l" | ||
py="m" | ||
whiteSpace="nowrap" | ||
overflow="hidden" | ||
textOverflow="ellipsis" | ||
background="var(--tango-colors-line1)" | ||
fontSize="12px" | ||
fontWeight={400} | ||
borderTop="1px solid var(--tango-colors-line2)" | ||
> | ||
{footerNode} | ||
</Box> | ||
)} | ||
</Box> | ||
</Draggable> | ||
} | ||
{...props} | ||
> | ||
{children} | ||
</Popover> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import React, { useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { noop } from '@music163/tango-helpers'; | ||
import { Box } from 'coral-system'; | ||
|
||
export interface PopoverProps { | ||
open?: boolean; | ||
/** | ||
* 浮层内容 | ||
*/ | ||
overlay: React.ReactNode; | ||
/** | ||
* 浮层打开或关闭时的回调 | ||
*/ | ||
onOpenChange?: (open: boolean) => void; | ||
/** | ||
* 浮层被遮挡时自动调整位置 | ||
*/ | ||
autoAdjustOverflow?: boolean; | ||
/** | ||
* 点击蒙层是否允许关闭 | ||
*/ | ||
maskClosable?: boolean; | ||
/** | ||
* 手动唤起时的位置 | ||
*/ | ||
left?: number; | ||
/** | ||
* 手动唤起时的位置 | ||
*/ | ||
top?: number; | ||
/** | ||
* z-index | ||
*/ | ||
zIndex?: number; | ||
/** | ||
* popoverStyle | ||
*/ | ||
popoverStyle?: React.CSSProperties; | ||
children?: React.ReactNode; | ||
} | ||
|
||
export const Popover: React.FC<PopoverProps> = ({ | ||
open, | ||
overlay, | ||
maskClosable = false, | ||
autoAdjustOverflow = true, | ||
left: controlledLeft, | ||
top: controlledTop, | ||
children, | ||
popoverStyle, | ||
onOpenChange = noop, | ||
zIndex = 9999, | ||
}) => { | ||
const [visible, setVisible] = useState(false); | ||
const [left, setLeft] = useState(0); | ||
const [top, setTop] = useState(0); | ||
const popoverRef = useRef<HTMLDivElement>(null); | ||
|
||
// 唤起位置受控 | ||
const isControlledPostion = useMemo( | ||
() => controlledLeft !== undefined || controlledTop !== undefined, | ||
[controlledLeft, controlledTop], | ||
); | ||
|
||
useEffect(() => { | ||
if (typeof controlledTop === 'number') { | ||
setTop(controlledTop); | ||
} | ||
if (typeof controlledLeft === 'number') { | ||
setLeft(controlledLeft); | ||
} | ||
}, [controlledTop, controlledLeft]); | ||
|
||
useLayoutEffect(() => { | ||
const handleDocumentClick = (e: MouseEvent) => { | ||
if ( | ||
maskClosable && | ||
visible && | ||
popoverRef.current && | ||
!popoverRef.current.contains(e.target as Node) | ||
) { | ||
setVisible(false); | ||
onOpenChange(false); | ||
} | ||
}; | ||
|
||
if (maskClosable && visible) { | ||
document.addEventListener('click', handleDocumentClick, true); | ||
} | ||
|
||
return () => { | ||
document.removeEventListener('click', handleDocumentClick); | ||
}; | ||
}, [maskClosable, onOpenChange, visible]); | ||
|
||
const handleClick = useCallback( | ||
(e: React.MouseEvent) => { | ||
e.preventDefault(); | ||
const x = e.clientX; | ||
const y = e.clientY; | ||
setLeft(x); | ||
setTop(y + 10); | ||
setVisible(true); | ||
onOpenChange(true); | ||
}, | ||
[onOpenChange], | ||
); | ||
|
||
useEffect(() => { | ||
setVisible(open); | ||
}, [open]); | ||
|
||
const getAdjustedPosition = () => { | ||
const popoverElement = popoverRef.current; | ||
if (popoverElement) { | ||
const popoverRect = popoverElement.getBoundingClientRect(); | ||
if (popoverRect.right > window.innerWidth) { | ||
setLeft(window.innerWidth - popoverRect.width); | ||
} | ||
if (popoverRect.bottom > window.innerHeight) { | ||
setTop(window.innerHeight - popoverRect.height); | ||
} | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
if (visible && autoAdjustOverflow) { | ||
getAdjustedPosition(); | ||
} | ||
}, [visible, autoAdjustOverflow]); | ||
|
||
const overlayStyle: React.CSSProperties = useMemo( | ||
() => ({ | ||
display: visible ? 'block' : 'none', | ||
position: 'fixed', | ||
left, | ||
top, | ||
zIndex, | ||
...popoverStyle, | ||
}), | ||
[left, popoverStyle, top, visible, zIndex], | ||
); | ||
|
||
const overlayDom = ( | ||
<Box className="popover"> | ||
<Box ref={popoverRef} className="overlay" style={overlayStyle}> | ||
{overlay} | ||
</Box> | ||
</Box> | ||
); | ||
|
||
return ( | ||
<> | ||
{!isControlledPostion && | ||
React.cloneElement(children as any, { | ||
onClick: (e: React.MouseEvent) => { | ||
e.stopPropagation(); | ||
handleClick(e); | ||
(children as any).props?.onClick?.(e); | ||
}, | ||
})} | ||
{visible ? ReactDOM.createPortal(overlayDom, document.body) : null} | ||
</> | ||
); | ||
}; |