mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 13:58:50 +08:00
refactor(core): doc property (#8465)
doc property upgraded to use orm. The visibility of the property are simplified to three types: `always show`, `always hide`, `hide when empty`, and the default is `always show`.  Added a sidebar view to manage properties  new property ui in workspace settings  Property lists can be collapsed 
This commit is contained in:
@@ -182,6 +182,11 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
||||
let previewPosition: DraggableDragPreviewPosition =
|
||||
options.dragPreviewPosition ?? 'native';
|
||||
|
||||
source.element.dataset['dragPreview'] = 'true';
|
||||
requestAnimationFrame(() => {
|
||||
delete source.element.dataset['dragPreview'];
|
||||
});
|
||||
|
||||
if (enableCustomDragPreview.current) {
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: (...args) => {
|
||||
|
||||
@@ -49,6 +49,12 @@ export const treeLine = style({
|
||||
height: 2,
|
||||
right: 0,
|
||||
},
|
||||
|
||||
selectors: {
|
||||
['&[data-no-terminal="true"]::before']: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const lineAboveStyles = style({
|
||||
@@ -143,7 +149,7 @@ export const left = style({
|
||||
|
||||
export const edgeLine = style({
|
||||
vars: {
|
||||
[terminalSize]: '8px',
|
||||
[terminalSize]: '6px',
|
||||
},
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
@@ -156,11 +162,17 @@ export const edgeLine = style({
|
||||
// Terminal
|
||||
'::before': {
|
||||
content: '""',
|
||||
width: terminalSize,
|
||||
height: terminalSize,
|
||||
width: 0,
|
||||
height: 0,
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
border: `${terminalSize} solid ${cssVar('--affine-primary-color')}`,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
|
||||
selectors: {
|
||||
['&[data-no-terminal="true"]::before']: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,14 +10,17 @@ import * as styles from './drop-indicator.css';
|
||||
export type DropIndicatorProps = {
|
||||
instruction?: Instruction | null;
|
||||
edge?: Edge | null;
|
||||
noTerminal?: boolean;
|
||||
};
|
||||
|
||||
function getTreeElement({
|
||||
instruction,
|
||||
isBlocked,
|
||||
noTerminal,
|
||||
}: {
|
||||
instruction: Exclude<Instruction, { type: 'instruction-blocked' }>;
|
||||
isBlocked: boolean;
|
||||
noTerminal?: boolean;
|
||||
}): ReactElement | null {
|
||||
const style = {
|
||||
[styles.horizontalIndent]: `${instruction.currentLevel * instruction.indentPerLevel}px`,
|
||||
@@ -31,6 +34,7 @@ function getTreeElement({
|
||||
<div
|
||||
className={clsx(styles.treeLine, styles.lineAboveStyles)}
|
||||
style={assignInlineVars(style)}
|
||||
data-no-terminal={noTerminal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +43,7 @@ function getTreeElement({
|
||||
<div
|
||||
className={clsx(styles.treeLine, styles.lineBelowStyles)}
|
||||
style={assignInlineVars(style)}
|
||||
data-no-terminal={noTerminal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +53,7 @@ function getTreeElement({
|
||||
<div
|
||||
className={clsx(styles.outlineStyles)}
|
||||
style={assignInlineVars(style)}
|
||||
data-no-terminal={noTerminal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +67,7 @@ function getTreeElement({
|
||||
<div
|
||||
className={clsx(styles.treeLine, styles.lineBelowStyles)}
|
||||
style={assignInlineVars(style)}
|
||||
data-no-terminal={noTerminal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -88,13 +95,14 @@ const edgeStyles: Record<Edge, string> = {
|
||||
right: styles.right,
|
||||
};
|
||||
|
||||
function getEdgeElement(edge: Edge, gap: number = 0) {
|
||||
function getEdgeElement(edge: Edge, gap: number = 0, noTerminal?: boolean) {
|
||||
const lineOffset = `calc(-0.5 * (${gap}px + 2px))`;
|
||||
|
||||
const orientation = edgeToOrientationMap[edge];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-no-terminal={noTerminal}
|
||||
className={clsx([
|
||||
styles.edgeLine,
|
||||
orientationStyles[orientation],
|
||||
@@ -105,9 +113,13 @@ function getEdgeElement(edge: Edge, gap: number = 0) {
|
||||
);
|
||||
}
|
||||
|
||||
export function DropIndicator({ instruction, edge }: DropIndicatorProps) {
|
||||
export function DropIndicator({
|
||||
instruction,
|
||||
edge,
|
||||
noTerminal,
|
||||
}: DropIndicatorProps) {
|
||||
if (edge) {
|
||||
return getEdgeElement(edge, 0);
|
||||
return getEdgeElement(edge, 0, noTerminal);
|
||||
}
|
||||
if (instruction) {
|
||||
if (instruction.type === 'instruction-blocked') {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './property';
|
||||
@@ -0,0 +1,163 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const propertyRoot = style({
|
||||
display: 'flex',
|
||||
minHeight: 32,
|
||||
position: 'relative',
|
||||
padding: '2px 0px 2px 2px',
|
||||
selectors: {
|
||||
'&[draggable="true"]': {
|
||||
cursor: 'grab',
|
||||
},
|
||||
'&[draggable="true"][data-dragging="true"]': {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
'&[draggable="true"]:before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
cursor: 'grab',
|
||||
top: '50%',
|
||||
left: 0,
|
||||
borderRadius: '2px',
|
||||
backgroundColor: cssVarV2('text/placeholder'),
|
||||
transform: 'translate(-6px, -50%)',
|
||||
transition: 'height 0.2s 0.1s, opacity 0.2s 0.1s',
|
||||
opacity: 0,
|
||||
height: '4px',
|
||||
width: '4px',
|
||||
willChange: 'height, opacity',
|
||||
},
|
||||
'&[draggable="true"]:after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
cursor: 'grab',
|
||||
top: '50%',
|
||||
left: 0,
|
||||
borderRadius: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
transform: 'translate(-8px, -50%)',
|
||||
height: '100%',
|
||||
width: '8px',
|
||||
willChange: 'height, opacity',
|
||||
},
|
||||
'&[draggable="true"]:hover:before': {
|
||||
height: 12,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const hide = style({
|
||||
// Visually hide the property while maintaining its position in the layout.
|
||||
// This ensures that any open menu remains in the same position when the property is hidden.
|
||||
overflow: 'hidden',
|
||||
height: '0px',
|
||||
minHeight: '0px',
|
||||
padding: '0px',
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const propertyNameContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
borderRadius: 4,
|
||||
fontSize: cssVar('fontSm'),
|
||||
padding: `6px 6px 6px 4px`,
|
||||
flexShrink: 0,
|
||||
lineHeight: '22px',
|
||||
userSelect: 'none',
|
||||
color: cssVarV2('text/secondary'),
|
||||
width: '160px',
|
||||
selectors: {
|
||||
'&[data-has-menu="true"]': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'&[data-has-menu="true"]:hover': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`[data-drag-preview] ${propertyNameContainer}:hover`, {
|
||||
backgroundColor: 'transparent',
|
||||
});
|
||||
|
||||
export const propertyNameInnerContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
export const propertyIconContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '2px',
|
||||
fontSize: 16,
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
|
||||
export const propertyNameContent = style({
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
|
||||
export const propertyValueContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
position: 'relative',
|
||||
borderRadius: 4,
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
padding: `6px`,
|
||||
flex: 1,
|
||||
':focus-visible': {
|
||||
outline: 'none',
|
||||
},
|
||||
'::placeholder': {
|
||||
color: cssVarV2('text/placeholder'),
|
||||
},
|
||||
selectors: {
|
||||
'&[data-readonly="false"]': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'&[data-readonly="false"]:hover': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
'&[data-readonly="false"]:focus-within': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const tableButton = style({
|
||||
alignSelf: 'flex-start',
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: `${cssVarV2('text/secondary')}`,
|
||||
padding: '0 6px',
|
||||
height: 36,
|
||||
fontWeight: 400,
|
||||
gap: 6,
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${tableButton} svg`, {
|
||||
fontSize: 16,
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
globalStyle(`${tableButton}:hover svg`, {
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { FrameIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { useDraggable, useDropTarget } from '../dnd';
|
||||
import { MenuItem } from '../menu';
|
||||
import {
|
||||
PropertyCollapsible,
|
||||
PropertyName,
|
||||
PropertyRoot,
|
||||
PropertyValue,
|
||||
} from './property';
|
||||
|
||||
export default {
|
||||
title: 'UI/Property',
|
||||
};
|
||||
|
||||
export const SingleProperty = () => {
|
||||
return (
|
||||
<>
|
||||
<PropertyRoot>
|
||||
<PropertyName name="Name" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot>
|
||||
<PropertyName name="Long nameeeeeeeeeeeeeeeee" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
|
||||
<PropertyRoot>
|
||||
<PropertyName
|
||||
name="With Menu"
|
||||
icon={<FrameIcon />}
|
||||
menuItems={<MenuItem>Menu</MenuItem>}
|
||||
/>
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
|
||||
<PropertyRoot>
|
||||
<PropertyName name="Readonly" icon={<FrameIcon />} />
|
||||
<PropertyValue readonly>Readonly Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DNDProperty = () => {
|
||||
const { dragRef: dragRef1 } = useDraggable(
|
||||
() => ({
|
||||
data: { text: 'hello' },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const { dragRef: dragRef2 } = useDraggable(
|
||||
() => ({
|
||||
data: { text: 'hello' },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const { dropTargetRef, closestEdge } = useDropTarget(
|
||||
() => ({
|
||||
closestEdge: {
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PropertyRoot ref={dragRef1}>
|
||||
<PropertyName name="Draggable" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot ref={dragRef2}>
|
||||
<PropertyName
|
||||
name="Draggable Menu"
|
||||
icon={<FrameIcon />}
|
||||
menuItems={<MenuItem>Menu</MenuItem>}
|
||||
/>
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot ref={dropTargetRef} dropIndicatorEdge={closestEdge}>
|
||||
<PropertyName name="DropTarget" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const HideEmptyProperty = () => {
|
||||
return (
|
||||
<>
|
||||
<PropertyRoot hideEmpty>
|
||||
<PropertyName name="Should not be hidden" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot hideEmpty>
|
||||
<PropertyName name="Should be hidden" icon={<FrameIcon />} />
|
||||
<PropertyValue isEmpty>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const BasicPropertyCollapsible = () => {
|
||||
return (
|
||||
<PropertyCollapsible collapsible>
|
||||
<PropertyRoot>
|
||||
<PropertyName name="Always show" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot hideEmpty>
|
||||
<PropertyName name="Hide with empty" icon={<FrameIcon />} />
|
||||
<PropertyValue isEmpty>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot hide>
|
||||
<PropertyName name="Hide" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
</PropertyCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export const PropertyCollapsibleCustomButton = () => {
|
||||
return (
|
||||
<PropertyCollapsible
|
||||
collapsible
|
||||
collapseButtonText={({ hide, isCollapsed }) =>
|
||||
`${isCollapsed ? 'Show' : 'Hide'} ${hide} properties`
|
||||
}
|
||||
>
|
||||
<PropertyRoot>
|
||||
<PropertyName name="Always show" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot hideEmpty>
|
||||
<PropertyName name="Hide with empty" icon={<FrameIcon />} />
|
||||
<PropertyValue isEmpty>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
<PropertyRoot hide>
|
||||
<PropertyName name="Hide" icon={<FrameIcon />} />
|
||||
<PropertyValue>Value</PropertyValue>
|
||||
</PropertyRoot>
|
||||
</PropertyCollapsible>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,276 @@
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { ArrowDownSmallIcon, ArrowUpSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
createContext,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { DropIndicator } from '../dnd';
|
||||
import { Menu } from '../menu';
|
||||
import * as styles from './property.css';
|
||||
|
||||
const PropertyTableContext = createContext<{
|
||||
mountProperty: (payload: { isHide: boolean }) => () => void;
|
||||
showAllHide: boolean;
|
||||
} | null>(null);
|
||||
|
||||
export const PropertyCollapsible = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<{
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
collapsed?: boolean;
|
||||
onCollapseChange?: (collapsed: boolean) => void;
|
||||
collapseButtonText?: (option: {
|
||||
total: number;
|
||||
hide: number;
|
||||
isCollapsed: boolean;
|
||||
}) => ReactNode;
|
||||
}> &
|
||||
HTMLProps<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
collapsible = true,
|
||||
collapsed,
|
||||
defaultCollapsed,
|
||||
onCollapseChange,
|
||||
collapseButtonText,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [propertyCount, setPropertyCount] = useState({ total: 0, hide: 0 });
|
||||
const [showAllHide, setShowAllHide] = useState(!defaultCollapsed);
|
||||
const finalCollapsible = collapsible ? propertyCount.hide !== 0 : false;
|
||||
const controlled = collapsed !== undefined;
|
||||
const finalShowAllHide = finalCollapsible
|
||||
? !controlled
|
||||
? showAllHide
|
||||
: !collapsed
|
||||
: true;
|
||||
|
||||
const mountProperty = useCallback((payload: { isHide: boolean }) => {
|
||||
setPropertyCount(prev => ({
|
||||
total: prev.total + 1,
|
||||
hide: prev.hide + (payload.isHide ? 1 : 0),
|
||||
}));
|
||||
return () => {
|
||||
setPropertyCount(prev => ({
|
||||
total: prev.total - 1,
|
||||
hide: prev.hide - (payload.isHide ? 1 : 0),
|
||||
}));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ mountProperty, showAllHide: finalShowAllHide }),
|
||||
[mountProperty, finalShowAllHide]
|
||||
);
|
||||
|
||||
const handleShowAllHide = useCallback(() => {
|
||||
setShowAllHide(!finalShowAllHide);
|
||||
onCollapseChange?.(finalShowAllHide);
|
||||
}, [finalShowAllHide, onCollapseChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-property-collapsible={finalCollapsible}
|
||||
data-property-collapsed={!finalShowAllHide}
|
||||
{...props}
|
||||
>
|
||||
<PropertyTableContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{finalCollapsible && (
|
||||
<Button
|
||||
variant="plain"
|
||||
prefix={
|
||||
!finalShowAllHide ? (
|
||||
<ArrowDownSmallIcon />
|
||||
) : (
|
||||
<ArrowUpSmallIcon />
|
||||
)
|
||||
}
|
||||
className={styles.tableButton}
|
||||
onClick={handleShowAllHide}
|
||||
data-testid="property-collapsible-button"
|
||||
>
|
||||
{collapseButtonText
|
||||
? collapseButtonText({
|
||||
total: propertyCount.total,
|
||||
hide: propertyCount.hide,
|
||||
isCollapsed: !finalShowAllHide,
|
||||
})
|
||||
: !finalShowAllHide
|
||||
? 'Show All'
|
||||
: 'Hide'}
|
||||
</Button>
|
||||
)}
|
||||
</PropertyTableContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
PropertyCollapsible.displayName = 'PropertyCollapsible';
|
||||
|
||||
const PropertyRootContext = createContext<{
|
||||
mountValue: (payload: { isEmpty: boolean }) => () => void;
|
||||
} | null>(null);
|
||||
|
||||
export const PropertyRoot = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
dropIndicatorEdge?: Edge | null;
|
||||
hideEmpty?: boolean;
|
||||
hide?: boolean;
|
||||
} & HTMLProps<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{ children, className, dropIndicatorEdge, hideEmpty, hide, ...props },
|
||||
ref
|
||||
) => {
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const context = useContext(PropertyTableContext);
|
||||
|
||||
const preferHide = hide || (hideEmpty && isEmpty);
|
||||
const showAllHide = context?.showAllHide;
|
||||
const shouldHide = preferHide && !showAllHide;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (context) {
|
||||
return context.mountProperty({ isHide: !!preferHide });
|
||||
}
|
||||
return;
|
||||
}, [context, preferHide]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
mountValue: (payload: { isEmpty: boolean }) => {
|
||||
setIsEmpty(payload.isEmpty);
|
||||
return () => {
|
||||
setIsEmpty(false);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[setIsEmpty]
|
||||
);
|
||||
|
||||
return (
|
||||
<PropertyRootContext.Provider value={contextValue}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
styles.propertyRoot,
|
||||
shouldHide && styles.hide,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DropIndicator edge={dropIndicatorEdge} />
|
||||
</div>
|
||||
</PropertyRootContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
PropertyRoot.displayName = 'PropertyRoot';
|
||||
|
||||
export const PropertyName = ({
|
||||
icon,
|
||||
name,
|
||||
className,
|
||||
menuItems,
|
||||
defaultOpenMenu,
|
||||
...props
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
name?: ReactNode;
|
||||
menuItems?: ReactNode;
|
||||
defaultOpenMenu?: boolean;
|
||||
} & HTMLProps<HTMLDivElement>) => {
|
||||
const [menuOpen, setMenuOpen] = useState(defaultOpenMenu);
|
||||
const hasMenu = !!menuItems;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!hasMenu) return;
|
||||
setMenuOpen(true);
|
||||
}, [hasMenu]);
|
||||
|
||||
const handleMenuClose = useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={clsx(styles.propertyNameContainer, className)}
|
||||
data-has-menu={hasMenu}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.propertyNameInnerContainer}>
|
||||
{icon && <div className={styles.propertyIconContainer}>{icon}</div>}
|
||||
<div className={styles.propertyNameContent}>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (menuOpen && menuItems) {
|
||||
// Do not mount <Menu /> when menuOpen is false, as <Menu /> will cause draggable to not work
|
||||
return (
|
||||
<Menu
|
||||
items={menuItems}
|
||||
rootOptions={{
|
||||
open: true,
|
||||
modal: true, // false will case bug
|
||||
onOpenChange: handleMenuClose,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
export const PropertyValue = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ readonly?: boolean; isEmpty?: boolean } & HTMLProps<HTMLDivElement>
|
||||
>(({ children, className, readonly, isEmpty, ...props }, ref) => {
|
||||
const context = useContext(PropertyRootContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (context) {
|
||||
return context.mountValue({ isEmpty: !!isEmpty });
|
||||
}
|
||||
return;
|
||||
}, [context, isEmpty]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(styles.propertyValueContainer, className)}
|
||||
data-readonly={readonly ? 'true' : 'false'}
|
||||
data-empty={isEmpty ? 'true' : 'false'}
|
||||
data-property-value
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
PropertyValue.displayName = 'PropertyValue';
|
||||
Reference in New Issue
Block a user