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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export const PendantPopover = (
const popoverHandlerRef = useRef<PopperHandler>(); const popoverHandlerRef = useRef<PopperHandler>();
return ( return (
<Popover <Popover
ref={popoverHandlerRef} popperHandlerRef={popoverHandlerRef}
pointerEnterDelay={300} pointerEnterDelay={300}
pointerLeaveDelay={200} pointerLeaveDelay={200}
placement="bottom-start" placement="bottom-start"
@ -26,13 +26,13 @@ export const PendantPopover = (
block={block} block={block}
endElement={ endElement={
<AddPendantPopover <AddPendantPopover
container={popoverProps.container}
block={block} block={block}
onSure={() => { onSure={() => {
popoverHandlerRef.current?.setVisible(false); popoverHandlerRef.current?.setVisible(false);
}} }}
offset={[0, -30]} offset={[0, -30]}
trigger="click" trigger="click"
useAddIcon={false}
/> />
} }
onClose={() => { onClose={() => {

View File

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

View File

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

View File

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

View File

@ -1,184 +1,180 @@
import React, { import React, {
forwardRef,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { /* eslint-disable no-restricted-imports */
MuiPopper, import PopperUnstyled from '@mui/base/PopperUnstyled';
MuiClickAwayListener as ClickAwayListener, /* eslint-disable no-restricted-imports */
MuiGrow as Grow, import ClickAwayListener from '@mui/base/ClickAwayListener';
} from '../mui'; /* eslint-disable no-restricted-imports */
import Grow from '@mui/material/Grow';
import { styled } from '../styled'; import { styled } from '../styled';
import { PopperProps, PopperHandler, VirtualElement } from './interface'; import { PopperProps, VirtualElement } from './interface';
import { useTheme } from '../theme';
import { PopperArrow } from './PopoverArrow'; import { PopperArrow } from './PopoverArrow';
export const Popper = forwardRef<PopperHandler, PopperProps>( export const Popper = ({
( children,
{ content,
children, anchorEl: propsAnchorEl,
content, placement = 'top-start',
anchor: propsAnchor, defaultVisible = false,
placement = 'top-start', visible: propsVisible,
defaultVisible = false, trigger = 'hover',
container, pointerEnterDelay = 100,
keepMounted = false, pointerLeaveDelay = 100,
visible: propsVisible, onVisibleChange,
trigger = 'hover', popoverStyle,
pointerEnterDelay = 100, popoverClassName,
pointerLeaveDelay = 100, anchorStyle,
onVisibleChange, anchorClassName,
popoverStyle, zIndex,
popoverClassName, offset = [0, 5],
anchorStyle, showArrow = false,
anchorClassName, popperHandlerRef,
zIndex, ...popperProps
offset = [0, 5], }: PopperProps) => {
showArrow = false, const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
}, const [visible, setVisible] = useState(defaultVisible);
ref const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
) => { const popperRef = useRef();
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null); const pointerLeaveTimer = useRef<number>();
const [visible, setVisible] = useState(defaultVisible); const pointerEnterTimer = useRef<number>();
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
const pointerLeaveTimer = useRef<number>(); const visibleControlledByParent = typeof propsVisible !== 'undefined';
const pointerEnterTimer = useRef<number>(); const isAnchorCustom = typeof propsAnchorEl !== 'undefined';
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 hasHoverTrigger = useMemo(() => {
return ( return (
<ClickAwayListener trigger === 'hover' ||
onClickAway={() => { (Array.isArray(trigger) && trigger.includes('hover'))
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]);
);
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 // The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({ const Container = styled('div')({
display: 'contents', 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 { CSSProperties, ReactNode, Ref } from 'react';
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui'; /* eslint-disable no-restricted-imports */
import {
type PopperUnstyledProps,
type PopperPlacementType,
} from '@mui/base/PopperUnstyled';
export type VirtualElement = { export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect; getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element; contextElement?: Element;
@ -21,26 +24,12 @@ export type PopperProps = {
// Popover trigger // Popover trigger
children?: ReactNode; 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 // Whether the default is implicit
defaultVisible?: boolean; defaultVisible?: boolean;
// Used to manually control the visibility of the Popover // Used to manually control the visibility of the Popover
visible?: boolean; 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 // TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[]; trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
@ -71,4 +60,6 @@ export type PopperProps = {
offset?: [number, number]; offset?: [number, number];
showArrow?: boolean; 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 PropsWithChildren, type CSSProperties } from 'react';
import { type PopperHandler, type PopperProps, Popper } from '../popper'; import { type PopperProps, Popper } from '../popper';
import { PopoverContainer, placementToContainerDirection } from '../popover'; import { PopoverContainer, placementToContainerDirection } from '../popover';
import type { TooltipProps } from './interface'; import type { TooltipProps } from './interface';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
@ -14,17 +14,15 @@ const useTooltipStyle = (): CSSProperties => {
}; };
}; };
export const Tooltip = forwardRef< export const Tooltip = (
PopperHandler, props: PropsWithChildren<PopperProps & TooltipProps>
PropsWithChildren<PopperProps & TooltipProps> ) => {
>((props, ref) => {
const { content, placement = 'top-start' } = props; const { content, placement = 'top-start' } = props;
const style = useTooltipStyle(); const style = useTooltipStyle();
// If there is no content, hide forever // If there is no content, hide forever
const visibleProp = content ? {} : { visible: false }; const visibleProp = content ? {} : { visible: false };
return ( return (
<Popper <Popper
ref={ref}
{...visibleProp} {...visibleProp}
placement="top" placement="top"
{...props} {...props}
@ -39,4 +37,4 @@ export const Tooltip = forwardRef<
} }
/> />
); );
}); };