@aiquants/virtualscroll 1.22.0 → 1.23.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 +22 -0
- package/README.md +36 -0
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +24 -0
- package/dist/ScrollPane.d.ts +24 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +25 -0
- package/dist/VirtualScroll.d.ts +25 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1334 -1316
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/package.json +1 -1
- package/src/ScrollBar.tsx +7 -0
- package/src/ScrollPane.tsx +164 -7
- package/src/TapScrollCircle.tsx +10 -0
- package/src/VirtualScroll.tsx +27 -0
- package/dist/_headFenwick.d.cts +0 -450
- package/dist/_headFenwick.d.ts +0 -451
- package/dist/_headFenwick.d.ts.map +0 -1
|
@@ -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
package/src/ScrollBar.tsx
CHANGED
|
@@ -1410,6 +1410,13 @@ export const ScrollBar = ({
|
|
|
1410
1410
|
* pointerup を待たずにドラッグ状態を破棄し、stale な pointerId の残留を防ぐ。
|
|
1411
1411
|
*/
|
|
1412
1412
|
const handleLostPointerCaptureOnTrack = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
1413
|
+
// ❗ 発生源がトラック自身のときだけ反応する。`lostpointercapture` はバブリングするため、
|
|
1414
|
+
// 子孫であるつまみやオーバーレイのキャプチャ遷移まで拾ってしまう。タッチ・ペンでは仕様
|
|
1415
|
+
// (暗黙のポインタキャプチャ) により着地点の子孫が先にキャプチャを持つので、この取り違えは
|
|
1416
|
+
// 実際に起こり得る。トラックのドラッグ状態を壊してよいのはトラック自身の喪失だけ。
|
|
1417
|
+
if (event.target !== event.currentTarget) {
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1413
1420
|
if (trackDragStateRef.current.pointerId !== event.pointerId) {
|
|
1414
1421
|
return
|
|
1415
1422
|
}
|
package/src/ScrollPane.tsx
CHANGED
|
@@ -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 = {
|
|
@@ -129,6 +150,21 @@ const isPointerDragAllowed = (pointerType: string, allowed: readonly ("mouse" |
|
|
|
129
150
|
return !known || allowed.includes(pointerType)
|
|
130
151
|
}
|
|
131
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Reports whether a pointer type can emit move events while no button is pressed.
|
|
155
|
+
* ボタン非押下のまま移動イベントを出し得るポインタ種別かどうかの判定処理。
|
|
156
|
+
*
|
|
157
|
+
* ❗ **タッチを含めてはならない。** 接地しているタッチポインタは仕様上つねに
|
|
158
|
+
* `buttons === 1` であり、ホバーが存在しないため接地終了後は二度とイベントを出さない。
|
|
159
|
+
* つまりタッチで `buttons === 0` が観測されるのはエンジン側の不具合に限られ
|
|
160
|
+
* (旧 Firefox の既知バグ等)、それを「pointerup の喪失」と解釈してドラッグを畳むと、
|
|
161
|
+
* エンジンの不具合がそのままスクロール不能として現れる。
|
|
162
|
+
*
|
|
163
|
+
* @param pointerType The event's pointer type. イベントのポインタ種別。
|
|
164
|
+
* @returns True when button-released moves are meaningful for the type. 非押下の移動が意味を持つ種別なら true。
|
|
165
|
+
*/
|
|
166
|
+
const isHoverCapablePointer = (pointerType: string): boolean => pointerType === "mouse" || pointerType === "pen"
|
|
167
|
+
|
|
132
168
|
const DEFAULT_INERTIA_OPTIONS: ResolvedScrollPaneInertiaOptions = {
|
|
133
169
|
maxVelocity: 6,
|
|
134
170
|
minVelocity: 0.02,
|
|
@@ -211,6 +247,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
211
247
|
visibleEndIndex,
|
|
212
248
|
renderOverlay,
|
|
213
249
|
initialScrollPosition = 0,
|
|
250
|
+
contentProps,
|
|
214
251
|
},
|
|
215
252
|
ref,
|
|
216
253
|
) => {
|
|
@@ -646,6 +683,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
646
683
|
)
|
|
647
684
|
|
|
648
685
|
const id = useId()
|
|
686
|
+
// コンテンツ要素の id。利用側が contentProps.id を渡した場合はそちらを正とし、
|
|
687
|
+
// ScrollBar の aria-controls も同じ要素を指すようにする
|
|
688
|
+
const contentId = contentProps?.id ?? id
|
|
649
689
|
|
|
650
690
|
const dragStateRef = useRef<DragState>({
|
|
651
691
|
pointerType: "",
|
|
@@ -825,6 +865,43 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
825
865
|
pushVelocitySample(event.clientY)
|
|
826
866
|
}
|
|
827
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Reports whether a terminal pointer event is authoritative enough to end this drag.
|
|
870
|
+
* 終端ポインタイベントがこのペインのドラッグを終わらせる権威を持つかどうかの判定処理。
|
|
871
|
+
*
|
|
872
|
+
* `true` を返すのは次のどちらかのときだけである。
|
|
873
|
+
*
|
|
874
|
+
* 1. ユーザーエージェントが発火したイベント (`isTrusted === true`)。
|
|
875
|
+
* 本物の中断はジェスチャの終了そのものなので、`target` が何であれ必ず従う。
|
|
876
|
+
* 2. このペイン自身を `target` とするイベント。宛先が明示されているため、
|
|
877
|
+
* 利用側やテストが意図してこのペインへ直接投げたものと解釈できる。
|
|
878
|
+
*
|
|
879
|
+
* 逆に `false` を返す — つまり従わない — のは「**`isTrusted` でなく、かつ
|
|
880
|
+
* このペイン以外を `target` とするイベント**」である。子孫・兄弟・`window` の
|
|
881
|
+
* いずれを `target` としていても区別なく従わない。生きた `pointerId` を載せた
|
|
882
|
+
* `pointercancel` を子孫へ `dispatchEvent` する実装 (長押しメニュー等) は
|
|
883
|
+
* 珍しくなく、それは `window` までバブルして届くため、真に受けると
|
|
884
|
+
* **健全なドラッグが指を離すまで死ぬ** (実ブラウザで再現確認済み)。
|
|
885
|
+
*
|
|
886
|
+
* ❗ **キャプチャの保持状況で判定してはならない。** `hasPointerCapture()` は
|
|
887
|
+
* `setPointerCapture()` 直後の**保留中**のキャプチャに対しても `true` を返す。
|
|
888
|
+
* ユーザーエージェントが `gotpointercapture` の処理前にポインタを取り消すと、
|
|
889
|
+
* 本物の `pointercancel` も後続の `lostpointercapture` も**旧保持者である子孫宛**の
|
|
890
|
+
* まま届くため、キャプチャ保持を条件にすると後始末の経路が 1 本残らず塞がり、
|
|
891
|
+
* `shouldCancelNextClick` が residue として残ってペイン内の次のクリックを 1 回
|
|
892
|
+
* 握り潰す。これは**キャプチャ保持を条件にしていた旧実装で実測した事象**であり、
|
|
893
|
+
* `isTrusted` を基準にする現在の実装ではそもそも成立しない (この取り違えを
|
|
894
|
+
* 構造的に起こさないため)。
|
|
895
|
+
*
|
|
896
|
+
* ❗ この判定は**終端イベント (`pointerup` / `pointercancel`) 専用**である。
|
|
897
|
+
* `pointermove` に適用してはならない。キャプチャ取得前の本物の `pointermove` は
|
|
898
|
+
* ヒット先の子孫を `target` として届くため、絞ると追従そのものが止まる。
|
|
899
|
+
*
|
|
900
|
+
* @param event The terminal pointer event to classify. 判定対象の終端ポインタイベント。
|
|
901
|
+
* @returns True when the event may end this pane's drag. ドラッグを終わらせてよい場合に true。
|
|
902
|
+
*/
|
|
903
|
+
const isAuthoritativeForDrag = (event: PointerEvent) => event.isTrusted || event.target === element
|
|
904
|
+
|
|
828
905
|
// pointerup を受け取れないままドラッグ継続が不可能になったときの共通終了処理。
|
|
829
906
|
// Shared teardown for cases where the drag can no longer continue without receiving a pointerup.
|
|
830
907
|
const abortDrag = (pointerId: number) => {
|
|
@@ -844,7 +921,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
844
921
|
}
|
|
845
922
|
// ボタン非押下の pointermove は pointerup の喪失 (iframe 上での解放・Alt+Tab 等) を意味する。
|
|
846
923
|
// stale な dragState を破棄し、hover 移動がカーソル追従スクロール化するのを根絶する。
|
|
847
|
-
|
|
924
|
+
// ❗ ホバーできる種別に限定する。接地中のタッチが 0 を報告するのはエンジンの不具合だけで、
|
|
925
|
+
// 従うとその不具合がドラッグの即死になる (判定理由は isHoverCapablePointer を参照)。
|
|
926
|
+
if (event.buttons === 0 && isHoverCapablePointer(event.pointerType)) {
|
|
848
927
|
abortDrag(event.pointerId)
|
|
849
928
|
return
|
|
850
929
|
}
|
|
@@ -906,6 +985,14 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
906
985
|
armOrphanedClickSuppression()
|
|
907
986
|
return
|
|
908
987
|
}
|
|
988
|
+
// ❗ ここには `isAuthoritativeForDrag` を掛けていない。掛けないのは意図的である。
|
|
989
|
+
// `pointerup` は「ジェスチャが正常に終わった」の意味しか持たず、合成されても
|
|
990
|
+
// 状態が壊れるのではなく 1 回のドラッグが早く終わるだけで、次の pointerdown から
|
|
991
|
+
// 通常どおり再開できる。`pointercancel` のように**ドラッグ枠を残したまま
|
|
992
|
+
// 追従だけ止める**という壊れ方をしないため、絞る必要が無い。
|
|
993
|
+
// 一方で絞ると、window を target として `pointerup` を投げる利用側・テストの
|
|
994
|
+
// ジェスチャが終了できなくなる (本物の `pointerup` の target は要素であって
|
|
995
|
+
// window ではないが、合成側はそこまで再現しないのが普通)。
|
|
909
996
|
|
|
910
997
|
if (state.isDragging && state.shouldCancelNextClick && event.cancelable) {
|
|
911
998
|
event.preventDefault()
|
|
@@ -958,11 +1045,39 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
958
1045
|
}
|
|
959
1046
|
|
|
960
1047
|
const handlePointerDown = (event: PointerEvent) => {
|
|
961
|
-
// アクティブなドラッグ中 (pointerId 保持中) の別ポインタの pointerdown は無視する (先勝ち)。
|
|
962
|
-
// 上書きを許すと 1 本目のドラッグが乗っ取られ、shouldCancelNextClick のクリアで
|
|
963
|
-
// 1 本目解放時の click 抑止も失われるため。
|
|
964
1048
|
if (dragStateRef.current.pointerId !== null) {
|
|
965
|
-
|
|
1049
|
+
// アクティブなドラッグ中の 2 本目以降 (非プライマリ) の pointerdown は無視する (先勝ち)。
|
|
1050
|
+
// 上書きを許すと 1 本目のドラッグが乗っ取られ、shouldCancelNextClick のクリアで
|
|
1051
|
+
// 1 本目解放時の click 抑止も失われるため。
|
|
1052
|
+
if (!event.isPrimary) {
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
// 別種別のポインタは、記録中のドラッグがまだ生きている可能性がある
|
|
1056
|
+
// (プライマリは種別ごとに 1 つ存在し得るため) ので奪わせない。
|
|
1057
|
+
//
|
|
1058
|
+
// ❗ この制限の代償: **異種のポインタからは残骸を明け渡せない**。
|
|
1059
|
+
// 例えばタッチの残骸が残った状態でマウスに持ち替えても奪還できず、
|
|
1060
|
+
// さらにここで早期 return するため `shouldCancelNextClick` の残骸も消えず、
|
|
1061
|
+
// マウスの次のクリックが 1 回握り潰される。それでも、生きているドラッグを
|
|
1062
|
+
// 誤って奪う方が実害が大きいと判断した。残骸は同種のポインタで明け渡せる。
|
|
1063
|
+
if (event.pointerType !== dragStateRef.current.pointerType) {
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
// **同種のプライマリ**が新たに接地したなら、前のポインタは既に終了しているとみなす。
|
|
1067
|
+
// Pointer Events Level 3 は「その種別にアクティブなポインタが他に無いときに
|
|
1068
|
+
// プライマリになる」と定めており、規範的な MUST ではないが主要 UA はこう振る舞う
|
|
1069
|
+
// (Chromium で実測確認済み)。なおマウスで 2 回目の pointerdown が来ない本当の根拠は
|
|
1070
|
+
// chorded button の遷移規則 (押されているボタンが 0 個から 1 個以上になったときだけ
|
|
1071
|
+
// pointerdown を発火する) であって、プライマリの規則ではない。
|
|
1072
|
+
//
|
|
1073
|
+
// dragState が残っているのは終端イベント (pointerup / pointercancel) を
|
|
1074
|
+
// 取りこぼしたということなので、残骸を捨てて新しいジェスチャへ明け渡す。
|
|
1075
|
+
// ここを無条件の先勝ちにすると、タッチでは復帰手段が一切無くなる:
|
|
1076
|
+
// タッチはホバーが無いため接地終了後にイベントを出さず、`buttons === 0` による
|
|
1077
|
+
// 自己修復は届かない。pointerId は仕様上 UA が再利用してよい (MAY) ため、
|
|
1078
|
+
// 番号の一致に依存した復帰も当てにできない。結果としてペインは
|
|
1079
|
+
// アンマウントされるまで永久に操作不能のまま固まる。
|
|
1080
|
+
abortDrag(dragStateRef.current.pointerId)
|
|
966
1081
|
}
|
|
967
1082
|
// 進行中ドラッグが無いのに抑止が残っているなら、click が来なかった前ジェスチャの残骸。
|
|
968
1083
|
// 新しいジェスチャへ持ち越すと無関係なクリックを飲み込むのでここで捨てる
|
|
@@ -1008,10 +1123,40 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1008
1123
|
armOrphanedClickSuppression()
|
|
1009
1124
|
return
|
|
1010
1125
|
}
|
|
1126
|
+
// アプリ層が投げた合成 pointercancel には従わない (判定理由は isAuthoritativeForDrag を参照)
|
|
1127
|
+
if (!isAuthoritativeForDrag(event)) {
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1011
1130
|
abortDrag(event.pointerId)
|
|
1012
1131
|
}
|
|
1013
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* Ends the drag when this pane itself loses the pointer capture it holds.
|
|
1135
|
+
* このペイン自身が保持しているポインタキャプチャを失ったときにドラッグを終了する処理。
|
|
1136
|
+
*
|
|
1137
|
+
* ❗ **発生源 (`event.target`) が自分自身のときだけ反応すること。**
|
|
1138
|
+
* `lostpointercapture` はバブリングする。そしてタッチ・ペンでは仕様
|
|
1139
|
+
* (Pointer Events Level 3 の暗黙のポインタキャプチャ) により、`pointerdown` の
|
|
1140
|
+
* 着地点が先にキャプチャを持つ。仮想リストで指が着地するのは通常
|
|
1141
|
+
* **このコンテンツ要素の子孫である行**である (`contentInsets` のパディング帯や
|
|
1142
|
+
* 空リストではこの要素自身が着地点になることもあるが、その場合は付け替えが
|
|
1143
|
+
* 起きないだけで無害)。`startDragging` がドラッグ確定時に祖先であるこの要素へ
|
|
1144
|
+
* キャプチャを**付け替える**と、旧保持者である子孫に `lostpointercapture` が
|
|
1145
|
+
* 発火し、**同じ `pointerId` を載せたまま**ここまでバブルしてくる。
|
|
1146
|
+
*
|
|
1147
|
+
* `pointerId` だけで判定すると、自分が起こした正常な付け替えを
|
|
1148
|
+
* 「OS によるキャプチャ強制剥奪」と誤認し、**開始した直後のドラッグを自分で殺す**。
|
|
1149
|
+
* 実機のスマートフォンでは「6px の閾値を跨いだ 1 フレームだけスクロールして、
|
|
1150
|
+
* あとは指を離すまで完全に無反応」という症状になる (実測で確認)。
|
|
1151
|
+
* マウスには暗黙のポインタキャプチャが無く付け替えが発生しないため、
|
|
1152
|
+
* 同じコードでもデスクトップだけは無傷で通る。これが
|
|
1153
|
+
* 「PC では動くのにスマートフォンだけ壊れる」の正体だった。
|
|
1154
|
+
*/
|
|
1014
1155
|
const handleLostPointerCapture = (event: PointerEvent) => {
|
|
1156
|
+
// 子孫から上がってきた通知は自分のキャプチャ喪失ではない (暗黙キャプチャの付け替え)
|
|
1157
|
+
if (event.target !== element) {
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1015
1160
|
const state = dragStateRef.current
|
|
1016
1161
|
if (state.pointerId !== event.pointerId) {
|
|
1017
1162
|
return
|
|
@@ -1097,9 +1242,21 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1097
1242
|
// 短いリスト上のタッチがページ全体のパンを妨げるデッドゾーン化を防ぐ。
|
|
1098
1243
|
// 直接操作 (指・ペン) のドラッグを許可していない場合も付けない。
|
|
1099
1244
|
// touch-action はペン入力にも効くため、判定は touch と pen の論理和にする
|
|
1245
|
+
//
|
|
1246
|
+
// ❗ この値はタッチ開始時にラッチされる (実測: 1ms 後に none へ変えても手遅れ)。
|
|
1247
|
+
// 中身が非同期に届くリストでは、「まだスクロール不能」な一瞬に指が着地すると
|
|
1248
|
+
// **そのジェスチャ 1 回分**はブラウザに接収されたまま戻らない。
|
|
1249
|
+
// 指を離して置き直せば次のジェスチャからは正常に効くため、損失は 1 ジェスチャに
|
|
1250
|
+
// 限られ恒久破損にはならない (実測で確認)。ドラッグ許可の判定 (isScrollable) と
|
|
1251
|
+
// 同一条件で揃えてあるため状態としても一貫している。
|
|
1252
|
+
// 非同期コンテンツを先読みして確保する挙動は意図的に入れていない
|
|
1253
|
+
// (空のペインがページのパンを殺すデッドゾーンになり、そちらの実害の方が大きいため)。
|
|
1100
1254
|
...(enablePointerDrag && isScrollable && pointerDragInputs.some((type) => type === "touch" || type === "pen") ? { touchAction: "none" } : {}),
|
|
1101
1255
|
}}
|
|
1102
|
-
|
|
1256
|
+
// ARIA 属性はコンテンツ要素に載せる。ルート要素にはスクロールバーも含まれるため、
|
|
1257
|
+
// 合成ウィジェットのロールをそちらに置くと不正な所有子が生まれる
|
|
1258
|
+
{...contentProps}
|
|
1259
|
+
id={contentId}>
|
|
1103
1260
|
{background}
|
|
1104
1261
|
{children(scrollPositionRef.current)}
|
|
1105
1262
|
</div>
|
|
@@ -1113,7 +1270,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1113
1270
|
enableTrackClick={enableTrackClick}
|
|
1114
1271
|
enableArrowButtons={enableArrowButtons}
|
|
1115
1272
|
scrollBarWidth={scrollBarWidth}
|
|
1116
|
-
ariaControls={
|
|
1273
|
+
ariaControls={contentId}
|
|
1117
1274
|
tapScrollCircleOptions={tapScrollCircleOptions}
|
|
1118
1275
|
itemCount={itemCount}
|
|
1119
1276
|
renderThumbOverlay={renderThumbOverlay}
|
package/src/TapScrollCircle.tsx
CHANGED
|
@@ -298,6 +298,16 @@ export const TapScrollCircle = memo(
|
|
|
298
298
|
|
|
299
299
|
const handleLostPointerCapture = useCallback(
|
|
300
300
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
301
|
+
// ❗ 発生源が自分自身のときだけ反応する。`lostpointercapture` はバブリングし、
|
|
302
|
+
// タッチ・ペンでは仕様 (Pointer Events Level 3 の暗黙のポインタキャプチャ) により
|
|
303
|
+
// `pointerdown` の着地点 — サークルの中身を描く子要素 — が先にキャプチャを持つ。
|
|
304
|
+
// `handlePointerDown` がルートへキャプチャを付け替えると、旧保持者である子孫に
|
|
305
|
+
// `lostpointercapture` が発火し、同じ `pointerId` を載せたままここへバブルしてくる。
|
|
306
|
+
// `pointerId` だけで判定すると、自分が起こした正常な付け替えを強制剥奪と誤認して
|
|
307
|
+
// **触れた瞬間にタップスクロールが死ぬ** (マウスは暗黙キャプチャが無いため無傷)。
|
|
308
|
+
if (event.target !== event.currentTarget) {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
301
311
|
if (pointerIdRef.current !== event.pointerId) {
|
|
302
312
|
return
|
|
303
313
|
}
|
package/src/VirtualScroll.tsx
CHANGED
|
@@ -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
|
/**
|
|
@@ -667,6 +692,7 @@ const VirtualScrollInner = <T,>(
|
|
|
667
692
|
scrollBarOptions,
|
|
668
693
|
behaviorOptions,
|
|
669
694
|
onWheelHorizontal,
|
|
695
|
+
contentProps,
|
|
670
696
|
}: VirtualScrollProps<T>,
|
|
671
697
|
ref: React.Ref<VirtualScrollHandle>,
|
|
672
698
|
) => {
|
|
@@ -1882,6 +1908,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1882
1908
|
wheelSpeedMultiplier={wheelSpeedMultiplier}
|
|
1883
1909
|
onWheelHorizontal={onWheelHorizontal}
|
|
1884
1910
|
contentInsets={resolvedInsets}
|
|
1911
|
+
contentProps={contentProps}
|
|
1885
1912
|
visibleStartIndex={visibleStartIndex}
|
|
1886
1913
|
visibleEndIndex={visibleEndIndex}
|
|
1887
1914
|
renderOverlay={renderOverlay}
|