@@ -2,20 +2,13 @@ import debounce from 'lodash.debounce'
22import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
33
44import type { SwiperContextValue , SwiperProps } from './types'
5+ import { useInitialScroll } from './useInitialScroll'
56import { useInterval } from './utils'
67
78export const useSwiperViewModel = ( { children, store } : SwiperProps ) : SwiperContextValue => {
89 const { autoplay, navigation, slides } = store
910 const { currentPage, setCurrentPage } = slides
1011 const ref = useRef < HTMLUListElement | null > ( null )
11- const hasInitializedRef = useRef ( false )
12- // Pending frame of the deferred initial scroll, `undefined` once it has run.
13- const initFrameRef = useRef < number > ( )
14- // Swallows the scroll-snap correction that follows our own initial scroll.
15- const skipSnapBackRef = useRef ( false )
16- const skipNextPageScrollRef = useRef ( false )
17- // Read inside the deferred frame so it reflects the viewport-corrected perView.
18- const firstPageToShowRef = useRef ( 0 )
1912
2013 const [ slidesLength , setSlidesLength ] = useState ( 0 )
2114 const [ isPrevDisabled , setIsPrevDisabled ] = useState ( false )
@@ -56,21 +49,47 @@ export const useSwiperViewModel = ({ children, store }: SwiperProps): SwiperCont
5649 }
5750 } , [ numberOfPage , currentPage , slides . currentSlidesPerView , ref , setCurrentPage , slides . gap ] )
5851
52+ // Navigation functions
53+
54+ const goTo = useCallback (
55+ ( page : number , isFirstInit = false ) => {
56+ const sliderContainer = ref ?. current
57+ const childWidth = sliderContainer ?. children ?. [ 0 ] ?. getBoundingClientRect ( ) ?. width || 0
58+
59+ sliderContainer ?. scrollTo ( {
60+ // We don't want to have a scroll effect when we first render the swiper
61+ behavior : ! isFirstInit ? 'smooth' : 'auto' ,
62+ left : page * ( childWidth + slides . gap ) * slides . currentSlidesPerView ,
63+ top : 0 ,
64+ } )
65+ } ,
66+ [ slides . currentSlidesPerView , slides . gap , ref ]
67+ )
68+
69+ const consumeSnapBackSkip = useInitialScroll ( {
70+ alignment : slides . alignment ,
71+ currentPage,
72+ currentSlidesPerView : slides . currentSlidesPerView ,
73+ goTo,
74+ initialIndex : slides . initialIndex ,
75+ numberOfPage,
76+ setCurrentPage,
77+ slidesLength,
78+ } )
79+
5980 const handleScroll = useMemo (
6081 ( ) =>
6182 debounce ( ( ) => {
6283 // Navigation state should always reflect the real geometry, guard or not
6384 getNavigationState ( )
6485
65- if ( skipSnapBackRef . current ) {
66- skipSnapBackRef . current = false
67-
86+ if ( consumeSnapBackSkip ( ) ) {
6887 return
6988 }
7089
7190 updatePage ( )
7291 } , 100 ) ,
73- [ getNavigationState , updatePage ]
92+ [ getNavigationState , updatePage , consumeSnapBackSkip ]
7493 )
7594
7695 // Cancel exactly one pending call on unmount. Depending on `[handleScroll]`
@@ -81,23 +100,6 @@ export const useSwiperViewModel = ({ children, store }: SwiperProps): SwiperCont
81100
82101 useEffect ( ( ) => ( ) => handleScrollRef . current . cancel ( ) , [ ] )
83102
84- // Navigation functions
85-
86- const goTo = useCallback (
87- ( page : number , isFirstInit = false ) => {
88- const sliderContainer = ref ?. current
89- const childWidth = sliderContainer ?. children ?. [ 0 ] ?. getBoundingClientRect ( ) ?. width || 0
90-
91- sliderContainer ?. scrollTo ( {
92- // We don't want to have a scroll effect when we first render the swiper
93- behavior : ! isFirstInit ? 'smooth' : 'auto' ,
94- left : page * ( childWidth + slides . gap ) * slides . currentSlidesPerView ,
95- top : 0 ,
96- } )
97- } ,
98- [ slides . currentSlidesPerView , slides . gap , ref ]
99- )
100-
101103 const isFirstPage = currentPage === 0
102104 const isLastPage = currentPage === numberOfPage - 1
103105
@@ -143,99 +145,11 @@ export const useSwiperViewModel = ({ children, store }: SwiperProps): SwiperCont
143145 return ( ) => window . removeEventListener ( 'keydown' , handleKeys )
144146 } , [ goPrev , goNext ] )
145147
146- const pageForInitialIndex =
147- slides . alignment === 'center'
148- ? // if centeredSlides is true, we calculate which number is the middle page
149- Math . floor ( numberOfPage / 2 )
150- : // if centeredSlides is false, we calculate on which page the number in firstSlideToShow props is
151- Math . ceil ( slides . initialIndex / slides . currentSlidesPerView ) - 1
152-
153- // `initialIndex` is 1-based, so its default of `0` computes to page -1, and a
154- // value past the last slide would scroll beyond the end and then fight the
155- // `isLastPage` branch of `updatePage`. An explicit `initialIndex: undefined`
156- // shadows the default and computes to NaN, so guard that too.
157- const firstPageToShow = Number . isFinite ( pageForInitialIndex )
158- ? Math . min ( Math . max ( pageForInitialIndex , 0 ) , numberOfPage - 1 )
159- : 0
160-
161- firstPageToShowRef . current = firstPageToShow
162-
163- useEffect ( ( ) => {
164- // Only navigate to the initial page once, when slidesLength is first known.
165- // `hasInitializedRef` is set synchronously as it also gates the
166- // external-`setCurrentPage` effect below.
167- if ( ! slidesLength || hasInitializedRef . current ) {
168- return
169- }
170-
171- hasInitializedRef . current = true
172-
173- // The scroll must land after the first paint: issued during the first layout
174- // pass it is reverted to 0 by the browser's `scroll-snap-type: x mandatory`
175- // correction. One frame is not enough — a callback scheduled from a passive
176- // effect still runs before the next paint. The second frame also lets
177- // `useViewportSize` commit, so `firstPageToShowRef` is breakpoint-correct.
178- initFrameRef . current = requestAnimationFrame ( ( ) => {
179- initFrameRef . current = requestAnimationFrame ( ( ) => {
180- initFrameRef . current = undefined
181-
182- const initialPage = firstPageToShowRef . current
183-
184- // The track already starts on the first page, so it needs no scroll
185- if ( initialPage <= 0 ) {
186- return
187- }
188-
189- skipSnapBackRef . current = true
190- goTo ( initialPage , true )
191- // `currentPage` is otherwise only synced by the scroll handler, which we
192- // just told to skip this scroll — so set it here, reading the freshest
193- // value to arm the redundant-scroll skip only if the page really changes.
194- setCurrentPage ( current => {
195- if ( current === initialPage ) {
196- return current
197- }
198-
199- skipNextPageScrollRef . current = true
200-
201- return initialPage
202- } )
203- } )
204- } )
205-
206- return ( ) => {
207- if ( initFrameRef . current === undefined ) {
208- return
209- }
210-
211- // The deferred scroll never ran (unmount, or a StrictMode remount) — let
212- // the next mount schedule it again
213- cancelAnimationFrame ( initFrameRef . current )
214- initFrameRef . current = undefined
215- hasInitializedRef . current = false
216- }
217- // eslint-disable-next-line react-hooks/exhaustive-deps
218- } , [ slidesLength ] )
219-
220148 // if the childrens changed we need to check again the arrow states
221149 useEffect ( ( ) => {
222150 getNavigationState ( )
223151 } , [ getNavigationState , children ] )
224152
225- // Triggers navigation when currentPage is changed by external setCurrentPage calls
226- useEffect ( ( ) => {
227- if ( skipNextPageScrollRef . current ) {
228- // The initial scroll already put us on this page, no need to scroll again
229- skipNextPageScrollRef . current = false
230-
231- return
232- }
233-
234- if ( hasInitializedRef . current ) {
235- goTo ( currentPage )
236- }
237- } , [ currentPage , goTo ] )
238-
239153 const contextValue = useMemo (
240154 ( ) => ( {
241155 navigation : {
0 commit comments