@aiquants/virtualscroll 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +68 -0
- package/dist/VirtualScroll.d.cts +88 -0
- package/dist/VirtualScroll.d.ts +88 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1562 -1492
- package/dist/residualQuantizer.d.cts +95 -0
- package/dist/residualQuantizer.d.ts +96 -0
- package/dist/residualQuantizer.d.ts.map +1 -0
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +216 -3
- package/src/index.ts +3 -0
- package/src/residualQuantizer.ts +133 -0
- package/src/styles/virtualscroll.css +15 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module residualQuantizer
|
|
3
|
+
* @description Residual-carrying delta quantizer for consumers that snap scroll positions to a
|
|
4
|
+
* fixed quantum (row height, column width). High-resolution trackpads emit many sub-quantum
|
|
5
|
+
* deltas; a consumer that rounds each delta independently rounds every one of them to zero and
|
|
6
|
+
* the list never moves. This accumulator carries the sub-quantum remainder across events and
|
|
7
|
+
* emits whole quanta as soon as they accumulate.
|
|
8
|
+
*
|
|
9
|
+
* @description スクロール位置を固定の量子 (行高・列幅) へスナップする消費側のための、
|
|
10
|
+
* 残差持ち越し型のデルタ量子化器。高解像度トラックパッドは 1 量子未満のデルタを大量に発行するため、
|
|
11
|
+
* イベントごとに独立して丸める消費側では**すべてが 0 に丸められ、ゆっくり撫でると 1px も動かない**。
|
|
12
|
+
* 本量子化器は量子未満の余りをイベントを跨いで持ち越し、貯まった分だけ量子単位で放出する。
|
|
13
|
+
*
|
|
14
|
+
* ❗ **時間は扱わない (意図的)。** アイドルタイムアウトでの残差破棄は「いつ捨てるか」の方針が
|
|
15
|
+
* 消費側の UI ごとに異なり、内蔵すると幽霊スクロール (古い残差が後から発火) か不感帯のどちらかを
|
|
16
|
+
* パッケージが押し付けることになる。時間起点の破棄が要る消費側は、自前のタイマーから
|
|
17
|
+
* {@link ResidualQuantizer.reset} を呼ぶこと。既定の防御は**方向反転での残差破棄**のみで、
|
|
18
|
+
* これは時間に依存せず「逆へ動かし始めたのに古い残差が先に順方向へ発火する」事故だけを塞ぐ。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link createResidualQuantizer}.
|
|
23
|
+
* {@link createResidualQuantizer} のオプション。
|
|
24
|
+
*/
|
|
25
|
+
export type ResidualQuantizerOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Quantum (px) each emission is a multiple of. Must be finite and positive — anything else
|
|
28
|
+
* throws at creation time (fail fast; a silent default would hide the caller's bug).
|
|
29
|
+
* 放出量の単位となる量子 (px)。有限かつ正であること — それ以外は生成時に throw する
|
|
30
|
+
* (Fail Fast。既定値へ黙って読み替えると呼び出し側のバグを隠す)。
|
|
31
|
+
*/
|
|
32
|
+
quantum: number
|
|
33
|
+
/**
|
|
34
|
+
* Drop the carried residue when the incoming delta's direction opposes it (default: `true`).
|
|
35
|
+
* Without this, scrolling +20px (quantum 24, nothing emitted) and then reversing emits the
|
|
36
|
+
* first reverse quantum "late" — the stale forward residue eats part of the reverse motion.
|
|
37
|
+
* 入力デルタの向きが持ち越し残差と逆のとき残差を捨てる (既定 `true`)。捨てないと、
|
|
38
|
+
* +20px (量子 24 で未放出) の後に反転したとき、古い順方向残差が逆方向の動きを食い、
|
|
39
|
+
* 最初の逆方向量子の発火が「遅れて」体感される。
|
|
40
|
+
*/
|
|
41
|
+
resetOnDirectionChange?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A quantizer instance. One instance per quantized axis — sharing one across axes mixes residues.
|
|
46
|
+
* 量子化器インスタンス。量子化する軸ごとに 1 つ持つこと — 軸間で共有すると残差が混線する。
|
|
47
|
+
*/
|
|
48
|
+
export type ResidualQuantizer = {
|
|
49
|
+
/**
|
|
50
|
+
* Accumulates `delta` and returns the emission: a multiple of the quantum (possibly 0),
|
|
51
|
+
* signed toward the accumulated direction. Non-finite deltas AND deltas whose magnitude
|
|
52
|
+
* exceeds `Number.MAX_SAFE_INTEGER` emit 0 and leave the residue untouched — beyond 2^53
|
|
53
|
+
* the float product stops being an EXACT multiple of the quantum, which is precisely the
|
|
54
|
+
* property a grid-snapping consumer relies on.
|
|
55
|
+
* `delta` を蓄積し、放出量 (量子の倍数。0 のこともある) を蓄積方向の符号で返す。
|
|
56
|
+
* 非有限のデルタ、および絶対値が `Number.MAX_SAFE_INTEGER` を超えるデルタは 0 を返し
|
|
57
|
+
* 残差を汚さない — 2^53 超では浮動小数点積が量子の**正確な**倍数でなくなり、グリッド
|
|
58
|
+
* スナップ消費側が依拠する性質そのものが壊れるため。
|
|
59
|
+
*
|
|
60
|
+
* @param delta - Incoming delta (px) / 入力デルタ (px)
|
|
61
|
+
* @returns Emitted amount (multiple of the quantum) / 放出量 (量子の倍数)
|
|
62
|
+
*/
|
|
63
|
+
push: (delta: number) => number
|
|
64
|
+
/**
|
|
65
|
+
* Returns the carried residue without mutating it (|residue| < quantum).
|
|
66
|
+
* 持ち越し中の残差を変更せずに返す (|残差| < 量子)。
|
|
67
|
+
*
|
|
68
|
+
* @returns Current residue (px) / 現在の残差 (px)
|
|
69
|
+
*/
|
|
70
|
+
peekResidue: () => number
|
|
71
|
+
/**
|
|
72
|
+
* Clears the residue. Call from consumer-side idle timers, on pointer leave, or when the
|
|
73
|
+
* quantized target (row/column layout) changes under the accumulator.
|
|
74
|
+
* 残差を破棄する。消費側のアイドルタイマー・ポインタ離脱・量子化対象 (行/列レイアウト) の
|
|
75
|
+
* 変更時に呼ぶ。
|
|
76
|
+
*/
|
|
77
|
+
reset: () => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a residual-carrying delta quantizer.
|
|
82
|
+
* 残差持ち越し型のデルタ量子化器を生成する処理。
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const quantizer = createResidualQuantizer({ quantum: rowHeight })
|
|
86
|
+
* const handleWheelVertical = (deltaY: number) => {
|
|
87
|
+
* const emitted = quantizer.push(deltaY)
|
|
88
|
+
* if (emitted !== 0) {
|
|
89
|
+
* snapScrollBy(emitted) // 常に量子の倍数
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @param options - Quantizer options / 量子化器のオプション
|
|
95
|
+
* @returns A new quantizer instance / 新しい量子化器インスタンス
|
|
96
|
+
* @throws {TypeError} When `quantum` is not a finite positive number / `quantum` が有限の正数でない場合
|
|
97
|
+
*/
|
|
98
|
+
export const createResidualQuantizer = (options: ResidualQuantizerOptions): ResidualQuantizer => {
|
|
99
|
+
const { quantum, resetOnDirectionChange = true } = options
|
|
100
|
+
// Fail Fast: 不正な量子は生成時に拒否する (押すたび NaN を配る量子化器を作らせない)
|
|
101
|
+
if (!(Number.isFinite(quantum) && quantum > 0)) {
|
|
102
|
+
throw new TypeError(`[createResidualQuantizer] quantum must be a finite positive number, received ${quantum}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let residue = 0
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
push: (delta: number): number => {
|
|
109
|
+
// 非有限デルタは残差を汚さず 0 を返す (蓄積器に NaN が入ると以後全放出が死ぬ)。
|
|
110
|
+
// ❗ |delta| > Number.MAX_SAFE_INTEGER も同じ契約で見送る: 2^53 超では
|
|
111
|
+
// trunc(residue / quantum) * quantum が浮動小数点丸めで「量子の正確な倍数」で
|
|
112
|
+
// なくなり、グリッドスナップ消費側へ桁ずれした放出を渡してしまう (実測:
|
|
113
|
+
// quantum 3 に対し 1e17 % 3 === 1)。残差は常に |残差| < 量子で汚染されない
|
|
114
|
+
if (!Number.isFinite(delta) || Math.abs(delta) > Number.MAX_SAFE_INTEGER) {
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
// 方向反転で古い残差を捨てる (逆方向の動き出しを古い順方向残差に食わせない)
|
|
118
|
+
if (resetOnDirectionChange && residue !== 0 && delta !== 0 && Math.sign(delta) !== Math.sign(residue)) {
|
|
119
|
+
residue = 0
|
|
120
|
+
}
|
|
121
|
+
residue += delta
|
|
122
|
+
// trunc は 0 方向への切り捨てなので、正負どちらの蓄積でも「量子に満たない分」が残差に残る。
|
|
123
|
+
// ❗ 末尾の + 0 は -0 の正規化 (負方向の空放出は -0 になり、Object.is 比較や表示を汚す)
|
|
124
|
+
const emitted = Math.trunc(residue / quantum) * quantum + 0
|
|
125
|
+
residue -= emitted
|
|
126
|
+
return emitted
|
|
127
|
+
},
|
|
128
|
+
peekResidue: () => residue,
|
|
129
|
+
reset: () => {
|
|
130
|
+
residue = 0
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -333,3 +333,18 @@
|
|
|
333
333
|
position: absolute;
|
|
334
334
|
width: 100%;
|
|
335
335
|
}
|
|
336
|
+
|
|
337
|
+
/* 支援技術専用ライブリージョン (liveRegion prop)。標準の visually-hidden パターン:
|
|
338
|
+
絶対配置 + 1px クリップで視覚とレイアウトから完全に消しつつ、AT からは読める。
|
|
339
|
+
display:none / visibility:hidden にすると aria-live ごと無効化されるため使えない */
|
|
340
|
+
.aqvs-live-region {
|
|
341
|
+
position: absolute;
|
|
342
|
+
width: 1px;
|
|
343
|
+
height: 1px;
|
|
344
|
+
margin: -1px;
|
|
345
|
+
padding: 0;
|
|
346
|
+
border: 0;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
clip-path: inset(50%);
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
}
|