forked from airbnb/visx
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new(annotation): add new Annotation components (airbnb#907)
* internal(annotation): move LinePathAnnotation to deprecated dir * new(annotation): add AnnotationLabel * new(annotation): add AnnotationConnector * test(annotation): add AnnotationLabel, AnnotationConnector boilerplate tests * new(annotation): add better dir structure, add working Annotation + EditableAnnotation * new(annotation): add drag handlers to EditableAnnotation, refactor to useDrag * fix(annotation): fix EditableAnnotation callbacks * new(annotation): add CircleSubject, LineSubject, grab cursor to EditableAnnotation * internal(annotation/EditableAnnotation): programmatically update drag state * test(annotation): add tests for each component * docs(annotation): update readme * new(annotation): polish api * types(annotation): remove undefined in HandlerArgs * new(annotation/Label): add resizeObserverPolyfill prop
- Loading branch information
1 parent
033a32c
commit 5387453
Showing
19 changed files
with
662 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import React from 'react'; | ||
import AnnotationContext from '../context/AnnotationContext'; | ||
import { AnnotationContextType } from '../types'; | ||
|
||
export type AnnotationProps = Pick<AnnotationContextType, 'x' | 'y' | 'dx' | 'dy'> & { | ||
/** Annotation children (Subject, Label, Connector) */ | ||
children: React.ReactNode; | ||
}; | ||
|
||
export default function Annotation({ x, y, dx, dy, children }: AnnotationProps) { | ||
return ( | ||
<AnnotationContext.Provider value={{ x, y, dx, dy }}>{children}</AnnotationContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React, { useContext } from 'react'; | ||
import cx from 'classnames'; | ||
import { AnnotationContextType } from '../types'; | ||
import AnnotationContext from '../context/AnnotationContext'; | ||
|
||
export type CircleSubjectProps = Pick<AnnotationContextType, 'x' | 'y'> & { | ||
/** Optional className to apply to CircleSubject in addition to 'visx-annotation-subject'. */ | ||
className?: string; | ||
/** Color of CircleSubject. */ | ||
stroke?: string; | ||
/** Radius of CircleSubject. */ | ||
radius?: number; | ||
}; | ||
|
||
export default function CircleSubject({ | ||
className, | ||
x: propsX, | ||
y: propsY, | ||
stroke = '#222', | ||
radius = 24, | ||
...restProps | ||
}: CircleSubjectProps & Omit<React.SVGProps<SVGCircleElement>, keyof CircleSubjectProps>) { | ||
// if props are provided, they take precedence over context | ||
const annotationContext = useContext(AnnotationContext); | ||
|
||
return ( | ||
<circle | ||
className={cx('visx-annotation-subject', 'visx-annotation-subject-circle', className)} | ||
cx={propsX || annotationContext.x} | ||
cy={propsY || annotationContext.y} | ||
r={radius} | ||
fill="transparent" | ||
pointerEvents="none" | ||
stroke={stroke} | ||
{...restProps} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { useContext } from 'react'; | ||
import cx from 'classnames'; | ||
import { AnnotationContextType } from '../types'; | ||
import AnnotationContext from '../context/AnnotationContext'; | ||
|
||
// @TODO | ||
// add end marker support | ||
|
||
export type AnnotationConnectorProps = Pick<AnnotationContextType, 'x' | 'y' | 'dx' | 'dy'> & { | ||
/** Optional className to apply to container in addition to 'visx-annotation-connector'. */ | ||
className?: string; | ||
/** Connector type. */ | ||
type?: 'line' | 'elbow'; | ||
/** Color of the connector line. */ | ||
stroke?: string; | ||
/** Optional additional props. */ | ||
pathProps?: React.SVGProps<SVGPathElement>; | ||
}; | ||
|
||
export default function AnnotationConnector({ | ||
className, | ||
x: propsX, | ||
y: propsY, | ||
dx: propsDx, | ||
dy: propsDy, | ||
type = 'elbow', | ||
stroke = '#222', | ||
pathProps, | ||
}: AnnotationConnectorProps) { | ||
// if props are provided, they take precedence over context | ||
const annotationContext = useContext(AnnotationContext); | ||
const x0 = propsX == null ? annotationContext.x ?? 0 : propsX; | ||
const y0 = propsY == null ? annotationContext.y ?? 0 : propsY; | ||
const dx = propsDx == null ? annotationContext.dx ?? 0 : propsDx; | ||
const dy = propsDy == null ? annotationContext.dy ?? 0 : propsDy; | ||
let x1: number = x0; // only used with elbow type | ||
let y1: number = y0; | ||
const x2 = x0 + dx; | ||
const y2 = y0 + dy; | ||
|
||
if (type === 'elbow') { | ||
// if dx < dy, find the intesection of y=x or y=-x from subject, with vertical line to label | ||
if (Math.abs(dx) <= Math.abs(dy)) { | ||
// target line is vertical x = x2 | ||
x1 = x2; | ||
// intersection with y=x line (if dy > 0) or y=x (if dy < 0) | ||
const sign = dy > 0 ? 1 : -1; | ||
y1 = y0 + sign * Math.abs(x1 - x0); | ||
} | ||
// if dx > dy, find the intesection of y=x or y=-x from subject, with horizontal line to label | ||
else { | ||
// target line is horizontal y = y2 | ||
y1 = y2; | ||
// find intersection with y=-x line (if dx > 0) or y=x (if dx < 0) | ||
const sign = dx > 0 ? 1 : -1; | ||
x1 = x0 + sign * Math.abs(y1 - y0); | ||
} | ||
} | ||
|
||
return ( | ||
<path | ||
className={cx('visx-annotation-connector', className)} | ||
strokeWidth={1} | ||
stroke={stroke} | ||
fill="transparent" | ||
pointerEvents="none" | ||
d={`M${x0},${y0}${type === 'elbow' ? `L${x1},${y1}` : ''}L${x2},${y2}`} | ||
{...pathProps} | ||
/> | ||
); | ||
} |
184 changes: 184 additions & 0 deletions
184
packages/visx-annotation/src/components/EditableAnnotation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
/* eslint-disable react/jsx-handler-names */ | ||
import React, { useCallback, useRef } from 'react'; | ||
import useDrag, { UseDrag, HandlerArgs as DragHandlerArgs } from '@visx/drag/lib/useDrag'; | ||
import { AnnotationContextType } from '../types'; | ||
import Annotation from './Annotation'; | ||
|
||
export type EditableAnnotationProps = Pick<AnnotationContextType, 'x' | 'y' | 'dx' | 'dy'> & { | ||
/** Width of the possible drag canvas (e.g., SVG container). */ | ||
width: number; | ||
/** Height of the possible drag canvas (e.g., SVG container). */ | ||
height: number; | ||
/** Annotation children (Subject, Label, Connector) */ | ||
children: React.ReactNode; | ||
/** Optional circle props to set on the subject drag handle. */ | ||
subjectDragHandleProps?: React.SVGProps<SVGCircleElement>; | ||
/** Optional circle props to set on the label drag handle. */ | ||
labelDragHandleProps?: React.SVGProps<SVGCircleElement>; | ||
/** Callback invoked on drag start. */ | ||
onDragStart?: ({ x, y, dx, dy, event }: HandlerArgs) => void; | ||
/** Callback invoked on drag move. */ | ||
onDragMove?: ({ x, y, dx, dy, event }: HandlerArgs) => void; | ||
/** Callback invoked on drag end. */ | ||
onDragEnd?: ({ x, y, dx, dy, event }: HandlerArgs) => void; | ||
}; | ||
|
||
export type HandlerArgs = { | ||
x: number; | ||
y: number; | ||
dx: number; | ||
dy: number; | ||
event: React.MouseEvent | React.TouchEvent; | ||
}; | ||
|
||
const defaultDragHandleProps = { | ||
r: 10, | ||
fill: 'transparent', | ||
stroke: '#777', | ||
strokeDasharray: '4,2', | ||
strokeWidth: 2, | ||
}; | ||
|
||
export default function EditableAnnotation({ | ||
x: subjectX = 0, | ||
y: subjectY = 0, | ||
dx: labelDx = 0, | ||
dy: labelDy = 0, | ||
children, | ||
width, | ||
height, | ||
subjectDragHandleProps, | ||
labelDragHandleProps, | ||
onDragStart, | ||
onDragMove, | ||
onDragEnd, | ||
}: EditableAnnotationProps) { | ||
// chicken before the egg, we need these to reference drag state | ||
// in drag callbacks which are defined before useDrag() state is available | ||
const subjectDragRef = useRef<UseDrag>(); | ||
const labelDragRef = useRef<UseDrag>(); | ||
|
||
const handleDragStart = useCallback( | ||
({ event }: DragHandlerArgs) => { | ||
if (onDragStart) { | ||
onDragStart({ | ||
event, | ||
x: subjectX + (subjectDragRef.current?.dx ?? 0), | ||
y: subjectY + (subjectDragRef.current?.dy ?? 0), | ||
dx: labelDx + (labelDragRef.current?.dx ?? 0), | ||
dy: labelDy + (labelDragRef.current?.dy ?? 0), | ||
}); | ||
} | ||
}, | ||
[labelDx, labelDy, onDragStart, subjectX, subjectY], | ||
); | ||
|
||
const handleDragMove = useCallback( | ||
({ event }: DragHandlerArgs) => { | ||
if (onDragMove) { | ||
onDragMove({ | ||
event, | ||
x: subjectX + (subjectDragRef.current?.dx ?? 0), | ||
y: subjectY + (subjectDragRef.current?.dy ?? 0), | ||
dx: labelDx + (labelDragRef.current?.dx ?? 0), | ||
dy: labelDy + (labelDragRef.current?.dy ?? 0), | ||
}); | ||
} | ||
}, | ||
[labelDx, labelDy, onDragMove, subjectX, subjectY], | ||
); | ||
|
||
const handleDragEnd = useCallback( | ||
({ event }: DragHandlerArgs) => { | ||
if (onDragEnd) { | ||
onDragEnd({ | ||
event, | ||
x: subjectX + (subjectDragRef.current?.dx ?? 0), | ||
y: subjectY + (subjectDragRef.current?.dy ?? 0), | ||
dx: labelDx + (labelDragRef.current?.dx ?? 0), | ||
dy: labelDy + (labelDragRef.current?.dy ?? 0), | ||
}); | ||
} | ||
}, | ||
[labelDx, labelDy, onDragEnd, subjectX, subjectY], | ||
); | ||
|
||
const subjectDrag = useDrag({ | ||
onDragStart: handleDragStart, | ||
onDragMove: handleDragMove, | ||
onDragEnd: handleDragEnd, | ||
x: subjectX, | ||
y: subjectY, | ||
}); | ||
|
||
const labelDrag = useDrag({ | ||
onDragStart: handleDragStart, | ||
onDragMove: handleDragMove, | ||
onDragEnd: handleDragEnd, | ||
x: labelDx, | ||
y: labelDy, | ||
}); | ||
|
||
// enable referencing these in the callbacks defined before useDrag is called | ||
subjectDragRef.current = subjectDrag; | ||
labelDragRef.current = labelDrag; | ||
|
||
return ( | ||
<> | ||
<Annotation | ||
x={subjectX + subjectDrag.dx} | ||
y={subjectY + subjectDrag.dy} | ||
dx={labelDx + labelDrag.dx} | ||
dy={labelDy + labelDrag.dy} | ||
> | ||
{children} | ||
</Annotation> | ||
{subjectDrag.isDragging && ( | ||
<rect | ||
width={width} | ||
height={height} | ||
onMouseMove={subjectDrag.dragMove} | ||
onMouseUp={subjectDrag.dragEnd} | ||
fill="transparent" | ||
/> | ||
)} | ||
<circle | ||
cx={subjectX} | ||
cy={subjectY} | ||
transform={`translate(${subjectDrag.dx},${subjectDrag.dy})`} | ||
onMouseMove={subjectDrag.dragMove} | ||
onMouseUp={subjectDrag.dragEnd} | ||
onMouseDown={subjectDrag.dragStart} | ||
onTouchStart={subjectDrag.dragStart} | ||
onTouchMove={subjectDrag.dragMove} | ||
onTouchEnd={subjectDrag.dragEnd} | ||
cursor={subjectDrag.isDragging ? 'grabbing' : 'grab'} | ||
{...defaultDragHandleProps} | ||
{...subjectDragHandleProps} | ||
/> | ||
{labelDrag.isDragging && ( | ||
<rect | ||
width={width} | ||
height={height} | ||
onMouseMove={labelDrag.dragMove} | ||
onMouseUp={labelDrag.dragEnd} | ||
fill="transparent" | ||
/> | ||
)} | ||
<circle | ||
cx={subjectX + subjectDrag.dx + labelDx} | ||
cy={subjectY + subjectDrag.dy + labelDy} | ||
transform={`translate(${labelDrag.dx},${labelDrag.dy})`} | ||
onMouseMove={labelDrag.dragMove} | ||
onMouseUp={labelDrag.dragEnd} | ||
onMouseDown={labelDrag.dragStart} | ||
onTouchStart={labelDrag.dragStart} | ||
onTouchMove={labelDrag.dragMove} | ||
onTouchEnd={labelDrag.dragEnd} | ||
cursor={labelDrag.isDragging ? 'grabbing' : 'grab'} | ||
{...defaultDragHandleProps} | ||
{...labelDragHandleProps} | ||
/> | ||
</> | ||
); | ||
} |
Oops, something went wrong.