@aiquants/virtualscroll 3.8.0 → 3.8.2
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 +75 -0
- package/README.md +61 -6
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1496 -1489
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +176 -76
- package/src/styles/virtualscroll.css +22 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;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:1px solid #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{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #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;-webkit-tap-highlight-color:transparent;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[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.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;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;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;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;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:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.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-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.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}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid{grid-template-rows:minmax(0,1fr) auto;width:100%;height:100%;display:grid;position:relative;overflow:hidden}.aqvs-grid-has-frozen-rows{grid-template-rows:auto minmax(0,1fr) auto}.aqvs-grid-has-trailing-rows{grid-template-rows:minmax(0,1fr) auto auto}.aqvs-grid-has-frozen-rows.aqvs-grid-has-trailing-rows{grid-template-rows:auto minmax(0,1fr) auto auto}.aqvs-grid-frozen-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-frozen-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-trailing-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-trailing-rows-inner{position:absolute;bottom:0;left:0;right:0}.aqvs-grid-trailing-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-main{min-width:0;min-height:0;position:relative}.aqvs-grid-row{width:100%;height:100%;transform:translateX(var(--aqvs-grid-hx-residual,0px));position:relative;overflow:visible}.aqvs-grid-cell{height:100%;position:absolute;top:0;overflow:hidden}.aqvs-grid-row.aqvs-grid-row-frozen-host{transform:none}.aqvs-grid-row-scroll{height:100%;top:0;left:var(--aqvs-grid-frozen-width,0px);right:var(--aqvs-grid-trailing-width,0px);position:absolute;overflow:hidden}.aqvs-grid-row-scroll-inner{width:100%;height:100%;transform:translateX(calc(var(--aqvs-grid-hx-residual,0px) - var(--aqvs-grid-frozen-width,0px)));position:absolute;top:0;left:0}.aqvs-grid-row-trailing{height:100%;top:0;left:max(var(--aqvs-grid-frozen-width,0px), calc(100% - var(--aqvs-grid-trailing-width,0px)));position:absolute;right:0;overflow:hidden}.aqvs-grid-row-trailing-inner{height:100%;width:var(--aqvs-grid-trailing-width,0px);position:absolute;top:0;right:0}.aqvs-grid-hbar-strip{min-width:0;padding-left:var(--aqvs-grid-frozen-width,0px);position:relative}.aqvs-grid-live-region{clip:rect(0 0 0 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid-tap-circle-wrapper{z-index:50;pointer-events:auto;position:absolute}.aqvs-grid-tap-circle{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (forced-colors:active){.aqvs-tap-scroll-circle{outline-offset:-1px;border-radius:50%;outline:1px solid canvastext}}}
|
|
2
|
+
@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;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:1px solid #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{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #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;-webkit-tap-highlight-color:transparent;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[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.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;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;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;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;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:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.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-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.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}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid{grid-template-rows:minmax(0,1fr) auto;width:100%;height:100%;display:grid;position:relative;overflow:hidden}.aqvs-grid-has-frozen-rows{grid-template-rows:auto minmax(0,1fr) auto}.aqvs-grid-has-trailing-rows{grid-template-rows:minmax(0,1fr) auto auto}.aqvs-grid-has-frozen-rows.aqvs-grid-has-trailing-rows{grid-template-rows:auto minmax(0,1fr) auto auto}.aqvs-grid-frozen-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-frozen-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-trailing-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-trailing-rows-inner{position:absolute;bottom:0;left:0;right:0}.aqvs-grid-trailing-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-main{min-width:0;min-height:0;position:relative}.aqvs-grid-row{width:100%;height:100%;transform:translateX(var(--aqvs-grid-hx-residual,0px));position:relative;overflow:visible}.aqvs-grid-cell{height:100%;position:absolute;top:0;overflow:hidden}.aqvs-grid-row.aqvs-grid-row-frozen-host{transform:none}.aqvs-grid-row-scroll{height:100%;top:0;left:var(--aqvs-grid-frozen-width,0px);right:var(--aqvs-grid-trailing-width,0px);position:absolute;overflow:hidden}.aqvs-grid-row-scroll-inner{width:100%;height:100%;transform:translateX(calc(var(--aqvs-grid-hx-residual,0px) - var(--aqvs-grid-frozen-width,0px)));position:absolute;top:0;left:0}.aqvs-grid-row-trailing{height:100%;top:0;left:max(var(--aqvs-grid-frozen-width,0px), calc(100% - var(--aqvs-grid-trailing-width,0px)));position:absolute;right:0;overflow:hidden}.aqvs-grid-row-trailing-inner{height:100%;width:var(--aqvs-grid-trailing-width,0px);position:absolute;top:0;right:0}.aqvs-grid-hbar-strip{min-width:0;padding-left:var(--aqvs-grid-frozen-width,0px);position:relative}.aqvs-grid-live-region{clip:rect(0 0 0 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid-tap-circle-wrapper{z-index:50;pointer-events:auto;position:absolute}.aqvs-grid-tap-circle{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (forced-colors:active){.aqvs-tap-scroll-circle{outline-offset:-1px;border-radius:50%;outline:1px solid canvastext}}@media (prefers-reduced-motion:reduce){.aqvs-tap-scroll-circle,.aqvs-tap-scroll-circle *,.aqvs-grid-tap-circle,.aqvs-scrollbar-tap-circle-wrapper,.aqvs-scrollbar-arrow-button,.aqvs-scrollbar-thumb,.aqvs-scroll-to-edge-button,.aqvs-scroll-to-edge-overlay{transition:none!important;animation:none!important}}}
|
|
@@ -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 theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;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:1px solid #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{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #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;-webkit-tap-highlight-color:transparent;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[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.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;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;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;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;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:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.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-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.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}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid{grid-template-rows:minmax(0,1fr) auto;width:100%;height:100%;display:grid;position:relative;overflow:hidden}.aqvs-grid-has-frozen-rows{grid-template-rows:auto minmax(0,1fr) auto}.aqvs-grid-has-trailing-rows{grid-template-rows:minmax(0,1fr) auto auto}.aqvs-grid-has-frozen-rows.aqvs-grid-has-trailing-rows{grid-template-rows:auto minmax(0,1fr) auto auto}.aqvs-grid-frozen-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-frozen-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-trailing-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-trailing-rows-inner{position:absolute;bottom:0;left:0;right:0}.aqvs-grid-trailing-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-main{min-width:0;min-height:0;position:relative}.aqvs-grid-row{width:100%;height:100%;transform:translateX(var(--aqvs-grid-hx-residual,0px));position:relative;overflow:visible}.aqvs-grid-cell{height:100%;position:absolute;top:0;overflow:hidden}.aqvs-grid-row.aqvs-grid-row-frozen-host{transform:none}.aqvs-grid-row-scroll{height:100%;top:0;left:var(--aqvs-grid-frozen-width,0px);right:var(--aqvs-grid-trailing-width,0px);position:absolute;overflow:hidden}.aqvs-grid-row-scroll-inner{width:100%;height:100%;transform:translateX(calc(var(--aqvs-grid-hx-residual,0px) - var(--aqvs-grid-frozen-width,0px)));position:absolute;top:0;left:0}.aqvs-grid-row-trailing{height:100%;top:0;left:max(var(--aqvs-grid-frozen-width,0px), calc(100% - var(--aqvs-grid-trailing-width,0px)));position:absolute;right:0;overflow:hidden}.aqvs-grid-row-trailing-inner{height:100%;width:var(--aqvs-grid-trailing-width,0px);position:absolute;top:0;right:0}.aqvs-grid-hbar-strip{min-width:0;padding-left:var(--aqvs-grid-frozen-width,0px);position:relative}.aqvs-grid-live-region{clip:rect(0 0 0 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid-tap-circle-wrapper{z-index:50;pointer-events:auto;position:absolute}.aqvs-grid-tap-circle{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (forced-colors:active){.aqvs-tap-scroll-circle{outline-offset:-1px;border-radius:50%;outline:1px solid canvastext}}}@layer utilities;
|
|
3
|
+
@layer theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;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:1px solid #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{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #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;-webkit-tap-highlight-color:transparent;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[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.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;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;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;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;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:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.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-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.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}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid{grid-template-rows:minmax(0,1fr) auto;width:100%;height:100%;display:grid;position:relative;overflow:hidden}.aqvs-grid-has-frozen-rows{grid-template-rows:auto minmax(0,1fr) auto}.aqvs-grid-has-trailing-rows{grid-template-rows:minmax(0,1fr) auto auto}.aqvs-grid-has-frozen-rows.aqvs-grid-has-trailing-rows{grid-template-rows:auto minmax(0,1fr) auto auto}.aqvs-grid-frozen-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-frozen-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-trailing-rows{min-width:0;position:relative;overflow:hidden}.aqvs-grid-trailing-rows-inner{position:absolute;bottom:0;left:0;right:0}.aqvs-grid-trailing-band-row{position:absolute;left:0;overflow:hidden}.aqvs-grid-main{min-width:0;min-height:0;position:relative}.aqvs-grid-row{width:100%;height:100%;transform:translateX(var(--aqvs-grid-hx-residual,0px));position:relative;overflow:visible}.aqvs-grid-cell{height:100%;position:absolute;top:0;overflow:hidden}.aqvs-grid-row.aqvs-grid-row-frozen-host{transform:none}.aqvs-grid-row-scroll{height:100%;top:0;left:var(--aqvs-grid-frozen-width,0px);right:var(--aqvs-grid-trailing-width,0px);position:absolute;overflow:hidden}.aqvs-grid-row-scroll-inner{width:100%;height:100%;transform:translateX(calc(var(--aqvs-grid-hx-residual,0px) - var(--aqvs-grid-frozen-width,0px)));position:absolute;top:0;left:0}.aqvs-grid-row-trailing{height:100%;top:0;left:max(var(--aqvs-grid-frozen-width,0px), calc(100% - var(--aqvs-grid-trailing-width,0px)));position:absolute;right:0;overflow:hidden}.aqvs-grid-row-trailing-inner{height:100%;width:var(--aqvs-grid-trailing-width,0px);position:absolute;top:0;right:0}.aqvs-grid-hbar-strip{min-width:0;padding-left:var(--aqvs-grid-frozen-width,0px);position:relative}.aqvs-grid-live-region{clip:rect(0 0 0 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.aqvs-grid-tap-circle-wrapper{z-index:50;pointer-events:auto;position:absolute}.aqvs-grid-tap-circle{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (forced-colors:active){.aqvs-tap-scroll-circle{outline-offset:-1px;border-radius:50%;outline:1px solid canvastext}}@media (prefers-reduced-motion:reduce){.aqvs-tap-scroll-circle,.aqvs-tap-scroll-circle *,.aqvs-grid-tap-circle,.aqvs-scrollbar-tap-circle-wrapper,.aqvs-scrollbar-arrow-button,.aqvs-scrollbar-thumb,.aqvs-scroll-to-edge-button,.aqvs-scroll-to-edge-overlay{transition:none!important;animation:none!important}}}@layer utilities;
|
package/package.json
CHANGED
package/src/VirtualScroll.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
1
|
+
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
|
|
2
2
|
import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
|
|
3
3
|
import { Logger } from "./logger.ts"
|
|
4
4
|
import { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneProps } from "./ScrollPane.tsx"
|
|
@@ -601,6 +601,174 @@ const windowOf = (element: Element): Window => {
|
|
|
601
601
|
return view
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Unsubscribe that removes nothing: the subscription of a window without media queries, or of a wrapper with no
|
|
606
|
+
* painting window yet.
|
|
607
|
+
*
|
|
608
|
+
* 何も外さない購読解除 (メディアクエリを持たないウィンドウ、または描くウィンドウがまだ無いラッパーの購読)。
|
|
609
|
+
*/
|
|
610
|
+
const unsubscribeNothing = (): void => {}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Server snapshot of the painting ratio for `useSyncExternalStore`. A server has no screen, so it is `null`
|
|
614
|
+
* and the wrapper keeps the exact translate; hydration renders with it too, so the hydrated markup matches the
|
|
615
|
+
* server HTML, and React replaces it with the snapped translate right after hydration.
|
|
616
|
+
*
|
|
617
|
+
* `useSyncExternalStore` に渡す、描く比のサーバーのスナップショット。サーバーに画面は無いので `null` で、
|
|
618
|
+
* ラッパーは厳密な平行移動のまま。ハイドレーションもこの値で描くのでサーバーの HTML と一致し、
|
|
619
|
+
* ハイドレーションの直後に React が揃えた平行移動へ置き換える。
|
|
620
|
+
*
|
|
621
|
+
* @returns Always `null` / 常に `null`
|
|
622
|
+
*/
|
|
623
|
+
const readServerDevicePixelRatio = (): null => null
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Returns the window of the JavaScript realm that runs React, or `null` outside a browser realm (server rendering).
|
|
627
|
+
* It is the expected painting window of a wrapper that has not attached yet: render cannot see the document it
|
|
628
|
+
* will be inserted into, and a host renders into its own window's document unless it deliberately renders into
|
|
629
|
+
* another window's document (an iframe or an opened window), which the attach detects (see `ItemsWrapper`).
|
|
630
|
+
*
|
|
631
|
+
* React を実行している JavaScript のレルムのウィンドウを返す処理 (ブラウザのレルムの外 = サーバー描画では
|
|
632
|
+
* `null`)。まだ取り付いていないラッパーの、描くと見込むウィンドウ。描画からは挿入先の文書が見えず、ホストは
|
|
633
|
+
* 別のウィンドウの文書 (iframe・開いたウィンドウ) へ意図して描くのでない限り自分のウィンドウの文書へ描くため。
|
|
634
|
+
* 見込みの確かめは取り付けが行う (`ItemsWrapper` を参照)。
|
|
635
|
+
*
|
|
636
|
+
* @returns The realm's window, or `null` without one / レルムのウィンドウ (無ければ `null`)
|
|
637
|
+
*/
|
|
638
|
+
const readRealmWindow = (): Window | null => (typeof window === "undefined" ? null : window)
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Subscribes to the device-pixel-ratio changes of a window (browser zoom, or the window moving to a screen of
|
|
642
|
+
* another density) through a `(resolution: <ratio>dppx)` media-query watch that is re-armed at each new ratio.
|
|
643
|
+
* A window without media queries (jsdom) cannot change its ratio, so nothing is armed there.
|
|
644
|
+
*
|
|
645
|
+
* ウィンドウの装置の画素比の変化 (ブラウザの拡大縮小・密度の違う画面へのウィンドウの移動) を、比が変わる
|
|
646
|
+
* たびに新しい比で張り直す `(resolution: <比>dppx)` のメディアクエリの監視で購読する処理。メディアクエリを
|
|
647
|
+
* 持たないウィンドウ (jsdom) は比が変わり得ないので何も張らない。
|
|
648
|
+
*
|
|
649
|
+
* @param view - Window whose ratio is watched / 比を監視するウィンドウ
|
|
650
|
+
* @param onChange - Called after each ratio change, once the watch is re-armed at the new ratio / 比が変わるたび、新しい比で監視を張り直した後に呼ぶ
|
|
651
|
+
* @returns Unsubscribe that removes the current watch / 今の監視を外す購読解除
|
|
652
|
+
*/
|
|
653
|
+
const subscribeToDevicePixelRatio = (view: Window, onChange: () => void): (() => void) => {
|
|
654
|
+
if (typeof view.matchMedia !== "function") {
|
|
655
|
+
return unsubscribeNothing
|
|
656
|
+
}
|
|
657
|
+
let resolutionQuery = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`)
|
|
658
|
+
/**
|
|
659
|
+
* Re-arms the watch at the window's new ratio, then reports the change.
|
|
660
|
+
*
|
|
661
|
+
* ウィンドウの新しい比で監視を張り直してから変化を知らせる処理。
|
|
662
|
+
*/
|
|
663
|
+
const handleResolutionChange = (): void => {
|
|
664
|
+
resolutionQuery.removeEventListener("change", handleResolutionChange)
|
|
665
|
+
// ❗ `(resolution: Ndppx)` が知らせるのは比 N との一致・不一致の切り替わりだけ。N 以外の比どうしの変化
|
|
666
|
+
// (もう一度の拡大縮小・さらに別の画面への移動) を捉えるには、変化のたびに新しい比で張り直すしかない
|
|
667
|
+
resolutionQuery = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`)
|
|
668
|
+
resolutionQuery.addEventListener("change", handleResolutionChange)
|
|
669
|
+
onChange()
|
|
670
|
+
}
|
|
671
|
+
resolutionQuery.addEventListener("change", handleResolutionChange)
|
|
672
|
+
/**
|
|
673
|
+
* Removes the current watch.
|
|
674
|
+
*
|
|
675
|
+
* 今の監視を外す処理。
|
|
676
|
+
*/
|
|
677
|
+
const unsubscribe = (): void => {
|
|
678
|
+
resolutionQuery.removeEventListener("change", handleResolutionChange)
|
|
679
|
+
}
|
|
680
|
+
return unsubscribe
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Props of `ItemsWrapper`.
|
|
685
|
+
*
|
|
686
|
+
* `ItemsWrapper` の props。
|
|
687
|
+
*/
|
|
688
|
+
type ItemsWrapperProps = {
|
|
689
|
+
/** Exact translate in CSS px: top inset + render anchor - pane position / 厳密な平行移動 (CSS px。上のインセット + 描画アンカー - ペイン位置) */
|
|
690
|
+
readonly translateY: number
|
|
691
|
+
/** The rendered rows and the bottom inset / 描いた行と下のインセット */
|
|
692
|
+
readonly children: ReactNode
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The items wrapper: a compositor layer (`will-change: transform`) moved by `translateY`, snapped to the
|
|
697
|
+
* device-pixel grid of the window that paints it (`snapToDevicePixelGrid`). Rule: the translate that reaches
|
|
698
|
+
* the screen is always snapped with the ratio of the window that paints the wrapper. Render reads the ratio
|
|
699
|
+
* through `useSyncExternalStore` from the expected painting window — the realm's window (`readRealmWindow`)
|
|
700
|
+
* until the wrapper has attached, since render cannot see the target document. The attach confirms it with the
|
|
701
|
+
* wrapper's own window (`windowOf`): when they agree (a list rendered into its own window's document), the first
|
|
702
|
+
* commit is already snapped and nothing is scheduled in the commit phase; when they differ (a list rendered into
|
|
703
|
+
* an iframe or an opened window), the attach switches to that window; the update is scheduled in the commit
|
|
704
|
+
* phase, so React re-renders the wrapper synchronously before the browser paints. Ratio changes re-render
|
|
705
|
+
* through the store subscription (`subscribeToDevicePixelRatio`), outside the commit phase. The server snapshot
|
|
706
|
+
* is `null`: server HTML and hydration carry the exact translate, replaced by the snapped one right after
|
|
707
|
+
* hydration.
|
|
708
|
+
*
|
|
709
|
+
* 行ラッパー。`translateY` で動かす合成層 (`will-change: transform`) で、平行移動は描くウィンドウの装置の画素の
|
|
710
|
+
* 格子へ揃える (`snapToDevicePixelGrid`)。規則は「画面へ届く平行移動は、常にラッパーを描くウィンドウの比で
|
|
711
|
+
* 揃っている」。描画は `useSyncExternalStore` で、描くと見込むウィンドウから比を読む — 描画からは挿入先の文書が
|
|
712
|
+
* 見えないので、ラッパーが取り付くまではレルムのウィンドウ (`readRealmWindow`)。取り付けがラッパー自身の
|
|
713
|
+
* ウィンドウ (`windowOf`) と突き合わせて確かめ、一致すれば (自分のウィンドウの文書に描いた一覧) 最初の確定から
|
|
714
|
+
* 揃っていて確定の段では何も予約しない。違えば (iframe・開いたウィンドウに描いた一覧) 取り付けがそのウィンドウへ
|
|
715
|
+
* 切り替え、この更新は確定の段で予約されるので、React はブラウザの paint の前にラッパーを同期で描き直す。
|
|
716
|
+
* 比の変化はストアの購読 (`subscribeToDevicePixelRatio`) で確定の段の外から描き直す。サーバーのスナップショットは
|
|
717
|
+
* `null` で、サーバーの HTML とハイドレーションは厳密な平行移動を持ち、ハイドレーションの直後に揃えた値へ置き換わる。
|
|
718
|
+
*
|
|
719
|
+
* @param props - The exact translate and the wrapped rows / 厳密な平行移動と包む行
|
|
720
|
+
* @returns The wrapper element / ラッパーの要素
|
|
721
|
+
* @throws {Error} When the wrapper attaches to a document without a window (see `windowOf`) / ウィンドウを持たない文書へ取り付いたとき (`windowOf` を参照)
|
|
722
|
+
* @throws {RangeError} When the painting window reports a ratio that is not a finite number > 0 (see `snapToDevicePixelGrid`) / 描くウィンドウの比が 0 より大きい有限数でないとき (`snapToDevicePixelGrid` を参照)
|
|
723
|
+
*/
|
|
724
|
+
const ItemsWrapper = ({ translateY, children }: ItemsWrapperProps) => {
|
|
725
|
+
const [paintingWindow, setPaintingWindow] = useState<Window | null>(readRealmWindow)
|
|
726
|
+
/**
|
|
727
|
+
* Subscribes the store to the painting window's ratio changes.
|
|
728
|
+
*
|
|
729
|
+
* 描くウィンドウの比の変化へストアを購読させる処理。
|
|
730
|
+
*/
|
|
731
|
+
const subscribe = useCallback((onChange: () => void): (() => void) => (paintingWindow === null ? unsubscribeNothing : subscribeToDevicePixelRatio(paintingWindow, onChange)), [paintingWindow])
|
|
732
|
+
/**
|
|
733
|
+
* Reads the painting window's current ratio (`null` while no window is known).
|
|
734
|
+
*
|
|
735
|
+
* 描くウィンドウの今の比を読む処理 (ウィンドウが分からない間は `null`)。
|
|
736
|
+
*/
|
|
737
|
+
const readDevicePixelRatio = useCallback((): number | null => (paintingWindow === null ? null : paintingWindow.devicePixelRatio), [paintingWindow])
|
|
738
|
+
// 大域の devicePixelRatio と同名にしない — 宣言を消したとき参照が黙って大域のウィンドウの比へ化けるため
|
|
739
|
+
const paintingRatio = useSyncExternalStore(subscribe, readDevicePixelRatio, readServerDevicePixelRatio)
|
|
740
|
+
/**
|
|
741
|
+
* Ref callback that confirms the expected painting window against the window of the wrapper's document.
|
|
742
|
+
*
|
|
743
|
+
* 描くと見込んだウィンドウを、ラッパーの文書のウィンドウと突き合わせて確かめる ref コールバック。
|
|
744
|
+
*/
|
|
745
|
+
const confirmPaintingWindow = useCallback(
|
|
746
|
+
(wrapper: HTMLDivElement | null): void => {
|
|
747
|
+
if (wrapper === null) {
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
const view = windowOf(wrapper)
|
|
751
|
+
// 同じウィンドウなら描画で読んだ比が正しく、確定の段の更新 (paint 前の同期の描き直し) は要らない
|
|
752
|
+
if (view !== paintingWindow) {
|
|
753
|
+
setPaintingWindow(view)
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
[paintingWindow],
|
|
757
|
+
)
|
|
758
|
+
// ❗ ラッパーは合成層なので、平行移動が装置の画素の端数を持つとブラウザは層ごと再標本化する (実測: -2440.5px で
|
|
759
|
+
// 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み、下辺の隙間が消える)。揃えるのは層の平行移動だけでよい —
|
|
760
|
+
// 層の中の行の端数の位置は描画が画素へ揃える
|
|
761
|
+
const wrapperTranslateY = paintingRatio === null ? translateY : snapToDevicePixelGrid(translateY, paintingRatio)
|
|
762
|
+
// ❗ 層の様式は will-change: transform だけ。perspective や 3D の変換を足すと Chromium は描画の切り取り (cull rect) を
|
|
763
|
+
// 外して描くが、行を非 2D の変換の下で合成するので画素比によっては層ごと再標本化する (実測: 比 1.25 の明色で選択の輪と
|
|
764
|
+
// フォーカスの輪の間の帯が混ざる)。窓をずらす 1 段がはみ出しを切る中身の行を描き直す費用は、その代わりに受け入れる
|
|
765
|
+
return (
|
|
766
|
+
<div ref={confirmPaintingWindow} className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${wrapperTranslateY}px)`, willChange: "transform" }}>
|
|
767
|
+
{children}
|
|
768
|
+
</div>
|
|
769
|
+
)
|
|
770
|
+
}
|
|
771
|
+
|
|
604
772
|
/**
|
|
605
773
|
* Pixels emitted per horizontal arrow key press. Matches the ~40px browsers scroll for an arrow key.
|
|
606
774
|
*
|
|
@@ -2507,74 +2675,14 @@ const VirtualScrollInner = <T,>(
|
|
|
2507
2675
|
visibleStartIndex,
|
|
2508
2676
|
])
|
|
2509
2677
|
|
|
2510
|
-
// 行ラッパーを描くウィンドウの装置の画素比。ラッパーの最初の取り付けまで (サーバー描画と初回描画) は null。
|
|
2511
|
-
// 大域の devicePixelRatio と同名にしない — 宣言を消したとき参照が黙って大域のウィンドウの比へ化けるため
|
|
2512
|
-
const [wrapperDevicePixelRatio, setWrapperDevicePixelRatio] = useState<number | null>(null)
|
|
2513
|
-
|
|
2514
|
-
/**
|
|
2515
|
-
* Ref callback of the items wrapper. Commits the device-pixel ratio of the window the wrapper is painted in
|
|
2516
|
-
* (`windowOf`, never the global `window`), and commits it again whenever that ratio changes (browser zoom,
|
|
2517
|
-
* or the window moving to a screen of another density) through a `(resolution: <ratio>dppx)` media-query
|
|
2518
|
-
* watch that is re-armed at each new ratio. The first commit runs in React's commit phase, so a client mount
|
|
2519
|
-
* paints the snapped translate in its first frame. A window without media queries (jsdom) cannot change its
|
|
2520
|
-
* ratio: the ratio read at attach applies and no watch is armed. The callback identity is stable, so React
|
|
2521
|
-
* calls it once per attach. React 19 calls the returned cleanup on detach; it calls the ref with `null`
|
|
2522
|
-
* only when the attach threw (no cleanup exists), and that call does nothing.
|
|
2523
|
-
*
|
|
2524
|
-
* 行ラッパーの ref コールバック。ラッパーを描くウィンドウ (`windowOf`。大域の `window` ではない) の装置の
|
|
2525
|
-
* 画素比を状態へ確定し、比が変わるたび (ブラウザの拡大縮小・密度の違う画面へのウィンドウの移動) に
|
|
2526
|
-
* `(resolution: <比>dppx)` のメディアクエリの監視で確定し直す (監視は新しい比で張り直す)。最初の確定は
|
|
2527
|
-
* React の確定の段で走るので、クライアントでのマウントは最初のフレームから揃えた平行移動で描く。
|
|
2528
|
-
* メディアクエリを持たないウィンドウ (jsdom) は比が変わり得ないので、取り付け時に読んだ比を使い監視は
|
|
2529
|
-
* 張らない。コールバックの同一性は安定しているので、React は取り付け 1 回につき 1 回だけ呼ぶ。取り外し時に
|
|
2530
|
-
* React 19 は返した後始末を呼び、ref を `null` で呼ぶのは取り付けが投げて後始末が無いときだけ (何もしない)。
|
|
2531
|
-
*
|
|
2532
|
-
* @param wrapper - The attached items wrapper, or `null` on the detach that follows an attach that threw / 取り付けた行ラッパー (投げた取り付けの後の取り外しでは `null`)
|
|
2533
|
-
* @returns Cleanup that removes the resolution watch, or `undefined` for `null` / 解像度の監視を外す後始末 (`null` には `undefined`)
|
|
2534
|
-
* @throws {Error} When the wrapper's document has no window (see `windowOf`) / ラッパーの文書がウィンドウを持たないとき (`windowOf` を参照)
|
|
2535
|
-
*/
|
|
2536
|
-
const observeItemsWrapperDevicePixelRatio = useCallback((wrapper: HTMLDivElement | null): (() => void) | undefined => {
|
|
2537
|
-
// 後始末を返した ref を React 19 は null で呼ばない。null が届くのは取り付けが投げた後だけで、外す監視も無い
|
|
2538
|
-
if (wrapper === null) {
|
|
2539
|
-
return undefined
|
|
2540
|
-
}
|
|
2541
|
-
const view = windowOf(wrapper)
|
|
2542
|
-
let resolutionQuery: MediaQueryList | undefined
|
|
2543
|
-
/**
|
|
2544
|
-
* Commits the window's current device-pixel ratio and re-arms the resolution watch at that ratio.
|
|
2545
|
-
*
|
|
2546
|
-
* ウィンドウの現在の装置の画素比を確定し、その比で解像度の監視を張り直す処理。
|
|
2547
|
-
*/
|
|
2548
|
-
const syncDevicePixelRatio = (): void => {
|
|
2549
|
-
resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
|
|
2550
|
-
const ratio = view.devicePixelRatio
|
|
2551
|
-
setWrapperDevicePixelRatio(ratio)
|
|
2552
|
-
// ❗ `(resolution: Ndppx)` が知らせるのは比 N との一致・不一致の切り替わりだけ。N 以外の比どうしの変化
|
|
2553
|
-
// (もう一度の拡大縮小・さらに別の画面への移動) を捉えるには、変化のたびに新しい比で張り直すしかない。
|
|
2554
|
-
// matchMedia を持たないウィンドウ (jsdom) は比が変わり得ないので監視だけを省く (読んだ比は使う)
|
|
2555
|
-
resolutionQuery = typeof view.matchMedia === "function" ? view.matchMedia(`(resolution: ${ratio}dppx)`) : undefined
|
|
2556
|
-
resolutionQuery?.addEventListener("change", syncDevicePixelRatio)
|
|
2557
|
-
}
|
|
2558
|
-
syncDevicePixelRatio()
|
|
2559
|
-
/**
|
|
2560
|
-
* Removes the resolution watch; React calls it when the wrapper detaches.
|
|
2561
|
-
*
|
|
2562
|
-
* 解像度の監視を外す後始末 (ラッパーの取り外し時に React が呼ぶ)。
|
|
2563
|
-
*/
|
|
2564
|
-
const stopObservingDevicePixelRatio = (): void => {
|
|
2565
|
-
resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
|
|
2566
|
-
}
|
|
2567
|
-
return stopObservingDevicePixelRatio
|
|
2568
|
-
}, [])
|
|
2569
|
-
|
|
2570
2678
|
/**
|
|
2571
2679
|
* Renders the items wrapper for the pane's current scroll position (ScrollPane's `children` render prop).
|
|
2572
|
-
* The wrapper carries the scroll offset, the top inset and the render anchor as one translate,
|
|
2573
|
-
* the device-pixel grid
|
|
2680
|
+
* The wrapper carries the scroll offset, the top inset and the render anchor as one translate, which
|
|
2681
|
+
* `ItemsWrapper` snaps to the device-pixel grid of the window that paints it; rows keep their exact tops.
|
|
2574
2682
|
*
|
|
2575
2683
|
* ペインの現在のスクロール位置に対する行ラッパーを描く処理 (ScrollPane の `children` の描画関数)。スクロール位置・
|
|
2576
|
-
* 上のインセット・描画アンカーを 1
|
|
2577
|
-
*
|
|
2684
|
+
* 上のインセット・描画アンカーを 1 つの平行移動としてラッパーが運び、`ItemsWrapper` がそれを描くウィンドウの
|
|
2685
|
+
* 装置の画素の格子へ揃える。行の上端は厳密値のまま。
|
|
2578
2686
|
*
|
|
2579
2687
|
* @param currentScrollPosition - The pane's scroll position in PANE coordinates / ペイン座標のスクロール位置
|
|
2580
2688
|
* @returns The items wrapper, or the empty state when there are no items / 行ラッパー (項目が無ければ空状態)
|
|
@@ -2611,12 +2719,6 @@ const VirtualScrollInner = <T,>(
|
|
|
2611
2719
|
// 毎フレームのリフローを避けコンポジタのみで完結させる。
|
|
2612
2720
|
// アンカーは visibleItems と同じ memo の返り値を使い、行 top との整合を保証する。
|
|
2613
2721
|
const containerTop = resolvedInsets.top + renderAnchor - rawEffectiveScrollPosition
|
|
2614
|
-
// ❗ ラッパーは合成層 (willChange: transform) なので、平行移動が装置の画素の端数を持つとブラウザは層ごと
|
|
2615
|
-
// 再標本化する (実測: -2440.5px で 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み、下辺の隙間が消える)。
|
|
2616
|
-
// 揃えるのは層の平行移動だけでよい — 層の中の行の端数の位置は描画が画素へ揃える。比が決まるのは
|
|
2617
|
-
// ラッパーの取り付け時なので、それまで (サーバー描画と初回描画) は描く画面が無く厳密値のまま書く。
|
|
2618
|
-
// クライアントのマウントは取り付けの確定が paint 前に再描画するので、端数のまま描かれることはない
|
|
2619
|
-
const wrapperTranslateY = wrapperDevicePixelRatio === null ? containerTop : snapToDevicePixelGrid(containerTop, wrapperDevicePixelRatio)
|
|
2620
2722
|
|
|
2621
2723
|
const bottomInset =
|
|
2622
2724
|
resolvedInsets.bottom > 0 ? (
|
|
@@ -2633,21 +2735,19 @@ const VirtualScrollInner = <T,>(
|
|
|
2633
2735
|
|
|
2634
2736
|
Logger.debug("[VirtualScroll] Rendering items", () => ({
|
|
2635
2737
|
containerTop,
|
|
2636
|
-
wrapperDevicePixelRatio,
|
|
2637
|
-
wrapperTranslateY,
|
|
2638
2738
|
logicalScrollPosition,
|
|
2639
2739
|
resolvedInsets,
|
|
2640
2740
|
effectiveScrollPosition,
|
|
2641
2741
|
}))
|
|
2642
2742
|
|
|
2643
2743
|
return (
|
|
2644
|
-
<
|
|
2744
|
+
<ItemsWrapper translateY={containerTop}>
|
|
2645
2745
|
{visibleItems}
|
|
2646
2746
|
{bottomInset}
|
|
2647
|
-
</
|
|
2747
|
+
</ItemsWrapper>
|
|
2648
2748
|
)
|
|
2649
2749
|
},
|
|
2650
|
-
[callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition,
|
|
2750
|
+
[callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems],
|
|
2651
2751
|
)
|
|
2652
2752
|
|
|
2653
2753
|
const currentRange = useMemo<VirtualScrollRange>(
|
|
@@ -557,3 +557,25 @@
|
|
|
557
557
|
border-radius: 50%;
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
|
+
|
|
561
|
+
/* 動きを減らす設定 (prefers-reduced-motion: reduce) では、パッケージが動かす部品はどれも遷移もアニメーションも持たず、
|
|
562
|
+
* 状態の変化を即座に描く。セレクタは、このファイルの規則が遷移を宣言する部品のすべて — タップスクロールサークル
|
|
563
|
+
* (グリッドのサークルを含む) とバーのサークルの器の出入り・矢印ボタンの色・つまみのホバーとドラッグの拡大・
|
|
564
|
+
* 端へ戻るボタンの押下の縮小と地色・その覆いの出入り — と、サークルの中のビジュアル (既定のビジュアル・
|
|
565
|
+
* tapScrollCircleSampleVisual・消費側の renderVisual)。ビジュアルは遷移をインラインで書くので、それに勝てるのは
|
|
566
|
+
* スタイルシートの !important だけ (カスケードは重要度をインラインかどうかより先に比べる)。拡大・縮小・色の状態は
|
|
567
|
+
* 即座に切り替わり、行の中身 (消費側の部品) と動かない部品には触れない。遷移を宣言する部品を足すときはここにも足す
|
|
568
|
+
* (reducedMotion.spec.tsx がこのファイルから動く部品を導いて照合する)。 */
|
|
569
|
+
@media (prefers-reduced-motion: reduce) {
|
|
570
|
+
.aqvs-tap-scroll-circle,
|
|
571
|
+
.aqvs-tap-scroll-circle *,
|
|
572
|
+
.aqvs-grid-tap-circle,
|
|
573
|
+
.aqvs-scrollbar-tap-circle-wrapper,
|
|
574
|
+
.aqvs-scrollbar-arrow-button,
|
|
575
|
+
.aqvs-scrollbar-thumb,
|
|
576
|
+
.aqvs-scroll-to-edge-button,
|
|
577
|
+
.aqvs-scroll-to-edge-overlay {
|
|
578
|
+
transition: none !important;
|
|
579
|
+
animation: none !important;
|
|
580
|
+
}
|
|
581
|
+
}
|