@aiquants/virtualscroll 3.1.0 → 3.1.1

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/dist/utils.d.cts CHANGED
@@ -12,3 +12,18 @@
12
12
  * @returns The clamped value. / クランプされた値。
13
13
  */
14
14
  export declare const minmax: (value: number, min: number, max: number) => number;
15
+ /**
16
+ * Computes an element's CSS transform scale along one axis (visual px / layout px).
17
+ *
18
+ * 要素の指定軸方向 CSS transform スケール (視覚px / レイアウトpx) を算出。
19
+ * 祖先に transform: scale(...) があると client 座標 (視覚px) がレイアウトpx とズレるため、
20
+ * その比率でドラッグ量・クリック座標を補正する。分母には整数丸めされる offsetWidth/offsetHeight
21
+ * ではなく、transform の影響を受けず小数精度を持つ getComputedStyle のレイアウト寸法
22
+ * (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
23
+ * 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
24
+ *
25
+ * @param element The element to measure. / 計測対象要素。
26
+ * @param axis The axis to measure along ("x" = width, "y" = height). / 計測軸。
27
+ * @returns The visual/layout scale ratio (1 when unmeasurable). / 視覚/レイアウト比 (計測不能時 1)。
28
+ */
29
+ export declare const getAxisScale: (element: HTMLElement, axis: "x" | "y") => number;
package/dist/utils.d.ts CHANGED
@@ -12,4 +12,19 @@
12
12
  * @returns The clamped value. / クランプされた値。
13
13
  */
14
14
  export declare const minmax: (value: number, min: number, max: number) => number;
15
+ /**
16
+ * Computes an element's CSS transform scale along one axis (visual px / layout px).
17
+ *
18
+ * 要素の指定軸方向 CSS transform スケール (視覚px / レイアウトpx) を算出。
19
+ * 祖先に transform: scale(...) があると client 座標 (視覚px) がレイアウトpx とズレるため、
20
+ * その比率でドラッグ量・クリック座標を補正する。分母には整数丸めされる offsetWidth/offsetHeight
21
+ * ではなく、transform の影響を受けず小数精度を持つ getComputedStyle のレイアウト寸法
22
+ * (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
23
+ * 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
24
+ *
25
+ * @param element The element to measure. / 計測対象要素。
26
+ * @param axis The axis to measure along ("x" = width, "y" = height). / 計測軸。
27
+ * @returns The visual/layout scale ratio (1 when unmeasurable). / 視覚/レイアウト比 (計測不能時 1)。
28
+ */
29
+ export declare const getAxisScale: (element: HTMLElement, axis: "x" | "y") => number;
15
30
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAEhE,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAEhE,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,KAAG,MA0BpE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
package/src/ScrollBar.tsx CHANGED
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
8
8
  import { twMerge } from "tailwind-merge"
9
9
  import { Logger } from "./logger.ts"
10
10
  import { TapScrollCircle, type TapScrollCircleDragState, type TapScrollCircleHandle, type TapScrollCircleRenderProps } from "./TapScrollCircle.tsx"
11
- import { minmax } from "./utils.ts"
11
+ import { getAxisScale, minmax } from "./utils.ts"
12
12
 
13
13
  /**
14
14
  * Configuration for scrollbar orientation (vertical or horizontal).
@@ -1055,35 +1055,8 @@ export const ScrollBar = ({
1055
1055
  * (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
1056
1056
  * 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
1057
1057
  */
1058
- const getMainAxisScale = useCallback(
1059
- (element: HTMLElement) => {
1060
- const rect = element.getBoundingClientRect()
1061
- const visualSize = horizontal ? rect.width : rect.height
1062
- // computed style の値 (px 文字列) を数値化する。解釈不能なら 0 とみなす。
1063
- const parseSize = (value: string) => {
1064
- const parsed = Number.parseFloat(value)
1065
- return Number.isFinite(parsed) ? parsed : 0
1066
- }
1067
- const computed = window.getComputedStyle(element)
1068
- const contentSize = parseSize(horizontal ? computed.width : computed.height)
1069
- // rect (border-box) と同系になるよう content + padding + border を合成する。
1070
- const computedLayoutSize =
1071
- contentSize > 0
1072
- ? contentSize +
1073
- parseSize(horizontal ? computed.paddingLeft : computed.paddingTop) +
1074
- parseSize(horizontal ? computed.paddingRight : computed.paddingBottom) +
1075
- parseSize(horizontal ? computed.borderLeftWidth : computed.borderTopWidth) +
1076
- parseSize(horizontal ? computed.borderRightWidth : computed.borderBottomWidth)
1077
- : 0
1078
- // computed が取れない環境 (jsdom 等) では従来の offset 寸法へフォールバックする。
1079
- const layoutSize = computedLayoutSize > 0 ? computedLayoutSize : horizontal ? element.offsetWidth : element.offsetHeight
1080
- if (layoutSize <= 0 || visualSize <= 0) {
1081
- return 1
1082
- }
1083
- return visualSize / layoutSize
1084
- },
1085
- [horizontal],
1086
- )
1058
+ // 本体は共有ヘルパ getAxisScale (utils.ts — ScrollPane のポインタパンと同一 SSOT)
1059
+ const getMainAxisScale = useCallback((element: HTMLElement) => getAxisScale(element, horizontal ? "x" : "y"), [horizontal])
1087
1060
 
1088
1061
  /**
1089
1062
  * Pointer move handler for thumb dragging.
@@ -2,7 +2,7 @@ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayo
2
2
  import { twMerge } from "tailwind-merge"
3
3
  import { Logger } from "./logger.ts"
4
4
  import { ScrollBar, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_CANCEL_EVENT } from "./ScrollBar.tsx"
5
- import { minmax } from "./utils.ts"
5
+ import { getAxisScale, minmax } from "./utils.ts"
6
6
  import { resolveWheelAxes } from "./wheelAxes.ts"
7
7
  import { markWheelConsumed, wasWheelConsumed } from "./wheelConsumption.ts"
8
8
 
@@ -231,6 +231,8 @@ type DragState = {
231
231
  shouldCancelNextClick: boolean
232
232
  clickResetTimer: number | null
233
233
  velocitySamples: DragSample[]
234
+ /** press 時に採取した縦軸の視覚/レイアウト比 (祖先 transform scale — 補正の分母)。 */
235
+ scale: number
234
236
  }
235
237
 
236
238
  /**
@@ -989,6 +991,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
989
991
  shouldCancelNextClick: false,
990
992
  clickResetTimer: null,
991
993
  velocitySamples: [],
994
+ scale: 1,
992
995
  })
993
996
 
994
997
  const enablePointerDragRef = useRef(enablePointerDrag)
@@ -1084,6 +1087,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1084
1087
  state.isDragging = false
1085
1088
  state.shouldCancelNextClick = shouldKeepClickSuppression
1086
1089
  state.velocitySamples = []
1090
+ state.scale = 1
1087
1091
  // 状態を確定させてから解放する (同期 lostpointercapture に引き継ぎを壊させない)
1088
1092
  if (element?.hasPointerCapture(releasedPointerId)) {
1089
1093
  element.releasePointerCapture(releasedPointerId)
@@ -1107,6 +1111,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1107
1111
  state.startScroll = 0
1108
1112
  state.isDragging = false
1109
1113
  state.velocitySamples = []
1114
+ state.scale = 1
1110
1115
  }
1111
1116
 
1112
1117
  const clearClickTimer = () => {
@@ -1239,7 +1244,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1239
1244
 
1240
1245
  pushVelocitySample(event.clientY)
1241
1246
 
1242
- const deltaY = event.clientY - state.startClientY
1247
+ // ÷scale: 視覚 px の指移動をレイアウト (論理) px へ (発動閾値 6px は指の物理
1248
+ // 移動量の判別なので視覚 px のまま — 意図的非対称)
1249
+ const deltaY = (event.clientY - state.startClientY) / (state.scale || 1)
1243
1250
  const nextPosition = state.startScroll - deltaY
1244
1251
  scrollToRef.current(nextPosition)
1245
1252
 
@@ -1309,7 +1316,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1309
1316
  if (lastSample && firstSample && lastSample.time !== firstSample.time) {
1310
1317
  const deltaClientY = lastSample.clientY - firstSample.clientY
1311
1318
  const deltaTime = lastSample.time - firstSample.time
1312
- inertiaVelocity = -(deltaClientY / deltaTime)
1319
+ // ÷scale: フリック速度も視覚 px/ms のままだと慣性走行距離が z 倍に歪む
1320
+ inertiaVelocity = -(deltaClientY / deltaTime) / (state.scale || 1)
1313
1321
  }
1314
1322
  }
1315
1323
 
@@ -1405,6 +1413,11 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1405
1413
  state.pointerType = event.pointerType
1406
1414
  state.startClientY = event.clientY
1407
1415
  state.startScroll = scrollPositionRef.current
1416
+ // 祖先 transform scale の押下時採取 (ScrollBar サム / トラックと同じ press キャッシュ
1417
+ // 規約): 視覚 px の指移動をレイアウト px へ換算しないと scale z 下でコンテンツが
1418
+ // 指速の z 倍で動く。move 毎の gBCR + getComputedStyle は 60-120Hz 熱経路の
1419
+ // 強制レイアウトになるため押下時 1 回に固定する
1420
+ state.scale = getAxisScale(element, "y")
1408
1421
  state.isDragging = false
1409
1422
  state.shouldCancelNextClick = false
1410
1423
  state.velocitySamples = []
package/src/utils.ts CHANGED
@@ -14,3 +14,45 @@
14
14
  export const minmax = (value: number, min: number, max: number): number => {
15
15
  return Math.max(min, Math.min(max, value))
16
16
  }
17
+
18
+ /**
19
+ * Computes an element's CSS transform scale along one axis (visual px / layout px).
20
+ *
21
+ * 要素の指定軸方向 CSS transform スケール (視覚px / レイアウトpx) を算出。
22
+ * 祖先に transform: scale(...) があると client 座標 (視覚px) がレイアウトpx とズレるため、
23
+ * その比率でドラッグ量・クリック座標を補正する。分母には整数丸めされる offsetWidth/offsetHeight
24
+ * ではなく、transform の影響を受けず小数精度を持つ getComputedStyle のレイアウト寸法
25
+ * (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
26
+ * 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
27
+ *
28
+ * @param element The element to measure. / 計測対象要素。
29
+ * @param axis The axis to measure along ("x" = width, "y" = height). / 計測軸。
30
+ * @returns The visual/layout scale ratio (1 when unmeasurable). / 視覚/レイアウト比 (計測不能時 1)。
31
+ */
32
+ export const getAxisScale = (element: HTMLElement, axis: "x" | "y"): number => {
33
+ const isX = axis === "x"
34
+ const rect = element.getBoundingClientRect()
35
+ const visualSize = isX ? rect.width : rect.height
36
+ // computed style の値 (px 文字列) を数値化する。解釈不能なら 0 とみなす。
37
+ const parseSize = (value: string) => {
38
+ const parsed = Number.parseFloat(value)
39
+ return Number.isFinite(parsed) ? parsed : 0
40
+ }
41
+ const computed = window.getComputedStyle(element)
42
+ const contentSize = parseSize(isX ? computed.width : computed.height)
43
+ // rect (border-box) と同系になるよう content + padding + border を合成する。
44
+ const computedLayoutSize =
45
+ contentSize > 0
46
+ ? contentSize +
47
+ parseSize(isX ? computed.paddingLeft : computed.paddingTop) +
48
+ parseSize(isX ? computed.paddingRight : computed.paddingBottom) +
49
+ parseSize(isX ? computed.borderLeftWidth : computed.borderTopWidth) +
50
+ parseSize(isX ? computed.borderRightWidth : computed.borderBottomWidth)
51
+ : 0
52
+ // computed が取れない環境 (jsdom 等) では従来の offset 寸法へフォールバックする。
53
+ const layoutSize = computedLayoutSize > 0 ? computedLayoutSize : isX ? element.offsetWidth : element.offsetHeight
54
+ if (layoutSize <= 0 || visualSize <= 0) {
55
+ return 1
56
+ }
57
+ return visualSize / layoutSize
58
+ }