@ixfx/components 0.10.2 → 0.10.4

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.
Files changed (54) hide show
  1. package/bundle/index.d.ts +89 -28
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +298 -9
  4. package/bundle/index.js.map +1 -1
  5. package/dist/{button-DsFbV4XD.js → button-B7mgqtxR.js} +36 -2
  6. package/dist/button-B7mgqtxR.js.map +1 -0
  7. package/dist/button-BYuf72aR.d.ts +63 -0
  8. package/dist/button-BYuf72aR.d.ts.map +1 -0
  9. package/dist/button.d.ts +2 -21
  10. package/dist/button.js +1 -1
  11. package/dist/{data-grid-pv6eaJCZ.d.ts → data-grid-DE1_gZNk.d.ts} +7 -2
  12. package/dist/data-grid-DE1_gZNk.d.ts.map +1 -0
  13. package/dist/data-grid.d.ts +2 -2
  14. package/dist/data-grid.js +56 -0
  15. package/dist/data-grid.js.map +1 -1
  16. package/dist/{detail-list-BA3RVVCB.d.ts → detail-list-CaStSzYN.d.ts} +5 -3
  17. package/dist/detail-list-CaStSzYN.d.ts.map +1 -0
  18. package/dist/detail-list.d.ts +4 -3
  19. package/dist/detail-list.js +61 -1
  20. package/dist/detail-list.js.map +1 -1
  21. package/dist/{grid-list-DWXJoRnK.d.ts → grid-list-BJDxBL0x.d.ts} +15 -2
  22. package/dist/{grid-list-DWXJoRnK.d.ts.map → grid-list-BJDxBL0x.d.ts.map} +1 -1
  23. package/dist/{grid-list-CIc4jo-x.js → grid-list-CaMcdpFJ.js} +100 -7
  24. package/dist/grid-list-CaMcdpFJ.js.map +1 -0
  25. package/dist/grid-list.d.ts +2 -2
  26. package/dist/grid-list.js +1 -1
  27. package/dist/{grouped-item-lister-fdZHVxFM.d.ts → grouped-item-lister-DETte0Dh.d.ts} +2 -2
  28. package/dist/{grouped-item-lister-fdZHVxFM.d.ts.map → grouped-item-lister-DETte0Dh.d.ts.map} +1 -1
  29. package/dist/grouped-item-lister.d.ts +1 -1
  30. package/dist/index.d.ts +9 -27
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/{list-selection-types-B5ioL-dG.d.ts → list-selection-types-PjKdhF-a.d.ts} +1 -9
  34. package/dist/list-selection-types-PjKdhF-a.d.ts.map +1 -0
  35. package/dist/list-viewport-types-BRZsLcvp.d.ts +23 -0
  36. package/dist/list-viewport-types-BRZsLcvp.d.ts.map +1 -0
  37. package/dist/{vertical-list-BWLqy3PJ.d.ts → vertical-list-DfcNsa4p.d.ts} +10 -2
  38. package/dist/vertical-list-DfcNsa4p.d.ts.map +1 -0
  39. package/dist/vertical-list.d.ts +4 -3
  40. package/dist/vertical-list.js +47 -1
  41. package/dist/vertical-list.js.map +1 -1
  42. package/docs-user/data-grid.md +9 -0
  43. package/docs-user/detail-list.md +8 -0
  44. package/docs-user/grid-list.md +16 -1
  45. package/docs-user/index.json +2 -2
  46. package/docs-user/vertical-list.md +7 -0
  47. package/package.json +1 -1
  48. package/dist/button-DsFbV4XD.js.map +0 -1
  49. package/dist/button.d.ts.map +0 -1
  50. package/dist/data-grid-pv6eaJCZ.d.ts.map +0 -1
  51. package/dist/detail-list-BA3RVVCB.d.ts.map +0 -1
  52. package/dist/grid-list-CIc4jo-x.js.map +0 -1
  53. package/dist/list-selection-types-B5ioL-dG.d.ts.map +0 -1
  54. package/dist/vertical-list-BWLqy3PJ.d.ts.map +0 -1
package/bundle/index.js CHANGED
@@ -5715,6 +5715,20 @@ const buttonStyles = i$3`
5715
5715
  :host([subtle]) button:focus-visible:not(:disabled) {
5716
5716
  background: color-mix(in oklch, var(--surface-4, #ccc) 70%, transparent);
5717
5717
  }
5718
+
5719
+ :host([toggleable]) button:not(:disabled) {
5720
+ opacity: 0.7;
5721
+ }
5722
+
5723
+ :host([toggleable]) button:hover:not(:disabled) {
5724
+ opacity: 0.85;
5725
+ }
5726
+
5727
+ :host([toggleable][value]) button:not(:disabled) {
5728
+ opacity: 1;
5729
+ outline: var(--ixfx-button-toggle-outline-width, 2px) solid var(--ixfx-button-toggle-outline-color, oklch(from var(--accent, #666) l c calc(h + 130)));
5730
+ outline-offset: var(--ixfx-button-toggle-outline-offset, 2px);
5731
+ }
5718
5732
  `;
5719
5733
  //#endregion
5720
5734
  //#region src/button/button.ts
@@ -5726,7 +5740,19 @@ let ButtonElement = class ButtonElement extends i {
5726
5740
  this.hideLabel = false;
5727
5741
  this.hasIcon = false;
5728
5742
  this.subtle = false;
5743
+ this.toggleable = false;
5744
+ this.value = false;
5745
+ this.#handleClick = () => {
5746
+ if (!this.toggleable || this.disabled) return;
5747
+ this.value = !this.value;
5748
+ this.dispatchEvent(new CustomEvent(`toggle`, {
5749
+ detail: { value: this.value },
5750
+ bubbles: true,
5751
+ composed: true
5752
+ }));
5753
+ };
5729
5754
  }
5755
+ #handleClick;
5730
5756
  _renderIcon() {
5731
5757
  if (this.hideIcon || !this.icon) return A;
5732
5758
  if (this.icon.startsWith(`<svg`)) return b`<span class="button-icon">${unsafeHTML(this.icon)}</span>`;
@@ -5736,7 +5762,7 @@ let ButtonElement = class ButtonElement extends i {
5736
5762
  render() {
5737
5763
  this.hasIcon = !this.hideIcon && !!this.icon;
5738
5764
  const label = this.hideLabel ? A : b`<span class="button-label"><slot></slot></span>`;
5739
- return b`<button ?disabled=${this.disabled}>${this._renderIcon()}${label}</button>`;
5765
+ return b`<button ?disabled=${this.disabled} @click=${this.#handleClick}>${this._renderIcon()}${label}</button>`;
5740
5766
  }
5741
5767
  static {
5742
5768
  this.styles = [prominenceStyles, buttonStyles];
@@ -5763,6 +5789,14 @@ __decorate([property({
5763
5789
  type: Boolean,
5764
5790
  reflect: true
5765
5791
  })], ButtonElement.prototype, "subtle", void 0);
5792
+ __decorate([property({
5793
+ type: Boolean,
5794
+ reflect: true
5795
+ })], ButtonElement.prototype, "toggleable", void 0);
5796
+ __decorate([property({
5797
+ type: Boolean,
5798
+ reflect: true
5799
+ })], ButtonElement.prototype, "value", void 0);
5766
5800
  ButtonElement = __decorate([safeCustomElement(`ixfx-button`)], ButtonElement);
5767
5801
  //#endregion
5768
5802
  //#region src/button/radio-group.ts
@@ -14627,6 +14661,12 @@ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
14627
14661
  PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
14628
14662
  //#endregion
14629
14663
  //#region src/data-grid/data-grid.ts
14664
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
14665
+ function sameKeys$3(a, b) {
14666
+ if (a === void 0 || a.length !== b.length) return false;
14667
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
14668
+ return true;
14669
+ }
14630
14670
  let DataGridElement = class DataGridElement extends i {
14631
14671
  constructor(..._args) {
14632
14672
  super(..._args);
@@ -14719,6 +14759,7 @@ let DataGridElement = class DataGridElement extends i {
14719
14759
  this._colOffsets = [];
14720
14760
  this._colWidthSum = 0;
14721
14761
  this._raf = 0;
14762
+ this.#viewportChangeRaf = 0;
14722
14763
  this.#appendInProgress = false;
14723
14764
  this.#resortRaf = 0;
14724
14765
  this._drag = null;
@@ -14740,6 +14781,9 @@ let DataGridElement = class DataGridElement extends i {
14740
14781
  this._scrollLeft = el.scrollLeft;
14741
14782
  if (this._headerScrollEl) this._headerScrollEl.scrollLeft = el.scrollLeft;
14742
14783
  this.requestUpdate();
14784
+ cancelAnimationFrame(this.#viewportChangeRaf);
14785
+ this.#viewportChangeRaf = 0;
14786
+ this.#emitViewportChangeIfChanged();
14743
14787
  });
14744
14788
  };
14745
14789
  this._onKeyDown = (e) => {
@@ -15173,6 +15217,10 @@ let DataGridElement = class DataGridElement extends i {
15173
15217
  #enteringRowKeys;
15174
15218
  #rowReflowTimer;
15175
15219
  #dataGridRenderedOnce;
15220
+ #viewportChangeRaf;
15221
+ /** Key sets as of the last `datagrid-viewport-change` dispatch, for dedupe. */
15222
+ #lastViewportRenderedKeys;
15223
+ #lastViewportVisibleKeys;
15176
15224
  #appendInProgress;
15177
15225
  #resortRaf;
15178
15226
  connectedCallback() {
@@ -15188,6 +15236,8 @@ let DataGridElement = class DataGridElement extends i {
15188
15236
  this.removeEventListener(`keydown`, this._onKeyDown);
15189
15237
  this.removeEventListener(`focusout`, this._onFocusOut);
15190
15238
  cancelAnimationFrame(this._raf);
15239
+ cancelAnimationFrame(this.#viewportChangeRaf);
15240
+ this.#viewportChangeRaf = 0;
15191
15241
  this.#unregisterSearchCommands?.();
15192
15242
  this.#unregisterSearchCommands = void 0;
15193
15243
  this.#searchRegistry = void 0;
@@ -15228,6 +15278,7 @@ let DataGridElement = class DataGridElement extends i {
15228
15278
  if (w === this._viewportW && h === this._viewportH) return;
15229
15279
  this._viewportW = w;
15230
15280
  this._viewportH = h;
15281
+ this.#scheduleViewportChangeEmit();
15231
15282
  }
15232
15283
  /**
15233
15284
  * Force a fresh re-measure of the viewport and re-render. Normally
@@ -15242,6 +15293,7 @@ let DataGridElement = class DataGridElement extends i {
15242
15293
  }
15243
15294
  updated(changed) {
15244
15295
  super.updated(changed);
15296
+ if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`filterPredicate`) || changed.has(`rowHeight`)) this.#scheduleViewportChangeEmit();
15245
15297
  if (changed.has(`_editing`) || changed.has(`_editingRow`)) requestAnimationFrame(() => {
15246
15298
  let sel = null;
15247
15299
  if (this._editing) sel = `.row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] input, .row[data-key="${this._editing.rowKey}"] .cell[data-col="${this._editing.colKey}"] select`;
@@ -16021,6 +16073,44 @@ let DataGridElement = class DataGridElement extends i {
16021
16073
  const { startRow, endRow } = this.getVisibleRange();
16022
16074
  return this._flat.slice(startRow, endRow).filter((f) => f.type === `row`).map((f) => f.key);
16023
16075
  }
16076
+ /**
16077
+ * Row keys strictly intersecting the viewport (no overscan), mirroring
16078
+ * `ixfx-grid-list.visibleKeys()` for the `datagrid-viewport-change` detail.
16079
+ */
16080
+ #visibleKeys() {
16081
+ const rh = this.rowHeight;
16082
+ const startRow = Math.max(0, Math.floor(this._scrollTop / rh));
16083
+ const endRow = Math.min(this._flat.length, Math.ceil((this._scrollTop + this._viewportH) / rh));
16084
+ return this._flat.slice(startRow, endRow).filter((f) => f.type === `row`).map((f) => f.key);
16085
+ }
16086
+ /** Coalesce viewport-change dispatches to at most one per animation frame. */
16087
+ #scheduleViewportChangeEmit() {
16088
+ if (this.#viewportChangeRaf !== 0) return;
16089
+ this.#viewportChangeRaf = requestAnimationFrame(() => {
16090
+ this.#viewportChangeRaf = 0;
16091
+ this.#emitViewportChangeIfChanged();
16092
+ });
16093
+ }
16094
+ /**
16095
+ * Dispatch `datagrid-viewport-change` with the current key sets — unless
16096
+ * neither set changed since the last dispatch, so coalesced frames and
16097
+ * no-op scrolls stay silent.
16098
+ */
16099
+ #emitViewportChangeIfChanged() {
16100
+ const renderedKeys = this.renderedKeys();
16101
+ const visibleKeys = this.#visibleKeys();
16102
+ if (sameKeys$3(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$3(this.#lastViewportVisibleKeys, visibleKeys)) return;
16103
+ this.#lastViewportRenderedKeys = renderedKeys;
16104
+ this.#lastViewportVisibleKeys = visibleKeys;
16105
+ this.dispatchEvent(new CustomEvent(`datagrid-viewport-change`, {
16106
+ detail: {
16107
+ renderedKeys,
16108
+ visibleKeys
16109
+ },
16110
+ bubbles: true,
16111
+ composed: true
16112
+ }));
16113
+ }
16024
16114
  _findFirstVisibleCol(scrollLeft) {
16025
16115
  let lo = 0;
16026
16116
  let hi = this._colOffsets.length - 1;
@@ -16619,6 +16709,12 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
16619
16709
  const ITEM_SELECTOR$3 = `:scope > li`;
16620
16710
  const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
16621
16711
  const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
16712
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
16713
+ function sameKeys$2(a, b) {
16714
+ if (a === void 0 || a.length !== b.length) return false;
16715
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
16716
+ return true;
16717
+ }
16622
16718
  let DetailListElement = class DetailListElement extends i {
16623
16719
  constructor(..._args) {
16624
16720
  super(..._args);
@@ -16637,6 +16733,7 @@ let DetailListElement = class DetailListElement extends i {
16637
16733
  this.#positions = /* @__PURE__ */ new Map();
16638
16734
  this.#geometryScheduled = false;
16639
16735
  this.#sortRaf = 0;
16736
+ this.#viewportScrollRaf = 0;
16640
16737
  this.#animatedItems = /* @__PURE__ */ new WeakSet();
16641
16738
  this.#builtFromProvider = [];
16642
16739
  this.#providerBuilt = 0;
@@ -16730,6 +16827,13 @@ let DetailListElement = class DetailListElement extends i {
16730
16827
  getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
16731
16828
  highlightKey: SEARCH_HIGHLIGHT_KEY
16732
16829
  }) });
16830
+ this.#onListScroll = () => {
16831
+ if (this.#viewportScrollRaf !== 0) return;
16832
+ this.#viewportScrollRaf = requestAnimationFrame(() => {
16833
+ this.#viewportScrollRaf = 0;
16834
+ this.#emitViewportChangeIfChanged();
16835
+ });
16836
+ };
16733
16837
  this._onPointerOver = (e) => {
16734
16838
  const item = this._findListItem(e.composedPath());
16735
16839
  if (item) this.tickled.handlePointerEnter(item);
@@ -16821,6 +16925,10 @@ let DetailListElement = class DetailListElement extends i {
16821
16925
  #positions;
16822
16926
  #geometryScheduled;
16823
16927
  #sortRaf;
16928
+ #viewportScrollRaf;
16929
+ /** Key sets as of the last viewport-change dispatch, for dedupe. */
16930
+ #lastViewportRenderedKeys;
16931
+ #lastViewportVisibleKeys;
16824
16932
  #resizeObserver;
16825
16933
  #mutationObserver;
16826
16934
  /** Items that have already played (or been skipped for) their enter animation. */
@@ -16960,6 +17068,8 @@ let DetailListElement = class DetailListElement extends i {
16960
17068
  this.removeEventListener(`dblclick`, this._onDblClick);
16961
17069
  this.removeEventListener(`wheel`, this._onWheel);
16962
17070
  cancelAnimationFrame(this.#sortRaf);
17071
+ cancelAnimationFrame(this.#viewportScrollRaf);
17072
+ this.#viewportScrollRaf = 0;
16963
17073
  this.#resizeObserver?.disconnect();
16964
17074
  this.#resizeObserver = void 0;
16965
17075
  this.#mutationObserver?.disconnect();
@@ -17312,6 +17422,7 @@ let DetailListElement = class DetailListElement extends i {
17312
17422
  col: colIdx,
17313
17423
  row: rowIdx
17314
17424
  })));
17425
+ this.#scheduleViewportChangeEmit();
17315
17426
  }
17316
17427
  #positionOf(el) {
17317
17428
  let pos = this.#positions.get(el);
@@ -17603,6 +17714,45 @@ let DetailListElement = class DetailListElement extends i {
17603
17714
  cancelable: true
17604
17715
  }));
17605
17716
  }
17717
+ /** Scroll on the `part="list"` element — rAF-coalesced viewport-change check. */
17718
+ #onListScroll;
17719
+ /** Coalesce non-scroll viewport checks to at most one per animation frame. */
17720
+ #scheduleViewportChangeEmit() {
17721
+ if (this.#viewportScrollRaf !== 0) return;
17722
+ this.#viewportScrollRaf = requestAnimationFrame(() => {
17723
+ this.#viewportScrollRaf = 0;
17724
+ this.#emitViewportChangeIfChanged();
17725
+ });
17726
+ }
17727
+ /**
17728
+ * Keys of everything currently rendered. The list has no virtual window —
17729
+ * every non-hidden item is in the DOM — so this is all keyed items, mirroring
17730
+ * `ixfx-vertical-list.renderedKeys()` in its non-virtualized mode.
17731
+ */
17732
+ #renderedKeys() {
17733
+ const out = [];
17734
+ for (const el of this._getVisibleItems()) {
17735
+ const key = keyForElement(el);
17736
+ if (key !== void 0) out.push(key);
17737
+ }
17738
+ return out;
17739
+ }
17740
+ /**
17741
+ * Dispatch `detail-list-viewport-change` with the current key sets — unless
17742
+ * neither set changed since the last dispatch, so coalesced frames and
17743
+ * no-op scrolls stay silent.
17744
+ */
17745
+ #emitViewportChangeIfChanged() {
17746
+ const renderedKeys = this.#renderedKeys();
17747
+ const visibleKeys = this.visibleKeys();
17748
+ if (sameKeys$2(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$2(this.#lastViewportVisibleKeys, visibleKeys)) return;
17749
+ this.#lastViewportRenderedKeys = renderedKeys;
17750
+ this.#lastViewportVisibleKeys = visibleKeys;
17751
+ this._dispatch(`detail-list-viewport-change`, {
17752
+ renderedKeys,
17753
+ visibleKeys
17754
+ });
17755
+ }
17606
17756
  _activateItem(item) {
17607
17757
  if (this.interactionMode !== `manual`) this.selection.replace(item);
17608
17758
  this._dispatch(`list-activate`, { item });
@@ -17761,7 +17911,7 @@ let DetailListElement = class DetailListElement extends i {
17761
17911
  const progress = this.#data.progress;
17762
17912
  return b`
17763
17913
  <div class="container">
17764
- <ul part="list" class="scroll-fade-x">
17914
+ <ul part="list" class="scroll-fade-x" @scroll=${this.#onListScroll}>
17765
17915
  <slot></slot>
17766
17916
  </ul>
17767
17917
  ${this.#marqueeRect ? b`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
@@ -23629,12 +23779,19 @@ function columnOf(index, cols) {
23629
23779
  function rowOf(index, cols) {
23630
23780
  return cols <= 1 ? index : Math.floor(index / cols);
23631
23781
  }
23782
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
23783
+ function sameKeys$1(a, b) {
23784
+ if (a === void 0 || a.length !== b.length) return false;
23785
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
23786
+ return true;
23787
+ }
23632
23788
  let GridListElement = class GridListElement extends i {
23633
23789
  constructor(..._args) {
23634
23790
  super(..._args);
23635
23791
  this.selectionMode = `single`;
23636
23792
  this.interactionMode = `standard`;
23637
23793
  this.disableHoverTickle = false;
23794
+ this.disableScrollHoverTickle = false;
23638
23795
  this.itemSize = 160;
23639
23796
  this.gap = 8;
23640
23797
  this.orientation = `grid`;
@@ -23691,6 +23848,9 @@ let GridListElement = class GridListElement extends i {
23691
23848
  itemWrap: this.#itemWrap,
23692
23849
  hoverDisabled: () => this.disableHoverTickle || this.#dragSelect.isDragging || this.#nativeDrag.isDragging
23693
23850
  });
23851
+ this.#scrollHoverSuppress = false;
23852
+ this.#lastPointerX = NaN;
23853
+ this.#lastPointerY = NaN;
23694
23854
  this.#marqueePreview = /* @__PURE__ */ new Set();
23695
23855
  this.#nativeDragging = false;
23696
23856
  this.#dragOriginKey = void 0;
@@ -23773,6 +23933,7 @@ let GridListElement = class GridListElement extends i {
23773
23933
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
23774
23934
  this.#onScroll = (e) => {
23775
23935
  const el = e.target;
23936
+ this.#markScrollHoverSuppress();
23776
23937
  this.#cancelScrollRaf();
23777
23938
  this.#scrollRaf = requestAnimationFrame(() => {
23778
23939
  this.#scrollRaf = 0;
@@ -23789,9 +23950,23 @@ let GridListElement = class GridListElement extends i {
23789
23950
  const vp = this.#viewportRef.value ?? e.currentTarget;
23790
23951
  if (!vp) return;
23791
23952
  if (vp.scrollWidth <= vp.clientWidth + 1) return;
23953
+ this.#markScrollHoverSuppress();
23792
23954
  vp.scrollLeft += e.deltaX + e.deltaY;
23793
23955
  e.preventDefault();
23794
23956
  };
23957
+ this.#onHostPointerMove = (e) => {
23958
+ const moved = !Number.isNaN(this.#lastPointerX) && (e.clientX !== this.#lastPointerX || e.clientY !== this.#lastPointerY);
23959
+ this.#lastPointerX = e.clientX;
23960
+ this.#lastPointerY = e.clientY;
23961
+ if (!moved || !this.#scrollHoverSuppress) return;
23962
+ this.#endScrollHoverSuppress();
23963
+ if (this.disableHoverTickle) return;
23964
+ const key = this.shadowRoot?.elementFromPoint?.(e.clientX, e.clientY)?.closest(`.cell`)?.getAttribute(`data-key`);
23965
+ if (key) this.tickled.handlePointerEnter(key);
23966
+ };
23967
+ this.#onHostPointerLeave = () => {
23968
+ this.#endScrollHoverSuppress();
23969
+ };
23795
23970
  this._onKeyDown = (e) => {
23796
23971
  if ((e.ctrlKey || e.metaKey) && e.key === `f`) return;
23797
23972
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
@@ -24037,7 +24212,10 @@ let GridListElement = class GridListElement extends i {
24037
24212
  transition: background var(--transition), border-color var(--transition);
24038
24213
  }
24039
24214
  :host(:not([caption-style="separate"])) .cell.tickled,
24040
- :host(:not([caption-style="separate"]):not([disable-hover-tickle])) .cell:hover {
24215
+ /* [data-scroll-hover-suppress] is set while a scroll gesture moves
24216
+ cells beneath a stationary pointer (see disable-scroll-hover-tickle):
24217
+ the :hover highlight is gated so the visual cursor holds still too. */
24218
+ :host(:not([caption-style="separate"]):not([disable-hover-tickle]):not([data-scroll-hover-suppress])) .cell:hover {
24041
24219
  background: var(--item-bg-tickled);
24042
24220
  border: var(--item-border-tickled);
24043
24221
  }
@@ -24113,7 +24291,7 @@ let GridListElement = class GridListElement extends i {
24113
24291
  border: 1px solid transparent;
24114
24292
  }
24115
24293
  :host([caption-style="separate"]) .cell.tickled .thumb,
24116
- :host([caption-style="separate"]:not([disable-hover-tickle])) .cell:hover .thumb {
24294
+ :host([caption-style="separate"]:not([disable-hover-tickle]):not([data-scroll-hover-suppress])) .cell:hover .thumb {
24117
24295
  background: var(--grid-list-thumb-bg-tickled, var(--item-bg-tickled));
24118
24296
  border: var(--item-border-tickled);
24119
24297
  }
@@ -24234,6 +24412,16 @@ let GridListElement = class GridListElement extends i {
24234
24412
  */
24235
24413
  #sourceElement;
24236
24414
  #source;
24415
+ /**
24416
+ * True while cell `pointerenter` tickling is suppressed by an active or
24417
+ * just-finished scroll gesture (`disable-scroll-hover-tickle`). While set,
24418
+ * the host carries `[data-scroll-hover-suppress]`, which gates the CSS
24419
+ * `:hover` highlight so the visual cursor does not churn either.
24420
+ */
24421
+ #scrollHoverSuppress;
24422
+ /** Last pointer position seen over the host; real movement ends suppression. */
24423
+ #lastPointerX;
24424
+ #lastPointerY;
24237
24425
  /** Live drag-select preview: covered keys + the surface-relative rect. */
24238
24426
  #marqueePreview;
24239
24427
  #marqueeRect;
@@ -24403,6 +24591,8 @@ let GridListElement = class GridListElement extends i {
24403
24591
  [`caption-backdrop`, this.captionBackdrop]
24404
24592
  ]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
24405
24593
  this.addEventListener(`keydown`, this._onKeyDown);
24594
+ this.addEventListener(`pointermove`, this.#onHostPointerMove, { passive: true });
24595
+ this.addEventListener(`pointerleave`, this.#onHostPointerLeave);
24406
24596
  this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
24407
24597
  this.#mutationObserver.observe(this, {
24408
24598
  childList: true,
@@ -24416,6 +24606,9 @@ let GridListElement = class GridListElement extends i {
24416
24606
  super.disconnectedCallback();
24417
24607
  this.#stopActiveCaptionPan();
24418
24608
  this.removeEventListener(`keydown`, this._onKeyDown);
24609
+ this.removeEventListener(`pointermove`, this.#onHostPointerMove);
24610
+ this.removeEventListener(`pointerleave`, this.#onHostPointerLeave);
24611
+ this.#endScrollHoverSuppress();
24419
24612
  this.#resizeObserver?.disconnect();
24420
24613
  this.#resizeObserver = void 0;
24421
24614
  this.#mutationObserver?.disconnect();
@@ -24486,10 +24679,23 @@ let GridListElement = class GridListElement extends i {
24486
24679
  this.requestUpdate();
24487
24680
  }, REFLOW_WINDOW_MS);
24488
24681
  }
24682
+ /** Key sets as of the last `grid-list-viewport-change` dispatch, for dedupe. */
24683
+ #lastViewportRenderedKeys;
24684
+ #lastViewportVisibleKeys;
24685
+ /**
24686
+ * Dispatch `grid-list-viewport-change` with the current key sets — unless
24687
+ * neither set changed since the last dispatch, so coalesced frames and
24688
+ * no-op scrolls stay silent.
24689
+ */
24489
24690
  #emitViewportChange() {
24691
+ const renderedKeys = this.renderedKeys();
24692
+ const visibleKeys = this.visibleKeys();
24693
+ if (sameKeys$1(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys$1(this.#lastViewportVisibleKeys, visibleKeys)) return;
24694
+ this.#lastViewportRenderedKeys = renderedKeys;
24695
+ this.#lastViewportVisibleKeys = visibleKeys;
24490
24696
  this._dispatch(`grid-list-viewport-change`, {
24491
- renderedKeys: this.renderedKeys(),
24492
- visibleKeys: this.visibleKeys()
24697
+ renderedKeys,
24698
+ visibleKeys
24493
24699
  });
24494
24700
  }
24495
24701
  firstUpdated() {
@@ -24599,6 +24805,7 @@ let GridListElement = class GridListElement extends i {
24599
24805
  this.selection.clearAnchor();
24600
24806
  this.#syncAriaMultiselectable();
24601
24807
  }
24808
+ if (changed.has(`disableScrollHoverTickle`) && !this.disableScrollHoverTickle) this.#endScrollHoverSuppress();
24602
24809
  if (changed.has(`itemsDraggable`)) this.#nativeDrag.refreshDraggable();
24603
24810
  if (changed.has(`orientation`)) {
24604
24811
  const vp = this.#viewportRef.value;
@@ -25018,7 +25225,7 @@ let GridListElement = class GridListElement extends i {
25018
25225
  ?data-marquee=${marquee}
25019
25226
  @click=${(e) => this.#onCellClick(e, key)}
25020
25227
  @dblclick=${() => this.#onCellActivate(key)}
25021
- @pointerenter=${() => this.tickled.handlePointerEnter(key)}
25228
+ @pointerenter=${() => this.#handleCellPointerEnter(key)}
25022
25229
  >
25023
25230
  <div class="content" part="content">${this.#source.renderCellContent(index)}</div>
25024
25231
  ${showCaption ? this.#renderCaption(caption, captionCss, tickled) : ``}
@@ -25102,7 +25309,7 @@ let GridListElement = class GridListElement extends i {
25102
25309
  ?data-marquee=${marquee}
25103
25310
  @click=${(e) => this.#onCellClick(e, key)}
25104
25311
  @dblclick=${() => this.#onCellActivate(key)}
25105
- @pointerenter=${() => this.tickled.handlePointerEnter(key)}
25312
+ @pointerenter=${() => this.#handleCellPointerEnter(key)}
25106
25313
  >
25107
25314
  <div class="thumb" part="thumb" style=${thumbStyle}>
25108
25315
  ${this.#source.renderCellContent(index)}
@@ -25123,6 +25330,37 @@ let GridListElement = class GridListElement extends i {
25123
25330
  * left to the browser when there is no vertical component.
25124
25331
  */
25125
25332
  #onWheel;
25333
+ /**
25334
+ * Cell `pointerenter` funnel. While a scroll gesture moves cells beneath a
25335
+ * stationary pointer (`disable-scroll-hover-tickle`), the freshly
25336
+ * virtualised cell's enter event is ignored so the cursor holds still.
25337
+ */
25338
+ #handleCellPointerEnter(key) {
25339
+ if (this.#scrollHoverSuppress) return;
25340
+ this.tickled.handlePointerEnter(key);
25341
+ }
25342
+ /** Open the scroll-hover suppression window (no-op unless opted in and not already open). */
25343
+ #markScrollHoverSuppress() {
25344
+ if (!this.disableScrollHoverTickle || this.#scrollHoverSuppress) return;
25345
+ this.#scrollHoverSuppress = true;
25346
+ this.toggleAttribute(`data-scroll-hover-suppress`, true);
25347
+ }
25348
+ #endScrollHoverSuppress() {
25349
+ if (!this.#scrollHoverSuppress) return;
25350
+ this.#scrollHoverSuppress = false;
25351
+ this.toggleAttribute(`data-scroll-hover-suppress`, false);
25352
+ }
25353
+ /**
25354
+ * A real pointer movement ends scroll-hover suppression. The first
25355
+ * `pointerenter` on a real move is swallowed by design (it can be
25356
+ * indistinguishable from a virtualisation enter), so the cell already under
25357
+ * the pointer is re-tickled here — otherwise the cursor would lag one cell
25358
+ * boundary behind the pointer. The very first tracked `pointermove` only
25359
+ * establishes the baseline; it cannot know the pointer moved.
25360
+ */
25361
+ #onHostPointerMove;
25362
+ /** The pointer leaving the list ends suppression, so re-entry hovers normally. */
25363
+ #onHostPointerLeave;
25126
25364
  #cancelScrollRaf() {
25127
25365
  if (this.#scrollRaf !== 0) {
25128
25366
  cancelAnimationFrame(this.#scrollRaf);
@@ -25206,6 +25444,11 @@ __decorate([property({
25206
25444
  attribute: `disable-hover-tickle`,
25207
25445
  reflect: true
25208
25446
  })], GridListElement.prototype, "disableHoverTickle", void 0);
25447
+ __decorate([property({
25448
+ type: Boolean,
25449
+ attribute: `disable-scroll-hover-tickle`,
25450
+ reflect: true
25451
+ })], GridListElement.prototype, "disableScrollHoverTickle", void 0);
25209
25452
  __decorate([property({
25210
25453
  type: Number,
25211
25454
  attribute: `item-size`
@@ -28183,6 +28426,12 @@ const ITEM_SELECTOR = `li:not([data-virtual-spacer]), [data-list-item]:not([data
28183
28426
  * vertical rows are shorter relative to the viewport, so 3.
28184
28427
  */
28185
28428
  const VIRTUAL_OVERSCAN = 3;
28429
+ /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
28430
+ function sameKeys(a, b) {
28431
+ if (a === void 0 || a.length !== b.length) return false;
28432
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
28433
+ return true;
28434
+ }
28186
28435
  let VerticalListElement = class VerticalListElement extends i {
28187
28436
  constructor(..._args) {
28188
28437
  super(..._args);
@@ -28297,11 +28546,11 @@ let VerticalListElement = class VerticalListElement extends i {
28297
28546
  highlightKey: `vertical-list-search`
28298
28547
  }) });
28299
28548
  this.#onScroll = () => {
28300
- if (!this.#virtualActive()) return;
28301
28549
  if (this.#scrollRaf !== 0) return;
28302
28550
  this.#scrollRaf = requestAnimationFrame(() => {
28303
28551
  this.#scrollRaf = 0;
28304
28552
  this.#syncVirtualWindow();
28553
+ this.#emitViewportChangeIfChanged();
28305
28554
  });
28306
28555
  };
28307
28556
  this._onPointerOver = (e) => {
@@ -28394,6 +28643,9 @@ let VerticalListElement = class VerticalListElement extends i {
28394
28643
  #suspendAdapterRebuild;
28395
28644
  #scrollRaf;
28396
28645
  #resizeObserver;
28646
+ /** Key sets as of the last viewport-change dispatch, for dedupe. */
28647
+ #lastViewportRenderedKeys;
28648
+ #lastViewportVisibleKeys;
28397
28649
  #topSpacer;
28398
28650
  #bottomSpacer;
28399
28651
  /** Last measured viewport height — kept so a 0 read can't collapse the window. */
@@ -28521,6 +28773,14 @@ let VerticalListElement = class VerticalListElement extends i {
28521
28773
  }
28522
28774
  }
28523
28775
  /**
28776
+ * The shadow scroll list only exists after the first render; start
28777
+ * watching it so resizes move the window (virtualized) and the visible
28778
+ * set (plain) too.
28779
+ */
28780
+ firstUpdated() {
28781
+ this.#ensureResizeObserved();
28782
+ }
28783
+ /**
28524
28784
  * Animate in items appended to the light DOM (however they got there —
28525
28785
  * {@link addItem}, direct `appendChild`, a framework binding). A bulk populate
28526
28786
  * (roughly everything at once) is left alone, as are the wrapper `<li>`s the
@@ -28530,6 +28790,7 @@ let VerticalListElement = class VerticalListElement extends i {
28530
28790
  if (!this.isConnected) return;
28531
28791
  if (this.#virtualActive()) {
28532
28792
  this.#syncVirtualWindow(this.#reconcileRecords(records));
28793
+ this.#scheduleViewportChangeEmit();
28533
28794
  return;
28534
28795
  }
28535
28796
  const fresh = [];
@@ -28540,6 +28801,7 @@ let VerticalListElement = class VerticalListElement extends i {
28540
28801
  this.#animatedItems.add(node);
28541
28802
  fresh.push(node);
28542
28803
  }
28804
+ this.#scheduleViewportChangeEmit();
28543
28805
  if (fresh.length === 0) return;
28544
28806
  if (this.#isBulkPopulate(fresh.length)) return;
28545
28807
  for (const el of fresh) animateListItemIn(el);
@@ -29093,6 +29355,7 @@ let VerticalListElement = class VerticalListElement extends i {
29093
29355
  if (!ul) return;
29094
29356
  this.#resizeObserver ??= new ResizeObserver(() => {
29095
29357
  this.#syncVirtualWindow();
29358
+ this.#scheduleViewportChangeEmit();
29096
29359
  });
29097
29360
  this.#resizeObserver.observe(ul);
29098
29361
  }
@@ -29172,6 +29435,32 @@ let VerticalListElement = class VerticalListElement extends i {
29172
29435
  this._syncItemStates();
29173
29436
  this._syncNativeDraggable();
29174
29437
  this.#nativeDrag.refreshDraggable();
29438
+ this.#scheduleViewportChangeEmit();
29439
+ }
29440
+ /** Coalesce content/layout viewport checks to at most one animation frame. */
29441
+ #scheduleViewportChangeEmit() {
29442
+ if (this.#scrollRaf !== 0) return;
29443
+ this.#scrollRaf = requestAnimationFrame(() => {
29444
+ this.#scrollRaf = 0;
29445
+ this.#syncVirtualWindow();
29446
+ this.#emitViewportChangeIfChanged();
29447
+ });
29448
+ }
29449
+ /**
29450
+ * Dispatch `vertical-list-viewport-change` with the current key sets — unless
29451
+ * neither set changed since the last dispatch, so coalesced frames and
29452
+ * no-op scrolls stay silent.
29453
+ */
29454
+ #emitViewportChangeIfChanged() {
29455
+ const renderedKeys = this.renderedKeys();
29456
+ const visibleKeys = this.visibleKeys();
29457
+ if (sameKeys(this.#lastViewportRenderedKeys, renderedKeys) && sameKeys(this.#lastViewportVisibleKeys, visibleKeys)) return;
29458
+ this.#lastViewportRenderedKeys = renderedKeys;
29459
+ this.#lastViewportVisibleKeys = visibleKeys;
29460
+ this._dispatch(`vertical-list-viewport-change`, {
29461
+ renderedKeys,
29462
+ visibleKeys
29463
+ });
29175
29464
  }
29176
29465
  _getVisibleItems() {
29177
29466
  return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {