Merge pull request #220 from toeverything/feat/block-pendant

Feat/block pendant
This commit is contained in:
Qi 2022-08-12 19:12:08 +08:00 committed by GitHub
commit 451e490dfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 228 additions and 213 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

@ -4,7 +4,11 @@ import {
Popover,
type PopoverProps,
PopperHandler,
Tag,
type PopperProps,
} from '@toeverything/components/ui';
import { TagsIcon } from '@toeverything/components/icons';
import { CreatePendantPanel } from './pendant-operation-panel';
import { IconButton } from './StyledComponent';
import { AsyncBlock } from '../editor';
@ -13,17 +17,21 @@ type Props = {
block: AsyncBlock;
onSure?: () => void;
iconStyle?: CSSProperties;
useAddIcon?: boolean;
} & Omit<PopoverProps, 'content'>;
export const AddPendantPopover = ({
block,
onSure,
iconStyle,
useAddIcon = true,
...popoverProps
}: Props) => {
const popoverHandlerRef = useRef<PopperHandler>();
const popperRef = useRef<any>();
return (
<Popover
ref={popoverHandlerRef}
popperHandlerRef={popoverHandlerRef}
popperRef={popperRef}
content={
<CreatePendantPanel
block={block}
@ -31,6 +39,9 @@ export const AddPendantPopover = ({
popoverHandlerRef.current?.setVisible(false);
onSure?.();
}}
onTypeChange={() => {
popperRef.current?.update?.();
}}
/>
}
placement="bottom-start"
@ -38,9 +49,25 @@ export const AddPendantPopover = ({
style={{ padding: 0 }}
{...popoverProps}
>
<IconButton style={{ marginRight: 12, ...iconStyle }}>
<Add sx={{ fontSize: 14 }} />
</IconButton>
{useAddIcon ? (
<IconButton style={{ marginRight: 12, ...iconStyle }}>
<Add sx={{ fontSize: 14 }} />
</IconButton>
) : (
<Tag
style={{
background: '#F5F7F8',
color: '#98ACBD',
marginRight: 12,
marginBottom: 8,
}}
startElement={
<TagsIcon style={{ fontSize: 14, marginRight: 4 }} />
}
>
Tag App
</Tag>
)}
</Popover>
);
};

View File

@ -98,8 +98,8 @@ export const pendantConfig: { [key: string]: PendantConfig } = {
},
[PendantTypes.Status]: {
iconName: IconNames.STATUS,
background: ['#C5FBE0', '#FFF5AB', '#FFCECE', '#E3DEFF'],
color: ['#05683D', '#896406', '#AF1212', '#511AAB'],
background: ['#FFCECE', '#FFF5AB', '#C5FBE0', '#E3DEFF'],
color: ['#AF1212', '#896406', '#05683D', '#511AAB'],
},
[PendantTypes.Select]: {
iconName: IconNames.SINGLE_SELECT,

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

@ -11,6 +11,7 @@ export type ModifyPanelProps = {
iconConfig?: PendantConfig;
isStatusSelect?: boolean;
property?: RecastMetaProperty;
onTypeChange?: (type: PendantTypes) => void;
};
export type ModifyPanelContentProps = {

View File

@ -24,9 +24,11 @@ import { useOnCreateSure } from './hooks';
export const CreatePendantPanel = ({
block,
onSure,
onTypeChange,
}: {
block: AsyncBlock;
onSure?: () => void;
onTypeChange?: (option: PendantOptions) => void;
}) => {
const [selectedOption, setSelectedOption] = useState<PendantOptions>();
const [fieldName, setFieldName] = useState<string>('');
@ -37,6 +39,10 @@ export const CreatePendantPanel = ({
setFieldName(generateRandomFieldName(selectedOption.type));
}, [selectedOption]);
useEffect(() => {
onTypeChange?.(selectedOption);
}, [selectedOption, onTypeChange]);
return (
<StyledPopoverWrapper>
<StyledOperationTitle>Add Field</StyledOperationTitle>

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"
@ -26,13 +26,13 @@ export const PendantPopover = (
block={block}
endElement={
<AddPendantPopover
container={popoverProps.container}
block={block}
onSure={() => {
popoverHandlerRef.current?.setVisible(false);
}}
offset={[0, -30]}
trigger="click"
useAddIcon={false}
/>
}
onClose={() => {

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

@ -88,7 +88,7 @@ export const generateInitialOptions = (
) => {
if (type === PendantTypes.Status) {
return [
generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
generateBasicOption({ index: 0, iconConfig, name: 'Not Started' }),
generateBasicOption({
index: 1,
iconConfig,

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,180 @@
import React, {
forwardRef,
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 { useTheme } from '../theme';
import { PopperProps, VirtualElement } from './interface';
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?: number;
}>(({ 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')[];
@ -71,4 +60,6 @@ export type PopperProps = {
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<
}
/>
);
});
};