544
return domElement;
545
}
546
547
+export function cloneMutableInstance(
548
+ instance: Instance,
549
+ keepChildren: boolean,
550
+): Instance {
551
+ return instance.cloneNode(keepChildren);
552
+}
553
+
554
export function appendInitialChild(
555
parentInstance: Instance,
556
child: Instance | TextInstance,
616
return textNode;
617
}
618
619
+export function cloneMutableTextInstance(
620
+ textInstance: TextInstance,
621
+): TextInstance {
622
+ return textInstance.cloneNode(false);
623
+}
624
+
625
let currentPopstateTransitionEvent: Event | null = null;
626
export function shouldAttemptEagerTransition(): boolean {
627
const event = window.event;
1220
}
1221
1222
export function restoreRootViewTransitionName(rootContainer: Container): void {
1223
+ let containerInstance: Instance;
1224
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
1225
+ containerInstance = (rootContainer: any).body;
1226
+ } else if (rootContainer.nodeName === 'HTML') {
1227
+ containerInstance = (rootContainer.ownerDocument.body: any);
1228
+ } else {
1229
+ // If the container is not the whole document, then we ideally should probably
1230
+ // clone the whole document outside of the React too.
1231
+ containerInstance = (rootContainer: any);
1232
+ }
1233
+ // $FlowFixMe[prop-missing]
1234
+ if (containerInstance.style.viewTransitionName === 'root') {
1235
+ // If we moved the root view transition name to the container in a gesture
1236
+ // we need to restore it now.
1237
+ containerInstance.style.viewTransitionName = '';
1238
+ }
1239
+ const documentElement: null | HTMLElement =
1240
+ containerInstance.ownerDocument.documentElement;
1241
+ if (
1242
+ documentElement !== null &&
1243
+ // $FlowFixMe[prop-missing]
1244
+ documentElement.style.viewTransitionName === 'none'
1245
+ ) {
1246
+ // $FlowFixMe[prop-missing]
1247
+ documentElement.style.viewTransitionName = '';
1248
+ }
1249
+}
1250
+
1251
+function getComputedTransform(style: CSSStyleDeclaration): string {
1252
+ // Gets the merged transform of all the short hands.
1253
+ const computedStyle: any = style;
1254
+ let transform: string = computedStyle.transform;
1255
+ if (transform === 'none') {
1256
+ transform = '';
1257
+ }
1258
+ const scale: string = computedStyle.scale;
1259
+ if (scale !== 'none' && scale !== '') {
1260
+ const parts = scale.split(' ');
1261
+ transform =
1262
+ (parts.length === 3 ? 'scale3d' : 'scale') +
1263
+ '(' +
1264
+ parts.join(', ') +
1265
+ ') ' +
1266
+ transform;
1267
+ }
1268
+ const rotate: string = computedStyle.rotate;
1269
+ if (rotate !== 'none' && rotate !== '') {
1270
+ const parts = rotate.split(' ');
1271
+ if (parts.length === 1) {
1272
+ transform = 'rotate(' + parts[0] + ') ' + transform;
1273
+ } else if (parts.length === 2) {
1274
+ transform =
1275
+ 'rotate' + parts[0].toUpperCase() + '(' + parts[1] + ') ' + transform;
1276
+ } else {
1277
+ transform = 'rotate3d(' + parts.join(', ') + ') ' + transform;
1278
+ }
1279
+ }
1280
+ const translate: string = computedStyle.translate;
1281
+ if (translate !== 'none' && translate !== '') {
1282
+ const parts = translate.split(' ');
1283
+ transform =
1284
+ (parts.length === 3 ? 'translate3d' : 'translate') +
1285
+ '(' +
1286
+ parts.join(', ') +
1287
+ ') ' +
1288
+ transform;
1289
+ }
1290
+ return transform;
1291
+}
1292
+
1293
+function moveOutOfViewport(
1294
+ originalStyle: CSSStyleDeclaration,
1295
+ element: HTMLElement,
1296
+): void {
1297
+ // Apply a transform that safely puts the whole element outside the viewport
1298
+ // while still letting it paint its "old" state to a snapshot.
1299
+ const transform = getComputedTransform(originalStyle);
1300
+ // Clear the long form properties.
1301
+ // $FlowFixMe
1302
+ element.style.translate = 'none';
1303
+ // $FlowFixMe
1304
+ element.style.scale = 'none';
1305
+ // $FlowFixMe
1306
+ element.style.rotate = 'none';
1307
+ // Apply a translate to move it way out of the viewport. This is applied first
1308
+ // so that it is in the coordinate space of the parent and not after applying
1309
+ // other transforms. That's why we need to merge the long form properties.
1310
+ // TODO: Ideally we'd adjust for the parent's rotate/scale. Otherwise when
1311
+ // we move back the ::view-transition-group we might overshoot or undershoot.
1312
+ element.style.transform = 'translate(-20000px, -20000px) ' + transform;
1313
+}
1314
+
1315
+function moveOldFrameIntoViewport(keyframe: any): void {
1316
+ // In the resulting View Transition Animation, the first frame will be offset.
1317
+ const computedTransform: ?string = keyframe.transform;
1318
+ if (computedTransform != null) {
1319
+ let transform = computedTransform === 'none' ? '' : computedTransform;
1320
+ transform = 'translate(20000px, 20000px) ' + transform;
1321
+ keyframe.transform = transform;
1322
+ }
1323
+}
1324
+
1325
+export function cloneRootViewTransitionContainer(
1326
+ rootContainer: Container,
1327
+): Instance {
1328
+ // This implies that we're not going to animate the root document but instead
1329
+ // the clone so we first clear the name of the root container.
1330
const documentElement: null | HTMLElement =
1331
rootContainer.nodeType === DOCUMENT_NODE
1332
? (rootContainer: any).documentElement
1334
if (
1335
documentElement !== null &&
1336
// $FlowFixMe[prop-missing]
1217
- documentElement.style.viewTransitionName === 'none'
1337
+ documentElement.style.viewTransitionName === ''
1338
) {
1339
// $FlowFixMe[prop-missing]
1220
- documentElement.style.viewTransitionName = '';
1340
+ documentElement.style.viewTransitionName = 'none';
1341
+ }
1342
+
1343
+ let containerInstance: HTMLElement;
1344
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
1345
+ containerInstance = (rootContainer: any).body;
1346
+ } else if (rootContainer.nodeName === 'HTML') {
1347
+ containerInstance = (rootContainer.ownerDocument.body: any);
1348
+ } else {
1349
+ // If the container is not the whole document, then we ideally should probably
1350
+ // clone the whole document outside of the React too.
1351
+ containerInstance = (rootContainer: any);
1352
+ }
1353
+
1354
+ const containerParent = containerInstance.parentNode;
1355
+ if (containerParent === null) {
1356
+ throw new Error('Cannot use a useSwipeTransition() in a detached root.');
1357
}
1358
+
1359
+ const clone: HTMLElement = containerInstance.cloneNode(false);
1360
+
1361
+ const computedStyle = getComputedStyle(containerInstance);
1362
+
1363
+ if (
1364
+ computedStyle.position === 'absolute' ||
1365
+ computedStyle.position === 'fixed'
1366
+ ) {
1367
+ // If the style is already absolute, we don't have to do anything because it'll appear
1368
+ // in the same place.
1369
+ } else {
1370
+ // Otherwise we need to absolutely position the clone in the same location as the original.
1371
+ let positionedAncestor: HTMLElement = containerParent;
1372
+ while (
1373
+ positionedAncestor.parentNode != null &&
1374
+ positionedAncestor.parentNode.nodeType !== DOCUMENT_NODE
1375
+ ) {
1376
+ if (getComputedStyle(positionedAncestor).position !== 'static') {
1377
+ break;
1378
+ }
1379
+ // $FlowFixMe: This is refined.
1380
+ positionedAncestor = positionedAncestor.parentNode;
1381
+ }
1382
+
1383
+ const positionedAncestorStyle: any = positionedAncestor.style;
1384
+ const containerInstanceStyle: any = containerInstance.style;
1385
+ // Clear the transform while we're measuring since it affects the bounding client rect.
1386
+ const prevAncestorTranslate = positionedAncestorStyle.translate;
1387
+ const prevAncestorScale = positionedAncestorStyle.scale;
1388
+ const prevAncestorRotate = positionedAncestorStyle.rotate;
1389
+ const prevAncestorTransform = positionedAncestorStyle.transform;
1390
+ const prevTranslate = containerInstanceStyle.translate;
1391
+ const prevScale = containerInstanceStyle.scale;
1392
+ const prevRotate = containerInstanceStyle.rotate;
1393
+ const prevTransform = containerInstanceStyle.transform;
1394
+ positionedAncestorStyle.translate = 'none';
1395
+ positionedAncestorStyle.scale = 'none';
1396
+ positionedAncestorStyle.rotate = 'none';
1397
+ positionedAncestorStyle.transform = 'none';
1398
+ containerInstanceStyle.translate = 'none';
1399
+ containerInstanceStyle.scale = 'none';
1400
+ containerInstanceStyle.rotate = 'none';
1401
+ containerInstanceStyle.transform = 'none';
1402
+
1403
+ const ancestorRect = positionedAncestor.getBoundingClientRect();
1404
+ const rect = containerInstance.getBoundingClientRect();
1405
+
1406
+ const cloneStyle = clone.style;
1407
+ cloneStyle.position = 'absolute';
1408
+ cloneStyle.top = rect.top - ancestorRect.top + 'px';
1409
+ cloneStyle.left = rect.left - ancestorRect.left + 'px';
1410
+ cloneStyle.width = rect.width + 'px';
1411
+ cloneStyle.height = rect.height + 'px';
1412
+ cloneStyle.margin = '0px';
1413
+ cloneStyle.boxSizing = 'border-box';
1414
+
1415
+ positionedAncestorStyle.translate = prevAncestorTranslate;
1416
+ positionedAncestorStyle.scale = prevAncestorScale;
1417
+ positionedAncestorStyle.rotate = prevAncestorRotate;
1418
+ positionedAncestorStyle.transform = prevAncestorTransform;
1419
+ containerInstanceStyle.translate = prevTranslate;
1420
+ containerInstanceStyle.scale = prevScale;
1421
+ containerInstanceStyle.rotate = prevRotate;
1422
+ containerInstanceStyle.transform = prevTransform;
1423
+ }
1424
+
1425
+ // For this transition the container will act as the root. Nothing outside of it should
1426
+ // be affected anyway. This lets us transition from the cloned container to the original.
1427
+ // $FlowFixMe[prop-missing]
1428
+ clone.style.viewTransitionName = 'root';
1429
+
1430
+ // Move out of the viewport so that it's still painted for the snapshot but is not visible
1431
+ // for the frame where the snapshot happens.
1432
+ moveOutOfViewport(computedStyle, clone);
1433
+
1434
+ // Insert the clone after the root container as a sibling. This may inject a body
1435
+ // as the next sibling of an existing body. document.body will still point to the
1436
+ // first one and any id selectors will still find the first one. That's why it's
1437
+ // important that it's after the existing node.
1438
+ containerInstance.parentNode.insertBefore(
1439
+ clone,
1440
+ containerInstance.nextSibling,
1441
+ );
1442
+
1443
+ return clone;
1444
+}
1445
+
1446
+export function removeRootViewTransitionClone(
1447
+ rootContainer: Container,
1448
+ clone: Instance,
1449
+): void {
1450
+ let containerInstance: Instance;
1451
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
1452
+ containerInstance = (rootContainer: any).body;
1453
+ } else if (rootContainer.nodeName === 'HTML') {
1454
+ containerInstance = (rootContainer.ownerDocument.body: any);
1455
+ } else {
1456
+ // If the container is not the whole document, then we ideally should probably
1457
+ // clone the whole document outside of the React too.
1458
+ containerInstance = (rootContainer: any);
1459
+ }
1460
+ const containerParent = containerInstance.parentNode;
1461
+ if (containerParent === null) {
1462
+ throw new Error('Cannot use a useSwipeTransition() in a detached root.');
1463
+ }
1464
+ // We assume that the clone is still within the same parent.
1465
+ containerParent.removeChild(clone);
1466
+
1467
+ // Now the root is on the containerInstance itself until we call restoreRootViewTransitionName.
1468
+ containerInstance.style.viewTransitionName = 'root';
1469
}
1470
1471
export type InstanceMeasurement = {
1664
...
1665
};
1666
1667
+function mergeTranslate(translateA: ?string, translateB: ?string): string {
1668
+ if (!translateA || translateA === 'none') {
1669
+ return translateB || '';
1670
+ }
1671
+ if (!translateB || translateB === 'none') {
1672
+ return translateA || '';
1673
+ }
1674
+ const partsA = translateA.split(' ');
1675
+ const partsB = translateB.split(' ');
1676
+ let i;
1677
+ let result = '';
1678
+ for (i = 0; i < partsA.length && i < partsB.length; i++) {
1679
+ if (i > 0) {
1680
+ result += ' ';
1681
+ }
1682
+ result += 'calc(' + partsA[i] + ' + ' + partsB[i] + ')';
1683
+ }
1684
+ for (; i < partsA.length; i++) {
1685
+ result += ' ' + partsA[i];
1686
+ }
1687
+ for (; i < partsB.length; i++) {
1688
+ result += ' ' + partsB[i];
1689
+ }
1690
+ return result;
1691
+}
1692
+
1693
+function animateGesture(
1694
+ keyframes: any,
1695
+ targetElement: Element,
1696
+ pseudoElement: string,
1697
+ timeline: AnimationTimeline,
1698
+ rangeStart: number,
1699
+ rangeEnd: number,
1700
+ moveFirstFrameIntoViewport: boolean,
1701
+ moveAllFramesIntoViewport: boolean,
1702
+) {
1703
+ for (let i = 0; i < keyframes.length; i++) {
1704
+ const keyframe = keyframes[i];
1705
+ // Delete any easing since we always apply linear easing to gestures.
1706
+ delete keyframe.easing;
1707
+ delete keyframe.computedOffset;
1708
+ // Chrome returns "auto" for width/height which is not a valid value to
1709
+ // animate to. Similarly, transform: "none" is actually lack of transform.
1710
+ if (keyframe.width === 'auto') {
1711
+ delete keyframe.width;
1712
+ }
1713
+ if (keyframe.height === 'auto') {
1714
+ delete keyframe.height;
1715
+ }
1716
+ if (keyframe.transform === 'none') {
1717
+ delete keyframe.transform;
1718
+ }
1719
+ if (moveAllFramesIntoViewport) {
1720
+ if (keyframe.transform == null) {
1721
+ // If a transform is not explicitly specified to override the auto
1722
+ // generated one on the pseudo element, then we need to adjust it to
1723
+ // put it back into the viewport. We don't know the offset relative to
1724
+ // the screen so instead we use the translate prop to do a relative
1725
+ // adjustment.
1726
+ // TODO: If the "transform" was manually overridden on the pseudo
1727
+ // element itself and no longer the auto generated one, then we shouldn't
1728
+ // adjust it. I'm not sure how to detect this.
1729
+ if (keyframe.translate == null || keyframe.translate === '') {
1730
+ // TODO: If there's a CSS rule targeting translate on the pseudo element
1731
+ // already we need to merge it.
1732
+ const elementTranslate: ?string = (getComputedStyle(
1733
+ targetElement,
1734
+ pseudoElement,
1735
+ ): any).translate;
1736
+ keyframe.translate = mergeTranslate(
1737
+ elementTranslate,
1738
+ '20000px 20000px',
1739
+ );
1740
+ } else {
1741
+ keyframe.translate = mergeTranslate(
1742
+ keyframe.translate,
1743
+ '20000px 20000px',
1744
+ );
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ if (moveFirstFrameIntoViewport) {
1750
+ // If this is the generated animation that does a FLIP matrix translation
1751
+ // from the old position, we need to adjust it from the out of viewport
1752
+ // position. If this is going from old to new it only applies to first
1753
+ // keyframe. Otherwise it applies to every keyframe.
1754
+ moveOldFrameIntoViewport(keyframes[0]);
1755
+ }
1756
+ const reverse = rangeStart > rangeEnd;
1757
+ const anim = targetElement.animate(keyframes, {
1758
+ pseudoElement: pseudoElement,
1759
+ // Set the timeline to the current gesture timeline to drive the updates.
1760
+ timeline: timeline,
1761
+ // We reset all easing functions to linear so that it feels like you
1762
+ // have direct impact on the transition and to avoid double bouncing
1763
+ // from scroll bouncing.
1764
+ easing: 'linear',
1765
+ // We fill in both direction for overscroll.
1766
+ fill: 'both',
1767
+ // Range start needs to be higher than range end. If it goes in reverse
1768
+ // we reverse the whole animation below.
1769
+ rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
1770
+ rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
1771
+ });
1772
+ if (!reverse) {
1773
+ // We play all gestures in reverse, except if we're in reverse direction
1774
+ // in which case we need to play it in reverse of the reverse.
1775
+ anim.reverse();
1776
+ // In Safari, there's a bug where the starting position isn't immediately
1777
+ // picked up from the ScrollTimeline for one frame.
1778
+ // $FlowFixMe[cannot-resolve-name]
1779
+ anim.currentTime = CSS.percent(100);
1780
+ }
1781
+}
1782
+
1783
export function startGestureTransition(
1784
rootContainer: Container,
1785
+ timeline: GestureTimeline,
1786
+ rangeStart: number,
1787
+ rangeEnd: number,
1788
transitionTypes: null | TransitionTypes,
1789
mutationCallback: () => void,
1790
animateCallback: () => void,
1801
});
1802
// $FlowFixMe[prop-missing]
1803
ownerDocument.__reactViewTransition = transition;
1438
- let blockingAnim = null;
1439
- const readyCallback = () => {
1804
+ const readyCallback = (x: any) => {
1805
+ const documentElement: Element = (ownerDocument.documentElement: any);
1806
+ // Loop through all View Transition Animations.
1807
+ const animations = documentElement.getAnimations({subtree: true});
1808
+ // First do a pass to collect all known group and new items so we can look
1809
+ // up if they exist later.
1810
+ const foundGroups: Set<string> = new Set();
1811
+ const foundNews: Set<string> = new Set();
1812
+ for (let i = 0; i < animations.length; i++) {
1813
+ // $FlowFixMe
1814
+ const pseudoElement: ?string = animations[i].effect.pseudoElement;
1815
+ if (pseudoElement == null) {
1816
+ } else if (pseudoElement.startsWith('::view-transition-group')) {
1817
+ foundGroups.add(pseudoElement.slice(23));
1818
+ } else if (pseudoElement.startsWith('::view-transition-new')) {
1819
+ // TODO: This is not really a sufficient detection because if the new
1820
+ // pseudo element might exist but have animations disabled on it.
1821
+ foundNews.add(pseudoElement.slice(21));
1822
+ }
1823
+ }
1824
+ for (let i = 0; i < animations.length; i++) {
1825
+ const anim = animations[i];
1826
+ const effect: KeyframeEffect = (anim.effect: any);
1827
+ // $FlowFixMe
1828
+ const pseudoElement: ?string = effect.pseudoElement;
1829
+ if (
1830
+ pseudoElement != null &&
1831
+ pseudoElement.startsWith('::view-transition')
1832
+ ) {
1833
+ // Ideally we could mutate the existing animation but unfortunately
1834
+ // the mutable APIs seem less tested and therefore are lacking or buggy.
1835
+ // Therefore we create a new animation instead.
1836
+ anim.cancel();
1837
+ let isGeneratedGroupAnim = false;
1838
+ let isExitGroupAnim = false;
1839
+ if (pseudoElement.startsWith('::view-transition-group')) {
1840
+ const groupName = pseudoElement.slice(23);
1841
+ if (foundNews.has(groupName)) {
1842
+ // If this has both "new" and "old" state we expect this to be an auto-generated
1843
+ // animation that started outside the viewport. We need to adjust this first frame
1844
+ // to be inside the viewport.
1845
+ // $FlowFixMe[prop-missing]
1846
+ const animationName: ?string = anim.animationName;
1847
+ isGeneratedGroupAnim =
1848
+ animationName != null &&
1849
+ // $FlowFixMe[prop-missing]
1850
+ animationName.startsWith('-ua-view-transition-group-anim-');
1851
+ } else {
1852
+ // If this has only an "old" state then the pseudo element will be outside
1853
+ // the viewport. If any keyframes don't override "transform" we need to
1854
+ // adjust them.
1855
+ isExitGroupAnim = true;
1856
+ }
1857
+ // TODO: If this has only an old state and no new state,
1858
+ }
1859
+ animateGesture(
1860
+ effect.getKeyframes(),
1861
+ // $FlowFixMe: Always documentElement atm.
1862
+ effect.target,
1863
+ pseudoElement,
1864
+ timeline,
1865
+ rangeStart,
1866
+ rangeEnd,
1867
+ isGeneratedGroupAnim,
1868
+ isExitGroupAnim,
1869
+ );
1870
+ if (pseudoElement.startsWith('::view-transition-old')) {
1871
+ const groupName = pseudoElement.slice(21);
1872
+ if (!foundGroups.has(groupName) && !foundNews.has(groupName)) {
1873
+ foundGroups.add(groupName);
1874
+ // We haven't seen any group animation with this name. Since the old
1875
+ // state was outside the viewport we need to put it back. Since we
1876
+ // can't programmatically target the element itself, we use an
1877
+ // animation to adjust it.
1878
+ // This usually happens for exit animations where the element has
1879
+ // the old position.
1880
+ // If we also have a "new" state then we skip this because it means
1881
+ // someone manually disabled the auto-generated animation. We need to
1882
+ // treat the old state as having the position of the "new" state which
1883
+ // will happen by default.
1884
+ const pseudoElementName = '::view-transition-group' + groupName;
1885
+ animateGesture(
1886
+ [{}, {}],
1887
+ // $FlowFixMe: Always documentElement atm.
1888
+ effect.target,
1889
+ pseudoElementName,
1890
+ timeline,
1891
+ rangeStart,
1892
+ rangeEnd,
1893
+ false,
1894
+ true, // We let the helper apply the translate
1895
+ );
1896
+ }
1897
+ }
1898
+ }
1899
+ }
1900
// View Transitions with ScrollTimeline has a quirk where they end if the
1901
// ScrollTimeline ever reaches 100% but that doesn't mean we're done because
1902
// you can swipe back again. We can prevent this by adding a paused Animation
1903
// that never stops. This seems to keep all running Animations alive until
1904
// we explicitly abort (or something forces the View Transition to cancel).
1445
- const documentElement: Element = (ownerDocument.documentElement: any);
1446
- blockingAnim = documentElement.animate([{}, {}], {
1905
+ const blockingAnim = documentElement.animate([{}, {}], {
1906
pseudoElement: '::view-transition',
1907
duration: 1,
1908
});
1909
blockingAnim.pause();
1910
animateCallback();
1911
};
1453
- transition.ready.then(readyCallback, readyCallback);
1912
+ // In Chrome, "new" animations are not ready in the ready callback. We have to wait
1913
+ // until requestAnimationFrame before we can observe them through getAnimations().
1914
+ // However, in Safari, that would cause a flicker because we're applying them late.
1915
+ // TODO: Think of a feature detection for this instead.
1916
+ const readyForAnimations =
1917
+ navigator.userAgent.indexOf('Chrome') !== -1
1918
+ ? () => requestAnimationFrame(readyCallback)
1919
+ : readyCallback;
1920
+ transition.ready.then(readyForAnimations, readyCallback);
1921
transition.finished.then(() => {
1455
- if (blockingAnim !== null) {
1456
- // In Safari, we need to manually clear this or it'll block future transitions.
1457
- blockingAnim.cancel();
1922
+ // In Safari, we need to manually cancel all manually start animations
1923
+ // or it'll block future transitions.
1924
+ const documentElement: Element = (ownerDocument.documentElement: any);
1925
+ const animations = documentElement.getAnimations({subtree: true});
1926
+ for (let i = 0; i < animations.length; i++) {
1927
+ const anim = animations[i];
1928
+ const effect: KeyframeEffect = (anim.effect: any);
1929
+ // $FlowFixMe
1930
+ const pseudo: ?string = effect.pseudoElement;
1931
+ if (pseudo != null && pseudo.startsWith('::view-transition')) {
1932
+ anim.cancel();
1933
+ }
1934
}
1935
// $FlowFixMe[prop-missing]
1936
if (ownerDocument.__reactViewTransition === transition) {