Skip to content

Commit

Permalink
Merge pull request #183 from pierpo/feature/clickable-arrows
Browse files Browse the repository at this point in the history
feature: add mouse events for arrows
  • Loading branch information
pierpo authored May 7, 2024
2 parents 1a6dd43 + f92e831 commit 544bb92
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ The `Relation` type has the following shape:
order?: number, // higher order means arrow will be drawn on top of the others
className?: string, // CSS class selectors on the SVG arrow
style: ArcherStyle,
domAttributes?: DOMAttributes<SVGElement>, // Allows to make selectable arrows by passing dom attributes like onMouseHover
cursor?: Property.Cursor, // Allows to customize the hovering cursor of the arrow. Will only work if domAttributes is present
hitSlop?: number, // Allows to make the selectable arrow thicker. Will only work if domAttributes is present
}
```

Expand Down
6 changes: 5 additions & 1 deletion example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SixthExample from './SixthExample';
import SeventhExample from './SeventhExample';
import EighthExample from './EighthExample';
import NinthExample from './NinthExample';
import TenthExample from './TenthExample';

const getExample = (id: number) => {
switch (id) {
Expand Down Expand Up @@ -38,6 +39,9 @@ const getExample = (id: number) => {
case 9:
return NinthExample;

case 10:
return TenthExample;

default:
return SecondExample;
}
Expand All @@ -54,7 +58,7 @@ const App = () => {
<div>
<h2>Example {exampleId}</h2>
<p>Choose an example:</p>
{[...Array(9).keys()].map((value) => (
{[...Array(10).keys()].map((value) => (
<button
key={value}
onClick={() => setExampleId(value + 1)}
Expand Down
113 changes: 113 additions & 0 deletions example/TenthExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import ArcherContainer from '../src/ArcherContainer/ArcherContainer';
import ArcherElement from '../src/ArcherElement/ArcherElement';
const rootStyle = {
display: 'flex',
justifyContent: 'center',
};
const rowStyle = {
margin: '200px 0',
display: 'flex',
justifyContent: 'space-between',
};
const boxStyle = {
padding: '10px',
border: '1px solid black',
};

const TenthExample = () => {
const [isHovering, setIsHovering] = useState(false);

return (
<div
style={{
height: '500px',
margin: '50px',
}}
>
<ArcherContainer strokeColor="red">
<div style={rootStyle}>
<ArcherElement
id="root"
relations={[
{
targetId: 'element2',
targetAnchor: 'top',
sourceAnchor: 'bottom',
style: {
strokeDasharray: '5,5',
strokeColor: isHovering ? 'green' : 'blue',
},
domAttributes: {
// The hovering style could be achieved with CSS as well
onMouseOver: () => {
setIsHovering(true);
},
onMouseOut: () => {
setIsHovering(false);
},
// The click however needs a props, obviously
onClick: () => {
console.log('you clicked me!');
},
},
hitSlop: 30,
cursor: 'grab',
},
]}
>
<div style={boxStyle}>Hover my arrow</div>
</ArcherElement>
</div>

<div style={rowStyle}>
<ArcherElement
id="element2"
relations={[
{
targetId: 'element3',
targetAnchor: 'left',
sourceAnchor: 'right',
style: {
strokeColor: 'blue',
strokeWidth: 1,
},
label: (
<div
style={{
marginTop: '-20px',
}}
>
Arrow 2
</div>
),
},
]}
>
<div style={boxStyle}>Element 2</div>
</ArcherElement>

<ArcherElement id="element3">
<div style={boxStyle}>Element 3</div>
</ArcherElement>

<ArcherElement
id="element4"
relations={[
{
targetId: 'root',
targetAnchor: 'right',
sourceAnchor: 'left',
label: 'Arrow 3',
},
]}
>
<div style={boxStyle}>Element 4</div>
</ArcherElement>
</div>
</ArcherContainer>
</div>
);
};

export default TenthExample;
12 changes: 12 additions & 0 deletions src/ArcherContainer/components/SvgArrows.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Property } from 'csstype';
import Vector2 from '../../geometry/Vector2';
import {
getPointCoordinatesFromAnchorPosition,
Expand All @@ -21,6 +22,8 @@ interface CommonProps {
offset: ArcherContainerProps['offset'];
uniqueId: string;
refs: Record<string, HTMLElement>;
hitSlop?: number;
cursor?: Property.Cursor;
}

const AdaptedArrow = (
Expand All @@ -35,6 +38,9 @@ const AdaptedArrow = (

const newEndShape = createShapeObj(style, props.endShape);

const domAttributes = props.domAttributes;
const cursor = props.cursor;
const hitSlop = props.hitSlop;
const newStrokeColor = style.strokeColor || props.strokeColor;
const newStrokeWidth = style.strokeWidth || props.strokeWidth;
const newStrokeDasharray = style.strokeDasharray || props.strokeDasharray;
Expand Down Expand Up @@ -86,6 +92,9 @@ const AdaptedArrow = (
enableStartMarker={!!newStartMarker}
disableEndMarker={!newEndMarker}
endShape={newEndShape}
domAttributes={domAttributes}
hitSlop={hitSlop}
cursor={cursor}
/>
);
};
Expand Down Expand Up @@ -116,6 +125,9 @@ export const SvgArrows = (
className={currentRelation.className}
label={currentRelation.label}
style={currentRelation.style || {}}
domAttributes={currentRelation.domAttributes}
hitSlop={currentRelation.hitSlop}
cursor={currentRelation.cursor}
startMarker={props.startMarker}
endMarker={props.endMarker}
endShape={props.endShape}
Expand Down
6 changes: 6 additions & 0 deletions src/ArcherElement/ArcherElement.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const generateSourceToTarget = (
className,
style,
order = 0,
domAttributes,
cursor,
hitSlop,
}: RelationType) => ({
source: {
id: encodeId(id),
Expand All @@ -38,6 +41,9 @@ export const generateSourceToTarget = (
label,
style,
order,
domAttributes,
cursor,
hitSlop,
}),
);
};
25 changes: 24 additions & 1 deletion src/SvgArrow/SvgArrow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import React, { DOMAttributes } from 'react';
import { Property } from 'csstype';
import Vector2 from '../geometry/Vector2';
import { AnchorPositionType, ValidLineStyles } from '../types';
import { computeArrowDirectionVector } from './SvgArrow.helper';
Expand All @@ -19,6 +20,9 @@ type Props = {
enableStartMarker?: boolean;
disableEndMarker?: boolean;
endShape: Record<string, any>;
domAttributes?: DOMAttributes<SVGElement>;
hitSlop?: number;
cursor?: Property.Cursor;
};

export function computeArrowPointAccordingToArrowHead(
Expand Down Expand Up @@ -198,6 +202,9 @@ const SvgArrow = ({
enableStartMarker,
disableEndMarker,
endShape,
domAttributes,
hitSlop = 10,
cursor = 'pointer',
}: Props) => {
const actualArrowLength = endShape.circle
? endShape.circle.radius * 2
Expand Down Expand Up @@ -280,6 +287,22 @@ const SvgArrow = ({
markerStart={enableStartMarker ? markerUrl : undefined}
markerEnd={disableEndMarker ? undefined : markerUrl}
/>

{/* This a thicker fake path to grab DOM events - makes clicking on the arrow more usable */}
{domAttributes && (
<path
d={pathString}
style={{
fill: 'none',
stroke: 'rgba(0, 0, 0, 0)',
strokeWidth: hitSlop,
cursor: domAttributes ? cursor : 'initial',
pointerEvents: 'all',
}}
{...domAttributes}
/>
)}

{arrowLabel && (
<foreignObject
x={xLabel}
Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { DOMAttributes } from 'react';
import { Property } from 'csstype';

export type ValidLineStyles = 'angle' | 'straight' | 'curve';

export type AnchorPositionType = 'top' | 'bottom' | 'left' | 'right' | 'middle';
Expand All @@ -10,6 +13,11 @@ export type RelationType = {
label?: React.ReactNode | null | undefined;
className?: string;
style?: LineType;
domAttributes?: DOMAttributes<SVGElement>;
/** Allows to make the mouse selectable arrow thicker */
hitSlop?: number;
/** Allows to customize the hovering cursor for a selectable arrow */
cursor?: Property.Cursor;
};

export type EntityRelationType = {
Expand All @@ -24,6 +32,9 @@ export type SourceToTargetType = {
order: number;
label?: React.ReactNode | null | undefined;
style?: LineType;
domAttributes?: DOMAttributes<SVGElement>;
hitSlop?: number;
cursor?: Property.Cursor;
};

export type ArrowShapeType = {
Expand Down

0 comments on commit 544bb92

Please sign in to comment.