Skip to content

Commit

Permalink
Added support for customizable tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
determinationlove committed Feb 10, 2025
1 parent 8405b42 commit 517cb9a
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 83 deletions.
10 changes: 10 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React from 'react';
import { ThemeProvider, theme as reablocksTheme } from 'reablocks';
import theme from './theme';

export const decorators = [
Story => (
<ThemeProvider theme={reablocksTheme}>
<Story />
</ThemeProvider>
)
]

export const parameters = {
layout: 'centered',
docs: {
Expand Down
3 changes: 3 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
165 changes: 86 additions & 79 deletions src/symbols/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export interface NodeProps<T = any> extends NodeDragEvents<NodeData<T>, PortData
icon: ReactElement<IconProps, typeof Icon>;
label: ReactElement<LabelProps, typeof Label>;
port: ReactElement<PortProps, typeof Port>;
tooltip?: React.ElementType;
}

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 }) => {
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 }) => {
const nodeRef = useRef<SVGRectElement | null>(null);
const controls = useAnimation();
const { canLinkNode, enteredNode, selections, readonly, ...canvas } = useCanvas();
Expand Down Expand Up @@ -287,86 +288,92 @@ export const Node: FC<Partial<NodeProps>> = ({ id, x, y, ports, labels, height,
}}
animate={controls}
>
<motion.rect
{...bind()}
ref={nodeRef}
tabIndex={-1}
onKeyDown={onKeyDownCallback}
onClick={onClickCallback}
onTouchStart={onTouchStartCallback}
onMouseEnter={onMouseEnterCallback}
onMouseLeave={onMouseLeaveCallback}
className={classNames(css.rect, className, properties?.className, {
[css.active]: isActive,
[css.disabled]: isDisabled,
[css.unlinkable]: isLinkable === false && !isNodeDrag,
[css.dragging]: dragging,
[css.children]: nodes?.length > 0,
[css.deleteHovered]: deleteHovered,
[css.selectionDisabled]: !canSelect
})}
style={style}
height={height}
width={width}
rx={rx}
ry={ry}
initial={{
opacity: 0
}}
animate={{
opacity: 1,
transition: !animated ? { type: false, duration: 0 } : {}
}}
/>
{children && <Fragment>{typeof children === 'function' ? (children as NodeChildrenAsFunction)(nodeChildProps) : children}</Fragment>}
{icon && properties.icon && <CloneElement<IconProps> element={icon} {...properties.icon} />}
{label && labels?.length > 0 && labels.map((l, index) => <CloneElement<LabelProps> element={label} key={index} {...(l as LabelProps)} />)}
{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}`} />)}
{!isDisabled && isActive && !readonly && remove && removable && (
<CloneElement<RemoveProps>
element={remove}
y={height / 2}
x={width}
onClick={(event: React.MouseEvent<SVGGElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
onRemove?.(event, properties);
setDeleteHovered(false);
}}
onEnter={() => setDeleteHovered(true)}
onLeave={() => setDeleteHovered(false)}
/>
)}
<g>
{edges?.length > 0 &&
edges.map((e: any) => {
const element = typeof childEdge === 'function' ? childEdge(e) : childEdge;
return (
<CloneElement<EdgeProps>
key={e.id}
element={element}
id={`${id}-edge-${e.id}`}
disabled={isDisabled}
{...e}
properties={{
...e.properties,
...(e.data ? { data: e.data } : {})
<foreignObject width={width} height={height} style={{ pointerEvents: 'none' }}>
<Tooltip>
<svg width={width} height={height}>
<motion.rect
{...bind()}
ref={nodeRef}
tabIndex={-1}
onKeyDown={onKeyDownCallback}
onClick={onClickCallback}
onTouchStart={onTouchStartCallback}
onMouseEnter={onMouseEnterCallback}
onMouseLeave={onMouseLeaveCallback}
className={classNames(css.rect, className, properties?.className, {
[css.active]: isActive,
[css.disabled]: isDisabled,
[css.unlinkable]: isLinkable === false && !isNodeDrag,
[css.dragging]: dragging,
[css.children]: nodes?.length > 0,
[css.deleteHovered]: deleteHovered,
[css.selectionDisabled]: !canSelect
})}
style={{ ...style, pointerEvents: 'auto' }}
height={height}
width={width}
rx={rx}
ry={ry}
initial={{
opacity: 1
}}
animate={{
opacity: 1,
transition: !animated ? { type: false, duration: 0 } : {}
}}
/>
{children && <Fragment>{typeof children === 'function' ? (children as NodeChildrenAsFunction)(nodeChildProps) : children}</Fragment>}
{icon && properties.icon && <CloneElement<IconProps> element={icon} {...properties.icon} />}
{label && labels?.length > 0 && labels.map((l, index) => <CloneElement<LabelProps> element={label} key={index} {...(l as LabelProps)} />)}
{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}`} />)}
{!isDisabled && isActive && !readonly && remove && removable && (
<CloneElement<RemoveProps>
element={remove}
y={height / 2}
x={width}
onClick={(event: React.MouseEvent<SVGGElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
onRemove?.(event, properties);
setDeleteHovered(false);
}}
onEnter={() => setDeleteHovered(true)}
onLeave={() => setDeleteHovered(false)}
/>
);
})}
{nodes?.length > 0 &&
nodes.map(({ children, ...n }: any) => {
const element = typeof childNode === 'function' ? childNode(n) : childNode;
const elementDisabled = element.props?.disabled != null ? element.props.disabled : disabled;
const elementAnimated = element.props?.animated != null ? element.props.animated : animated;
const elementDraggable = element.props?.draggable != null ? element.props.draggable : draggable;
const elementLinkable = element.props?.linkable != null ? element.props.linkable : linkable;
const elementSelectable = element.props?.selectable != null ? element.props.selectable : selectable;
const elementRemovable = element.props?.removable != null ? element.props.removable : removable;
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} />;
})}
</g>
)}
<g>
{edges?.length > 0 &&
edges.map((e: any) => {
const element = typeof childEdge === 'function' ? childEdge(e) : childEdge;
return (
<CloneElement<EdgeProps>
key={e.id}
element={element}
id={`${id}-edge-${e.id}`}
disabled={isDisabled}
{...e}
properties={{
...e.properties,
...(e.data ? { data: e.data } : {})
}}
/>
);
})}
{nodes?.length > 0 &&
nodes.map(({ children, ...n }: any) => {
const element = typeof childNode === 'function' ? childNode(n) : childNode;
const elementDisabled = element.props?.disabled != null ? element.props.disabled : disabled;
const elementAnimated = element.props?.animated != null ? element.props.animated : animated;
const elementDraggable = element.props?.draggable != null ? element.props.draggable : draggable;
const elementLinkable = element.props?.linkable != null ? element.props.linkable : linkable;
const elementSelectable = element.props?.selectable != null ? element.props.selectable : selectable;
const elementRemovable = element.props?.removable != null ? element.props.removable : removable;
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} />;
})}
</g>
</svg>
</Tooltip>
</foreignObject>
</motion.g>
);
};
21 changes: 20 additions & 1 deletion stories/Basic.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Popover } from 'reablocks';
import React, { useEffect, useRef, useState } from 'react';
import { Canvas, CanvasRef } from '../src/Canvas';
import { createEdgeFromNodes, detectCircular, hasLink } from '../src/helpers';
import { Node, Edge, MarkerArrow, Port, Icon, Label, Remove, Add, NodeProps, EdgeProps, Arrow } from '../src/symbols';
import '../src/index.css';
import { Add, Arrow, Edge, EdgeProps, Icon, Label, MarkerArrow, Node, NodeProps, Port, Remove } from '../src/symbols';
import { CanvasPosition, EdgeData, NodeData } from '../src/types';
import { popoverTheme } from '../test/PopoverTheme';

export default {
title: 'Demos/Basic',
Expand Down Expand Up @@ -166,7 +169,23 @@ export const CustomElements = () => (
{...node}
onClick={() => console.log(node.properties.data)}
style={{ fill: node.properties.data?.gender === 'male' ? 'blue' : 'red' }}
tooltip={(props) => (
<Popover
theme={popoverTheme}
trigger={'hover'}
closeOnClick={true}
content={
<div>
<h1>This is {node.properties.text}!</h1>
<p>you can also use Popover from other libraries such as Antd</p>
</div>
}
>
{props.children}
</Popover>
)}
/>

)}
edge={(edge: EdgeProps) => (
<Edge
Expand Down
29 changes: 26 additions & 3 deletions stories/Drag.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Popover } from 'reablocks';
import React, { useState } from 'react';
import { Canvas } from '../src/Canvas';
import { Node, Edge, MarkerArrow, Port, Icon, Arrow, Label, Remove, Add, NodeProps } from '../src/symbols';
import { EdgeData, NodeData } from '../src/types';
import { createEdgeFromNodes, hasLink, removeAndUpsertNodes } from '../src/helpers';
import { Add, Arrow, Edge, Icon, Label, MarkerArrow, Node, NodeProps, Port, Remove } from '../src/symbols';
import { EdgeData, NodeData } from '../src/types';
import { popoverTheme } from '../test/PopoverTheme';

export default {
title: 'Demos/Drag',
Expand Down Expand Up @@ -194,7 +196,28 @@ export const NodeRearranging = () => {
<Canvas
nodes={nodes}
edges={edges}
node={<Node dragType="node" />}
node={
(node: NodeProps) => (
<Node
dragType="node"
tooltip={(props) => (
<Popover
theme={popoverTheme}
trigger={'hover'}
closeOnClick={true}
content={
<div>
<h1>This is {node.properties.text}!</h1>
<p>you can also use Popover from other libraries such as Antd</p>
</div>
}
>
{props.children}
</Popover>

)}
/>)
}
onNodeLinkCheck={(_event, from: NodeData, to: NodeData) => {
if (from.id === to.id) {
return false;
Expand Down
76 changes: 76 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
/* eslint-disable max-len */
import plugin from 'tailwindcss/plugin';
import { colorPalette } from 'reablocks';

module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/reablocks/**/*.{js,jsx,ts,tsx}',
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: colorPalette.blue[500],
active: colorPalette.blue[500],
hover: colorPalette.blue[600],
inactive: colorPalette.blue[200]
},
secondary: {
DEFAULT: colorPalette.gray[700],
active: colorPalette.gray[700],
hover: colorPalette.gray[800],
inactive: colorPalette.gray[400]
},
success: {
DEFAULT: colorPalette.green[500],
active: colorPalette.green[500],
hover: colorPalette.green[600]
},
error: {
DEFAULT: colorPalette.red[500],
active: colorPalette.red[500],
hover: colorPalette.red[600]
},
warning: {
DEFAULT: colorPalette.orange[500],
active: colorPalette.orange[500],
hover: colorPalette.orange[600]
},
info: {
DEFAULT: colorPalette.blue[500],
active: colorPalette.blue[500],
hover: colorPalette.blue[600]
},
background: {
level1: colorPalette.white,
level2: colorPalette.gray[950],
level3: colorPalette.gray[900],
level4: colorPalette.gray[800],
},
panel: {
DEFAULT: colorPalette['black-pearl'],
accent: colorPalette['charade']
},
surface: {
DEFAULT: colorPalette['charade'],
accent: colorPalette.blue[500]
},
typography: {
DEFAULT: colorPalette['athens-gray'],
},
accent: {
DEFAULT: colorPalette['waterloo'],
active: colorPalette['anakiwa']
},
}
}
},
plugins: [
plugin(({ addVariant }) => {
addVariant('disabled-within', '&:has(input:is(:disabled),button:is(:disabled))');
})
],
};
15 changes: 15 additions & 0 deletions test/Popover.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.base {
white-space: nowrap;
text-align: center;
will-change: transform, opacity;
background-color: rgb(196, 196, 196);
color: black;
border: 1px solid rgb(116, 116, 116);
border-radius: 0.25rem;
padding: 0.5rem;
pointer-events: none;

.disablePointer {
cursor: not-allowed;
}
}
8 changes: 8 additions & 0 deletions test/PopoverTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PopoverTheme } from 'reablocks';

import css from './Popover.module.css';

export const popoverTheme: PopoverTheme = {
base: css.base,
disablePadding: css.disablePadding,
};
1 change: 1 addition & 0 deletions test/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.module.css';

0 comments on commit 517cb9a

Please sign in to comment.