framer-motion 7.6.7 → 7.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,9 +26,18 @@ const transformAxes = ["", "X", "Y", "Z"];
26
26
  * which has a noticeable difference in spring animations
27
27
  */
28
28
  const animationTarget = 1000;
29
+ let id = 0;
29
30
  function createProjectionNode({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
30
31
  return class ProjectionNode {
31
32
  constructor(elementId, latestValues = {}, parent = defaultParent === null || defaultParent === void 0 ? void 0 : defaultParent()) {
33
+ /**
34
+ * A unique ID generated for every projection node.
35
+ */
36
+ this.id = id++;
37
+ /**
38
+ * An id that represents a unique session instigated by startUpdate.
39
+ */
40
+ this.animationId = 0;
32
41
  /**
33
42
  * A Set containing all this component's children. This is used to iterate
34
43
  * through the children.
@@ -55,6 +64,11 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
55
64
  * and if one node is dirtied, they all are.
56
65
  */
57
66
  this.isLayoutDirty = false;
67
+ /**
68
+ * Flag to true if we think the projection calculations for this or any
69
+ * child might need recalculating as a result of an updated transform or layout animation.
70
+ */
71
+ this.isProjectionDirty = false;
58
72
  /**
59
73
  * Block layout updates for instant layout transitions throughout the tree.
60
74
  */
@@ -137,8 +151,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
137
151
  hasListeners(name) {
138
152
  return this.eventHandlers.has(name);
139
153
  }
140
- registerPotentialNode(id, node) {
141
- this.potentialNodes.set(id, node);
154
+ registerPotentialNode(elementId, node) {
155
+ this.potentialNodes.set(elementId, node);
142
156
  }
143
157
  /**
144
158
  * Lifecycles
@@ -271,6 +285,7 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
271
285
  return;
272
286
  this.isUpdating = true;
273
287
  (_a = this.nodes) === null || _a === void 0 ? void 0 : _a.forEach(resetRotation);
288
+ this.animationId++;
274
289
  }
275
290
  willUpdate(shouldNotifyListeners = true) {
276
291
  var _a, _b, _c;
@@ -285,11 +300,7 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
285
300
  for (let i = 0; i < this.path.length; i++) {
286
301
  const node = this.path[i];
287
302
  node.shouldResetTransform = true;
288
- /**
289
- * TODO: Check we haven't updated the scroll
290
- * since the last didUpdate
291
- */
292
- node.updateScroll();
303
+ node.updateScroll("snapshot");
293
304
  }
294
305
  const { layoutId, layout } = this.options;
295
306
  if (layoutId === undefined && !layout)
@@ -405,10 +416,20 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
405
416
  this.notifyListeners("measure", this.layout.layoutBox);
406
417
  (_a = this.options.visualElement) === null || _a === void 0 ? void 0 : _a.notify("LayoutMeasure", this.layout.layoutBox, prevLayout === null || prevLayout === void 0 ? void 0 : prevLayout.layoutBox);
407
418
  }
408
- updateScroll() {
409
- if (this.options.layoutScroll && this.instance) {
410
- this.isScrollRoot = checkIsScrollRoot(this.instance);
411
- this.scroll = measureScroll(this.instance);
419
+ updateScroll(phase = "measure") {
420
+ let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
421
+ if (this.scroll &&
422
+ this.scroll.animationId === this.root.animationId &&
423
+ this.scroll.phase === phase) {
424
+ needsMeasurement = false;
425
+ }
426
+ if (needsMeasurement) {
427
+ this.scroll = {
428
+ animationId: this.root.animationId,
429
+ phase,
430
+ isRoot: checkIsScrollRoot(this.instance),
431
+ offset: measureScroll(this.instance),
432
+ };
412
433
  }
413
434
  }
414
435
  resetTransform() {
@@ -430,6 +451,7 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
430
451
  }
431
452
  }
432
453
  measure(removeTransform = true) {
454
+ var _a;
433
455
  const pageBox = this.measurePageBox();
434
456
  let layoutBox = this.removeElementScroll(pageBox);
435
457
  /**
@@ -441,10 +463,17 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
441
463
  layoutBox = this.removeTransform(layoutBox);
442
464
  }
443
465
  roundBox(layoutBox);
466
+ const positionStyle = (_a = this.options.visualElement) === null || _a === void 0 ? void 0 : _a.readValue("position");
467
+ const position = positionStyle === "fixed" || positionStyle === "sticky"
468
+ ? positionStyle
469
+ : "static";
444
470
  return {
471
+ animationId: this.root.animationId,
445
472
  measuredBox: pageBox,
446
473
  layoutBox,
447
474
  latestValues: {},
475
+ source: this.id,
476
+ position,
448
477
  };
449
478
  }
450
479
  measurePageBox() {
@@ -455,8 +484,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
455
484
  // Remove viewport scroll to give page-relative coordinates
456
485
  const { scroll } = this.root;
457
486
  if (scroll) {
458
- translateAxis(box.x, scroll.x);
459
- translateAxis(box.y, scroll.y);
487
+ translateAxis(box.x, scroll.offset.x);
488
+ translateAxis(box.y, scroll.offset.y);
460
489
  }
461
490
  return box;
462
491
  }
@@ -469,13 +498,13 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
469
498
  */
470
499
  for (let i = 0; i < this.path.length; i++) {
471
500
  const node = this.path[i];
472
- const { scroll, options, isScrollRoot } = node;
501
+ const { scroll, options } = node;
473
502
  if (node !== this.root && scroll && options.layoutScroll) {
474
503
  /**
475
504
  * If this is a new scroll root, we want to remove all previous scrolls
476
505
  * from the viewport box.
477
506
  */
478
- if (isScrollRoot) {
507
+ if (scroll.isRoot) {
479
508
  copyBoxInto(boxWithoutScroll, box);
480
509
  const { scroll: rootScroll } = this.root;
481
510
  /**
@@ -483,12 +512,12 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
483
512
  * to the measured bounding box.
484
513
  */
485
514
  if (rootScroll) {
486
- translateAxis(boxWithoutScroll.x, -rootScroll.x);
487
- translateAxis(boxWithoutScroll.y, -rootScroll.y);
515
+ translateAxis(boxWithoutScroll.x, -rootScroll.offset.x);
516
+ translateAxis(boxWithoutScroll.y, -rootScroll.offset.y);
488
517
  }
489
518
  }
490
- translateAxis(boxWithoutScroll.x, scroll.x);
491
- translateAxis(boxWithoutScroll.y, scroll.y);
519
+ translateAxis(boxWithoutScroll.x, scroll.offset.x);
520
+ translateAxis(boxWithoutScroll.y, scroll.offset.y);
492
521
  }
493
522
  }
494
523
  return boxWithoutScroll;
@@ -503,8 +532,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
503
532
  node.scroll &&
504
533
  node !== node.root) {
505
534
  transformBox(withTransforms, {
506
- x: -node.scroll.x,
507
- y: -node.scroll.y,
535
+ x: -node.scroll.offset.x,
536
+ y: -node.scroll.offset.y,
508
537
  });
509
538
  }
510
539
  if (!hasTransform(node.latestValues))
@@ -542,6 +571,7 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
542
571
  */
543
572
  setTargetDelta(delta) {
544
573
  this.targetDelta = delta;
574
+ this.isProjectionDirty = true;
545
575
  this.root.scheduleUpdateProjection();
546
576
  }
547
577
  setOptions(options) {
@@ -565,6 +595,14 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
565
595
  */
566
596
  resolveTargetDelta() {
567
597
  var _a;
598
+ /**
599
+ * Propagate isProjectionDirty. Nodes are ordered by depth, so if the parent here
600
+ * is dirty we can simply pass this forward.
601
+ */
602
+ this.isProjectionDirty || (this.isProjectionDirty = this.getLead().isProjectionDirty ||
603
+ Boolean(this.parent && this.parent.isProjectionDirty));
604
+ if (!this.isProjectionDirty)
605
+ return;
568
606
  const { layout, layoutId } = this.options;
569
607
  /**
570
608
  * If we have no layout, we can't perform projection, so early return
@@ -668,6 +706,9 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
668
706
  }
669
707
  calcProjection() {
670
708
  var _a;
709
+ if (!this.isProjectionDirty)
710
+ return;
711
+ this.isProjectionDirty = false;
671
712
  const { layout, layoutId } = this.options;
672
713
  /**
673
714
  * If this section of the tree isn't animating we can
@@ -738,7 +779,7 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
738
779
  }
739
780
  }
740
781
  setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
741
- var _a;
782
+ var _a, _b;
742
783
  const snapshot = this.snapshot;
743
784
  const snapshotLatestValues = (snapshot === null || snapshot === void 0 ? void 0 : snapshot.latestValues) || {};
744
785
  const mixedValues = { ...this.latestValues };
@@ -746,8 +787,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
746
787
  this.relativeTarget = this.relativeTargetOrigin = undefined;
747
788
  this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
748
789
  const relativeLayout = createBox();
749
- const isSharedLayoutAnimation = snapshot === null || snapshot === void 0 ? void 0 : snapshot.isShared;
750
- const isOnlyMember = (((_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.members.length) || 0) <= 1;
790
+ const isSharedLayoutAnimation = (snapshot === null || snapshot === void 0 ? void 0 : snapshot.source) !== ((_a = this.layout) === null || _a === void 0 ? void 0 : _a.source);
791
+ const isOnlyMember = (((_b = this.getStack()) === null || _b === void 0 ? void 0 : _b.members.length) || 0) <= 1;
751
792
  const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
752
793
  !isOnlyMember &&
753
794
  this.options.crossfade === true &&
@@ -928,25 +969,30 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
928
969
  return;
929
970
  // If there's no detected rotation values, we can early return without a forced render.
930
971
  let hasRotate = false;
931
- // Keep a record of all the values we've reset
932
- const resetValues = {};
933
- // Check the rotate value of all axes and reset to 0
934
- for (let i = 0; i < transformAxes.length; i++) {
935
- const axis = transformAxes[i];
936
- const key = "rotate" + axis;
937
- // If this rotation doesn't exist as a motion value, then we don't
938
- // need to reset it
939
- if (!visualElement.getStaticValue(key)) {
940
- continue;
941
- }
972
+ /**
973
+ * An unrolled check for rotation values. Most elements don't have any rotation and
974
+ * skipping the nested loop and new object creation is 50% faster.
975
+ */
976
+ const { latestValues } = visualElement;
977
+ if (latestValues.rotate ||
978
+ latestValues.rotateX ||
979
+ latestValues.rotateY ||
980
+ latestValues.rotateZ) {
942
981
  hasRotate = true;
943
- // Record the rotation and then temporarily set it to 0
944
- resetValues[key] = visualElement.getStaticValue(key);
945
- visualElement.setStaticValue(key, 0);
946
982
  }
947
983
  // If there's no rotation values, we don't need to do any more.
948
984
  if (!hasRotate)
949
985
  return;
986
+ const resetValues = {};
987
+ // Check the rotate value of all axes and reset to 0
988
+ for (let i = 0; i < transformAxes.length; i++) {
989
+ const key = "rotate" + transformAxes[i];
990
+ // Record the rotation and then temporarily set it to 0
991
+ if (latestValues[key]) {
992
+ resetValues[key] = latestValues[key];
993
+ visualElement.setStaticValue(key, 0);
994
+ }
995
+ }
950
996
  // Force a render of this element to apply the transform with all rotations
951
997
  // set to 0.
952
998
  visualElement === null || visualElement === void 0 ? void 0 : visualElement.render();
@@ -1088,11 +1134,12 @@ function notifyLayoutUpdate(node) {
1088
1134
  node.hasListeners("didUpdate")) {
1089
1135
  const { layoutBox: layout, measuredBox: measuredLayout } = node.layout;
1090
1136
  const { animationType } = node.options;
1137
+ const isShared = snapshot.source !== node.layout.source;
1091
1138
  // TODO Maybe we want to also resize the layout snapshot so we don't trigger
1092
1139
  // animations for instance if layout="size" and an element has only changed position
1093
1140
  if (animationType === "size") {
1094
1141
  eachAxis((axis) => {
1095
- const axisSnapshot = snapshot.isShared
1142
+ const axisSnapshot = isShared
1096
1143
  ? snapshot.measuredBox[axis]
1097
1144
  : snapshot.layoutBox[axis];
1098
1145
  const length = calcLength(axisSnapshot);
@@ -1102,7 +1149,7 @@ function notifyLayoutUpdate(node) {
1102
1149
  }
1103
1150
  else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
1104
1151
  eachAxis((axis) => {
1105
- const axisSnapshot = snapshot.isShared
1152
+ const axisSnapshot = isShared
1106
1153
  ? snapshot.measuredBox[axis]
1107
1154
  : snapshot.layoutBox[axis];
1108
1155
  const length = calcLength(layout[axis]);
@@ -1112,7 +1159,7 @@ function notifyLayoutUpdate(node) {
1112
1159
  const layoutDelta = createDelta();
1113
1160
  calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
1114
1161
  const visualDelta = createDelta();
1115
- if (snapshot.isShared) {
1162
+ if (isShared) {
1116
1163
  calcBoxDelta(visualDelta, node.applyTransform(measuredLayout, true), snapshot.measuredBox);
1117
1164
  }
1118
1165
  else {
@@ -1208,7 +1255,7 @@ const defaultLayoutTransition = {
1208
1255
  duration: 0.45,
1209
1256
  ease: [0.4, 0, 0.1, 1],
1210
1257
  };
1211
- function mountNodeEarly(node, id) {
1258
+ function mountNodeEarly(node, elementId) {
1212
1259
  /**
1213
1260
  * Rather than searching the DOM from document we can search the
1214
1261
  * path for the deepest mounted ancestor and search from there
@@ -1221,7 +1268,7 @@ function mountNodeEarly(node, id) {
1221
1268
  }
1222
1269
  }
1223
1270
  const searchElement = searchNode && searchNode !== node.root ? searchNode.instance : document;
1224
- const element = searchElement.querySelector(`[data-projection-id="${id}"]`);
1271
+ const element = searchElement.querySelector(`[data-projection-id="${elementId}"]`);
1225
1272
  if (element)
1226
1273
  node.mount(element, true);
1227
1274
  }
@@ -62,7 +62,6 @@ class NodeStack {
62
62
  node.snapshot = prevLead.snapshot;
63
63
  node.snapshot.latestValues =
64
64
  prevLead.animationValues || prevLead.latestValues;
65
- node.snapshot.isShared = true;
66
65
  }
67
66
  if ((_a = node.root) === null || _a === void 0 ? void 0 : _a.isUpdating) {
68
67
  node.isLayoutDirty = true;
@@ -1,5 +1,5 @@
1
- const identityProjection = "translate3d(0px, 0px, 0) scale(1, 1) scale(1, 1)";
2
1
  function buildProjectionTransform(delta, treeScale, latestTransform) {
2
+ let transform = "";
3
3
  /**
4
4
  * The translations we use to calculate are always relative to the viewport coordinate space.
5
5
  * But when we apply scales, we also scale the coordinate space of an element and its children.
@@ -8,12 +8,16 @@ function buildProjectionTransform(delta, treeScale, latestTransform) {
8
8
  */
9
9
  const xTranslate = delta.x.translate / treeScale.x;
10
10
  const yTranslate = delta.y.translate / treeScale.y;
11
- let transform = `translate3d(${xTranslate}px, ${yTranslate}px, 0) `;
11
+ if (xTranslate || yTranslate) {
12
+ transform = `translate3d(${xTranslate}px, ${yTranslate}px, 0) `;
13
+ }
12
14
  /**
13
15
  * Apply scale correction for the tree transform.
14
16
  * This will apply scale to the screen-orientated axes.
15
17
  */
16
- transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
18
+ if (treeScale.x !== 1 || treeScale.y !== 1) {
19
+ transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
20
+ }
17
21
  if (latestTransform) {
18
22
  const { rotate, rotateX, rotateY } = latestTransform;
19
23
  if (rotate)
@@ -29,8 +33,10 @@ function buildProjectionTransform(delta, treeScale, latestTransform) {
29
33
  */
30
34
  const elementScaleX = delta.x.scale * treeScale.x;
31
35
  const elementScaleY = delta.y.scale * treeScale.y;
32
- transform += `scale(${elementScaleX}, ${elementScaleY})`;
33
- return transform === identityProjection ? "none" : transform;
36
+ if (elementScaleX !== 1 || elementScaleY !== 1) {
37
+ transform += `scale(${elementScaleX}, ${elementScaleY})`;
38
+ }
39
+ return transform || "none";
34
40
  }
35
41
 
36
- export { buildProjectionTransform, identityProjection };
42
+ export { buildProjectionTransform };
@@ -8,8 +8,8 @@ function measurePageBox(element, rootProjectionNode, transformPagePoint) {
8
8
  const viewportBox = measureViewportBox(element, transformPagePoint);
9
9
  const { scroll } = rootProjectionNode;
10
10
  if (scroll) {
11
- translateAxis(viewportBox.x, scroll.x);
12
- translateAxis(viewportBox.y, scroll.y);
11
+ translateAxis(viewportBox.x, scroll.offset.x);
12
+ translateAxis(viewportBox.y, scroll.offset.y);
13
13
  }
14
14
  return viewportBox;
15
15
  }
@@ -11,6 +11,7 @@ import { SubscriptionManager } from '../utils/subscription-manager.mjs';
11
11
  import { motionValue } from '../value/index.mjs';
12
12
  import { isWillChangeMotionValue } from '../value/use-will-change/is.mjs';
13
13
  import { isMotionValue } from '../value/utils/is-motion-value.mjs';
14
+ import { transformProps } from './html/utils/transform.mjs';
14
15
  import { variantPriorityOrder } from './utils/animation-state.mjs';
15
16
  import { isControllingVariants, isVariantNode } from './utils/is-controlling-variants.mjs';
16
17
  import { isVariantLabel } from './utils/is-variant-label.mjs';
@@ -181,10 +182,14 @@ class VisualElement {
181
182
  this.current = null;
182
183
  }
183
184
  bindToMotionValue(key, value) {
185
+ const valueIsTransform = transformProps.has(key);
184
186
  const removeOnChange = value.onChange((latestValue) => {
185
187
  this.latestValues[key] = latestValue;
186
188
  this.props.onUpdate &&
187
189
  sync.update(this.notifyUpdate, false, true);
190
+ if (valueIsTransform && this.projection) {
191
+ this.projection.isProjectionDirty = true;
192
+ }
188
193
  });
189
194
  const removeOnRenderRequest = value.onRenderRequest(this.scheduleRender);
190
195
  this.valueSubscriptions.set(key, () => {
@@ -22,7 +22,7 @@ function updateMotionValuesFromProps(element, next, prev) {
22
22
  * and warn against mismatches.
23
23
  */
24
24
  if (process.env.NODE_ENV === "development") {
25
- warnOnce(nextValue.version === "7.6.7", `Attempting to mix Framer Motion versions ${nextValue.version} with 7.6.7 may not work as expected.`);
25
+ warnOnce(nextValue.version === "7.6.9", `Attempting to mix Framer Motion versions ${nextValue.version} with 7.6.9 may not work as expected.`);
26
26
  }
27
27
  }
28
28
  else if (isMotionValue(prevValue)) {
@@ -24,7 +24,7 @@ class MotionValue {
24
24
  * This will be replaced by the build step with the latest version number.
25
25
  * When MotionValues are provided to motion components, warn if versions are mixed.
26
26
  */
27
- this.version = "7.6.7";
27
+ this.version = "7.6.9";
28
28
  /**
29
29
  * Duration, in milliseconds, since last updating frame.
30
30
  *