Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(drag): Add restrictToPath as a Drag parameter #1379

Merged
merged 3 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/visx-drag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@types/react": "*",
"@visx/event": "2.1.2",
"@visx/point": "^2.1.0",
"prop-types": "^15.5.10"
}
}
2 changes: 2 additions & 0 deletions packages/visx-drag/src/Drag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function Drag({
y,
isDragging,
restrict,
restrictToPath,
}: DragProps) {
const drag = useDrag({
resetOnStart,
Expand All @@ -46,6 +47,7 @@ export default function Drag({
dy,
isDragging,
restrict,
restrictToPath,
});

return (
Expand Down
90 changes: 46 additions & 44 deletions packages/visx-drag/src/useDrag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Point, subtractPoints, sumPoints } from '@visx/point';
import { localPoint } from '@visx/event';
import useStateWithCallback from './util/useStateWithCallback';
import clampNumber from './util/clampNumber';
import restrictPoint from './util/restrictPoint';
import useSamplesAlongPath from './util/useSamplesAlongPath';

export type MouseTouchOrPointerEvent = React.MouseEvent | React.TouchEvent | React.PointerEvent;

Expand Down Expand Up @@ -38,6 +40,8 @@ export type UseDragOptions = {
yMin?: number;
yMax?: number;
};
/** Limit drag to an SVG path. Overrides `restrict` constraints */
valtism marked this conversation as resolved.
Show resolved Hide resolved
restrictToPath?: SVGGeometryElement | null;
};

export type DragState = {
Expand Down Expand Up @@ -75,20 +79,11 @@ export default function useDrag({
dy,
isDragging,
restrict = {},
restrictToPath,
}: UseDragOptions | undefined = {}): UseDrag {
// use ref to detect prop changes
const positionPropsRef = useRef({ x, y, dx, dy });

const { xMin, xMax, yMin, yMax } = restrict;
const clampX = useCallback(
(num: number) => clampNumber(num, xMin ?? -Infinity, xMax ?? Infinity),
[xMax, xMin],
);
const clampY = useCallback(
(num: number) => clampNumber(num, yMin ?? -Infinity, yMax ?? Infinity),
[yMax, yMin],
);

const [dragState, setDragStateWithCallback] = useStateWithCallback<DragState>({
x,
y,
Expand All @@ -98,10 +93,9 @@ export default function useDrag({
});

// Track distance between pointer on dragStart and position of element being dragged
const [dragStartPointerOffset, setDragStartPointerOffset] = useState<{
x: number;
y: number;
}>({ x: 0, y: 0 });
const [dragStartPointerOffset, setDragStartPointerOffset] = useState<Point>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for making Point more consistent (rather than mixing { x, y } objects)

new Point({ x: 0, y: 0 }),
);

// if prop position changes, update state
useEffect(() => {
Expand All @@ -128,30 +122,32 @@ export default function useDrag({
}
}, [dragState.isDragging, isDragging, setDragStateWithCallback]);

const restrictToPathSamples = useSamplesAlongPath(restrictToPath);

const handleDragStart = useCallback(
(event: MouseTouchOrPointerEvent) => {
event.persist();

setDragStateWithCallback(
(currState) => {
const currentPoint = {
x: (currState.x || 0) + currState.dx,
y: (currState.y || 0) + currState.dy,
};
const eventPoint = localPoint(event) || { x: 0, y: 0 };
// eslint-disable-next-line no-shadow
const { x = 0, y = 0, dx, dy } = currState;
const currentPoint = new Point({
x: (x || 0) + dx,
y: (y || 0) + dy,
});
const eventPoint = localPoint(event) || new Point({ x: 0, y: 0 });
const point = snapToPointer ? eventPoint : currentPoint;
const dragPoint = restrictPoint(point, restrictToPathSamples, restrict);

setDragStartPointerOffset({
x: currentPoint.x - eventPoint.x,
y: currentPoint.y - eventPoint.y,
});
setDragStartPointerOffset(subtractPoints(currentPoint, eventPoint));

return {
isDragging: true,
dx: resetOnStart ? 0 : currState.dx,
dy: resetOnStart ? 0 : currState.dy,
x: resetOnStart ? clampX(point.x) : clampX(point.x) - currState.dx,
y: resetOnStart ? clampY(point.y) : clampY(point.y) - currState.dy,
x: resetOnStart ? dragPoint.x : dragPoint.x - currState.dx,
y: resetOnStart ? dragPoint.y : dragPoint.y - currState.dy,
};
},
onDragStart &&
Expand All @@ -160,7 +156,14 @@ export default function useDrag({
}),
);
},
[clampX, clampY, onDragStart, resetOnStart, setDragStateWithCallback, snapToPointer],
[
onDragStart,
resetOnStart,
restrict,
restrictToPathSamples,
setDragStateWithCallback,
snapToPointer,
],
);

const handleDragMove = useCallback(
Expand All @@ -169,19 +172,19 @@ export default function useDrag({

setDragStateWithCallback(
(currState) => {
const point = localPoint(event) || { x: 0, y: 0 };
return currState.isDragging
? {
...currState,
isDragging: true,
dx: snapToPointer
? clampX(point.x) - (currState.x || 0)
: clampX(point.x + dragStartPointerOffset.x) - (currState.x || 0),
dy: snapToPointer
? clampY(point.y) - (currState.y || 0)
: clampY(point.y + dragStartPointerOffset.y) - (currState.y || 0),
}
: currState;
if (!currState.isDragging) return currState;
// eslint-disable-next-line no-shadow
const { x = 0, y = 0 } = currState;
const pointerPoint = localPoint(event) || new Point({ x: 0, y: 0 });
const point = snapToPointer
? pointerPoint
: sumPoints(pointerPoint, dragStartPointerOffset);
const dragPoint = restrictPoint(point, restrictToPathSamples, restrict);
return {
...currState,
dx: dragPoint.x - x,
dy: dragPoint.y - y,
};
},
onDragMove &&
((currState) => {
Expand All @@ -193,10 +196,9 @@ export default function useDrag({
setDragStateWithCallback,
onDragMove,
snapToPointer,
dragStartPointerOffset.x,
dragStartPointerOffset.y,
clampX,
clampY,
dragStartPointerOffset,
restrictToPathSamples,
restrict,
],
);

Expand Down
13 changes: 13 additions & 0 deletions packages/visx-drag/src/util/getClosestPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Gets closest point from list of points */
export default function getClosestPoint(point: { x: number; y: number }, samples: DOMPoint[]) {
let closestPoint = point;
let minDistance = Infinity;
for (const sample of samples) {
const distance = Math.sqrt((sample.x - point.x) ** 2 + (sample.y - point.y) ** 2);
if (distance < minDistance) {
minDistance = distance;
closestPoint = { x: sample.x, y: sample.y };
}
}
return closestPoint;
}
18 changes: 18 additions & 0 deletions packages/visx-drag/src/util/restrictPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { UseDragOptions } from '../useDrag';
import clampNumber from './clampNumber';
import getClosestPoint from './getClosestPoint';

/** Restrict a point to an area, or samples along a path. */
export default function restrictPoint(
point: { x: number; y: number },
samples: DOMPoint[],
restrict: UseDragOptions['restrict'] = {},
) {
if (samples.length > 0) {
return getClosestPoint(point, samples);
}
return {
x: clampNumber(point.x, restrict.xMin ?? -Infinity, restrict.xMax ?? Infinity),
y: clampNumber(point.y, restrict.yMin ?? -Infinity, restrict.yMax ?? Infinity),
};
}
26 changes: 26 additions & 0 deletions packages/visx-drag/src/util/useSamplesAlongPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMemo } from 'react';

function getSamples(restrictToPath: SVGGeometryElement, transform?: DOMMatrix, precision = 1) {
if (!restrictToPath) return [];
const samples = [];
const pathLength = restrictToPath.getTotalLength();
for (let sampleLength = 0; sampleLength <= pathLength; sampleLength += precision) {
const sample = restrictToPath.getPointAtLength(sampleLength);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, looks like the returned DOMPoint is not supported in IE, tho .getPointAtLength supposedly is for SVGPathElements (not SVGGeometryElement generally) and I'm pretty sure we use it elsewhere. also IE end of life is in June 2022 so maybe no need to worry about it 🎉

const transformedSample = sample.matrixTransform(transform);
samples.push(transformedSample);
}
return samples;
}

/** Return samples along a path, relative to the parent SVG */
export default function useSamplesAlongPath(restrictToPath?: SVGGeometryElement | null) {
const samples = useMemo(() => {
if (!restrictToPath) return [];
const transform = restrictToPath.getCTM() || new DOMMatrix();
return getSamples(restrictToPath, transform);
// The path can transform without triggering a re-render,
// so we need to update the samples whenever the length changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [restrictToPath?.getTotalLength()]);
return samples;
}
2 changes: 2 additions & 0 deletions packages/visx-point/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default as Point } from './Point';
export { default as sumPoints } from './sumPoints';
export { default as subtractPoints } from './subtractPoints';
8 changes: 8 additions & 0 deletions packages/visx-point/src/subtractPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Point from './Point';

export default function subtractPoints(point1: Point, point2: Point) {
return new Point({
x: point1.x - point2.x,
y: point1.y - point2.y,
});
}
8 changes: 8 additions & 0 deletions packages/visx-point/src/sumPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Point from './Point';

export default function sumPoints(point1: Point, point2: Point) {
return new Point({
x: point1.x + point2.x,
y: point1.y + point2.y,
});
}