feat : update modal style
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {describe, expect, it, vi} from 'vitest'
|
||||
import {defineComponent, nextTick, ref} from 'vue'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import {
|
||||
@@ -90,4 +90,31 @@ describe('useInfiniteWheel — composable', () => {
|
||||
api().onKeydown(new KeyboardEvent('keydown', {key: 'ArrowUp'}))
|
||||
expect(changes.at(-1)).toBe(23)
|
||||
})
|
||||
|
||||
// Anti-boucle navigateur : un scroll programmatique déclenche une rafale d'évènements
|
||||
// scroll (animation/snap). Ils ne doivent PAS être pris pour du scroll utilisateur,
|
||||
// sinon settle() ré-émet en boucle et corrompt le patch DOM de Vue.
|
||||
it('n\'émet pas en double quand un scroll programmatique déclenche une rafale de scroll', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const changes: number[] = []
|
||||
const {wrapper, api} = mountWheelHarness(9, (i) => changes.push(i))
|
||||
await nextTick()
|
||||
const el = wrapper.element as HTMLElement
|
||||
changes.length = 0
|
||||
|
||||
api().scrollToIndex(12)
|
||||
|
||||
el.dispatchEvent(new Event('scroll'))
|
||||
el.dispatchEvent(new Event('scroll'))
|
||||
el.dispatchEvent(new Event('scroll'))
|
||||
|
||||
vi.advanceTimersByTime(300)
|
||||
|
||||
expect(changes).toEqual([12])
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,8 +36,25 @@ export function useInfiniteWheel(
|
||||
options: UseInfiniteWheelOptions,
|
||||
) {
|
||||
const centeredIndex = ref(options.initialIndex())
|
||||
let programmatic = false
|
||||
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
|
||||
// Fenêtre de suppression : ignore les évènements scroll provoqués par NOS
|
||||
// repositionnements programmatiques (et les réajustements de scroll-snap), qui
|
||||
// arrivent en rafale. Un booléen one-shot n'en absorberait qu'un seul : les
|
||||
// suivants seraient pris pour du scroll utilisateur → settle() → onChange en
|
||||
// boucle (re-render ré-entrant qui corrompt le patch DOM dans le navigateur).
|
||||
let suppressed = false
|
||||
let suppressTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Scroll programmatique INSTANTANÉ : pas de 'smooth', dont l'animation multi-frames
|
||||
// émettrait justement la rafale d'évènements scroll problématique.
|
||||
function applyScroll(top: number) {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
suppressed = true
|
||||
if (suppressTimer) clearTimeout(suppressTimer)
|
||||
suppressTimer = setTimeout(() => { suppressed = false }, 100)
|
||||
el.scrollTop = top
|
||||
}
|
||||
|
||||
function readCentered() {
|
||||
const el = containerRef.value
|
||||
@@ -48,38 +65,22 @@ export function useInfiniteWheel(
|
||||
function settle() {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const corrected = loopCorrection(el.scrollTop, options.itemHeight, options.length)
|
||||
if (corrected !== el.scrollTop) {
|
||||
programmatic = true
|
||||
el.scrollTop = corrected
|
||||
}
|
||||
readCentered()
|
||||
options.onChange(centeredIndex.value)
|
||||
const corrected = loopCorrection(el.scrollTop, options.itemHeight, options.length)
|
||||
if (corrected !== el.scrollTop) applyScroll(corrected)
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (programmatic) {
|
||||
programmatic = false
|
||||
return
|
||||
}
|
||||
if (suppressed) return
|
||||
readCentered()
|
||||
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
||||
scrollEndTimer = setTimeout(settle, 120)
|
||||
}
|
||||
|
||||
function scrollToIndex(index: number, smooth = true) {
|
||||
const el = containerRef.value
|
||||
function scrollToIndex(index: number) {
|
||||
centeredIndex.value = index
|
||||
if (el) {
|
||||
programmatic = true
|
||||
const top = scrollTopForValueIndex(index, options.itemHeight, options.length)
|
||||
if (smooth && typeof el.scrollTo === 'function') {
|
||||
el.scrollTo({top, behavior: 'smooth'})
|
||||
}
|
||||
else {
|
||||
el.scrollTop = top
|
||||
}
|
||||
}
|
||||
applyScroll(scrollTopForValueIndex(index, options.itemHeight, options.length))
|
||||
options.onChange(index)
|
||||
}
|
||||
|
||||
@@ -103,13 +104,13 @@ export function useInfiniteWheel(
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
el.addEventListener('scroll', onScroll, {passive: true})
|
||||
programmatic = true
|
||||
el.scrollTop = scrollTopForValueIndex(options.initialIndex(), options.itemHeight, options.length)
|
||||
applyScroll(scrollTopForValueIndex(options.initialIndex(), options.itemHeight, options.length))
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
containerRef.value?.removeEventListener('scroll', onScroll)
|
||||
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
||||
if (suppressTimer) clearTimeout(suppressTimer)
|
||||
})
|
||||
|
||||
return {centeredIndex, scrollToIndex, step, onKeydown}
|
||||
|
||||
Reference in New Issue
Block a user