Skip to content

Commit

Permalink
feat: add drag panel & popover component
Browse files Browse the repository at this point in the history
  • Loading branch information
BoBoooooo committed May 20, 2024
1 parent 791fbb1 commit 12fef59
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 0 deletions.
122 changes: 122 additions & 0 deletions packages/ui/src/drag-panel.tsx
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>
);
}
2 changes: 2 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ export * from './tabs';
export * from './select-action';
export * from './copy-clipboard';
export * from './tag-select';
export * from './popover';
export * from './drag-panel';
166 changes: 166 additions & 0 deletions packages/ui/src/popover.tsx
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}
</>
);
};

0 comments on commit 12fef59

Please sign in to comment.