mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: modify styles & ui components
This commit is contained in:
64
packages/app/src/ui/popover/index.tsx
Normal file
64
packages/app/src/ui/popover/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from 'react';
|
||||
import type { CSSProperties, PropsWithChildren, ReactNode } from 'react';
|
||||
import Grow from '@mui/material/Grow';
|
||||
|
||||
import { styled } from '@/styles';
|
||||
|
||||
type PopoverProps = {
|
||||
popoverContent?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const StyledPopoverContainer = styled('div')({
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const StyledPopoverWrapper = styled('div')({
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
paddingTop: '46px',
|
||||
zIndex: 1000,
|
||||
});
|
||||
const StyledPopover = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '248px',
|
||||
background: theme.colors.popoverBackground,
|
||||
boxShadow: theme.shadow.popover,
|
||||
color: theme.colors.popoverColor,
|
||||
borderRadius: '10px 0px 10px 10px',
|
||||
padding: '8px 4px',
|
||||
position: 'absolute',
|
||||
top: '46px',
|
||||
right: '0',
|
||||
};
|
||||
});
|
||||
export const Popover = ({
|
||||
children,
|
||||
popoverContent,
|
||||
style = {},
|
||||
}: PropsWithChildren<PopoverProps>) => {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<StyledPopoverContainer
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShow(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
<Grow in={show}>
|
||||
<StyledPopoverWrapper>
|
||||
<StyledPopover>{popoverContent}</StyledPopover>
|
||||
</StyledPopoverWrapper>
|
||||
</Grow>
|
||||
</StyledPopoverContainer>
|
||||
);
|
||||
};
|
||||
93
packages/app/src/ui/popper/PopoverArrow.tsx
Normal file
93
packages/app/src/ui/popper/PopoverArrow.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
import { PopperArrowProps } from './interface';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
|
||||
({ placement }, ref) => {
|
||||
return <StyledArrow placement={placement} ref={ref} />;
|
||||
}
|
||||
);
|
||||
|
||||
const getArrowStyle = (placement: PopperArrowProps['placement']) => {
|
||||
if (placement.indexOf('bottom') === 0) {
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginTop: '-0.9em',
|
||||
width: '3em',
|
||||
height: '1em',
|
||||
'&::before': {
|
||||
borderWidth: '0 1em 1em 1em',
|
||||
borderColor: `transparent transparent #98ACBD transparent`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (placement.indexOf('top') === 0) {
|
||||
return {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
marginBottom: '-0.9em',
|
||||
width: '3em',
|
||||
height: '1em',
|
||||
'&::before': {
|
||||
borderWidth: '1em 1em 0 1em',
|
||||
borderColor: `#98ACBD transparent transparent transparent`,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (placement.indexOf('left') === 0) {
|
||||
return {
|
||||
right: 0,
|
||||
marginRight: '-0.9em',
|
||||
height: '3em',
|
||||
width: '1em',
|
||||
'&::before': {
|
||||
borderWidth: '1em 0 1em 1em',
|
||||
borderColor: `transparent transparent transparent #98ACBD`,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (placement.indexOf('right') === 0) {
|
||||
return {
|
||||
left: 0,
|
||||
marginLeft: '-0.9em',
|
||||
height: '3em',
|
||||
width: '1em',
|
||||
'&::before': {
|
||||
borderWidth: '1em 1em 1em 0',
|
||||
borderColor: `transparent #98ACBD transparent transparent`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
display: 'none',
|
||||
};
|
||||
};
|
||||
|
||||
const StyledArrow = styled('span')<{
|
||||
placement: PopperArrowProps['placement'];
|
||||
}>(({ placement }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
fontSize: '7px',
|
||||
width: '3em',
|
||||
'::before': {
|
||||
content: '""',
|
||||
margin: 'auto',
|
||||
display: 'block',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderStyle: 'solid',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
|
||||
...getArrowStyle(placement),
|
||||
};
|
||||
});
|
||||
189
packages/app/src/ui/popper/Popper.tsx
Normal file
189
packages/app/src/ui/popper/Popper.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import PopperUnstyled from '@mui/base/PopperUnstyled';
|
||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||
import Grow from '@mui/material/Grow';
|
||||
|
||||
import { styled } from '@/styles';
|
||||
|
||||
import { PopperProps, VirtualElement } from './interface';
|
||||
import { PopperArrow } from './PopoverArrow';
|
||||
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,
|
||||
onClick,
|
||||
onClickAway,
|
||||
...popperProps
|
||||
}: PopperProps) => {
|
||||
// @ts-ignore
|
||||
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
|
||||
const [visible, setVisible] = useState(defaultVisible);
|
||||
// @ts-ignore
|
||||
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
|
||||
const popperRef = useRef();
|
||||
const pointerLeaveTimer = useRef<number>();
|
||||
const pointerEnterTimer = useRef<number>();
|
||||
|
||||
const visibleControlledByParent = typeof propsVisible !== 'undefined';
|
||||
const isAnchorCustom = typeof propsAnchorEl !== '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 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);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={() => {
|
||||
if (visibleControlledByParent) {
|
||||
onClickAway?.();
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
{isAnchorCustom ? null : (
|
||||
<div
|
||||
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
|
||||
onClick={e => {
|
||||
if (!hasClickTrigger || visibleControlledByParent) {
|
||||
onClick?.(e);
|
||||
return;
|
||||
}
|
||||
setVisible(!visible);
|
||||
}}
|
||||
onPointerEnter={onPointerEnterHandler}
|
||||
onPointerLeave={onPointerLeaveHandler}
|
||||
style={anchorStyle}
|
||||
className={anchorClassName}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
<BasicStyledPopper
|
||||
// @ts-ignore
|
||||
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 && (
|
||||
// @ts-ignore
|
||||
<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, {
|
||||
shouldForwardProp: (propName: string) =>
|
||||
!['zIndex'].some(name => name === propName),
|
||||
})<{
|
||||
zIndex?: number;
|
||||
}>(({ zIndex, theme }) => {
|
||||
return {
|
||||
zIndex: zIndex,
|
||||
};
|
||||
});
|
||||
2
packages/app/src/ui/popper/index.ts
Normal file
2
packages/app/src/ui/popper/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './interface';
|
||||
export * from './Popper';
|
||||
66
packages/app/src/ui/popper/interface.ts
Normal file
66
packages/app/src/ui/popper/interface.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { CSSProperties, ReactNode, Ref } from 'react';
|
||||
import {
|
||||
type PopperPlacementType,
|
||||
type PopperUnstyledProps,
|
||||
} from '@mui/base/PopperUnstyled';
|
||||
export type VirtualElement = {
|
||||
getBoundingClientRect: () => ClientRect | DOMRect;
|
||||
contextElement?: Element;
|
||||
};
|
||||
|
||||
export type PopperHandler = {
|
||||
setVisible: (visible: boolean) => void;
|
||||
};
|
||||
|
||||
export type PopperArrowProps = {
|
||||
placement: PopperPlacementType;
|
||||
};
|
||||
|
||||
export type PopperProps = {
|
||||
// Popover content
|
||||
content: ReactNode;
|
||||
|
||||
// Popover trigger
|
||||
children?: ReactNode;
|
||||
|
||||
// Whether the default is implicit
|
||||
defaultVisible?: boolean;
|
||||
|
||||
// Used to manually control the visibility of the Popover
|
||||
visible?: boolean;
|
||||
|
||||
// TODO: support focus
|
||||
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
|
||||
|
||||
// How long does it take for the mouse to display the Popover, in milliseconds
|
||||
pointerEnterDelay?: number;
|
||||
|
||||
// How long does it take to hide the Popover after the mouse moves out, in milliseconds
|
||||
pointerLeaveDelay?: number;
|
||||
|
||||
// Callback fired when the component closed or open
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
|
||||
// Popover container style
|
||||
popoverStyle?: CSSProperties;
|
||||
|
||||
// Popover container class name
|
||||
popoverClassName?: string;
|
||||
|
||||
// Anchor style
|
||||
anchorStyle?: CSSProperties;
|
||||
|
||||
// Anchor class name
|
||||
anchorClassName?: string;
|
||||
|
||||
// Popover z-index
|
||||
zIndex?: number;
|
||||
|
||||
offset?: [number, number];
|
||||
|
||||
showArrow?: boolean;
|
||||
|
||||
popperHandlerRef?: Ref<PopperHandler>;
|
||||
|
||||
onClickAway?: () => void;
|
||||
} & Omit<PopperUnstyledProps, 'open' | 'ref'>;
|
||||
53
packages/app/src/ui/shared/Container.tsx
Normal file
53
packages/app/src/ui/shared/Container.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { styled } from '@/styles';
|
||||
import { PopperPlacementType } from '@mui/material';
|
||||
|
||||
export type PopperDirection =
|
||||
| 'none'
|
||||
| 'left-top'
|
||||
| 'left-bottom'
|
||||
| 'right-top'
|
||||
| 'right-bottom';
|
||||
|
||||
const getBorderRadius = (direction: PopperDirection, radius = '0') => {
|
||||
const map: Record<PopperDirection, string> = {
|
||||
none: `${radius}`,
|
||||
'left-top': `0 ${radius} ${radius} ${radius}`,
|
||||
'left-bottom': `${radius} ${radius} ${radius} 0`,
|
||||
'right-top': `${radius} 0 ${radius} ${radius}`,
|
||||
'right-bottom': `${radius} ${radius} 0 ${radius}`,
|
||||
};
|
||||
return map[direction];
|
||||
};
|
||||
|
||||
export const placementToContainerDirection: Record<
|
||||
PopperPlacementType,
|
||||
PopperDirection
|
||||
> = {
|
||||
top: 'none',
|
||||
'top-start': 'left-bottom',
|
||||
'top-end': 'right-bottom',
|
||||
right: 'none',
|
||||
'right-start': 'left-top',
|
||||
'right-end': 'left-bottom',
|
||||
bottom: 'none',
|
||||
'bottom-start': 'left-top',
|
||||
'bottom-end': 'right-top',
|
||||
left: 'none',
|
||||
'left-start': 'right-top',
|
||||
'left-end': 'right-bottom',
|
||||
auto: 'none',
|
||||
'auto-start': 'none',
|
||||
'auto-end': 'none',
|
||||
};
|
||||
|
||||
export const StyledPopperContainer = styled('div')<{
|
||||
placement?: PopperPlacementType;
|
||||
}>(({ theme, placement = 'top' }) => {
|
||||
const direction = placementToContainerDirection[placement];
|
||||
const borderRadius = getBorderRadius(direction, theme.radius.popover);
|
||||
return {
|
||||
borderRadius,
|
||||
};
|
||||
});
|
||||
|
||||
export default StyledPopperContainer;
|
||||
32
packages/app/src/ui/tooltip/Tooltip.tsx
Normal file
32
packages/app/src/ui/tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type PropsWithChildren } from 'react';
|
||||
import StyledPopperContainer from '../shared/Container';
|
||||
import { Popper, type PopperProps } from '../popper';
|
||||
import { styled } from '@/styles';
|
||||
import type { TooltipProps } from '@mui/material';
|
||||
|
||||
const StyledTooltip = styled(StyledPopperContainer)(({ theme }) => {
|
||||
return {
|
||||
boxShadow: theme.shadow.tooltip,
|
||||
padding: '4px 12px',
|
||||
backgroundColor:
|
||||
theme.mode === 'dark'
|
||||
? theme.colors.popoverBackground
|
||||
: theme.colors.primaryColor,
|
||||
color: '#fff',
|
||||
fontSize: theme.font.xs,
|
||||
};
|
||||
});
|
||||
|
||||
export const Tooltip = (
|
||||
props: PropsWithChildren<PopperProps & Omit<TooltipProps, 'title'>>
|
||||
) => {
|
||||
const { content, placement = 'top-start' } = props;
|
||||
// If there is no content, hide forever
|
||||
return content ? (
|
||||
<Popper
|
||||
{...props}
|
||||
showArrow={false}
|
||||
content={<StyledTooltip placement={placement}>{content}</StyledTooltip>}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
1
packages/app/src/ui/tooltip/index.tsx
Normal file
1
packages/app/src/ui/tooltip/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Tooltip';
|
||||
Reference in New Issue
Block a user