@aiquants/virtualscroll 2.2.0 → 2.3.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": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -1723,9 +1723,27 @@ const VirtualScrollInner = <T,>(
1723
1723
  }, [contentSize, renderingEndIndex, renderingStartIndex, resolvedInsets.top, scheduleRangeEffect, scrollPosition, visibleEndIndex, visibleStartIndex])
1724
1724
 
1725
1725
  /**
1726
- * Produces visible item nodes and schedules height reconciliation.
1726
+ * Renders the auto-hiding Top/Bottom pill overlay, neutralized while hidden.
1727
1727
  *
1728
- * 可視アイテムを描画しつつ高さとの差分更新を手配。
1728
+ * 自動非表示の Top/Bottom ピルのオーバーレイを描画し、非表示中は無効化する処理。
1729
+ *
1730
+ * ❗ 非表示は `opacity: 0` で表現するため、要素は DOM に残り続ける (フェードのために
1731
+ * アンマウントしない)。`opacity` はフォーカス可能性に影響せず、CSS の `pointer-events: none`
1732
+ * はポインタ操作しか止めないため、無効化しないと**キーボード操作者は一覧を通過するたびに
1733
+ * 「何も見えないのにフォーカスが止まる」箇所を必ず 1 つ踏む**。しかもフォーカスリング
1734
+ * (`:focus-visible`) は `opacity: 0` の要素に描かれるため見えず、そこで Enter/Space を押すと
1735
+ * 一覧が先頭または末尾へ飛ぶ (`pointer-events: none` はキーボードからの活性化を素通しする)。
1736
+ *
1737
+ * 無効化は焦点・支援技術・ポインタの 3 軸を 2 層で塞ぐ:
1738
+ *
1739
+ * 1. オーバーレイの `inert`: 焦点・ヒットテスト・アクセシビリティツリー露出を一括で外す
1740
+ * (この 1 属性が本質的な修正。React 19 は真偽値プロップとして扱い、false では属性を出さない)。
1741
+ * 2. ピルの `tabIndex`: `inert` 未実装のブラウザ (Firefox 112 未満など) でもタブ順から外れる
1742
+ * ようにする下限。`inert` と同じ `isVisible` から導出するため両者が食い違うことはない。
1743
+ *
1744
+ * ポインタ軸の下限は CSS 側の `.aqvs-scroll-to-edge-overlay[data-visible="false"]
1745
+ * .aqvs-scroll-to-edge-button { pointer-events: none }` が担う (配布 CSS 未読込のホストでは
1746
+ * `inert` が、`inert` 未実装のブラウザでは CSS が、互いの穴を埋める)。
1729
1747
  */
1730
1748
  const renderOverlay = useCallback(() => {
1731
1749
  if (!enableScrollToTopBottomButtons) {
@@ -1736,12 +1754,14 @@ const VirtualScrollInner = <T,>(
1736
1754
  const isTop = scrollDirection === "up"
1737
1755
 
1738
1756
  return (
1739
- <div className="aqvs-scroll-to-edge-overlay" data-visible={isVisible}>
1757
+ <div className="aqvs-scroll-to-edge-overlay" data-visible={isVisible} inert={!isVisible}>
1740
1758
  {isTop ? (
1741
1759
  <div className="aqvs-scroll-to-edge-button-container aqvs-scroll-to-edge-button-container-top">
1742
1760
  <button
1743
1761
  type="button"
1744
1762
  className="aqvs-scroll-to-edge-button"
1763
+ // 非表示中はタブ順から外す (inert 未実装ブラウザ向けの下限)
1764
+ tabIndex={isVisible ? 0 : -1}
1745
1765
  onClick={(e) => {
1746
1766
  e.stopPropagation()
1747
1767
  isProgrammaticScrollRef.current = true
@@ -1756,6 +1776,8 @@ const VirtualScrollInner = <T,>(
1756
1776
  <button
1757
1777
  type="button"
1758
1778
  className="aqvs-scroll-to-edge-button"
1779
+ // 非表示中はタブ順から外す (inert 未実装ブラウザ向けの下限)
1780
+ tabIndex={isVisible ? 0 : -1}
1759
1781
  onClick={(e) => {
1760
1782
  e.stopPropagation()
1761
1783
  isProgrammaticScrollRef.current = true