/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
fixtures/view-transition/src/components/SwipeRecognizer.js
255 строк
7 KB
Sebastian Markbåge
Disable ScrollTimeline in Safari (#33499)
03 июл 2025, 00:01
Не верифицирован
03 июл 2025, 00:01
dcf83f7
Код
Авторство
О чём код?
import React, { useRef, useEffect, startTransition, unstable_startGestureTransition as startGestureTransition, } 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 // recognizer to drive a View Transition. export default function SwipeRecognizer({ action, children, direction, gesture, }) { if (direction == null) { direction = 'left'; } const axis = direction === 'left' || direction === 'right' ? 'x' : 'y'; 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 (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, axis: axis, }); } else { scrollTimeline = new ScrollTimelinePolyfill({ source: scrollRef.current, axis: axis, }); } activeGesture.current = startGestureTransition( scrollTimeline, () => { gesture(direction); }, direction === 'left' || direction === 'up' ? { rangeStart: 100, rangeEnd: 0, } : { rangeStart: 0, rangeEnd: 100, } ); } 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') { const halfway = (scrollElement.scrollWidth - scrollElement.clientWidth) / 2; changed = direction === 'left' ? scrollElement.scrollLeft < halfway : scrollElement.scrollLeft > halfway; } else { const halfway = (scrollElement.scrollHeight - scrollElement.clientHeight) / 2; changed = direction === 'up' ? scrollElement.scrollTop < halfway : scrollElement.scrollTop > halfway; } onGestureEnd(changed); } useEffect(() => { const scrollElement = scrollRef.current; switch (direction) { case 'left': scrollElement.scrollLeft = scrollElement.scrollWidth - scrollElement.clientWidth; break; case 'right': scrollElement.scrollLeft = 0; break; case 'up': scrollElement.scrollTop = scrollElement.scrollHeight - scrollElement.clientHeight; break; case 'down': scrollElement.scrollTop = 0; break; default: break; } }, [direction]); const scrollStyle = { position: 'relative', padding: '0px', margin: '0px', border: '0px', width: axis === 'x' ? '100%' : null, height: axis === 'y' ? '100%' : null, overflow: 'scroll hidden', // Disable overscroll on Safari which moves the sticky content. // Unfortunately, this also means that we disable chaining. We should only disable // it if the parent is not scrollable in this axis. overscrollBehaviorX: axis === 'x' ? 'none' : 'auto', overscrollBehaviorY: axis === 'y' ? 'none' : 'auto', scrollSnapType: axis + ' mandatory', scrollbarWidth: 'none', }; const overScrollStyle = { position: 'relative', padding: '0px', margin: '0px', border: '0px', width: axis === 'x' ? '200%' : null, height: axis === 'y' ? '200%' : null, }; const snapStartStyle = { position: 'absolute', padding: '0px', margin: '0px', border: '0px', width: axis === 'x' ? '50%' : '100%', height: axis === 'y' ? '50%' : '100%', left: '0px', top: '0px', scrollSnapAlign: 'center', }; const snapEndStyle = { position: 'absolute', padding: '0px', margin: '0px', border: '0px', width: axis === 'x' ? '50%' : '100%', height: axis === 'y' ? '50%' : '100%', right: '0px', bottom: '0px', scrollSnapAlign: 'center', }; // By placing the content in a sticky box we ensure that it doesn't move when // we scroll. Unless done so by the View Transition. const stickyStyle = { position: 'sticky', padding: '0px', margin: '0px', border: '0px', left: '0px', top: '0px', width: axis === 'x' ? '50%' : null, height: axis === 'y' ? '50%' : null, overflow: 'hidden', }; return ( <div style={scrollStyle} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} onTouchCancel={onTouchEnd} onScroll={onScroll} onScrollEnd={onScrollEnd} ref={scrollRef}> <div style={overScrollStyle}> <div style={snapStartStyle} /> <div style={snapEndStyle} /> <div style={stickyStyle}>{children}</div> </div> </div> ); }