feat : math de molette infinie pour le sélecteur d'heure (MUI-39)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 09:59:28 +02:00
parent 661910a8d9
commit 9fdf324b0e
2 changed files with 68 additions and 0 deletions
@@ -0,0 +1,45 @@
import {describe, expect, it} from 'vitest'
import {
CENTER_OFFSET,
VISIBLE_ROWS,
loopCorrection,
scrollTopForValueIndex,
valueIndexFromScroll,
} from './useInfiniteWheel'
const H = 40 // itemHeight
const LEN = 24 // ex. heures
describe('useInfiniteWheel — math pure', () => {
it('expose 5 lignes visibles et un offset central de 2', () => {
expect(VISIBLE_ROWS).toBe(5)
expect(CENTER_OFFSET).toBe(2)
})
it('scrollTopForValueIndex et valueIndexFromScroll font un aller-retour', () => {
for (const index of [0, 1, 9, 23]) {
const top = scrollTopForValueIndex(index, H, LEN)
expect(valueIndexFromScroll(top, H, LEN)).toBe(index)
}
})
it('valueIndexFromScroll boucle en modulo', () => {
const top = scrollTopForValueIndex(0, H, LEN)
expect(valueIndexFromScroll(top + LEN * H, H, LEN)).toBe(0)
})
it('loopCorrection laisse le scroll de la copie du milieu inchangé', () => {
const top = scrollTopForValueIndex(12, H, LEN)
expect(loopCorrection(top, H, LEN)).toBe(top)
})
it('loopCorrection ramène vers le milieu quand on dérive vers le haut', () => {
const drifted = scrollTopForValueIndex(0, H, LEN) - LEN * H
expect(loopCorrection(drifted, H, LEN)).toBe(drifted + LEN * H)
})
it('loopCorrection ramène vers le milieu quand on dérive vers le bas', () => {
const drifted = scrollTopForValueIndex(0, H, LEN) + LEN * H
expect(loopCorrection(drifted, H, LEN)).toBe(drifted - LEN * H)
})
})
@@ -0,0 +1,23 @@
export const VISIBLE_ROWS = 5
export const CENTER_OFFSET = (VISIBLE_ROWS - 1) / 2 // 2
/** Index de valeur logique (0..length-1) centré pour un scrollTop donné. */
export function valueIndexFromScroll(scrollTop: number, itemHeight: number, length: number): number {
const flat = Math.round(scrollTop / itemHeight) + CENTER_OFFSET
return ((flat % length) + length) % length
}
/** scrollTop qui centre l'index donné dans la copie du milieu (buffer à 3 copies). */
export function scrollTopForValueIndex(valueIndex: number, itemHeight: number, length: number): number {
const flat = length + valueIndex - CENTER_OFFSET
return flat * itemHeight
}
/** Recentre le scrollTop dans la copie du milieu [length, 2*length) si on a dérivé. */
export function loopCorrection(scrollTop: number, itemHeight: number, length: number): number {
const block = length * itemHeight
const centeredFlat = Math.round(scrollTop / itemHeight) + CENTER_OFFSET
if (centeredFlat < length) return scrollTop + block
if (centeredFlat >= 2 * length) return scrollTop - block
return scrollTop
}