Skip to content

Commit bce969c

Browse files
Added support for customizable tooltip
Added support for customizable tooltip (with reablocks or else component library)
1 parent ccfb337 commit bce969c

File tree

9 files changed

+221
-82
lines changed

9 files changed

+221
-82
lines changed

.storybook/preview.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
import React from 'react';
2+
import { ThemeProvider, theme as reablocksTheme } from 'reablocks';
13
import theme from './theme';
24

5+
export const decorators = [
6+
Story => (
7+
<ThemeProvider theme={reablocksTheme}>
8+
<Story />
9+
</ThemeProvider>
10+
)
11+
]
12+
313
export const parameters = {
414
layout: 'centered',
515
docs: {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;

src/symbols/Node/Node.tsx

Lines changed: 86 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ export interface NodeProps<T = any> extends NodeDragEvents<NodeData<T>, PortData
7474
icon: ReactElement<IconProps, typeof Icon>;
7575
label: ReactElement<LabelProps, typeof Label>;
7676
port: ReactElement<PortProps, typeof Port>;
77+
tooltip?: React.ElementType;
7778
}
7879

79-
export const Node: FC<Partial<NodeProps>> = ({ id, x, y, ports, labels, height, width, properties, animated, className, rx = 2, ry = 2, offsetX = 0, offsetY = 0, icon, disabled, style, children, nodes, edges, draggable = true, linkable = true, selectable = true, removable = true, dragType = 'multiportOnly', dragCursor = 'crosshair', childEdge = <Edge />, childNode = <Node />, remove = <Remove />, port = <Port />, label = <Label />, onRemove, onDrag, onDragStart, onDragEnd, onClick, onKeyDown, onEnter, onLeave }) => {
80+
export const Node: FC<Partial<NodeProps>> = ({ id, x, y, ports, labels, height, width, properties, animated, className, rx = 2, ry = 2, offsetX = 0, offsetY = 0, icon, disabled, style, children, nodes, edges, draggable = true, linkable = true, selectable = true, removable = true, dragType = 'multiportOnly', dragCursor = 'crosshair', childEdge = <Edge />, childNode = <Node />, remove = <Remove />, port = <Port />, label = <Label />, tooltip: Tooltip = React.Fragment, onRemove, onDrag, onDragStart, onDragEnd, onClick, onKeyDown, onEnter, onLeave }) => {
8081
const nodeRef = useRef<SVGRectElement | null>(null);
8182
const controls = useAnimation();
8283
const { canLinkNode, enteredNode, selections, readonly, ...canvas } = useCanvas();
@@ -287,86 +288,92 @@ export const Node: FC<Partial<NodeProps>> = ({ id, x, y, ports, labels, height,
287288
}}
288289
animate={controls}
289290
>
290-
<motion.rect
291-
{...bind()}
292-
ref={nodeRef}
293-
tabIndex={-1}
294-
onKeyDown={onKeyDownCallback}
295-
onClick={onClickCallback}
296-
onTouchStart={onTouchStartCallback}
297-
onMouseEnter={onMouseEnterCallback}
298-
onMouseLeave={onMouseLeaveCallback}
299-
className={classNames(css.rect, className, properties?.className, {
300-
[css.active]: isActive,
301-
[css.disabled]: isDisabled,
302-
[css.unlinkable]: isLinkable === false && !isNodeDrag,
303-
[css.dragging]: dragging,
304-
[css.children]: nodes?.length > 0,
305-
[css.deleteHovered]: deleteHovered,
306-
[css.selectionDisabled]: !canSelect
307-
})}
308-
style={style}
309-
height={height}
310-
width={width}
311-
rx={rx}
312-
ry={ry}
313-
initial={{
314-
opacity: 0
315-
}}
316-
animate={{
317-
opacity: 1,
318-
transition: !animated ? { type: false, duration: 0 } : {}
319-
}}
320-
/>
321-
{children && <Fragment>{typeof children === 'function' ? (children as NodeChildrenAsFunction)(nodeChildProps) : children}</Fragment>}
322-
{icon && properties.icon && <CloneElement<IconProps> element={icon} {...properties.icon} />}
323-
{label && labels?.length > 0 && labels.map((l, index) => <CloneElement<LabelProps> element={label} key={index} {...(l as LabelProps)} />)}
324-
{port && ports?.length > 0 && ports.map((p) => <CloneElement<PortProps> element={port} key={p.id} active={!isMultiPort && dragging} disabled={isDisabled || !linkable} offsetX={newX} offsetY={newY} onDragStart={onDragStartCallback} onDrag={onDragCallback} onDragEnd={onDragEndCallback} {...(p as PortProps)} id={`${id}-port-${p.id}`} />)}
325-
{!isDisabled && isActive && !readonly && remove && removable && (
326-
<CloneElement<RemoveProps>
327-
element={remove}
328-
y={height / 2}
329-
x={width}
330-
onClick={(event: React.MouseEvent<SVGGElement, MouseEvent>) => {
331-
event.preventDefault();
332-
event.stopPropagation();
333-
onRemove?.(event, properties);
334-
setDeleteHovered(false);
335-
}}
336-
onEnter={() => setDeleteHovered(true)}
337-
onLeave={() => setDeleteHovered(false)}
338-
/>
339-
)}
340-
<g>
341-
{edges?.length > 0 &&
342-
edges.map((e: any) => {
343-
const element = typeof childEdge === 'function' ? childEdge(e) : childEdge;
344-
return (
345-
<CloneElement<EdgeProps>
346-
key={e.id}
347-
element={element}
348-
id={`${id}-edge-${e.id}`}
349-
disabled={isDisabled}
350-
{...e}
351-
properties={{
352-
...e.properties,
353-
...(e.data ? { data: e.data } : {})
291+
<foreignObject width={width} height={height} style={{ pointerEvents: 'none' }}>
292+
<Tooltip>
293+
<svg width={width} height={height}>
294+
<motion.rect
295+
{...bind()}
296+
ref={nodeRef}
297+
tabIndex={-1}
298+
onKeyDown={onKeyDownCallback}
299+
onClick={onClickCallback}
300+
onTouchStart={onTouchStartCallback}
301+
onMouseEnter={onMouseEnterCallback}
302+
onMouseLeave={onMouseLeaveCallback}
303+
className={classNames(css.rect, className, properties?.className, {
304+
[css.active]: isActive,
305+
[css.disabled]: isDisabled,
306+
[css.unlinkable]: isLinkable === false && !isNodeDrag,
307+
[css.dragging]: dragging,
308+
[css.children]: nodes?.length > 0,
309+
[css.deleteHovered]: deleteHovered,
310+
[css.selectionDisabled]: !canSelect
311+
})}
312+
style={{ ...style, pointerEvents: 'auto' }}
313+
height={height}
314+
width={width}
315+
rx={rx}
316+
ry={ry}
317+
initial={{
318+
opacity: 1
319+
}}
320+
animate={{
321+
opacity: 1,
322+
transition: !animated ? { type: false, duration: 0 } : {}
323+
}}
324+
/>
325+
{children && <Fragment>{typeof children === 'function' ? (children as NodeChildrenAsFunction)(nodeChildProps) : children}</Fragment>}
326+
{icon && properties.icon && <CloneElement<IconProps> element={icon} {...properties.icon} />}
327+
{label && labels?.length > 0 && labels.map((l, index) => <CloneElement<LabelProps> element={label} key={index} {...(l as LabelProps)} />)}
328+
{port && ports?.length > 0 && ports.map((p) => <CloneElement<PortProps> element={port} key={p.id} active={!isMultiPort && dragging} disabled={isDisabled || !linkable} offsetX={newX} offsetY={newY} onDragStart={onDragStartCallback} onDrag={onDragCallback} onDragEnd={onDragEndCallback} {...(p as PortProps)} id={`${id}-port-${p.id}`} />)}
329+
{!isDisabled && isActive && !readonly && remove && removable && (
330+
<CloneElement<RemoveProps>
331+
element={remove}
332+
y={height / 2}
333+
x={width}
334+
onClick={(event: React.MouseEvent<SVGGElement, MouseEvent>) => {
335+
event.preventDefault();
336+
event.stopPropagation();
337+
onRemove?.(event, properties);
338+
setDeleteHovered(false);
354339
}}
340+
onEnter={() => setDeleteHovered(true)}
341+
onLeave={() => setDeleteHovered(false)}
355342
/>
356-
);
357-
})}
358-
{nodes?.length > 0 &&
359-
nodes.map(({ children, ...n }: any) => {
360-
const element = typeof childNode === 'function' ? childNode(n) : childNode;
361-
const elementDisabled = element.props?.disabled != null ? element.props.disabled : disabled;
362-
const elementAnimated = element.props?.animated != null ? element.props.animated : animated;
363-
const elementDraggable = element.props?.draggable != null ? element.props.draggable : draggable;
364-
const elementLinkable = element.props?.linkable != null ? element.props.linkable : linkable;
365-
const elementSelectable = element.props?.selectable != null ? element.props.selectable : selectable;
366-
const elementRemovable = element.props?.removable != null ? element.props.removable : removable;
367-
return <CloneElement<NodeProps> key={n.id} element={element} id={`${id}-node-${n.id}`} disabled={elementDisabled} nodes={children} offsetX={newX} offsetY={newY} animated={elementAnimated} children={element.props.children} childNode={childNode} dragCursor={dragCursor} dragType={dragType} childEdge={childEdge} draggable={elementDraggable} linkable={elementLinkable} selectable={elementSelectable} removable={elementRemovable} onDragStart={onDragStart} onDrag={onDrag} onDragEnd={onDragEnd} onClick={onClick} onEnter={onEnter} onLeave={onLeave} onKeyDown={onKeyDown} onRemove={onRemove} {...n} />;
368-
})}
369-
</g>
343+
)}
344+
<g>
345+
{edges?.length > 0 &&
346+
edges.map((e: any) => {
347+
const element = typeof childEdge === 'function' ? childEdge(e) : childEdge;
348+
return (
349+
<CloneElement<EdgeProps>
350+
key={e.id}
351+
element={element}
352+
id={`${id}-edge-${e.id}`}
353+
disabled={isDisabled}
354+
{...e}
355+
properties={{
356+
...e.properties,
357+
...(e.data ? { data: e.data } : {})
358+
}}
359+
/>
360+
);
361+
})}
362+
{nodes?.length > 0 &&
363+
nodes.map(({ children, ...n }: any) => {
364+
const element = typeof childNode === 'function' ? childNode(n) : childNode;
365+
const elementDisabled = element.props?.disabled != null ? element.props.disabled : disabled;
366+
const elementAnimated = element.props?.animated != null ? element.props.animated : animated;
367+
const elementDraggable = element.props?.draggable != null ? element.props.draggable : draggable;
368+
const elementLinkable = element.props?.linkable != null ? element.props.linkable : linkable;
369+
const elementSelectable = element.props?.selectable != null ? element.props.selectable : selectable;
370+
const elementRemovable = element.props?.removable != null ? element.props.removable : removable;
371+
return <CloneElement<NodeProps> key={n.id} element={element} id={`${id}-node-${n.id}`} disabled={elementDisabled} nodes={children} offsetX={newX} offsetY={newY} animated={elementAnimated} children={element.props.children} childNode={childNode} dragCursor={dragCursor} dragType={dragType} childEdge={childEdge} draggable={elementDraggable} linkable={elementLinkable} selectable={elementSelectable} removable={elementRemovable} onDragStart={onDragStart} onDrag={onDrag} onDragEnd={onDragEnd} onClick={onClick} onEnter={onEnter} onLeave={onLeave} onKeyDown={onKeyDown} onRemove={onRemove} {...n} />;
372+
})}
373+
</g>
374+
</svg>
375+
</Tooltip>
376+
</foreignObject>
370377
</motion.g>
371378
);
372379
};

stories/Basic.stories.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { Popover } from 'reablocks';
12
import React, { useEffect, useRef, useState } from 'react';
23
import { Canvas, CanvasRef } from '../src/Canvas';
34
import { createEdgeFromNodes, detectCircular, hasLink } from '../src/helpers';
4-
import { Node, Edge, MarkerArrow, Port, Icon, Label, Remove, Add, NodeProps, EdgeProps, Arrow } from '../src/symbols';
5+
import '../src/index.css';
6+
import { Add, Arrow, Edge, EdgeProps, Icon, Label, MarkerArrow, Node, NodeProps, Port, Remove } from '../src/symbols';
57
import { CanvasPosition, EdgeData, NodeData } from '../src/types';
8+
import { popoverTheme } from '../test/PopoverTheme';
69

710
export default {
811
title: 'Demos/Basic',
@@ -166,7 +169,23 @@ export const CustomElements = () => (
166169
{...node}
167170
onClick={() => console.log(node.properties.data)}
168171
style={{ fill: node.properties.data?.gender === 'male' ? 'blue' : 'red' }}
172+
tooltip={(props) => (
173+
<Popover
174+
theme={popoverTheme}
175+
trigger={'hover'}
176+
closeOnClick={true}
177+
content={
178+
<div>
179+
<h1>This is {node.properties.text}!</h1>
180+
<p>you can also use Popover from other libraries such as Antd</p>
181+
</div>
182+
}
183+
>
184+
{props.children}
185+
</Popover>
186+
)}
169187
/>
188+
170189
)}
171190
edge={(edge: EdgeProps) => (
172191
<Edge

tailwind.config.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/** @type {import('tailwindcss').Config} */
2+
/* eslint-disable max-len */
3+
import plugin from 'tailwindcss/plugin';
4+
import { colorPalette } from 'reablocks';
5+
6+
module.exports = {
7+
content: [
8+
'./index.html',
9+
'./src/**/*.{js,ts,jsx,tsx}',
10+
'./node_modules/reablocks/**/*.{js,jsx,ts,tsx}',
11+
],
12+
theme: {
13+
extend: {
14+
colors: {
15+
primary: {
16+
DEFAULT: colorPalette.blue[500],
17+
active: colorPalette.blue[500],
18+
hover: colorPalette.blue[600],
19+
inactive: colorPalette.blue[200]
20+
},
21+
secondary: {
22+
DEFAULT: colorPalette.gray[700],
23+
active: colorPalette.gray[700],
24+
hover: colorPalette.gray[800],
25+
inactive: colorPalette.gray[400]
26+
},
27+
success: {
28+
DEFAULT: colorPalette.green[500],
29+
active: colorPalette.green[500],
30+
hover: colorPalette.green[600]
31+
},
32+
error: {
33+
DEFAULT: colorPalette.red[500],
34+
active: colorPalette.red[500],
35+
hover: colorPalette.red[600]
36+
},
37+
warning: {
38+
DEFAULT: colorPalette.orange[500],
39+
active: colorPalette.orange[500],
40+
hover: colorPalette.orange[600]
41+
},
42+
info: {
43+
DEFAULT: colorPalette.blue[500],
44+
active: colorPalette.blue[500],
45+
hover: colorPalette.blue[600]
46+
},
47+
background: {
48+
level1: colorPalette.white,
49+
level2: colorPalette.gray[950],
50+
level3: colorPalette.gray[900],
51+
level4: colorPalette.gray[800],
52+
},
53+
panel: {
54+
DEFAULT: colorPalette['black-pearl'],
55+
accent: colorPalette['charade']
56+
},
57+
surface: {
58+
DEFAULT: colorPalette['charade'],
59+
accent: colorPalette.blue[500]
60+
},
61+
typography: {
62+
DEFAULT: colorPalette['athens-gray'],
63+
},
64+
accent: {
65+
DEFAULT: colorPalette['waterloo'],
66+
active: colorPalette['anakiwa']
67+
},
68+
}
69+
}
70+
},
71+
plugins: [
72+
plugin(({ addVariant }) => {
73+
addVariant('disabled-within', '&:has(input:is(:disabled),button:is(:disabled))');
74+
})
75+
],
76+
};

test/Popover.module.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.base {
2+
white-space: nowrap;
3+
text-align: center;
4+
will-change: transform, opacity;
5+
background-color: rgb(196, 196, 196);
6+
color: black;
7+
border: 1px solid rgb(116, 116, 116);
8+
border-radius: 0.25rem;
9+
padding: 0.5rem;
10+
pointer-events: none;
11+
12+
.disablePointer {
13+
cursor: not-allowed;
14+
}
15+
}

test/PopoverTheme.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { PopoverTheme } from 'reablocks';
2+
3+
import css from './Popover.module.css';
4+
5+
export const popoverTheme: PopoverTheme = {
6+
base: css.base,
7+
disablePadding: css.disablePadding,
8+
};

test/typings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module '*.module.css';

0 commit comments

Comments
 (0)