@aiquants/virtualscroll 3.8.2 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "3.8.2",
3
+ "version": "3.9.0",
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
@@ -6,6 +6,7 @@
6
6
  import type { CSSProperties, ReactNode } from "react"
7
7
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
8
8
  import { twMerge } from "tailwind-merge"
9
+ import { snapToDevicePixelGrid, usePaintingDevicePixelRatio } from "./devicePixelGrid.ts"
9
10
  import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
10
11
  import { Logger } from "./logger.ts"
11
12
  import { capturePointer, releaseCapturedPointer } from "./pointerCapture.ts"
@@ -20,7 +21,8 @@ import { getAxisScale, minmax } from "./utils.ts"
20
21
  type OrientationConfig = {
21
22
  mainSizeKey: "width" | "height"
22
23
  crossSizeKey: "width" | "height"
23
- positionKey: "left" | "top"
24
+ /** The transform function that moves the thumb along the bar / つまみをバーに沿って動かす変換の関数 */
25
+ thumbTranslateFunction: "translateX" | "translateY"
24
26
  selectDelta: (deltaX: number, deltaY: number) => number
25
27
  getPointerCoordinate: (point: { clientX: number; clientY: number }) => number
26
28
  arrowIcons: [string, string]
@@ -302,7 +304,7 @@ const createOrientationConfig = (horizontal: boolean): OrientationConfig => {
302
304
  return {
303
305
  mainSizeKey: "width",
304
306
  crossSizeKey: "height",
305
- positionKey: "left",
307
+ thumbTranslateFunction: "translateX",
306
308
  selectDelta: (deltaX: number, _deltaY: number) => deltaX,
307
309
  getPointerCoordinate: ({ clientX }) => clientX,
308
310
  arrowIcons: ["◀", "▶"],
@@ -314,7 +316,7 @@ const createOrientationConfig = (horizontal: boolean): OrientationConfig => {
314
316
  return {
315
317
  mainSizeKey: "height",
316
318
  crossSizeKey: "width",
317
- positionKey: "top",
319
+ thumbTranslateFunction: "translateY",
318
320
  selectDelta: (_deltaX: number, deltaY: number) => deltaY,
319
321
  getPointerCoordinate: ({ clientY }) => clientY,
320
322
  arrowIcons: ["▲", "▼"],
@@ -633,9 +635,15 @@ export const computeAutoTapScrollMaxSpeedMultiplier = (itemCount?: number) => {
633
635
  }
634
636
 
635
637
  /**
636
- * A custom scrollbar component.
638
+ * A custom scrollbar component. The thumb wrapper stays at the start of the track (`top: 0; left: 0`) and moves along it
639
+ * by a 2D translate snapped to the device-pixel grid of the window that paints it, so a scroll step rewrites only that
640
+ * transform and runs no layout. `thumbPosition` in the overlay props stays the exact, unsnapped offset. Rendering into a
641
+ * document without a window throws when the thumb attaches (there is no device-pixel ratio to snap to).
637
642
  *
638
- * カスタムスクロールバーコンポーネント。
643
+ * カスタムスクロールバーコンポーネント。つまみの器はトラックの始端 (`top: 0; left: 0`) に置いたまま、描くウィンドウの装置の
644
+ * 画素の格子へ揃えた 2D の平行移動でトラックに沿って動くので、スクロールの 1 段が書き換えるのはその transform だけで、
645
+ * レイアウトは走らない。オーバーレイの props の `thumbPosition` は揃える前の厳密なオフセットのまま。ウィンドウを持たない
646
+ * 文書へ描くと、つまみの取り付けで例外を投げる (揃える装置の画素比が無い)。
639
647
  */
640
648
  export const ScrollBar = ({
641
649
  contentSize,
@@ -714,7 +722,7 @@ export const ScrollBar = ({
714
722
  onScroll,
715
723
  scrollPosition,
716
724
  })
717
- const { mainSizeKey, crossSizeKey, positionKey, selectDelta, getPointerCoordinate, arrowIcons, directionClass, orientation } = orientationConfig
725
+ const { mainSizeKey, crossSizeKey, thumbTranslateFunction, selectDelta, getPointerCoordinate, arrowIcons, directionClass, orientation } = orientationConfig
718
726
  const effectiveTapMaxDistance = Math.max(tapCircleMaxDistance, 1)
719
727
  // 表示領域に対するコンテンツの比率
720
728
  const scrollRatio = viewportSize / contentSize
@@ -730,6 +738,11 @@ export const ScrollBar = ({
730
738
  // サムがトラック外へはみ出さないよう、レンダー時に [0, effectiveTrackLength] へクランプする。
731
739
  const thumbPosition = maxScrollPosition <= 0 || effectiveTrackLength <= 0 ? 0 : minmax((scrollPosition / maxScrollPosition) * effectiveTrackLength, 0, effectiveTrackLength)
732
740
  const thumbCenter = thumbPosition + thumbSize / 2
741
+ const { ratio: thumbPaintingRatio, attach: attachThumbWrapper } = usePaintingDevicePixelRatio("[ScrollBar] the thumb")
742
+ // ❗ つまみは top / left ではなく平行移動で動かす。top / left の書き換えは、描画の窓を変えないスクロールの 1 段にも文書の根からの
743
+ // レイアウトを 1 回足す (実測: 40px の段 60 回で Layout 60 → 30 回、13.9 → 9.1 ms)。平行移動は装置の画素の格子へ揃えて縁を
744
+ // 滲ませず、つまみの長さはどの位置でも同じ装置の画素の数になる
745
+ const thumbTranslate = thumbPaintingRatio === null ? thumbPosition : snapToDevicePixelGrid(thumbPosition, thumbPaintingRatio, "none")
733
746
 
734
747
  // スクロールバーが表示されるかどうか
735
748
  const scrollBarVisible = contentSize > viewportSize || isDragging
@@ -1581,11 +1594,14 @@ export const ScrollBar = ({
1581
1594
  {/* DOMの再生成を防ぐため、常にレンダリングしてスタイルで表示制御を行う */}
1582
1595
  <div
1583
1596
  key="thumb-wrapper"
1597
+ ref={attachThumbWrapper}
1584
1598
  className="aqvs-scrollbar-thumb-wrapper"
1585
1599
  style={{
1586
1600
  [mainSizeKey]: thumbSize,
1587
- [positionKey]: thumbPosition,
1588
- ...(horizontal ? { top: 0, bottom: 0 } : { left: 0, right: 0 }),
1601
+ top: 0,
1602
+ left: 0,
1603
+ ...(horizontal ? { bottom: 0 } : { right: 0 }),
1604
+ transform: `${thumbTranslateFunction}(${thumbTranslate}px)`,
1589
1605
  }}
1590
1606
  // ルート同様、非表示はパッケージ CSS の [data-visible="false"] が担う
1591
1607
  data-visible={scrollBarVisible || isDragging ? "true" : "false"}