diff --git a/fixtures/view-transition/src/components/SwipeRecognizer.js b/fixtures/view-transition/src/components/SwipeRecognizer.js index 81b544dd1f..df4d743e1b 100644 --- a/fixtures/view-transition/src/components/SwipeRecognizer.js +++ b/fixtures/view-transition/src/components/SwipeRecognizer.js @@ -6,6 +6,14 @@ import React, { } from 'react'; import ScrollTimelinePolyfill from 'animation-timelines/scroll-timeline'; +import TouchPanTimeline from 'animation-timelines/touch-pan-timeline'; + +const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; +const isSafariMobile = + ua.indexOf('Safari') !== -1 && + (ua.indexOf('iPhone') !== -1 || + ua.indexOf('iPad') !== -1 || + ua.indexOf('iPod') !== -1); // Example of a Component that can recognize swipe gestures using a ScrollTimeline // without scrolling its own content. Allowing it to be used as an inert gesture @@ -23,13 +31,61 @@ export default function SwipeRecognizer({ const scrollRef = useRef(null); const activeGesture = useRef(null); + const touchTimeline = useRef(null); + + function onTouchStart(event) { + if (!isSafariMobile && typeof ScrollTimeline === 'function') { + // If not Safari and native ScrollTimeline is supported, then we use that. + return; + } + if (touchTimeline.current) { + // We can catch the gesture before it settles. + return; + } + const scrollElement = scrollRef.current; + const bounds = + axis === 'x' ? scrollElement.clientWidth : scrollElement.clientHeight; + const range = + direction === 'left' || direction === 'up' ? [bounds, 0] : [0, -bounds]; + const timeline = new TouchPanTimeline({ + touch: event, + source: scrollElement, + axis: axis, + range: range, + snap: range, + }); + touchTimeline.current = timeline; + timeline.settled.then(() => { + if (touchTimeline.current !== timeline) { + return; + } + touchTimeline.current = null; + const changed = + direction === 'left' || direction === 'up' + ? timeline.currentTime < 50 + : timeline.currentTime > 50; + onGestureEnd(changed); + }); + } + + function onTouchEnd() { + if (activeGesture.current === null) { + // If we didn't start a gesture before we release, we can release our + // timeline. + touchTimeline.current = null; + } + } + function onScroll() { if (activeGesture.current !== null) { return; } let scrollTimeline; - if (typeof ScrollTimeline === 'function') { + if (touchTimeline.current) { + // We're in a polyfilled touch gesture. Let's use that timeline instead. + scrollTimeline = touchTimeline.current; + } else if (typeof ScrollTimeline === 'function') { // eslint-disable-next-line no-undef scrollTimeline = new ScrollTimeline({ source: scrollRef.current, @@ -57,7 +113,23 @@ export default function SwipeRecognizer({ } ); } + function onGestureEnd(changed) { + // Reset scroll + if (changed) { + // Trigger side-effects + startTransition(action); + } + if (activeGesture.current !== null) { + const cancelGesture = activeGesture.current; + activeGesture.current = null; + cancelGesture(); + } + } function onScrollEnd() { + if (touchTimeline.current) { + // We have a touch gesture controlling the swipe. + return; + } let changed; const scrollElement = scrollRef.current; if (axis === 'x') { @@ -75,16 +147,7 @@ export default function SwipeRecognizer({ ? scrollElement.scrollTop < halfway : scrollElement.scrollTop > halfway; } - // Reset scroll - if (changed) { - // Trigger side-effects - startTransition(action); - } - if (activeGesture.current !== null) { - const cancelGesture = activeGesture.current; - activeGesture.current = null; - cancelGesture(); - } + onGestureEnd(changed); } useEffect(() => { @@ -176,6 +239,9 @@ export default function SwipeRecognizer({ return (