refactor: refactor popper

This commit is contained in:
QiShaoXuan 2022-08-12 18:49:47 +08:00
parent 82c3d773d6
commit f16bfecab8
8 changed files with 188 additions and 205 deletions

View File

@ -23,7 +23,7 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
? app.getScreenPoint([bounds.minX, bounds.minY])
: undefined;
const anchor = getAnchor({
const anchorEl = getAnchor({
x: point?.[0] || 0,
y: (point?.[1] || 0) + 40,
width: bounds?.width ? bounds.width * camera.zoom : 0,
@ -101,7 +101,7 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
<Popover
trigger="click"
visible={!!point}
anchor={anchor}
anchorEl={anchorEl}
popoverDirection="none"
content={
<PopoverContainer>

View File

@ -91,7 +91,7 @@ export const PendantHistoryPanel = ({
return (
<Popover
key={item.id}
ref={ref => {
popperHandlerRef={ref => {
popoverHandlerRef.current[item.id] = ref;
}}
placement="bottom-start"

View File

@ -17,7 +17,7 @@ export const PendantPopover = (
const popoverHandlerRef = useRef<PopperHandler>();
return (
<Popover
ref={popoverHandlerRef}
popperHandlerRef={popoverHandlerRef}
pointerEnterDelay={300}
pointerLeaveDelay={200}
placement="bottom-start"

View File

@ -56,7 +56,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
return (
<Popover
ref={ref => {
popperHandlerRef={ref => {
popoverHandlerRef.current[id] = ref;
}}
container={blockRenderContainerRef.current}
@ -107,7 +107,7 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
iconStyle={{ marginTop: 4 }}
trigger="click"
// trigger={isKanbanView ? 'hover' : 'click'}
container={blockRenderContainerRef.current}
// container={blockRenderContainerRef.current}
/>
</div>
</MuiFade>

View File

@ -1,6 +1,6 @@
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
import React, { forwardRef, type PropsWithChildren } from 'react';
import { type PopperHandler, Popper } from '../popper';
import React, { type PropsWithChildren } from 'react';
import { Popper } from '../popper';
import { PopoverContainer } from './Container';
import type { PopoverProps, PopoverDirection } from './interface';
@ -25,15 +25,11 @@ export const placementToContainerDirection: Record<
'auto-end': 'none',
};
export const Popover = forwardRef<
PopperHandler,
PropsWithChildren<PopoverProps>
>((props, ref) => {
export const Popover = (props: PropsWithChildren<PopoverProps>) => {
const { popoverDirection, placement, content, children, style } = props;
return (
<Popper
{...props}
ref={ref}
content={
<PopoverContainer
style={style}
@ -49,4 +45,4 @@ export const Popover = forwardRef<
{children}
</Popper>
);
});
};

View File

@ -1,184 +1,182 @@
import React, {
forwardRef,
CSSProperties,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import {
MuiPopper,
MuiClickAwayListener as ClickAwayListener,
MuiGrow as Grow,
} from '../mui';
/* eslint-disable no-restricted-imports */
import PopperUnstyled from '@mui/base/PopperUnstyled';
/* eslint-disable no-restricted-imports */
import ClickAwayListener from '@mui/base/ClickAwayListener';
/* eslint-disable no-restricted-imports */
import Grow from '@mui/material/Grow';
import { styled } from '../styled';
import { PopperProps, PopperHandler, VirtualElement } from './interface';
import { PopperProps, VirtualElement } from './interface';
import { useTheme } from '../theme';
import { PopperArrow } from './PopoverArrow';
export const Popper = forwardRef<PopperHandler, PopperProps>(
(
{
children,
content,
anchor: propsAnchor,
placement = 'top-start',
defaultVisible = false,
container,
keepMounted = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 100,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorStyle,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
},
ref
) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
const [visible, setVisible] = useState(defaultVisible);
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
export const Popper = ({
children,
content,
anchorEl: propsAnchorEl,
placement = 'top-start',
defaultVisible = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 100,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorStyle,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
popperHandlerRef,
...popperProps
}: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
const [visible, setVisible] = useState(defaultVisible);
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
const popperRef = useRef();
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchor !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const theme = useTheme();
const onPointerEnterHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(ref, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchorEl !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
<ClickAwayListener
onClickAway={() => {
setVisible(false);
}}
>
<Container>
{isAnchorCustom ? null : (
<div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => {
if (
!hasClickTrigger ||
visibleControlledByParent
) {
return;
}
setVisible(!visible);
}}
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={anchorStyle}
className={anchorClassName}
>
{children}
</div>
)}
<MuiPopper
open={
visibleControlledByParent ? propsVisible : visible
}
sx={{ zIndex: zIndex || theme.affine.zIndex.popover }}
anchorEl={isAnchorCustom ? propsAnchor : anchorEl}
placement={placement}
container={container}
keepMounted={keepMounted}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
>
{showArrow && (
<PopperArrow
placement={placement}
ref={setArrowRef}
/>
)}
{content}
</div>
</Grow>
)}
</MuiPopper>
</Container>
</ClickAwayListener>
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const onPointerEnterHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(popperHandlerRef, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
return (
<ClickAwayListener
onClickAway={() => {
setVisible(false);
}}
>
<Container>
{isAnchorCustom ? null : (
<div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => {
if (!hasClickTrigger || visibleControlledByParent) {
return;
}
setVisible(!visible);
}}
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={anchorStyle}
className={anchorClassName}
>
{children}
</div>
)}
<BasicStyledPopper
popperRef={popperRef}
open={visibleControlledByParent ? propsVisible : visible}
zIndex={zIndex}
anchorEl={isAnchorCustom ? propsAnchorEl : anchorEl}
placement={placement}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
{...popperProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
>
{showArrow && (
<PopperArrow
placement={placement}
ref={setArrowRef}
/>
)}
{content}
</div>
</Grow>
)}
</BasicStyledPopper>
</Container>
</ClickAwayListener>
);
};
// The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({
display: 'contents',
});
const BasicStyledPopper = styled(PopperUnstyled)<{
zIndex?: CSSProperties['zIndex'];
}>(({ zIndex, theme }) => {
return {
zIndex: zIndex || theme.affine.zIndex.popover,
};
});

View File

@ -1,6 +1,9 @@
import type { CSSProperties, ReactNode } from 'react';
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
import type { CSSProperties, ReactNode, Ref } from 'react';
/* eslint-disable no-restricted-imports */
import {
type PopperUnstyledProps,
type PopperPlacementType,
} from '@mui/base/PopperUnstyled';
export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element;
@ -21,26 +24,12 @@ export type PopperProps = {
// Popover trigger
children?: ReactNode;
// Position of Popover
placement?: PopperPlacementType;
// The popover will pop up based on the anchor position
// And if this parameter is passed, children will not be rendered
anchor?: VirtualElement | (() => VirtualElement);
// Whether the default is implicit
defaultVisible?: boolean;
// Used to manually control the visibility of the Popover
visible?: boolean;
// A HTML element or function that returns one. The container will have the portal children appended to it.
// By default, it uses the body of the top-level document object, so it's simply document.body most of the time.
container?: HTMLElement;
// Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Popper
keepMounted?: boolean;
// TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
@ -66,9 +55,11 @@ export type PopperProps = {
anchorClassName?: string;
// Popover z-index
zIndex?: number;
zIndex?: CSSProperties['zIndex'];
offset?: [number, number];
showArrow?: boolean;
};
popperHandlerRef?: Ref<PopperHandler>;
} & Omit<PopperUnstyledProps, 'open' | 'ref'>;

View File

@ -1,5 +1,5 @@
import { forwardRef, type PropsWithChildren, type CSSProperties } from 'react';
import { type PopperHandler, type PopperProps, Popper } from '../popper';
import { type PropsWithChildren, type CSSProperties } from 'react';
import { type PopperProps, Popper } from '../popper';
import { PopoverContainer, placementToContainerDirection } from '../popover';
import type { TooltipProps } from './interface';
import { useTheme } from '../theme';
@ -14,17 +14,15 @@ const useTooltipStyle = (): CSSProperties => {
};
};
export const Tooltip = forwardRef<
PopperHandler,
PropsWithChildren<PopperProps & TooltipProps>
>((props, ref) => {
export const Tooltip = (
props: PropsWithChildren<PopperProps & TooltipProps>
) => {
const { content, placement = 'top-start' } = props;
const style = useTooltipStyle();
// If there is no content, hide forever
const visibleProp = content ? {} : { visible: false };
return (
<Popper
ref={ref}
{...visibleProp}
placement="top"
{...props}
@ -39,4 +37,4 @@ export const Tooltip = forwardRef<
}
/>
);
});
};