@aiquants/virtualscroll 1.22.1 → 1.24.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.
@@ -1,3 +1,3 @@
1
1
  /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
2
  /*! @aiquants/virtualscroll standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
3
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--spacing:.25rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane-content{flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border-width:1px;border-color:#fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{background-color:#ffffffd9;border-width:1px;border-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;background-color:#e0e0e0;justify-content:center;align-items:center;font-size:.75rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border-radius:9999px;padding:.25rem 3rem;font-size:10px;font-weight:500;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.hidden{display:none}.inline{display:inline}.size-1{width:calc(var(--spacing) * 1);height:calc(var(--spacing) * 1)}.shrink{flex-shrink:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.border{border-style:var(--tw-border-style);border-width:1px}.opacity-0{opacity:0}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
3
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--spacing:.25rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane-content{flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border-width:1px;border-color:#fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{background-color:#ffffffd9;border-width:1px;border-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;background-color:#e0e0e0;justify-content:center;align-items:center;font-size:.75rem;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border-radius:9999px;padding:.25rem 3rem;font-size:10px;font-weight:500;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.static{position:static}.row-0{grid-row:0}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-1{width:calc(var(--spacing) * 1);height:calc(var(--spacing) * 1)}.shrink{flex-shrink:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.border{border-style:var(--tw-border-style);border-width:1px}.opacity-0{opacity:0}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -92,6 +92,27 @@ export type ScrollPaneProps = {
92
92
  renderOverlay?: () => React.ReactNode
93
93
  /** Initial scroll position. / 初期のスクロール位置。 */
94
94
  initialScrollPosition?: number
95
+ /**
96
+ * ARIA / identity attributes applied to the scrollable CONTENT element — the element that
97
+ * directly owns the rendered items and excludes the scrollbar and overlay chrome.
98
+ *
99
+ * This is the correct host for a composite widget role (`listbox` / `tree` / `grid`): the outer
100
+ * root also contains the scrollbar, whose `role="scrollbar"` would become an invalid owned child
101
+ * of that role. Supplying `id` here also overrides the generated one, so the scrollbar's
102
+ * `aria-controls` keeps pointing at the same element.
103
+ *
104
+ * The type is deliberately narrow: `className`, `style` and `ref` are not expressible, so a
105
+ * consumer cannot break the pane's layout through this seam.
106
+ *
107
+ * スクロール可能な「コンテンツ要素」へ適用する ARIA / 識別属性。行だけを直接所有し、
108
+ * スクロールバーとオーバーレイを含まない要素であり、合成ウィジェットのロール
109
+ * (`listbox` / `tree` / `grid`) を置くべき唯一正しい位置。ルート要素にはスクロールバーも
110
+ * 含まれるため、そちらに置くと `role="scrollbar"` が不正な所有子になる。`id` を渡すと生成 id を
111
+ * 上書きするので、スクロールバーの `aria-controls` も同じ要素を指し続ける。
112
+ *
113
+ * `className` / `style` / `ref` を表現できない型にしてあり、この口からレイアウトを壊せない。
114
+ */
115
+ contentProps?: React.AriaAttributes & { id?: string; role?: React.AriaRole }
95
116
  }
96
117
 
97
118
  export type ScrollPaneInertiaOptions = {
@@ -226,6 +247,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
226
247
  visibleEndIndex,
227
248
  renderOverlay,
228
249
  initialScrollPosition = 0,
250
+ contentProps,
229
251
  },
230
252
  ref,
231
253
  ) => {
@@ -661,6 +683,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
661
683
  )
662
684
 
663
685
  const id = useId()
686
+ // コンテンツ要素の id。利用側が contentProps.id を渡した場合はそちらを正とし、
687
+ // ScrollBar の aria-controls も同じ要素を指すようにする
688
+ const contentId = contentProps?.id ?? id
664
689
 
665
690
  const dragStateRef = useRef<DragState>({
666
691
  pointerType: "",
@@ -1228,7 +1253,10 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1228
1253
  // (空のペインがページのパンを殺すデッドゾーンになり、そちらの実害の方が大きいため)。
1229
1254
  ...(enablePointerDrag && isScrollable && pointerDragInputs.some((type) => type === "touch" || type === "pen") ? { touchAction: "none" } : {}),
1230
1255
  }}
1231
- id={id}>
1256
+ // ARIA 属性はコンテンツ要素に載せる。ルート要素にはスクロールバーも含まれるため、
1257
+ // 合成ウィジェットのロールをそちらに置くと不正な所有子が生まれる
1258
+ {...contentProps}
1259
+ id={contentId}>
1232
1260
  {background}
1233
1261
  {children(scrollPositionRef.current)}
1234
1262
  </div>
@@ -1242,7 +1270,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1242
1270
  enableTrackClick={enableTrackClick}
1243
1271
  enableArrowButtons={enableArrowButtons}
1244
1272
  scrollBarWidth={scrollBarWidth}
1245
- ariaControls={id}
1273
+ ariaControls={contentId}
1246
1274
  tapScrollCircleOptions={tapScrollCircleOptions}
1247
1275
  itemCount={itemCount}
1248
1276
  renderThumbOverlay={renderThumbOverlay}
@@ -139,6 +139,31 @@ export type VirtualScrollProps<T> = {
139
139
  behaviorOptions?: VirtualScrollBehaviorOptions
140
140
  /** Delegates horizontal wheel/trackpad delta to an upstream owner (e.g. a frozen-column grid). / 横ホイール量を上流へ委譲する。 */
141
141
  onWheelHorizontal?: (deltaX: number) => void
142
+ /**
143
+ * ARIA / identity attributes applied to the scrollable CONTENT element — the element that
144
+ * directly owns the rendered rows and excludes the scrollbar and overlay chrome.
145
+ *
146
+ * This is where a composite widget role belongs. Putting `role="listbox"` / `"tree"` / `"grid"`
147
+ * on the outer root instead would make the scrollbar (`role="scrollbar"`) an owned child of that
148
+ * role, which ARIA does not allow. Passing `id` here also overrides the generated one, so the
149
+ * scrollbar's `aria-controls` keeps referring to the same element.
150
+ *
151
+ * Intermediate wrappers between the content element and the rows carry no role, no global ARIA
152
+ * attribute and no tabindex, so they are transparent to the owned-element relationship — but
153
+ * only while `behaviorOptions.enableKeyboardNavigation` is `false`, which is what a consumer
154
+ * driving selection through `aria-activedescendant` should be doing anyway.
155
+ *
156
+ * スクロール可能な「コンテンツ要素」へ適用する ARIA / 識別属性。行だけを直接所有し、
157
+ * スクロールバーとオーバーレイを含まないため、合成ウィジェットのロールはここへ置く。
158
+ * ルート要素に置くとスクロールバー (`role="scrollbar"`) が不正な所有子になる。`id` を渡すと
159
+ * 生成 id を上書きし、スクロールバーの `aria-controls` も同じ要素を指し続ける。
160
+ *
161
+ * コンテンツ要素と行の間のラッパーはロール・グローバル ARIA 属性・tabindex のいずれも持たない
162
+ * ため所有関係に対して透過だが、それが成り立つのは
163
+ * `behaviorOptions.enableKeyboardNavigation` が `false` のときに限る (行ラッパーへ
164
+ * `tabIndex={-1}` が付くため)。`aria-activedescendant` 方式の利用側は元より false にすべきである。
165
+ */
166
+ contentProps?: ScrollPaneProps["contentProps"]
142
167
  }
143
168
 
144
169
  /**
@@ -155,7 +180,12 @@ const normalizeInsets = (insets?: ScrollPaneContentInsets): Required<ScrollPaneC
155
180
 
156
181
  const toLogicalPositionWithInset = (pane: number, top: number) => (pane <= top ? 0 : pane - top)
157
182
 
158
- const toPanePositionWithInset = (logical: number, top: number) => (logical <= 0 ? top : logical + top)
183
+ // 論理 0 は「本当の先頭」= ペイン 0 (上インセットが見えている状態) へ写像する。
184
+ // ❗ `logical <= 0 ? top` (インセットを隠して先頭行を上端密着にする端点) へ戻さないこと:
185
+ // マウント既定 (initialScroll 未指定 = ペイン 0) や ScrollPane のクランプ休止点 (ペイン 0) と
186
+ // 端点が食い違い、「保存した論理 0 を復元すると数 px スクロールされて見える」実害バグになる
187
+ // (toLogical はペイン [0..top] を論理 0 へ潰す不可逆写像のため、逆写像の端点選択がここで確定する)
188
+ const toPanePositionWithInset = (logical: number, top: number) => (logical <= 0 ? 0 : logical + top)
159
189
 
160
190
  /**
161
191
  * Maximum run length of consecutive zero-height rows scanned linearly before attempting an
@@ -547,15 +577,27 @@ const useThrottledInvoker = <Callback, Payload>(callbackRef: React.MutableRefObj
547
577
  // last: 最終実行時刻, id: RAF ID, arg: 待機中の引数
548
578
  const state = useRef({ last: 0, id: null as number | null, arg: null as Payload | null }).current
549
579
 
550
- // クリーンアップ: アンマウント時にRAFをキャンセル
580
+ // クリーンアップ: アンマウント時に RAF をキャンセルし、待機中のペイロードは**破棄せず同期配信**する。
581
+ // ❗ 破棄すると、フリング中のタブ切替などで確定済みの待機通知まで消費側へ届かず、
582
+ // 「スクロールのたびに書き戻す」型の位置保存が取りこぼす。なおこの flush が救えるのは
583
+ // 「キューに載った最後のペイロード」まで: 最後の render-loop tick 以後の移動 (最大 1 フレーム分、
584
+ // 慣性で概ね 100px 未満) は残余として許容する (読中でないフリング途中の位置のため実害は軽微)
551
585
  useEffect(
552
586
  () => () => {
553
587
  if (state.id !== null) {
554
588
  cancelAnimationFrame(state.id)
555
589
  state.id = null
556
590
  }
591
+ if (state.arg !== null && callbackRef.current) {
592
+ try {
593
+ invoke(callbackRef.current, state.arg)
594
+ } catch (e) {
595
+ console.error("[useThrottledInvoker] Error flushing pending callback on unmount", e)
596
+ }
597
+ state.arg = null
598
+ }
557
599
  },
558
- [state],
600
+ [state, callbackRef, invoke],
559
601
  )
560
602
 
561
603
  return useCallback(
@@ -667,6 +709,7 @@ const VirtualScrollInner = <T,>(
667
709
  scrollBarOptions,
668
710
  behaviorOptions,
669
711
  onWheelHorizontal,
712
+ contentProps,
670
713
  }: VirtualScrollProps<T>,
671
714
  ref: React.Ref<VirtualScrollHandle>,
672
715
  ) => {
@@ -1882,6 +1925,7 @@ const VirtualScrollInner = <T,>(
1882
1925
  wheelSpeedMultiplier={wheelSpeedMultiplier}
1883
1926
  onWheelHorizontal={onWheelHorizontal}
1884
1927
  contentInsets={resolvedInsets}
1928
+ contentProps={contentProps}
1885
1929
  visibleStartIndex={visibleStartIndex}
1886
1930
  visibleEndIndex={visibleEndIndex}
1887
1931
  renderOverlay={renderOverlay}