feat : update modal style

This commit is contained in:
2026-05-27 14:00:12 +02:00
parent 4d0c4d9a23
commit 3d0c881892
8 changed files with 115 additions and 48 deletions
@@ -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}