@aquera/nile-elements 2.0.18 → 2.0.19

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 (60) hide show
  1. package/README.md +5 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +404 -385
  5. package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js +1 -1
  6. package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js.map +1 -1
  7. package/dist/nile-breadcrumb/nile-breadcrumb.esm.js +2 -2
  8. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
  9. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
  10. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js +1 -1
  11. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js.map +1 -1
  12. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.esm.js +14 -2
  13. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +4 -4
  14. package/dist/nile-select/index.cjs.js +1 -1
  15. package/dist/nile-select/index.esm.js +1 -1
  16. package/dist/nile-select/nile-select.cjs.js +2 -2
  17. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  18. package/dist/nile-select/nile-select.esm.js +1 -1
  19. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  20. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  21. package/dist/nile-select/virtual-scroll-helper.esm.js +48 -47
  22. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
  23. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  24. package/dist/nile-virtual-select/nile-virtual-select.esm.js +2 -2
  25. package/dist/src/nile-breadcrumb/nile-breadcrumb.d.ts +7 -0
  26. package/dist/src/nile-breadcrumb/nile-breadcrumb.js +24 -1
  27. package/dist/src/nile-breadcrumb/nile-breadcrumb.js.map +1 -1
  28. package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js +51 -0
  29. package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js.map +1 -1
  30. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js +12 -0
  31. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js.map +1 -1
  32. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.d.ts +5 -0
  33. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +9 -0
  34. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
  35. package/dist/src/nile-select/nile-select.d.ts +1 -0
  36. package/dist/src/nile-select/nile-select.js +16 -1
  37. package/dist/src/nile-select/nile-select.js.map +1 -1
  38. package/dist/src/nile-select/nile-select.test.js +430 -5
  39. package/dist/src/nile-select/nile-select.test.js.map +1 -1
  40. package/dist/src/nile-select/virtual-scroll-helper.js +7 -4
  41. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  42. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +3 -1
  43. package/dist/src/nile-virtual-select/nile-virtual-select.js +14 -6
  44. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  45. package/dist/src/nile-virtual-select/nile-virtual-select.test.js +115 -1
  46. package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
  47. package/dist/src/version.js +1 -1
  48. package/dist/src/version.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +1 -1
  51. package/src/nile-breadcrumb/nile-breadcrumb.test.ts +57 -0
  52. package/src/nile-breadcrumb/nile-breadcrumb.ts +19 -0
  53. package/src/nile-breadcrumb-item/nile-breadcrumb-item.css.ts +12 -0
  54. package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +7 -0
  55. package/src/nile-select/nile-select.test.ts +560 -5
  56. package/src/nile-select/nile-select.ts +21 -1
  57. package/src/nile-select/virtual-scroll-helper.ts +7 -4
  58. package/src/nile-virtual-select/nile-virtual-select.test.ts +139 -1
  59. package/src/nile-virtual-select/nile-virtual-select.ts +17 -6
  60. package/vscode-html-custom-data.json +39 -14
@@ -1,4 +1,4 @@
1
- import { aTimeout, expect, fixture, html, nextFrame, waitUntil } from '@open-wc/testing';
1
+ import { aTimeout, expect, fixture, html, nextFrame, oneEvent, waitUntil } from '@open-wc/testing';
2
2
  import './nile-select';
3
3
  import '../nile-option/nile-option';
4
4
  import '../nile-input/nile-input';
@@ -661,7 +661,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
661
661
  });
662
662
  });
663
663
 
664
- describe('NileSelect enableVirtualScroll value sync', () => {
664
+ describe('NileSelect enableVirtualScroll value sync (showTooltip)', () => {
665
665
  const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
666
666
  const DATA = ROLES.map(role => ({ label: role, value: role }));
667
667
 
@@ -675,6 +675,7 @@ describe('NileSelect enableVirtualScroll value sync', () => {
675
675
  async function virtualSelect(tpl = html`<nile-select
676
676
  multiple
677
677
  .enableVirtualScroll=${true}
678
+ .showTooltip=${true}
678
679
  .data=${DATA}
679
680
  .value=${[]}
680
681
  ></nile-select>`) {
@@ -717,6 +718,7 @@ describe('NileSelect enableVirtualScroll value sync', () => {
717
718
  const { el, inner } = await virtualSelect(html`<nile-select
718
719
  multiple
719
720
  .enableVirtualScroll=${true}
721
+ .showTooltip=${true}
720
722
  .data=${DATA}
721
723
  .value=${['Administrators', 'Users', 'Guests']}
722
724
  ></nile-select>`);
@@ -747,6 +749,7 @@ describe('NileSelect enableVirtualScroll value sync', () => {
747
749
  it('V5. single-select virtual mode keeps the wrapper value in sync', async () => {
748
750
  const { el, inner } = await virtualSelect(html`<nile-select
749
751
  .enableVirtualScroll=${true}
752
+ .showTooltip=${true}
750
753
  .data=${DATA}
751
754
  ></nile-select>`);
752
755
 
@@ -761,7 +764,8 @@ describe('NileSelect enableVirtualScroll value sync', () => {
761
764
 
762
765
  it('V6. resizing the host does not re-render the wrapper without autoResize', async () => {
763
766
  const host = await fixture<HTMLElement>(html`<div style="width: 400px">
764
- <nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>
767
+ <nile-select multiple .enableVirtualScroll=${true}
768
+ .showTooltip=${true} .data=${DATA}></nile-select>
765
769
  </div>`);
766
770
  const el = host.querySelector('nile-select') as NileSelect;
767
771
  await el.updateComplete;
@@ -784,7 +788,7 @@ describe('NileSelect enableVirtualScroll value sync', () => {
784
788
  });
785
789
  });
786
790
 
787
- describe('NileSelect enableVirtualScroll + searchEnabled value sync', () => {
791
+ describe('NileSelect enableVirtualScroll + searchEnabled value sync (showTooltip)', () => {
788
792
  const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
789
793
  const DATA = ROLES.map(role => ({ label: role, value: role }));
790
794
 
@@ -809,6 +813,7 @@ describe('NileSelect enableVirtualScroll + searchEnabled value sync', () => {
809
813
  ?multiple=${multiple}
810
814
  .searchEnabled=${true}
811
815
  .enableVirtualScroll=${true}
816
+ .showTooltip=${true}
812
817
  .portal=${portal}
813
818
  .data=${DATA}
814
819
  .value=${value}
@@ -1594,7 +1599,7 @@ describe('NileSelect 2.0.8 compatibility matrix', () => {
1594
1599
  expect(inner.shadowRoot.querySelectorAll('.select__tags nile-tag').length).to.equal(1);
1595
1600
  });
1596
1601
 
1597
- it('C9. enableVirtualScroll: programmatic value and clear-all reach both selects', async () => {
1602
+ it('C9. enableVirtualScroll: programmatic value reaches the inner select; clear-all leaves the outer value alone (pre-2.0.9)', async () => {
1598
1603
  const el = await fixture<NileSelect>(html`<nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>`);
1599
1604
  await settle(el);
1600
1605
  const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
@@ -1604,6 +1609,24 @@ describe('NileSelect 2.0.8 compatibility matrix', () => {
1604
1609
  await inner.updateComplete;
1605
1610
  expect(inner.value).to.deep.equal(['a', 'c']);
1606
1611
 
1612
+ inner.unSelectAll();
1613
+ await inner.updateComplete;
1614
+ await settle(el);
1615
+ expect(inner.value).to.deep.equal([]);
1616
+ // Without showTooltip the inner select owns the selection; the outer value is not written back.
1617
+ expect(el.value).to.deep.equal(['a', 'c']);
1618
+ });
1619
+
1620
+ it('C9b. enableVirtualScroll + showTooltip: programmatic value and clear-all reach both selects', async () => {
1621
+ const el = await fixture<NileSelect>(html`<nile-select multiple showTooltip .enableVirtualScroll=${true} .data=${DATA}></nile-select>`);
1622
+ await settle(el);
1623
+ const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
1624
+
1625
+ el.value = ['a', 'c'];
1626
+ await settle(el);
1627
+ await inner.updateComplete;
1628
+ expect(inner.value).to.deep.equal(['a', 'c']);
1629
+
1607
1630
  inner.unSelectAll();
1608
1631
  await inner.updateComplete;
1609
1632
  await settle(el);
@@ -1676,3 +1699,535 @@ describe('NileSelect showtooltip tooltip on sub-pixel truncation', () => {
1676
1699
  }
1677
1700
  });
1678
1701
  });
1702
+
1703
+
1704
+ /**
1705
+ * enableVirtualScroll: the two value/data paths.
1706
+ *
1707
+ * - without `showTooltip` the wrapper behaves as it did before 2.0.9: the inner select owns the
1708
+ * selection, the wrapper `value` is not written back, and `.data` is re-applied as before;
1709
+ * - with `showTooltip` the inner value is synced up, `.data` is only re-applied when its reference
1710
+ * changes, and an external `value` change recomputes an open "Selected" view.
1711
+ *
1712
+ * These tests drive the rendered DOM (option mouseup, footer checkbox, search input, chip remove button)
1713
+ * rather than the inner select's methods.
1714
+ */
1715
+ type VsInner = HTMLElement & {
1716
+ value: string | string[];
1717
+ data: Array<{ value: string; label: string }>;
1718
+ showSelected: boolean;
1719
+ searchValue: string;
1720
+ maxOptionsVisible: number;
1721
+ updateComplete: Promise<boolean>;
1722
+ };
1723
+
1724
+ const vsMounted: VsInner[] = [];
1725
+
1726
+ const VS_OPTIONS = Array.from({ length: 300 }, (_, i) => ({ value: `option-${i + 1}`, label: `Option ${i + 1}` }));
1727
+
1728
+ function vsRoots(inner: VsInner): ParentNode[] {
1729
+ const portal = document.body.querySelector('.nile-virtual-select-portal-append');
1730
+ return [inner.shadowRoot!, ...(portal ? [portal] : [])];
1731
+ }
1732
+
1733
+ function vsAll(inner: VsInner, selector: string): HTMLElement[] {
1734
+ return vsRoots(inner).flatMap(root => Array.from(root.querySelectorAll<HTMLElement>(selector)));
1735
+ }
1736
+
1737
+ async function vsMount({
1738
+ showTooltip = false,
1739
+ multiple = true,
1740
+ search = false,
1741
+ portal = false,
1742
+ value = undefined as string | string[] | undefined,
1743
+ data = VS_OPTIONS,
1744
+ } = {}) {
1745
+ const initial = value ?? (multiple ? [] : '');
1746
+ const el = await fixture<NileSelect>(html`<nile-select
1747
+ ?multiple=${multiple}
1748
+ ?searchEnabled=${search}
1749
+ ?showTooltip=${showTooltip}
1750
+ .enableVirtualScroll=${true}
1751
+ .portal=${portal}
1752
+ .data=${data}
1753
+ .value=${initial}
1754
+ ></nile-select>`);
1755
+ await el.updateComplete;
1756
+ const inner = el.shadowRoot!.querySelector('nile-virtual-select') as unknown as VsInner;
1757
+ await inner.updateComplete;
1758
+ vsMounted.push(inner);
1759
+ let updates = 0;
1760
+ const update = (el as any).update;
1761
+ (el as any).update = function (this: NileSelect, changed: Map<string, unknown>) {
1762
+ updates++;
1763
+ return update.call(this, changed);
1764
+ };
1765
+ return { el, inner, updates: () => updates, resetUpdates: () => (updates = 0) };
1766
+ }
1767
+
1768
+ async function vsSettle(el: NileSelect, inner: VsInner) {
1769
+ await inner.updateComplete;
1770
+ await el.updateComplete;
1771
+ await inner.updateComplete;
1772
+ await aTimeout(30);
1773
+ }
1774
+
1775
+ async function vsOpen(inner: VsInner) {
1776
+ const shown = oneEvent(inner, 'nile-after-show');
1777
+ (inner.shadowRoot!.querySelector('[part="combobox"]') as HTMLElement).dispatchEvent(
1778
+ new MouseEvent('mousedown', { bubbles: true, composed: true, button: 0 })
1779
+ );
1780
+ await shown;
1781
+ await aTimeout(50);
1782
+ }
1783
+
1784
+ async function vsPick(el: NileSelect, inner: VsInner, value: string) {
1785
+ await waitUntil(
1786
+ () => vsAll(inner, 'nile-option').find(o => (o as any).value === value),
1787
+ `option ${value} was not rendered`
1788
+ );
1789
+ const option = vsAll(inner, 'nile-option').find(o => (o as any).value === value)!;
1790
+ option.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, composed: true, button: 0 }));
1791
+ await vsSettle(el, inner);
1792
+ }
1793
+
1794
+ async function vsToggleSelected(el: NileSelect, inner: VsInner) {
1795
+ (vsAll(inner, '.select__footer nile-checkbox')[0] as HTMLElement).click();
1796
+ await vsSettle(el, inner);
1797
+ }
1798
+
1799
+ async function vsType(el: NileSelect, inner: VsInner, text: string) {
1800
+ const input = vsAll(inner, '.select__search nile-input')[0].shadowRoot!.querySelector('input') as HTMLInputElement;
1801
+ input.value = text;
1802
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
1803
+ await vsSettle(el, inner);
1804
+ }
1805
+
1806
+ async function vsRemoveChip(el: NileSelect, inner: VsInner, index = 0) {
1807
+ const tag = inner.shadowRoot!.querySelectorAll('nile-tag')[index] as HTMLElement;
1808
+ const remove = tag.shadowRoot!.querySelector('[part="remove-button"], nile-icon-button, button') as HTMLElement;
1809
+ ((remove.shadowRoot?.querySelector('button') as HTMLElement | null) ?? remove).click();
1810
+ await vsSettle(el, inner);
1811
+ }
1812
+
1813
+ const VS_SEARCH = 'Option 99';
1814
+ const VS_SEARCH_HITS = VS_OPTIONS.filter(o => o.label.includes(VS_SEARCH)).length;
1815
+ const vsDataValues = (inner: VsInner) => inner.data.map(item => item.value);
1816
+
1817
+ // Known, pre-existing noise from @lit-labs/virtualizer, unrelated to these tests: a "ResizeObserver loop" notice
1818
+ // while a list is laid out, and a detached-list error when the visible list swaps between a short and a long data
1819
+ // set (for example leaving the Selected view). The browser raises them as window errors, which would fail
1820
+ // whichever test happens to be running. Only the new virtual-scroll describes below install this filter; their
1821
+ // assertions still decide pass or fail.
1822
+ // The virtualizer-only errors are matched on message AND on a `virtualizer` frame in the stack, so the same message
1823
+ // thrown from anywhere else still fails the test. "ResizeObserver loop" is a browser notice with no stack.
1824
+ const VS_VIRTUALIZER_ERRORS = ['ChildPart` has no `parentNode`', "reading 'insertBefore'"];
1825
+
1826
+ function vsIsKnownVirtualizerNoise(message: unknown, error?: Error | null): boolean {
1827
+ if (typeof message !== 'string') return false;
1828
+ if (message.includes('ResizeObserver loop')) return true;
1829
+ return (
1830
+ VS_VIRTUALIZER_ERRORS.some(known => message.includes(known)) &&
1831
+ !!error?.stack &&
1832
+ error.stack.toLowerCase().includes('virtualizer')
1833
+ );
1834
+ }
1835
+
1836
+ function vsIgnoreKnownVirtualizerErrors(): () => void {
1837
+ const previous = window.onerror;
1838
+ window.onerror = (message, source, lineno, colno, error) => {
1839
+ if (vsIsKnownVirtualizerNoise(message, error)) return true;
1840
+ return previous ? previous.call(window, message, source, lineno, colno, error) : false;
1841
+ };
1842
+ return () => {
1843
+ window.onerror = previous;
1844
+ };
1845
+ }
1846
+
1847
+ // Close every dropdown and let it finish hiding before its fixture goes away, so the virtualizer is not
1848
+ // still laying out a list that has already been detached. Registered per describe below, not file-wide.
1849
+ async function vsCloseDropdowns() {
1850
+ const mounted = vsMounted.splice(0);
1851
+ for (const inner of mounted) {
1852
+ if (!(inner as any).open) continue;
1853
+ const hidden = oneEvent(inner, 'nile-after-hide');
1854
+ (inner as any).open = false;
1855
+ await Promise.race([hidden, aTimeout(1500)]);
1856
+ }
1857
+ if (mounted.length) {
1858
+ await nextFrame();
1859
+ await nextFrame();
1860
+ await aTimeout(250);
1861
+ }
1862
+ document.body.querySelectorAll('.nile-virtual-select-portal-append').forEach(n => n.remove());
1863
+ }
1864
+
1865
+ describe('NileSelect enableVirtualScroll without showTooltip keeps the pre-2.0.9 behaviour', () => {
1866
+ let restoreOnError: () => void;
1867
+ before(() => {
1868
+ restoreOnError = vsIgnoreKnownVirtualizerErrors();
1869
+ });
1870
+ after(() => {
1871
+ restoreOnError();
1872
+ });
1873
+ afterEach(vsCloseDropdowns);
1874
+
1875
+ it('L1. a pick does not write the outer value', async () => {
1876
+ const { el, inner } = await vsMount();
1877
+ await vsOpen(inner);
1878
+ await vsPick(el, inner, 'option-1');
1879
+
1880
+ expect(inner.value).to.deep.equal(['option-1']);
1881
+ expect(el.value).to.deep.equal([]);
1882
+ });
1883
+
1884
+ it('L2. a normal pick, uncheck and chip removal cause no outer update', async () => {
1885
+ const { el, inner, updates, resetUpdates } = await vsMount({ value: ['option-9'] });
1886
+ await vsOpen(inner);
1887
+ resetUpdates();
1888
+
1889
+ await vsPick(el, inner, 'option-1');
1890
+ await vsPick(el, inner, 'option-1');
1891
+ await vsRemoveChip(el, inner, 0);
1892
+
1893
+ expect(updates()).to.equal(0);
1894
+ });
1895
+
1896
+ it('L3. a parent re-render pushes the stale outer value back over the selection', async () => {
1897
+ const { el, inner } = await vsMount();
1898
+ await vsOpen(inner);
1899
+ await vsPick(el, inner, 'option-1');
1900
+ expect(inner.value).to.deep.equal(['option-1']);
1901
+
1902
+ el.requestUpdate();
1903
+ await vsSettle(el, inner);
1904
+
1905
+ expect(inner.value).to.deep.equal([]);
1906
+ });
1907
+
1908
+ it('L4. nile-input / nile-change from the inner select are not synced (no sync listeners)', async () => {
1909
+ const { el, inner } = await vsMount({ value: ['option-1'] });
1910
+ const fire = (name: string, value: unknown) =>
1911
+ inner.dispatchEvent(new CustomEvent(name, { detail: { value }, bubbles: true, composed: true }));
1912
+
1913
+ fire('nile-input', ['option-2']);
1914
+ fire('nile-change', ['option-3']);
1915
+ await vsSettle(el, inner);
1916
+
1917
+ expect(el.value).to.deep.equal(['option-1']);
1918
+ });
1919
+
1920
+ it('L5. a direct syncValueFromVirtualSelect call does not change the outer value', async () => {
1921
+ const { el, inner } = await vsMount({ value: ['option-1'] });
1922
+ const event = new CustomEvent('nile-change', { detail: { value: ['option-2'] } });
1923
+ Object.defineProperty(event, 'currentTarget', { value: inner });
1924
+ Object.defineProperty(event, 'composedPath', { value: () => [inner] });
1925
+
1926
+ el.syncValueFromVirtualSelect(event as any);
1927
+ await vsSettle(el, inner);
1928
+
1929
+ expect(el.value).to.deep.equal(['option-1']);
1930
+ });
1931
+
1932
+ it('L6. Selected stays filtered through a normal uncheck', async () => {
1933
+ const { el, inner } = await vsMount();
1934
+ await vsOpen(inner);
1935
+ await vsPick(el, inner, 'option-1');
1936
+ await vsPick(el, inner, 'option-2');
1937
+ await vsPick(el, inner, 'option-3');
1938
+ await vsToggleSelected(el, inner);
1939
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2', 'option-3']);
1940
+
1941
+ await vsPick(el, inner, 'option-2');
1942
+
1943
+ expect(inner.showSelected).to.be.true;
1944
+ expect(inner.value).to.deep.equal(['option-1', 'option-3']);
1945
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2', 'option-3']);
1946
+ });
1947
+
1948
+ it('L7. search stays filtered through a pick and an unpick', async () => {
1949
+ const { el, inner } = await vsMount({ search: true });
1950
+ await vsOpen(inner);
1951
+ await vsType(el, inner, VS_SEARCH);
1952
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
1953
+
1954
+ await vsPick(el, inner, 'option-99');
1955
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
1956
+ expect(inner.searchValue).to.equal(VS_SEARCH);
1957
+
1958
+ await vsPick(el, inner, 'option-99');
1959
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
1960
+ expect(inner.value).to.deep.equal([]);
1961
+ });
1962
+
1963
+ it('L9. the wrapper pushes maxOptionsVisible to the inner select (pre-2.0.9 binding)', async () => {
1964
+ const { el, inner } = await vsMount({ value: ['option-1'] });
1965
+ const writes: number[] = [];
1966
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inner), 'maxOptionsVisible')!;
1967
+ Object.defineProperty(inner, 'maxOptionsVisible', {
1968
+ configurable: true,
1969
+ get() {
1970
+ return descriptor.get!.call(this);
1971
+ },
1972
+ set(value: number) {
1973
+ writes.push(value);
1974
+ descriptor.set!.call(this, value);
1975
+ },
1976
+ });
1977
+
1978
+ el.maxOptionsVisible = 5;
1979
+ await vsSettle(el, inner);
1980
+
1981
+ expect(writes).to.include(5);
1982
+ });
1983
+
1984
+ it('L10. the untouched default maxOptionsVisible is not pushed, so the inner select is not pinned as explicit', async () => {
1985
+ const { inner } = await vsMount();
1986
+
1987
+ expect((inner as any).maxOptionsVisibleExplicit).to.be.false;
1988
+ });
1989
+
1990
+ it('L8. data is re-applied on every wrapper render (plain binding, pre-2.0.9)', async () => {
1991
+ const { el, inner } = await vsMount({ value: ['option-1'] });
1992
+ await vsOpen(inner);
1993
+ // After opening, the inner select holds its own copy of the list.
1994
+ expect(inner.data).to.not.equal(el.data);
1995
+
1996
+ el.requestUpdate();
1997
+ await vsSettle(el, inner);
1998
+
1999
+ expect(inner.data).to.equal(el.data);
2000
+ });
2001
+ });
2002
+
2003
+ describe('NileSelect enableVirtualScroll with showTooltip: guarded data and external value updates', () => {
2004
+ let restoreOnError: () => void;
2005
+ before(() => {
2006
+ restoreOnError = vsIgnoreKnownVirtualizerErrors();
2007
+ });
2008
+ after(() => {
2009
+ restoreOnError();
2010
+ });
2011
+ afterEach(vsCloseDropdowns);
2012
+
2013
+ it('T1. Selected + uncheck stays filtered', async () => {
2014
+ const { el, inner } = await vsMount({ showTooltip: true });
2015
+ await vsOpen(inner);
2016
+ await vsPick(el, inner, 'option-1');
2017
+ await vsPick(el, inner, 'option-2');
2018
+ await vsPick(el, inner, 'option-3');
2019
+ await vsToggleSelected(el, inner);
2020
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2', 'option-3']);
2021
+
2022
+ await vsPick(el, inner, 'option-2');
2023
+
2024
+ expect(inner.showSelected).to.be.true;
2025
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2', 'option-3']);
2026
+ expect(el.value).to.deep.equal(['option-1', 'option-3']);
2027
+ });
2028
+
2029
+ it('T2. search + pick/unpick stays filtered', async () => {
2030
+ const { el, inner } = await vsMount({ showTooltip: true, search: true });
2031
+ await vsOpen(inner);
2032
+ await vsType(el, inner, VS_SEARCH);
2033
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
2034
+
2035
+ await vsPick(el, inner, 'option-99');
2036
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
2037
+ expect(inner.searchValue).to.equal(VS_SEARCH);
2038
+ expect(el.value).to.deep.equal(['option-99']);
2039
+
2040
+ await vsPick(el, inner, 'option-99');
2041
+ expect(inner.data.length).to.equal(VS_SEARCH_HITS);
2042
+ expect(el.value).to.deep.equal([]);
2043
+ });
2044
+
2045
+ it('T3. chip removal while Selected stays filtered', async () => {
2046
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2', 'option-3'] });
2047
+ await vsOpen(inner);
2048
+ await vsToggleSelected(el, inner);
2049
+ expect(inner.data.length).to.equal(3);
2050
+
2051
+ await vsRemoveChip(el, inner, 0);
2052
+
2053
+ expect(inner.showSelected).to.be.true;
2054
+ expect(inner.data.length).to.equal(3);
2055
+ expect(el.value).to.deep.equal(['option-2', 'option-3']);
2056
+ });
2057
+
2058
+ it('T4. portal: Selected + uncheck stays filtered', async () => {
2059
+ const { el, inner } = await vsMount({ showTooltip: true, portal: true });
2060
+ await vsOpen(inner);
2061
+ await waitUntil(() => document.body.querySelector('.nile-virtual-select-portal-append #listbox'), 'listbox never portalled');
2062
+ await vsPick(el, inner, 'option-1');
2063
+ await vsPick(el, inner, 'option-2');
2064
+ await vsToggleSelected(el, inner);
2065
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2']);
2066
+
2067
+ await vsPick(el, inner, 'option-2');
2068
+
2069
+ expect(inner.showSelected).to.be.true;
2070
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2']);
2071
+ expect(el.value).to.deep.equal(['option-1']);
2072
+ });
2073
+
2074
+ it('T5. picking, removing a chip and clearing sync the outer value; nile-change listeners see it', async () => {
2075
+ const { el, inner } = await vsMount({ showTooltip: true });
2076
+ const seen: unknown[] = [];
2077
+ el.addEventListener('nile-change', () => seen.push(el.value));
2078
+ await vsOpen(inner);
2079
+
2080
+ await vsPick(el, inner, 'option-1');
2081
+ await vsPick(el, inner, 'option-2');
2082
+ expect(el.value).to.deep.equal(['option-1', 'option-2']);
2083
+ expect(seen[0]).to.deep.equal(['option-1']);
2084
+
2085
+ await vsRemoveChip(el, inner, 0);
2086
+ expect(el.value).to.deep.equal(['option-2']);
2087
+
2088
+ (vsAll(inner, '.select__clear')[0] as HTMLElement).click();
2089
+ await vsSettle(el, inner);
2090
+ expect(el.value).to.deep.equal([]);
2091
+ });
2092
+
2093
+ it('T6. an external value replace while Selected recomputes the list', async () => {
2094
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2095
+ await vsOpen(inner);
2096
+ await vsToggleSelected(el, inner);
2097
+
2098
+ el.value = ['option-5'];
2099
+ await vsSettle(el, inner);
2100
+
2101
+ expect(inner.showSelected).to.be.true;
2102
+ expect(vsDataValues(inner)).to.deep.equal(['option-5']);
2103
+ });
2104
+
2105
+ it('T7. an external value that adds an item while Selected shows the new item', async () => {
2106
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2107
+ await vsOpen(inner);
2108
+ await vsToggleSelected(el, inner);
2109
+
2110
+ el.value = ['option-1', 'option-2', 'option-7'];
2111
+ await vsSettle(el, inner);
2112
+
2113
+ expect(vsDataValues(inner)).to.deep.equal(['option-1', 'option-2', 'option-7']);
2114
+ });
2115
+
2116
+ it('T8. an external value that removes an item while Selected drops it from the list', async () => {
2117
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2118
+ await vsOpen(inner);
2119
+ await vsToggleSelected(el, inner);
2120
+
2121
+ el.value = ['option-1'];
2122
+ await vsSettle(el, inner);
2123
+
2124
+ expect(vsDataValues(inner)).to.deep.equal(['option-1']);
2125
+ });
2126
+
2127
+ it('T9. an external empty value leaves the Selected view', async () => {
2128
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2129
+ await vsOpen(inner);
2130
+ await vsToggleSelected(el, inner);
2131
+
2132
+ el.value = [];
2133
+ await vsSettle(el, inner);
2134
+
2135
+ expect(inner.showSelected).to.be.false;
2136
+ expect(inner.data.length).to.equal(VS_OPTIONS.length);
2137
+ });
2138
+
2139
+ it('T10. a normal pick after an external value update still keeps the item listed', async () => {
2140
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2141
+ await vsOpen(inner);
2142
+ await vsToggleSelected(el, inner);
2143
+ el.value = ['option-5', 'option-6'];
2144
+ await vsSettle(el, inner);
2145
+
2146
+ await vsPick(el, inner, 'option-5');
2147
+
2148
+ expect(inner.value).to.deep.equal(['option-6']);
2149
+ expect(el.value).to.deep.equal(['option-6']);
2150
+ expect(inner.showSelected).to.be.true;
2151
+ expect(vsDataValues(inner)).to.deep.equal(['option-5', 'option-6']);
2152
+ });
2153
+
2154
+ it('T11. a normal pick does not re-send the data array', async () => {
2155
+ const { el, inner } = await vsMount({ showTooltip: true, value: ['option-1', 'option-2'] });
2156
+ await vsOpen(inner);
2157
+ await vsToggleSelected(el, inner);
2158
+ const before = inner.data;
2159
+
2160
+ await vsPick(el, inner, 'option-2');
2161
+
2162
+ expect(inner.data).to.equal(before);
2163
+ });
2164
+
2165
+ it('T12. turning showTooltip on at runtime keeps the selection', async () => {
2166
+ const { el, inner } = await vsMount({ showTooltip: false });
2167
+ await vsOpen(inner);
2168
+ await vsPick(el, inner, 'option-1');
2169
+ await vsPick(el, inner, 'option-2');
2170
+ expect(el.value).to.deep.equal([]);
2171
+ expect(inner.value).to.deep.equal(['option-1', 'option-2']);
2172
+
2173
+ el.showTooltip = true;
2174
+ await vsSettle(el, inner);
2175
+
2176
+ expect(el.value).to.deep.equal(['option-1', 'option-2']);
2177
+ expect(inner.value).to.deep.equal(['option-1', 'option-2']);
2178
+ });
2179
+
2180
+ it('T15. turning showTooltip on at runtime engages the fitted chip row', async () => {
2181
+ const { el, inner } = await vsMount({
2182
+ showTooltip: false,
2183
+ value: ['option-1', 'option-2', 'option-3', 'option-4', 'option-5'],
2184
+ });
2185
+ const fitted = () => !!inner.shadowRoot!.querySelector('.select__tags--fitted');
2186
+ expect(fitted()).to.be.false;
2187
+
2188
+ el.showTooltip = true;
2189
+ await vsSettle(el, inner);
2190
+
2191
+ await waitUntil(fitted, 'the fitted chip row never engaged');
2192
+ expect(el.value).to.deep.equal(['option-1', 'option-2', 'option-3', 'option-4', 'option-5']);
2193
+ });
2194
+
2195
+ it('T16. changing showTooltip and value in the same update keeps the consumer value', async () => {
2196
+ const { el, inner } = await vsMount({ showTooltip: false });
2197
+ await vsOpen(inner);
2198
+ await vsPick(el, inner, 'option-1');
2199
+ expect(inner.value).to.deep.equal(['option-1']);
2200
+ expect(el.value).to.deep.equal([]);
2201
+
2202
+ el.value = ['option-7'];
2203
+ el.showTooltip = true;
2204
+ await vsSettle(el, inner);
2205
+
2206
+ expect(el.value).to.deep.equal(['option-7']);
2207
+ expect(inner.value).to.deep.equal(['option-7']);
2208
+ });
2209
+
2210
+ it('T13. assigning a new data array propagates', async () => {
2211
+ const { el, inner } = await vsMount({ showTooltip: true });
2212
+ await vsOpen(inner);
2213
+
2214
+ el.data = [...VS_OPTIONS.slice(0, 10), { value: 'extra', label: 'Extra' }];
2215
+ await vsSettle(el, inner);
2216
+
2217
+ expect(inner.data.length).to.equal(11);
2218
+ expect(vsDataValues(inner)).to.include('extra');
2219
+ });
2220
+
2221
+ it('T14. mutating data in place (data.push) is not propagated; assign a new array instead', async () => {
2222
+ const data = VS_OPTIONS.slice(0, 20);
2223
+ const { el, inner } = await vsMount({ showTooltip: true, data });
2224
+ await vsOpen(inner);
2225
+
2226
+ data.push({ value: 'extra', label: 'Extra' });
2227
+ el.requestUpdate();
2228
+ await vsSettle(el, inner);
2229
+
2230
+ expect(inner.data.length).to.equal(20);
2231
+ expect(vsDataValues(inner)).to.not.include('extra');
2232
+ });
2233
+ });
@@ -474,11 +474,31 @@ export class NileSelect extends NileElement implements NileFormControl{
474
474
  }
475
475
 
476
476
  syncValueFromVirtualSelect(event: CustomEvent<{ value?: string | string[] }>) {
477
+ if (!this.showTooltip) return;
477
478
  if (event.composedPath()[0] !== event.currentTarget) return;
478
479
  if (!event.detail || !('value' in event.detail)) return;
479
480
  this.value = event.detail.value ?? (this.multiple ? [] : '');
480
481
  }
481
482
 
483
+ protected willUpdate(changedProperties: PropertyValues): void {
484
+ super.willUpdate(changedProperties);
485
+
486
+ if (
487
+ changedProperties.has('showTooltip') &&
488
+ changedProperties.get('showTooltip') === false &&
489
+ this.showTooltip &&
490
+ this.enableVirtualScroll &&
491
+ !changedProperties.has('value')
492
+ ) {
493
+ const inner = this.shadowRoot?.querySelector('nile-virtual-select') as
494
+ | { value: string | string[] }
495
+ | null;
496
+ if (inner && inner.value !== this.value) {
497
+ this.value = inner.value;
498
+ }
499
+ }
500
+ }
501
+
482
502
  protected updated(_changedProperties: PropertyValues): void {
483
503
  if(_changedProperties.has('searchEnabled')) {
484
504
  this.updateGroupStickyOffsets();
@@ -1165,7 +1185,7 @@ export class NileSelect extends NileElement implements NileFormControl{
1165
1185
 
1166
1186
  // Update the value and display label
1167
1187
  if (this.multiple) {
1168
- const hasOptions = this.getAllOptions().length > 0;
1188
+ const hasOptions = !this.showTooltip || this.getAllOptions().length > 0;
1169
1189
 
1170
1190
  if (hasOptions) {
1171
1191
  this.value = this.selectedOptions.map(el => el.value);