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/README.md CHANGED
@@ -61,8 +61,6 @@ Browse the documentation website at **<https://forgeselect.konexforge.com/docs/>
61
61
  - Internationalization (en/vi built in, custom string tables)
62
62
  - TypeScript Support (written in strict TypeScript, ships `.d.ts`)
63
63
 
64
- > Planned/in-progress capabilities — Angular/Svelte wrappers — are tracked in the [Roadmap](#roadmap) below and intentionally not listed above as shipped features.
65
-
66
64
  ## Installation
67
65
 
68
66
  ```bash
@@ -189,8 +187,8 @@ Forge Select is vanilla TypeScript/JavaScript, so it can be mounted inside any f
189
187
  - Vanilla JavaScript
190
188
  - React — via [`forge-select-react`](./packages/react/README.md) (`ForgeSelectReact` component, controlled `value`/`onChange`)
191
189
  - Vue — via [`forge-select-vue`](./packages/vue/README.md) (`ForgeSelectVue` component, `v-model` support)
192
- - Angular — mount manually for now; a dedicated wrapper is on the [Roadmap](#roadmap)
193
- - Svelte — mount manually for now; a dedicated wrapper is on the [Roadmap](#roadmap)
190
+ - Angular — mount manually (see [`docs/examples.md`](./docs/examples.md))
191
+ - Svelte — mount manually (see [`docs/examples.md`](./docs/examples.md))
194
192
  - Next.js
195
193
  - Nuxt
196
194
  - Astro
@@ -221,8 +219,6 @@ Run `npm run bench` for a reproducible JSON baseline covering bundle size, initi
221
219
  - [x] CSS Variables
222
220
  - [x] React Component
223
221
  - [x] Vue Component
224
- - [ ] Angular Component
225
- - [ ] Svelte Component
226
222
 
227
223
  ## Plugin Development Guide
228
224
 
package/dist/index.cjs CHANGED
@@ -351,6 +351,12 @@ var ForgeSelect = class {
351
351
  this.highlightedIndex = -1;
352
352
  this.typeaheadBuffer = "";
353
353
  this.typeaheadTimer = null;
354
+ // Keyed by the option object, not its value: duplicateValuePolicy only warns
355
+ // by default, so two options can share a value while carrying different
356
+ // labels, and a value-keyed cache would render the first one's content for
357
+ // both. Still explicitly bounded — a WeakMap would not collect anything while
358
+ // `data` holds every key, so scrolling a long list would retain a detached
359
+ // node per option visited.
354
360
  this.rowContentCache = /* @__PURE__ */ new Map();
355
361
  this.rowElementCache = /* @__PURE__ */ new Map();
356
362
  this.rowHeightCache = /* @__PURE__ */ new Map();
@@ -538,8 +544,21 @@ var ForgeSelect = class {
538
544
  this.typeaheadBuffer = "";
539
545
  this.highlightedIndex = -1;
540
546
  if (this.searchInput) {
547
+ const hadQuery = this.query !== "";
541
548
  this.searchInput.value = "";
542
549
  this.query = "";
550
+ if (hadQuery && this.opts.ajax) {
551
+ this.ajaxRequestId += 1;
552
+ if (this.ajaxTimer) {
553
+ clearTimeout(this.ajaxTimer);
554
+ this.ajaxTimer = null;
555
+ }
556
+ this.ajaxController?.abort();
557
+ this.ajaxController = null;
558
+ this.setLoading(false);
559
+ this.loadingMore = false;
560
+ this.remoteLoaded = false;
561
+ }
543
562
  }
544
563
  this.emitter.emit("close");
545
564
  for (const plugin of this.plugins) plugin.onClose?.(this);
@@ -1589,11 +1608,28 @@ var ForgeSelect = class {
1589
1608
  this.rowHeightCache.clear();
1590
1609
  this.rowOffsetsCache = null;
1591
1610
  }
1611
+ /** Identifies a row *position*, which is what measured heights are tied to. */
1592
1612
  rowKey(row, index) {
1593
1613
  if (row.kind === "option") return `option:${row.option.value}:${index}`;
1594
1614
  if (row.kind === "group") return `group:${row.label}:${index}`;
1595
1615
  return `${row.kind}:${index}`;
1596
1616
  }
1617
+ /**
1618
+ * Identifies a row's *content* for `<li>` recycling, deliberately without the
1619
+ * row index: filtering shifts every index below the first change, so an
1620
+ * index-keyed element cache misses on every keystroke — exactly when the list
1621
+ * re-renders most. renderRow() rewrites the element completely, so reusing it
1622
+ * at a new position is safe.
1623
+ *
1624
+ * Keys are not guaranteed unique within a render — duplicateValuePolicy
1625
+ * defaults to warning rather than rejecting duplicate values — so callers
1626
+ * must not hand the same element to two rows of one render.
1627
+ */
1628
+ rowElementKey(row) {
1629
+ if (row.kind === "option") return `option:${row.option.value}`;
1630
+ if (row.kind === "group") return `group:${row.label}`;
1631
+ return row.kind;
1632
+ }
1597
1633
  measuredRowHeight(index) {
1598
1634
  return this.opts.variableItemHeight ? this.rowHeightCache.get(this.rowKey(this.rows[index], index)) ?? this.opts.itemHeight : this.opts.itemHeight;
1599
1635
  }
@@ -1655,9 +1691,12 @@ var ForgeSelect = class {
1655
1691
  this.list.append(topSpacer);
1656
1692
  }
1657
1693
  const appended = [];
1694
+ const claimed = /* @__PURE__ */ new Set();
1658
1695
  for (let i = start; i < end; i++) {
1659
- const key = this.rowKey(this.rows[i], i);
1660
- const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
1696
+ const key = this.rowElementKey(this.rows[i]);
1697
+ const cached = this.rowElementCache.get(key);
1698
+ const element = this.renderRow(this.rows[i], cached && !claimed.has(cached) ? cached : void 0);
1699
+ claimed.add(element);
1661
1700
  this.rowElementCache.set(key, element);
1662
1701
  if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
1663
1702
  const oldest = this.rowElementCache.keys().next().value;
@@ -1827,7 +1866,7 @@ var ForgeSelect = class {
1827
1866
  }
1828
1867
  return holder;
1829
1868
  }
1830
- let cached = this.rowContentCache.get(option.value);
1869
+ let cached = this.rowContentCache.get(option);
1831
1870
  if (!cached) {
1832
1871
  const holder = document.createElement("span");
1833
1872
  holder.className = "forge-select__option-content";
@@ -1836,7 +1875,7 @@ var ForgeSelect = class {
1836
1875
  const oldest = this.rowContentCache.keys().next().value;
1837
1876
  this.rowContentCache.delete(oldest);
1838
1877
  }
1839
- this.rowContentCache.set(option.value, holder);
1878
+ this.rowContentCache.set(option, holder);
1840
1879
  cached = holder;
1841
1880
  }
1842
1881
  return cached.cloneNode(true);
@@ -1904,13 +1943,19 @@ var ForgeSelect = class {
1904
1943
  }
1905
1944
  return false;
1906
1945
  }
1946
+ /**
1947
+ * Reads the id off the row the render pass just marked, rather than rebuilding
1948
+ * it from the highlight index. Virtual scrolling drops off-window rows, so an
1949
+ * index-derived id can name an element that no longer exists, and
1950
+ * aria-activedescendant must resolve to a real one — a dangling reference
1951
+ * reads to assistive tech as no active option at all. Keyboard navigation
1952
+ * scrolls the highlight back into view, which restores the reference.
1953
+ */
1907
1954
  updateActiveDescendant() {
1908
1955
  const target = this.searchInput ?? this.control;
1909
- if (this.highlightedIndex >= 0) {
1910
- target.setAttribute("aria-activedescendant", `${this.uid}-nav-${this.highlightedIndex}`);
1911
- } else {
1912
- target.removeAttribute("aria-activedescendant");
1913
- }
1956
+ const highlighted = this.list.querySelector(".forge-select__option--highlighted");
1957
+ if (highlighted) target.setAttribute("aria-activedescendant", highlighted.id);
1958
+ else target.removeAttribute("aria-activedescendant");
1914
1959
  }
1915
1960
  // ---------------------------------------------------------------- remote data
1916
1961
  scheduleRemoteLoad(query, delay) {