forge-select 0.7.1 → 0.7.3

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/dist/index.js CHANGED
@@ -324,6 +324,12 @@ var ForgeSelect = class {
324
324
  this.highlightedIndex = -1;
325
325
  this.typeaheadBuffer = "";
326
326
  this.typeaheadTimer = null;
327
+ // Keyed by the option object, not its value: duplicateValuePolicy only warns
328
+ // by default, so two options can share a value while carrying different
329
+ // labels, and a value-keyed cache would render the first one's content for
330
+ // both. Still explicitly bounded — a WeakMap would not collect anything while
331
+ // `data` holds every key, so scrolling a long list would retain a detached
332
+ // node per option visited.
327
333
  this.rowContentCache = /* @__PURE__ */ new Map();
328
334
  this.rowElementCache = /* @__PURE__ */ new Map();
329
335
  this.rowHeightCache = /* @__PURE__ */ new Map();
@@ -511,8 +517,21 @@ var ForgeSelect = class {
511
517
  this.typeaheadBuffer = "";
512
518
  this.highlightedIndex = -1;
513
519
  if (this.searchInput) {
520
+ const hadQuery = this.query !== "";
514
521
  this.searchInput.value = "";
515
522
  this.query = "";
523
+ if (hadQuery && this.opts.ajax) {
524
+ this.ajaxRequestId += 1;
525
+ if (this.ajaxTimer) {
526
+ clearTimeout(this.ajaxTimer);
527
+ this.ajaxTimer = null;
528
+ }
529
+ this.ajaxController?.abort();
530
+ this.ajaxController = null;
531
+ this.setLoading(false);
532
+ this.loadingMore = false;
533
+ this.remoteLoaded = false;
534
+ }
516
535
  }
517
536
  this.emitter.emit("close");
518
537
  for (const plugin of this.plugins) plugin.onClose?.(this);
@@ -1562,11 +1581,28 @@ var ForgeSelect = class {
1562
1581
  this.rowHeightCache.clear();
1563
1582
  this.rowOffsetsCache = null;
1564
1583
  }
1584
+ /** Identifies a row *position*, which is what measured heights are tied to. */
1565
1585
  rowKey(row, index) {
1566
1586
  if (row.kind === "option") return `option:${row.option.value}:${index}`;
1567
1587
  if (row.kind === "group") return `group:${row.label}:${index}`;
1568
1588
  return `${row.kind}:${index}`;
1569
1589
  }
1590
+ /**
1591
+ * Identifies a row's *content* for `<li>` recycling, deliberately without the
1592
+ * row index: filtering shifts every index below the first change, so an
1593
+ * index-keyed element cache misses on every keystroke — exactly when the list
1594
+ * re-renders most. renderRow() rewrites the element completely, so reusing it
1595
+ * at a new position is safe.
1596
+ *
1597
+ * Keys are not guaranteed unique within a render — duplicateValuePolicy
1598
+ * defaults to warning rather than rejecting duplicate values — so callers
1599
+ * must not hand the same element to two rows of one render.
1600
+ */
1601
+ rowElementKey(row) {
1602
+ if (row.kind === "option") return `option:${row.option.value}`;
1603
+ if (row.kind === "group") return `group:${row.label}`;
1604
+ return row.kind;
1605
+ }
1570
1606
  measuredRowHeight(index) {
1571
1607
  return this.opts.variableItemHeight ? this.rowHeightCache.get(this.rowKey(this.rows[index], index)) ?? this.opts.itemHeight : this.opts.itemHeight;
1572
1608
  }
@@ -1628,9 +1664,12 @@ var ForgeSelect = class {
1628
1664
  this.list.append(topSpacer);
1629
1665
  }
1630
1666
  const appended = [];
1667
+ const claimed = /* @__PURE__ */ new Set();
1631
1668
  for (let i = start; i < end; i++) {
1632
- const key = this.rowKey(this.rows[i], i);
1633
- const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
1669
+ const key = this.rowElementKey(this.rows[i]);
1670
+ const cached = this.rowElementCache.get(key);
1671
+ const element = this.renderRow(this.rows[i], cached && !claimed.has(cached) ? cached : void 0);
1672
+ claimed.add(element);
1634
1673
  this.rowElementCache.set(key, element);
1635
1674
  if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
1636
1675
  const oldest = this.rowElementCache.keys().next().value;
@@ -1800,7 +1839,7 @@ var ForgeSelect = class {
1800
1839
  }
1801
1840
  return holder;
1802
1841
  }
1803
- let cached = this.rowContentCache.get(option.value);
1842
+ let cached = this.rowContentCache.get(option);
1804
1843
  if (!cached) {
1805
1844
  const holder = document.createElement("span");
1806
1845
  holder.className = "forge-select__option-content";
@@ -1809,7 +1848,7 @@ var ForgeSelect = class {
1809
1848
  const oldest = this.rowContentCache.keys().next().value;
1810
1849
  this.rowContentCache.delete(oldest);
1811
1850
  }
1812
- this.rowContentCache.set(option.value, holder);
1851
+ this.rowContentCache.set(option, holder);
1813
1852
  cached = holder;
1814
1853
  }
1815
1854
  return cached.cloneNode(true);
@@ -1877,13 +1916,19 @@ var ForgeSelect = class {
1877
1916
  }
1878
1917
  return false;
1879
1918
  }
1919
+ /**
1920
+ * Reads the id off the row the render pass just marked, rather than rebuilding
1921
+ * it from the highlight index. Virtual scrolling drops off-window rows, so an
1922
+ * index-derived id can name an element that no longer exists, and
1923
+ * aria-activedescendant must resolve to a real one — a dangling reference
1924
+ * reads to assistive tech as no active option at all. Keyboard navigation
1925
+ * scrolls the highlight back into view, which restores the reference.
1926
+ */
1880
1927
  updateActiveDescendant() {
1881
1928
  const target = this.searchInput ?? this.control;
1882
- if (this.highlightedIndex >= 0) {
1883
- target.setAttribute("aria-activedescendant", `${this.uid}-nav-${this.highlightedIndex}`);
1884
- } else {
1885
- target.removeAttribute("aria-activedescendant");
1886
- }
1929
+ const highlighted = this.list.querySelector(".forge-select__option--highlighted");
1930
+ if (highlighted) target.setAttribute("aria-activedescendant", highlighted.id);
1931
+ else target.removeAttribute("aria-activedescendant");
1887
1932
  }
1888
1933
  // ---------------------------------------------------------------- remote data
1889
1934
  scheduleRemoteLoad(query, delay) {