Skip to content

Commit acdd069

Browse files
Merge pull request #984 from framer/feature/individual-drag-elastic
Adding dragElastic as an object
2 parents e59bf90 + 72c67af commit acdd069

File tree

8 files changed

+164
-41
lines changed

8 files changed

+164
-41
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Framer Motion adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [3.9.0] 2021-03-02
6+
7+
### Added
8+
9+
- `dragElastic` now accepts per-axis elastic settings.
10+
511
## [3.8.2] 2021-03-01
612

713
### Fixed

api/framer-motion.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ export class DragControls {
209209
updateConstraints(): void;
210210
}
211211

212+
// @public (undocumented)
213+
export type DragElastic = boolean | number | Partial<BoundingBox2D>;
214+
212215
// @public (undocumented)
213216
export const DragFeature: MotionFeature;
214217

@@ -218,7 +221,7 @@ export interface DraggableProps extends DragHandlers {
218221
dragConstraints?: false | Partial<BoundingBox2D> | RefObject<Element>;
219222
dragControls?: DragControls;
220223
dragDirectionLock?: boolean;
221-
dragElastic?: boolean | number;
224+
dragElastic?: DragElastic;
222225
dragListener?: boolean;
223226
dragMomentum?: boolean;
224227
dragPropagation?: boolean;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "framer-motion",
3-
"version": "3.8.2",
3+
"version": "3.9.0",
44
"description": "A simple and powerful React animation library",
55
"main": "dist/framer-motion.cjs.js",
66
"module": "dist/es/index.js",

src/gestures/drag/VisualElementDragControls.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RefObject } from "react"
22
import { invariant } from "hey-listen"
33
import { progress } from "popmotion"
44
import { PanSession, AnyPointerEvent, PanInfo } from "../../gestures/PanSession"
5-
import { DraggableProps, DragHandler } from "./types"
5+
import { DraggableProps, DragHandler, ResolvedConstraints } from "./types"
66
import { Lock, getGlobalLock } from "./utils/lock"
77
import { isRefObject } from "../../utils/is-ref-object"
88
import { addPointerEvent } from "../../events/use-pointer-event"
@@ -12,16 +12,18 @@ import { TransformPoint2D, AxisBox2D, Point2D } from "../../types/geometry"
1212
import {
1313
convertBoundingBoxToAxisBox,
1414
convertAxisBoxToBoundingBox,
15+
axisBox,
1516
} from "../../utils/geometry"
1617
import { eachAxis } from "../../utils/each-axis"
1718
import {
1819
calcRelativeConstraints,
1920
calcConstrainedMinPoint,
20-
ResolvedConstraints,
2121
calcViewportConstraints,
2222
calcPositionFromProgress,
2323
applyConstraints,
2424
rebaseAxisConstraints,
25+
resolveDragElastic,
26+
defaultElastic,
2527
} from "./utils/constraints"
2628
import { getBoundingBox } from "../../render/dom/projection/measure"
2729
import { calcOrigin } from "../../utils/geometry/delta-calc"
@@ -78,6 +80,13 @@ export class VisualElementDragControls {
7880
*/
7981
private constraints: ResolvedConstraints | false = false
8082

83+
/**
84+
* The per-axis resolved elastic values.
85+
*
86+
* @internal
87+
*/
88+
private elastic: AxisBox2D = axisBox()
89+
8190
/**
8291
* A reference to the host component's latest props.
8392
*
@@ -287,7 +296,7 @@ export class VisualElementDragControls {
287296
}
288297

289298
resolveDragConstraints() {
290-
const { dragConstraints } = this.props
299+
const { dragConstraints, dragElastic } = this.props
291300

292301
if (dragConstraints) {
293302
this.constraints = isRefObject(dragConstraints)
@@ -303,6 +312,8 @@ export class VisualElementDragControls {
303312
this.constraints = false
304313
}
305314

315+
this.elastic = resolveDragElastic(dragElastic!)
316+
306317
/**
307318
* If we're outputting to external MotionValues, we want to rebase the measured constraints
308319
* from viewport-relative to component-relative.
@@ -384,9 +395,9 @@ export class VisualElementDragControls {
384395

385396
if (!isDragging) return
386397

387-
const { dragMomentum, dragElastic, onDragEnd } = this.props
398+
const { dragMomentum, onDragEnd } = this.props
388399

389-
if (dragMomentum || dragElastic) {
400+
if (dragMomentum || this.elastic) {
390401
const { velocity } = info
391402
this.animateDragEnd(velocity)
392403
}
@@ -439,22 +450,19 @@ export class VisualElementDragControls {
439450

440451
if (!offset || !axisValue) return
441452

442-
const { dragElastic } = this.props
443453
const nextValue = this.originPoint[axis]! + offset[axis]
444454
const update = this.constraints
445455
? applyConstraints(
446456
nextValue,
447457
this.constraints[axis],
448-
dragElastic as number
458+
this.elastic[axis]
449459
)
450460
: nextValue
451461

452462
axisValue.set(update)
453463
}
454464

455465
updateVisualElementAxis(axis: DragDirection, event: AnyPointerEvent) {
456-
const { dragElastic } = this.props
457-
458466
// Get the actual layout bounding box of the element
459467
const axisLayout = this.visualElement.getLayoutState().layout[axis]
460468

@@ -472,7 +480,7 @@ export class VisualElementDragControls {
472480
axisLength,
473481
axisProgress,
474482
this.constraints?.[axis],
475-
dragElastic as number
483+
this.elastic[axis]
476484
)
477485

478486
// Update the axis viewport target with this new min and the length
@@ -484,7 +492,7 @@ export class VisualElementDragControls {
484492
dragDirectionLock = false,
485493
dragPropagation = false,
486494
dragConstraints = false,
487-
dragElastic = 0.35,
495+
dragElastic = defaultElastic,
488496
dragMomentum = true,
489497
...remainingProps
490498
}: DragControlsProps & MotionProps) {

src/gestures/drag/types.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RefObject } from "react"
22
import { PanInfo } from "../PanSession"
33
import { Inertia, TargetAndTransition } from "../../types"
4-
import { BoundingBox2D } from "../../types/geometry"
4+
import { Axis, BoundingBox2D } from "../../types/geometry"
55
import { DragControls } from "./use-drag-controls"
66
import { MotionValue } from "../../value"
77
import { VariantLabels } from "../../motion/types"
@@ -11,6 +11,18 @@ export type DragHandler = (
1111
info: PanInfo
1212
) => void
1313

14+
export type DragElastic = boolean | number | Partial<BoundingBox2D>
15+
16+
export interface ResolvedConstraints {
17+
x: Partial<Axis>
18+
y: Partial<Axis>
19+
}
20+
21+
export interface ResolvedElastic {
22+
x: Axis
23+
y: Axis
24+
}
25+
1426
/**
1527
* @public
1628
*/
@@ -294,7 +306,12 @@ export interface DraggableProps extends DragHandlers {
294306

295307
/**
296308
* The degree of movement allowed outside constraints. 0 = no movement, 1 =
297-
* full movement. Set to `0.5` by default.
309+
* full movement.
310+
*
311+
* Set to `0.5` by default. Can also be set as `false` to disable movement.
312+
*
313+
* By passing an object of `top`/`right`/`bottom`/`left`, individual values can be set
314+
* per constraint. Any missing values will be set to `0`.
298315
*
299316
* @library
300317
*
@@ -316,7 +333,7 @@ export interface DraggableProps extends DragHandlers {
316333
* />
317334
* ```
318335
*/
319-
dragElastic?: boolean | number
336+
dragElastic?: DragElastic
320337

321338
/**
322339
* Apply momentum from the pan gesture to the component when dragging

src/gestures/drag/utils/__tests__/constraints.test.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,79 @@ import {
55
calcViewportAxisConstraints,
66
calcPositionFromProgress,
77
rebaseAxisConstraints,
8+
resolveDragElastic,
89
} from "../constraints"
910

11+
describe("resolveDragElastic", () => {
12+
test("Resolves false as 0", () => {
13+
expect(resolveDragElastic(false)).toEqual({
14+
x: { min: 0, max: 0 },
15+
y: { min: 0, max: 0 },
16+
})
17+
})
18+
test("Resolves true as default", () => {
19+
expect(resolveDragElastic(true)).toEqual({
20+
x: { min: 0.35, max: 0.35 },
21+
y: { min: 0.35, max: 0.35 },
22+
})
23+
})
24+
25+
test("Resolves number as object filled with number", () => {
26+
expect(resolveDragElastic(0.5)).toEqual({
27+
x: { min: 0.5, max: 0.5 },
28+
y: { min: 0.5, max: 0.5 },
29+
})
30+
})
31+
32+
test("Resolves object as object, with default values for missing values", () => {
33+
expect(
34+
resolveDragElastic({ top: 0.1, left: 0.2, bottom: 0.3, right: 0.4 })
35+
).toEqual({
36+
x: { min: 0.2, max: 0.4 },
37+
y: { min: 0.1, max: 0.3 },
38+
})
39+
expect(resolveDragElastic({ top: 0.1, right: 0.4 })).toEqual({
40+
x: { min: 0, max: 0.4 },
41+
y: { min: 0.1, max: 0 },
42+
})
43+
})
44+
})
45+
1046
describe("applyConstraints", () => {
1147
test("Returns points within the defined constraints", () => {
1248
expect(applyConstraints(10, { min: 0, max: 20 })).toBe(10)
1349
})
14-
test("Returns points outside the defined constraints when elastic is set to 1", () => {
15-
expect(applyConstraints(25, { min: 0, max: 20 }, 1)).toBe(25)
16-
expect(applyConstraints(-5, { min: 0, max: 20 }, 1)).toBe(-5)
17-
})
18-
test("Clamps points outside the defined constraints when elastic is set to 0", () => {
19-
expect(applyConstraints(25, { min: 0, max: 20 }, 0)).toBe(20)
20-
expect(applyConstraints(-5, { min: 0, max: 20 }, 0)).toBe(0)
21-
})
22-
test("Applies elastic factor if defined", () => {
23-
expect(applyConstraints(25, { min: 0, max: 20 }, 0.5)).toBe(22.5)
24-
expect(applyConstraints(-5, { min: 0, max: 20 }, 0.5)).toBe(-2.5)
25-
expect(applyConstraints(25, { min: 0, max: 20 }, 1)).toBe(25)
26-
expect(applyConstraints(-5, { min: 0, max: 20 }, 1)).toBe(-5)
27-
expect(applyConstraints(25, { min: 0, max: 20 }, 0)).toBe(20)
28-
expect(applyConstraints(-5, { min: 0, max: 20 }, 0)).toBe(0)
50+
test("Applies elastic factor", () => {
51+
expect(
52+
applyConstraints(25, { min: 0, max: 20 }, { min: 0.5, max: 0.5 })
53+
).toBe(22.5)
54+
expect(
55+
applyConstraints(25, { min: 0, max: 20 }, { min: 1, max: 0.5 })
56+
).toBe(22.5)
57+
expect(
58+
applyConstraints(-5, { min: 0, max: 20 }, { min: 0.5, max: 0.5 })
59+
).toBe(-2.5)
60+
expect(
61+
applyConstraints(-5, { min: 0, max: 20 }, { min: 0.5, max: 1 })
62+
).toBe(-2.5)
63+
expect(
64+
applyConstraints(25, { min: 0, max: 20 }, { min: 1, max: 1 })
65+
).toBe(25)
66+
expect(
67+
applyConstraints(-5, { min: 0, max: 20 }, { min: 1, max: 1 })
68+
).toBe(-5)
69+
expect(
70+
applyConstraints(25, { min: 0, max: 20 }, { min: 0, max: 0 })
71+
).toBe(20)
72+
expect(
73+
applyConstraints(25, { min: 0, max: 20 }, { min: 1, max: 0 })
74+
).toBe(20)
75+
expect(
76+
applyConstraints(-5, { min: 0, max: 20 }, { min: 0, max: 0 })
77+
).toBe(0)
78+
expect(
79+
applyConstraints(-5, { min: 0, max: 20 }, { min: 0, max: 1 })
80+
).toBe(0)
2981
})
3082
})
3183

src/gestures/drag/utils/constraints.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import {
66
} from "../../../types/geometry"
77
import { mix } from "popmotion"
88
import { calcOrigin } from "../../../utils/geometry/delta-calc"
9-
10-
export interface ResolvedConstraints {
11-
x: Partial<Axis>
12-
y: Partial<Axis>
13-
}
9+
import { DragElastic, ResolvedConstraints } from "../types"
1410

1511
/**
1612
* Apply constraints to a point. These constraints are both physical along an
@@ -20,14 +16,14 @@ export interface ResolvedConstraints {
2016
export function applyConstraints(
2117
point: number,
2218
{ min, max }: Partial<Axis>,
23-
elastic?: number
19+
elastic?: Axis
2420
): number {
2521
if (min !== undefined && point < min) {
2622
// If we have a min point defined, and this is outside of that, constrain
27-
point = elastic ? mix(min, point, elastic) : Math.max(point, min)
23+
point = elastic ? mix(min, point, elastic.min) : Math.max(point, min)
2824
} else if (max !== undefined && point > max) {
2925
// If we have a max point defined, and this is outside of that, constrain
30-
point = elastic ? mix(max, point, elastic) : Math.min(point, max)
26+
point = elastic ? mix(max, point, elastic.max) : Math.min(point, max)
3127
}
3228

3329
return point
@@ -46,7 +42,7 @@ export function calcConstrainedMinPoint(
4642
length: number,
4743
progress: number,
4844
constraints?: Partial<Axis>,
49-
elastic?: number
45+
elastic?: Axis
5046
): number {
5147
// Calculate a min point for this axis and apply it to the current pointer
5248
const min = point - length * progress
@@ -175,3 +171,40 @@ export function rebaseAxisConstraints(
175171

176172
return relativeConstraints
177173
}
174+
175+
export const defaultElastic = 0.35
176+
/**
177+
* Accepts a dragElastic prop and returns resolved elastic values for each axis.
178+
*/
179+
export function resolveDragElastic(dragElastic: DragElastic): AxisBox2D {
180+
if (dragElastic === false) {
181+
dragElastic = 0
182+
} else if (dragElastic === true) {
183+
dragElastic = defaultElastic
184+
}
185+
186+
return {
187+
x: resolveAxisElastic(dragElastic, "left", "right"),
188+
y: resolveAxisElastic(dragElastic, "top", "bottom"),
189+
}
190+
}
191+
192+
export function resolveAxisElastic(
193+
dragElastic: DragElastic,
194+
minLabel: string,
195+
maxLabel: string
196+
): Axis {
197+
return {
198+
min: resolvePointElastic(dragElastic, minLabel),
199+
max: resolvePointElastic(dragElastic, maxLabel),
200+
}
201+
}
202+
203+
export function resolvePointElastic(
204+
dragElastic: DragElastic,
205+
label: string
206+
): number {
207+
return typeof dragElastic === "number"
208+
? dragElastic
209+
: dragElastic[label] ?? 0
210+
}

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ export {
129129
export { EventInfo } from "./events/types"
130130
export { VisualElementLifecycles } from "./render/utils/lifecycles"
131131
export { MotionFeature, FeatureProps } from "./motion/features/types"
132-
export { DraggableProps, DragHandlers } from "./gestures/drag/types"
132+
export {
133+
DraggableProps,
134+
DragHandlers,
135+
DragElastic,
136+
} from "./gestures/drag/types"
133137
export { LayoutProps } from "./motion/features/layout/types"
134138
export { AnimatePresenceProps } from "./components/AnimatePresence/types"
135139
export { SharedLayoutProps } from "./components/AnimateSharedLayout/types"

0 commit comments

Comments
 (0)