@aquera/nile-elements 2.0.12 → 2.0.14

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 (83) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +132 -134
  3. package/dist/nile-auto-complete/nile-auto-complete.css.cjs.js +1 -1
  4. package/dist/nile-auto-complete/nile-auto-complete.css.cjs.js.map +1 -1
  5. package/dist/nile-auto-complete/nile-auto-complete.css.esm.js +5 -2
  6. package/dist/nile-chip/nile-chip.css.cjs.js +1 -1
  7. package/dist/nile-chip/nile-chip.css.cjs.js.map +1 -1
  8. package/dist/nile-chip/nile-chip.css.esm.js +2 -0
  9. package/dist/nile-combobox/nile-combobox.css.cjs.js +1 -1
  10. package/dist/nile-combobox/nile-combobox.css.cjs.js.map +1 -1
  11. package/dist/nile-combobox/nile-combobox.css.esm.js +9 -9
  12. package/dist/nile-command-menu/nile-command-menu.css.cjs.js +1 -1
  13. package/dist/nile-command-menu/nile-command-menu.css.cjs.js.map +1 -1
  14. package/dist/nile-command-menu/nile-command-menu.css.esm.js +5 -5
  15. package/dist/nile-command-menu-item/nile-command-menu-item.css.cjs.js +1 -1
  16. package/dist/nile-command-menu-item/nile-command-menu-item.css.cjs.js.map +1 -1
  17. package/dist/nile-command-menu-item/nile-command-menu-item.css.esm.js +2 -2
  18. package/dist/nile-option/nile-option.css.cjs.js +1 -1
  19. package/dist/nile-option/nile-option.css.cjs.js.map +1 -1
  20. package/dist/nile-option/nile-option.css.esm.js +1 -1
  21. package/dist/nile-select/nile-select.cjs.js +2 -2
  22. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  23. package/dist/nile-select/nile-select.css.cjs.js +1 -1
  24. package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
  25. package/dist/nile-select/nile-select.css.esm.js +19 -24
  26. package/dist/nile-select/nile-select.esm.js +43 -42
  27. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  28. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  29. package/dist/nile-select/virtual-scroll-helper.esm.js +2 -0
  30. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
  31. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  32. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
  33. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
  34. package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +20 -25
  35. package/dist/nile-virtual-select/nile-virtual-select.esm.js +26 -26
  36. package/dist/src/nile-auto-complete/nile-auto-complete.css.js +3 -0
  37. package/dist/src/nile-auto-complete/nile-auto-complete.css.js.map +1 -1
  38. package/dist/src/nile-chip/nile-chip.css.js +2 -0
  39. package/dist/src/nile-chip/nile-chip.css.js.map +1 -1
  40. package/dist/src/nile-combobox/nile-combobox.css.js +9 -9
  41. package/dist/src/nile-combobox/nile-combobox.css.js.map +1 -1
  42. package/dist/src/nile-command-menu/nile-command-menu.css.js +5 -5
  43. package/dist/src/nile-command-menu/nile-command-menu.css.js.map +1 -1
  44. package/dist/src/nile-command-menu-item/nile-command-menu-item.css.js +2 -2
  45. package/dist/src/nile-command-menu-item/nile-command-menu-item.css.js.map +1 -1
  46. package/dist/src/nile-option/nile-option.css.js +1 -1
  47. package/dist/src/nile-option/nile-option.css.js.map +1 -1
  48. package/dist/src/nile-select/nile-select.css.js +19 -24
  49. package/dist/src/nile-select/nile-select.css.js.map +1 -1
  50. package/dist/src/nile-select/nile-select.d.ts +18 -2
  51. package/dist/src/nile-select/nile-select.js +226 -64
  52. package/dist/src/nile-select/nile-select.js.map +1 -1
  53. package/dist/src/nile-select/nile-select.test.d.ts +1 -0
  54. package/dist/src/nile-select/nile-select.test.js +896 -17
  55. package/dist/src/nile-select/nile-select.test.js.map +1 -1
  56. package/dist/src/nile-select/virtual-scroll-helper.js +2 -0
  57. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  58. package/dist/src/nile-virtual-select/nile-virtual-select.css.js +20 -25
  59. package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
  60. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +10 -2
  61. package/dist/src/nile-virtual-select/nile-virtual-select.js +142 -30
  62. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  63. package/dist/src/nile-virtual-select/nile-virtual-select.test.d.ts +1 -0
  64. package/dist/src/nile-virtual-select/nile-virtual-select.test.js +331 -16
  65. package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
  66. package/dist/src/version.js +1 -1
  67. package/dist/src/version.js.map +1 -1
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +1 -1
  70. package/src/nile-auto-complete/nile-auto-complete.css.ts +3 -0
  71. package/src/nile-chip/nile-chip.css.ts +2 -0
  72. package/src/nile-combobox/nile-combobox.css.ts +9 -9
  73. package/src/nile-command-menu/nile-command-menu.css.ts +5 -5
  74. package/src/nile-command-menu-item/nile-command-menu-item.css.ts +2 -2
  75. package/src/nile-option/nile-option.css.ts +1 -1
  76. package/src/nile-select/nile-select.css.ts +19 -24
  77. package/src/nile-select/nile-select.test.ts +1112 -17
  78. package/src/nile-select/nile-select.ts +237 -67
  79. package/src/nile-select/virtual-scroll-helper.ts +2 -0
  80. package/src/nile-virtual-select/nile-virtual-select.css.ts +20 -25
  81. package/src/nile-virtual-select/nile-virtual-select.test.ts +404 -16
  82. package/src/nile-virtual-select/nile-virtual-select.ts +200 -79
  83. package/vscode-html-custom-data.json +9 -4
@@ -1,6 +1,7 @@
1
- import { aTimeout, expect, fixture, html, nextFrame } from '@open-wc/testing';
1
+ import { aTimeout, expect, fixture, html, nextFrame, waitUntil } from '@open-wc/testing';
2
2
  import './nile-select';
3
3
  import '../nile-option/nile-option';
4
+ import '../nile-input/nile-input';
4
5
  describe('NileSelect', () => {
5
6
  it('1. renders', async () => { const el = await fixture(html `<nile-select></nile-select>`); expect(el).to.exist; });
6
7
  it('2. shadow root', async () => { const el = await fixture(html `<nile-select></nile-select>`); expect(el.shadowRoot).to.not.be.null; });
@@ -132,7 +133,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
132
133
  }
133
134
  it('M1. multiple + no max-options-visible + autoResize=false renders fitted chips', async () => {
134
135
  const host = await fixture(html `<div style="width: 320px">
135
- <nile-select multiple .value=${LABELS}>${options()}</nile-select>
136
+ <nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
136
137
  </div>`);
137
138
  const el = host.querySelector('nile-select');
138
139
  await settle(el);
@@ -143,7 +144,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
143
144
  });
144
145
  it('M2. multiple + no max-options-visible + autoResize=true refits when the container resizes', async () => {
145
146
  const host = await fixture(html `<div style="width: 480px">
146
- <nile-select multiple autoresize .value=${LABELS}>${options()}</nile-select>
147
+ <nile-select showtooltip multiple autoresize .value=${LABELS}>${options()}</nile-select>
147
148
  </div>`);
148
149
  const el = host.querySelector('nile-select');
149
150
  await settle(el);
@@ -157,7 +158,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
157
158
  });
158
159
  it('M3. explicit max-options-visible switches to fixed-count layout', async () => {
159
160
  const host = await fixture(html `<div style="width: 320px">
160
- <nile-select multiple max-options-visible="2" .value=${LABELS}>
161
+ <nile-select showtooltip multiple max-options-visible="2" .value=${LABELS}>
161
162
  ${options()}
162
163
  </nile-select>
163
164
  </div>`);
@@ -170,7 +171,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
170
171
  });
171
172
  it('M3b. max-options-visible set as a property also switches to fixed-count layout', async () => {
172
173
  const host = await fixture(html `<div style="width: 320px">
173
- <nile-select multiple .value=${LABELS}>${options()}</nile-select>
174
+ <nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
174
175
  </div>`);
175
176
  const el = host.querySelector('nile-select');
176
177
  await settle(el);
@@ -182,7 +183,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
182
183
  });
183
184
  it('M3c. the default max-options-visible is still 3 and reading it does not opt out', async () => {
184
185
  const host = await fixture(html `<div style="width: 320px">
185
- <nile-select multiple .value=${LABELS}>${options()}</nile-select>
186
+ <nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
186
187
  </div>`);
187
188
  const el = host.querySelector('nile-select');
188
189
  await settle(el);
@@ -201,7 +202,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
201
202
  });
202
203
  it('M8. virtual-scroll mode keeps fitted chips when max-options-visible was never set', async () => {
203
204
  const host = await fixture(html `<div style="width: 320px">
204
- <nile-select
205
+ <nile-select showtooltip
205
206
  multiple
206
207
  enable-virtual-scroll
207
208
  .enableVirtualScroll=${true}
@@ -216,11 +217,12 @@ describe('NileSelect chip fitting behaviour matrix', () => {
216
217
  await inner.updateComplete;
217
218
  await nextFrame();
218
219
  const innerTags = inner.shadowRoot.querySelector('.select__tags');
220
+ expect(innerTags, 'inner .select__tags should render').to.exist;
219
221
  expect(innerTags.classList.contains('select__tags--fitted')).to.be.true;
220
222
  });
221
223
  it('M9. virtual-scroll mode forwards an explicit max-options-visible', async () => {
222
224
  const host = await fixture(html `<div style="width: 320px">
223
- <nile-select
225
+ <nile-select showtooltip
224
226
  multiple
225
227
  max-options-visible="2"
226
228
  .enableVirtualScroll=${true}
@@ -251,7 +253,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
251
253
  });
252
254
  it('M11. toggling max-options-visible does not leak resize observers or lose the observed container', async () => {
253
255
  const host = await fixture(html `<div style="width: 480px">
254
- <nile-select multiple autoresize .value=${LABELS}>
256
+ <nile-select showtooltip multiple autoresize .value=${LABELS}>
255
257
  ${options()}
256
258
  </nile-select>
257
259
  </div>`);
@@ -288,7 +290,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
288
290
  function liveTips(el) {
289
291
  return tips(el).filter(tip => !tip.hasAttribute('disabled'));
290
292
  }
291
- it('M12. no showtooltip attribute hides the chip tooltip', async () => {
293
+ it('M12. no showtooltip attribute renders no chip tooltips', async () => {
292
294
  const host = await fixture(html `<div style="width: 200px">
293
295
  <nile-select multiple .value=${LABELS}>${options()}</nile-select>
294
296
  </div>`);
@@ -296,8 +298,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
296
298
  await settle(el);
297
299
  expect(el.showTooltip).to.be.false;
298
300
  expect(el.hasAttribute('showtooltip')).to.be.false;
299
- expect(tips(el).length).to.be.greaterThan(0);
300
- expect(liveTips(el).length).to.equal(0);
301
+ expect(tips(el).length).to.equal(0);
301
302
  });
302
303
  it('M13. showtooltip shows the chip tooltip', async () => {
303
304
  const host = await fixture(html `<div style="width: 200px">
@@ -324,16 +325,21 @@ describe('NileSelect chip fitting behaviour matrix', () => {
324
325
  return content.scrollWidth > content.clientWidth + 1;
325
326
  }).length;
326
327
  }
327
- it('M16. without showtooltip a fitted chip is never truncated', async () => {
328
+ it('M16. without showtooltip the pre-2.0.9 chip row is used', async () => {
328
329
  const host = await fixture(html `<div style="width: 480px">
329
330
  <nile-select multiple .value=${LABELS}>${options()}</nile-select>
330
331
  </div>`);
331
332
  const el = host.querySelector('nile-select');
332
333
  await settle(el);
334
+ await aTimeout(20);
335
+ await settle(el);
333
336
  expect(el.showTooltip).to.be.false;
334
- expect(clippedChipCount(el)).to.equal(0);
335
- // The chips that could not fit went into the +N button, not into an ellipsis.
336
- expect(hiddenChips(el).length).to.be.greaterThan(0);
337
+ expect(tagsRoot(el).classList.contains('select__tags--fitted')).to.be.false;
338
+ expect(el.shadowRoot.querySelector('button.select__tags-count')).to.not.exist;
339
+ expect(chips(el).every(chip => !chip.style.maxWidth)).to.be.true;
340
+ const overflowCount = el.shadowRoot.querySelector('span.select__tags-count');
341
+ expect(overflowCount).to.exist;
342
+ expect(overflowCount.textContent).to.contain('More');
337
343
  });
338
344
  it('M17. showtooltip is what allows a fitted chip to be truncated', async () => {
339
345
  const host = await fixture(html `<div style="width: 480px">
@@ -366,7 +372,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
366
372
  });
367
373
  it('M15. the overflow button label and text never disagree, including first paint', async () => {
368
374
  const host = await fixture(html `<div style="width: 220px">
369
- <nile-select multiple .value=${LABELS}>${options()}</nile-select>
375
+ <nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
370
376
  </div>`);
371
377
  const el = host.querySelector('nile-select');
372
378
  // Before the layout frame runs, no button may claim a count it cannot back up.
@@ -445,5 +451,878 @@ describe('NileSelect chip fitting behaviour matrix', () => {
445
451
  expect(inner.showTooltip).to.be.true;
446
452
  expect(inner.chipMaxWidth).to.equal(240);
447
453
  });
454
+ it('G1. default chips are not capped or squeezed by the 2.0.9 chip styles', async () => {
455
+ const host = await fixture(html `<div style="width: 480px">
456
+ <nile-select multiple .value=${LABELS.slice(0, 2)}>${options()}</nile-select>
457
+ </div>`);
458
+ const el = host.querySelector('nile-select');
459
+ await settle(el);
460
+ const [chip] = chips(el);
461
+ expect(chip, 'select rendered no chips').to.exist;
462
+ expect(getComputedStyle(chip).maxWidth).to.equal('none');
463
+ expect(getComputedStyle(chip).minWidth).to.not.equal('0px');
464
+ });
465
+ it('G2. showtooltip caps chips at the chip max width', async () => {
466
+ const host = await fixture(html `<div style="width: 480px">
467
+ <nile-select multiple showtooltip .value=${LABELS.slice(0, 2)}>${options()}</nile-select>
468
+ </div>`);
469
+ const el = host.querySelector('nile-select');
470
+ await settle(el);
471
+ const [firstChip] = chips(el);
472
+ expect(firstChip, 'select rendered no chips').to.exist;
473
+ expect(getComputedStyle(firstChip).maxWidth).to.not.equal('none');
474
+ });
475
+ it('G3. autoresize without showtooltip refits with the 2.0.8 width fit', async () => {
476
+ const host = await fixture(html `<div style="width: 1200px">
477
+ <nile-select multiple autoresize .value=${LABELS}>${options()}</nile-select>
478
+ </div>`);
479
+ const el = host.querySelector('nile-select');
480
+ await settle(el);
481
+ await aTimeout(20);
482
+ const wide = el.maxOptionsVisible;
483
+ host.style.width = '300px';
484
+ await aTimeout(200);
485
+ await settle(el);
486
+ expect(tagsRoot(el).classList.contains('select__tags--fitted')).to.be.false;
487
+ expect(wide).to.equal(0);
488
+ expect(el.maxOptionsVisible).to.be.greaterThan(0);
489
+ expect(el.shadowRoot.querySelector('span.select__tags-count')).to.exist;
490
+ });
491
+ it('G4. toggling showTooltip switches between the fitted and the 2.0.8 chip row', async () => {
492
+ const host = await fixture(html `<div style="width: 320px">
493
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
494
+ </div>`);
495
+ const el = host.querySelector('nile-select');
496
+ await settle(el);
497
+ expect(tagsRoot(el).classList.contains('select__tags--fitted')).to.be.false;
498
+ el.showTooltip = true;
499
+ await settle(el);
500
+ expect(tagsRoot(el).classList.contains('select__tags--fitted')).to.be.true;
501
+ expect(el.shadowRoot.querySelector('button.select__tags-count')).to.exist;
502
+ el.showTooltip = false;
503
+ await settle(el);
504
+ await aTimeout(20);
505
+ await settle(el);
506
+ expect(tagsRoot(el).classList.contains('select__tags--fitted')).to.be.false;
507
+ expect(el.shadowRoot.querySelector('button.select__tags-count')).to.not.exist;
508
+ });
509
+ });
510
+ describe('NileSelect enableVirtualScroll value sync', () => {
511
+ const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
512
+ const DATA = ROLES.map(role => ({ label: role, value: role }));
513
+ async function virtualSelect(tpl = html `<nile-select
514
+ multiple
515
+ .enableVirtualScroll=${true}
516
+ .data=${DATA}
517
+ .value=${[]}
518
+ ></nile-select>`) {
519
+ const el = await fixture(tpl);
520
+ await el.updateComplete;
521
+ const inner = el.shadowRoot.querySelector('nile-virtual-select');
522
+ await inner.updateComplete;
523
+ return { el, inner };
524
+ }
525
+ async function settle(el, inner) {
526
+ await inner.updateComplete;
527
+ await el.updateComplete;
528
+ await inner.updateComplete;
529
+ }
530
+ it('V1. picking an option updates the wrapper value', async () => {
531
+ const { el, inner } = await virtualSelect();
532
+ inner.handleOptionSelection({ value: 'Administrators' });
533
+ await settle(el, inner);
534
+ expect(el.value).to.deep.equal(['Administrators']);
535
+ });
536
+ it('V2. the first pick survives a wrapper re-render', async () => {
537
+ const { el, inner } = await virtualSelect();
538
+ inner.handleOptionSelection({ value: 'Administrators' });
539
+ await settle(el, inner);
540
+ el.requestUpdate();
541
+ await settle(el, inner);
542
+ expect(inner.value).to.deep.equal(['Administrators']);
543
+ expect(el.value).to.deep.equal(['Administrators']);
544
+ });
545
+ it('V3. a removed chip stays removed after a wrapper re-render', async () => {
546
+ const { el, inner } = await virtualSelect(html `<nile-select
547
+ multiple
548
+ .enableVirtualScroll=${true}
549
+ .data=${DATA}
550
+ .value=${['Administrators', 'Users', 'Guests']}
551
+ ></nile-select>`);
552
+ inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Administrators' });
553
+ await settle(el, inner);
554
+ inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Users' });
555
+ await settle(el, inner);
556
+ el.requestUpdate();
557
+ await settle(el, inner);
558
+ expect(el.value).to.deep.equal(['Guests']);
559
+ expect(inner.value).to.deep.equal(['Guests']);
560
+ });
561
+ it('V4. nile-change listeners on the wrapper already see the new value', async () => {
562
+ const { el, inner } = await virtualSelect();
563
+ const seen = [];
564
+ el.addEventListener('nile-change', () => seen.push(el.value));
565
+ inner.handleOptionSelection({ value: 'Guests' });
566
+ await settle(el, inner);
567
+ expect(seen).to.deep.equal([['Guests']]);
568
+ });
569
+ it('V5. single-select virtual mode keeps the wrapper value in sync', async () => {
570
+ const { el, inner } = await virtualSelect(html `<nile-select
571
+ .enableVirtualScroll=${true}
572
+ .data=${DATA}
573
+ ></nile-select>`);
574
+ inner.handleOptionSelection({ value: 'Users' });
575
+ await settle(el, inner);
576
+ el.requestUpdate();
577
+ await settle(el, inner);
578
+ expect(el.value).to.equal('Users');
579
+ expect(inner.value).to.equal('Users');
580
+ });
581
+ it('V6. resizing the host does not re-render the wrapper without autoResize', async () => {
582
+ const host = await fixture(html `<div style="width: 400px">
583
+ <nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>
584
+ </div>`);
585
+ const el = host.querySelector('nile-select');
586
+ await el.updateComplete;
587
+ await nextFrame();
588
+ await nextFrame();
589
+ let updates = 0;
590
+ const update = el.update;
591
+ el.update = function (changed) {
592
+ updates++;
593
+ return update.call(this, changed);
594
+ };
595
+ host.style.width = '250px';
596
+ await nextFrame();
597
+ await nextFrame();
598
+ await aTimeout(50);
599
+ expect(updates).to.equal(0);
600
+ });
601
+ });
602
+ describe('NileSelect enableVirtualScroll + searchEnabled value sync', () => {
603
+ const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
604
+ const DATA = ROLES.map(role => ({ label: role, value: role }));
605
+ async function settle(el, inner) {
606
+ await inner.updateComplete;
607
+ await el.updateComplete;
608
+ await inner.updateComplete;
609
+ await nextFrame();
610
+ await el.updateComplete;
611
+ await inner.updateComplete;
612
+ }
613
+ async function searchableVirtual({ multiple = true, portal = false, value = ['Users'] } = {}) {
614
+ const el = await fixture(html `<nile-select
615
+ ?multiple=${multiple}
616
+ .searchEnabled=${true}
617
+ .enableVirtualScroll=${true}
618
+ .portal=${portal}
619
+ .data=${DATA}
620
+ .value=${value}
621
+ ></nile-select>`);
622
+ await el.updateComplete;
623
+ const inner = el.shadowRoot.querySelector('nile-virtual-select');
624
+ await inner.updateComplete;
625
+ inner.open = true;
626
+ await settle(el, inner);
627
+ if (portal) {
628
+ await waitUntil(() => document.body.querySelector('.nile-virtual-select-portal-append #listbox'), 'listbox never portalled');
629
+ }
630
+ await waitUntil(() => searchInput(inner)?.shadowRoot?.querySelector('input'), 'search box never rendered');
631
+ return { el, inner };
632
+ }
633
+ function searchInput(inner) {
634
+ const inShadow = inner.shadowRoot.querySelector('.select__search nile-input');
635
+ const inPortal = document.body.querySelector('.nile-virtual-select-portal-append .select__search nile-input');
636
+ return (inShadow ?? inPortal);
637
+ }
638
+ async function typeSearch(el, inner, text) {
639
+ const input = searchInput(inner).shadowRoot.querySelector('input');
640
+ input.value = text;
641
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
642
+ await settle(el, inner);
643
+ }
644
+ afterEach(() => {
645
+ document.body.querySelectorAll('.nile-virtual-select-portal-append').forEach(n => n.remove());
646
+ });
647
+ it('R1a. typing in the search box keeps the selected values (multiple)', async () => {
648
+ const { el, inner } = await searchableVirtual();
649
+ expect(searchInput(inner), 'search box rendered').to.exist;
650
+ await typeSearch(el, inner, 'Adm');
651
+ expect(el.value).to.deep.equal(['Users']);
652
+ expect(inner.value).to.deep.equal(['Users']);
653
+ });
654
+ it('R1b. typing in the search box keeps the selected value (single)', async () => {
655
+ const { el, inner } = await searchableVirtual({ multiple: false, value: 'Users' });
656
+ await typeSearch(el, inner, 'zz');
657
+ expect(el.value).to.equal('Users');
658
+ expect(inner.value).to.equal('Users');
659
+ });
660
+ it('R1c. clearing the search text does not clear the selection', async () => {
661
+ const { el, inner } = await searchableVirtual({ value: ['Users', 'Guests'] });
662
+ await typeSearch(el, inner, 'Gu');
663
+ await typeSearch(el, inner, '');
664
+ expect(el.value).to.deep.equal(['Users', 'Guests']);
665
+ await typeSearch(el, inner, 'Gu');
666
+ const clear = searchInput(inner).shadowRoot.querySelector('.input__clear');
667
+ expect(clear, 'search clear button rendered').to.exist;
668
+ clear.click();
669
+ await settle(el, inner);
670
+ expect(el.value).to.deep.equal(['Users', 'Guests']);
671
+ expect(inner.value).to.deep.equal(['Users', 'Guests']);
672
+ });
673
+ it('R1d. a real selection while searching still updates the wrapper value', async () => {
674
+ const { el, inner } = await searchableVirtual();
675
+ await typeSearch(el, inner, 'Gu');
676
+ inner.handleOptionSelection({ value: 'Guests' });
677
+ await settle(el, inner);
678
+ expect(el.value).to.deep.equal(['Users', 'Guests']);
679
+ el.requestUpdate();
680
+ await settle(el, inner);
681
+ expect(inner.value).to.deep.equal(['Users', 'Guests']);
682
+ });
683
+ it('R1e. removing a chip still updates the wrapper value', async () => {
684
+ const { el, inner } = await searchableVirtual({ value: ['Users', 'Guests'] });
685
+ await typeSearch(el, inner, 'U');
686
+ inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Users' });
687
+ await settle(el, inner);
688
+ expect(el.value).to.deep.equal(['Guests']);
689
+ });
690
+ it('R1f. nile-input and nile-change from the inner select sync; nested ones do not', async () => {
691
+ const { el, inner } = await searchableVirtual();
692
+ const nested = inner.shadowRoot.querySelector('.select__search');
693
+ const fire = (target, name, value) => target.dispatchEvent(new CustomEvent(name, { detail: { value }, bubbles: true, composed: true }));
694
+ fire(nested, 'nile-input', 'nested-input');
695
+ fire(nested, 'nile-change', 'nested-change');
696
+ expect(el.value).to.deep.equal(['Users']);
697
+ fire(inner, 'nile-input', ['Guests']);
698
+ expect(el.value).to.deep.equal(['Guests']);
699
+ fire(inner, 'nile-change', ['Administrators']);
700
+ expect(el.value).to.deep.equal(['Administrators']);
701
+ });
702
+ it('R1g. portal mode: list is portalled, search keeps the value, selection syncs', async () => {
703
+ const { el, inner } = await searchableVirtual({ portal: true });
704
+ const listbox = document.body.querySelector('.nile-virtual-select-portal-append #listbox');
705
+ expect(listbox, 'listbox moved under document.body').to.exist;
706
+ expect(inner.shadowRoot.querySelector('#listbox')).to.equal(null);
707
+ await typeSearch(el, inner, 'Adm');
708
+ expect(el.value).to.deep.equal(['Users']);
709
+ inner.handleOptionSelection({ value: 'Administrators' });
710
+ await settle(el, inner);
711
+ expect(el.value).to.deep.equal(['Users', 'Administrators']);
712
+ inner.open = false;
713
+ await settle(el, inner);
714
+ await waitUntil(() => !document.body.querySelector('.nile-virtual-select-portal-append'), 'portal not cleaned up');
715
+ });
716
+ });
717
+ describe('NileSelect chipMinWidth / chipMaxWidth attributes', () => {
718
+ const LABELS = ['Engineering Operations Team', 'Human Resources Department'];
719
+ const options = () => LABELS.map(label => html `<nile-option value=${label}>${label}</nile-option>`);
720
+ async function settle(el) {
721
+ await el.updateComplete;
722
+ await nextFrame();
723
+ await nextFrame();
724
+ await el.updateComplete;
725
+ }
726
+ const firstChip = (el) => el.shadowRoot.querySelector('.select__tags nile-tag');
727
+ it('R4a. defaults are 110 / 180 and are not written to the host', async () => {
728
+ const el = await fixture(html `<nile-select multiple>${options()}</nile-select>`);
729
+ await settle(el);
730
+ expect(el.chipMinWidth).to.equal(110);
731
+ expect(el.chipMaxWidth).to.equal(180);
732
+ expect(el.hasAttribute('chipminwidth')).to.be.false;
733
+ expect(el.hasAttribute('chipmaxwidth')).to.be.false;
734
+ });
735
+ it('R4b. the chipminwidth / chipmaxwidth attributes still set the properties', async () => {
736
+ const el = await fixture(html `<nile-select multiple chipminwidth="90" chipmaxwidth="150">${options()}</nile-select>`);
737
+ await settle(el);
738
+ expect(el.chipMinWidth).to.equal(90);
739
+ expect(el.chipMaxWidth).to.equal(150);
740
+ el.setAttribute('chipmaxwidth', '160');
741
+ await settle(el);
742
+ expect(el.chipMaxWidth).to.equal(160);
743
+ });
744
+ it('R4c. setting the properties programmatically works and does not add attributes', async () => {
745
+ const el = await fixture(html `<nile-select multiple>${options()}</nile-select>`);
746
+ await settle(el);
747
+ el.chipMinWidth = 95;
748
+ el.chipMaxWidth = 140;
749
+ await settle(el);
750
+ expect(el.chipMinWidth).to.equal(95);
751
+ expect(el.chipMaxWidth).to.equal(140);
752
+ expect(el.hasAttribute('chipmaxwidth')).to.be.false;
753
+ });
754
+ it('R4d. showtooltip still caps squeezed chips at chipMaxWidth (attribute and property)', async () => {
755
+ const host = await fixture(html `<div style="width: 300px">
756
+ <nile-select multiple showtooltip chipmaxwidth="150" .value=${LABELS}>${options()}</nile-select>
757
+ </div>`);
758
+ const el = host.querySelector('nile-select');
759
+ await settle(el);
760
+ const chip = firstChip(el);
761
+ expect(chip, 'select rendered no chips').to.exist;
762
+ expect(chip.getBoundingClientRect().width).to.be.within(1, 150.5);
763
+ el.chipMaxWidth = 120;
764
+ await settle(el);
765
+ await nextFrame();
766
+ expect(firstChip(el).getBoundingClientRect().width).to.be.within(1, 120.5);
767
+ });
768
+ it('R4e. virtual mode forwards explicit values without reflecting defaults on either host', async () => {
769
+ const data = LABELS.map(label => ({ label, value: label }));
770
+ const el = await fixture(html `<nile-select multiple .enableVirtualScroll=${true} .data=${data} chipmaxwidth="150"></nile-select>`);
771
+ await settle(el);
772
+ const inner = el.shadowRoot.querySelector('nile-virtual-select');
773
+ await inner.updateComplete;
774
+ expect(inner.chipMaxWidth).to.equal(150);
775
+ expect(inner.chipMinWidth).to.equal(110);
776
+ expect(el.hasAttribute('chipminwidth')).to.be.false;
777
+ expect(inner.hasAttribute('chipminwidth')).to.be.false;
778
+ expect(inner.hasAttribute('chipmaxwidth')).to.be.false;
779
+ });
780
+ });
781
+ describe('NileSelect footer width with and without noWidthSync', () => {
782
+ async function openSelect(width, noWidthSync) {
783
+ const host = await fixture(html `<div style="width: ${width}px">
784
+ <nile-select multiple .noWidthSync=${noWidthSync} .value=${['a']}>
785
+ <nile-option value="a">a</nile-option>
786
+ <nile-option value="b">b</nile-option>
787
+ </nile-select>
788
+ </div>`);
789
+ const el = host.querySelector('nile-select');
790
+ await el.updateComplete;
791
+ await el.show();
792
+ await nextFrame();
793
+ const listbox = el.shadowRoot.querySelector('#listbox');
794
+ const footer = el.shadowRoot.querySelector('.select__footer');
795
+ const combobox = el.shadowRoot.querySelector('[part~="combobox"]');
796
+ return { el, listbox, footer, combobox };
797
+ }
798
+ const labelShown = (footer, selector) => getComputedStyle(footer.querySelector(selector)).display !== 'none';
799
+ before(() => document.documentElement.style.setProperty('--nile-display-flex', 'flex'));
800
+ after(() => document.documentElement.style.removeProperty('--nile-display-flex'));
801
+ for (const width of [120, 400]) {
802
+ it(`R3a. noWidthSync + multiple at ${width}px does not collapse and shows the whole footer`, async () => {
803
+ const { listbox, footer } = await openSelect(width, true);
804
+ expect(listbox.offsetWidth).to.be.greaterThan(60);
805
+ expect(listbox.offsetWidth).to.be.at.least(footer.offsetWidth);
806
+ expect(footer.scrollWidth).to.be.at.most(footer.clientWidth + 1);
807
+ expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.true;
808
+ expect(labelShown(footer, '.select__footer-action .select__footer-label')).to.be.true;
809
+ });
810
+ }
811
+ it('R3b. width sync at normal width keeps the dropdown as wide as the trigger', async () => {
812
+ const { listbox, combobox, footer } = await openSelect(400, false);
813
+ expect(Math.abs(listbox.offsetWidth - combobox.offsetWidth)).to.be.below(2);
814
+ expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.true;
815
+ });
816
+ it('R3c. width sync at a narrow width still collapses the footer labels (container queries kept)', async () => {
817
+ const { listbox, combobox, footer } = await openSelect(130, false);
818
+ expect(Math.abs(listbox.offsetWidth - combobox.offsetWidth)).to.be.below(2);
819
+ expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.false;
820
+ });
821
+ });
822
+ describe('NileSelect showtooltip chip fit recovery (hidden, reconnect, re-parent)', () => {
823
+ const LABELS = [
824
+ 'Engineering Operations Team',
825
+ 'Human Resources Department',
826
+ 'Finance And Accounting Group',
827
+ 'Customer Success Organisation',
828
+ 'Product Design Studio Unit'
829
+ ];
830
+ const options = () => LABELS.map(label => html `<nile-option value=${label}>${label}</nile-option>`);
831
+ const NativeRO = window.ResizeObserver;
832
+ const active = new Map();
833
+ const liveCount = () => Array.from(active.values()).filter(targets => targets.size > 0).length;
834
+ before(() => {
835
+ window.ResizeObserver = class extends NativeRO {
836
+ observe(target, options) {
837
+ if (!active.has(this))
838
+ active.set(this, new Set());
839
+ active.get(this).add(target);
840
+ super.observe(target, options);
841
+ }
842
+ unobserve(target) {
843
+ active.get(this)?.delete(target);
844
+ super.unobserve(target);
845
+ }
846
+ disconnect() {
847
+ active.get(this)?.clear();
848
+ super.disconnect();
849
+ }
850
+ };
851
+ });
852
+ after(() => {
853
+ window.ResizeObserver = NativeRO;
854
+ });
855
+ async function frames(n = 4) {
856
+ for (let i = 0; i < n; i++)
857
+ await nextFrame();
858
+ }
859
+ async function settle(el) {
860
+ await el.updateComplete;
861
+ await frames();
862
+ await el.updateComplete;
863
+ await frames(2);
864
+ }
865
+ const chipsOf = (el) => Array.from(el.shadowRoot.querySelectorAll('.select__tags nile-tag'));
866
+ const watchActive = (el) => !!el.measurableWatch;
867
+ function fitOf(el) {
868
+ const chips = chipsOf(el);
869
+ const visible = chips.filter(chip => chip.style.display !== 'none');
870
+ const button = el.shadowRoot.querySelector('button.select__tags-count');
871
+ return {
872
+ chips: chips.length,
873
+ visible: visible.length,
874
+ more: button ? button.textContent.trim() : '',
875
+ widths: visible.map(chip => chip.offsetWidth)
876
+ };
877
+ }
878
+ function box(width, display = 'block') {
879
+ const div = document.createElement('div');
880
+ div.style.width = `${width}px`;
881
+ div.style.display = display;
882
+ document.body.appendChild(div);
883
+ boxes.push(div);
884
+ return div;
885
+ }
886
+ let boxes = [];
887
+ afterEach(() => {
888
+ boxes.forEach(b => b.remove());
889
+ boxes = [];
890
+ });
891
+ async function makeSelect(parent, { showTooltip = true, autoResize = false, value = LABELS } = {}) {
892
+ const el = document.createElement('nile-select');
893
+ el.multiple = true;
894
+ el.showTooltip = showTooltip;
895
+ el.autoResize = autoResize;
896
+ for (const label of LABELS) {
897
+ const option = document.createElement('nile-option');
898
+ option.value = label;
899
+ option.textContent = label;
900
+ el.appendChild(option);
901
+ }
902
+ el.value = value;
903
+ parent.appendChild(el);
904
+ await settle(el);
905
+ return el;
906
+ }
907
+ async function reference(width, opts = {}) {
908
+ const el = await makeSelect(box(width), opts);
909
+ const fit = fitOf(el);
910
+ el.parentElement.remove();
911
+ return fit;
912
+ }
913
+ function expectFitted(actual, expected, el) {
914
+ expect(actual.chips).to.equal(expected.chips);
915
+ expect(actual.visible, 'visible chips').to.equal(expected.visible);
916
+ expect(actual.more, '+N button').to.equal(expected.more);
917
+ actual.widths.forEach((w, i) => {
918
+ expect(w, `chip ${i} width`).to.be.within(el.chipMinWidth - 1, el.chipMaxWidth + 1);
919
+ expect(Math.abs(w - expected.widths[i]), `chip ${i} matches reference`).to.be.at.most(2);
920
+ });
921
+ }
922
+ it('R5-ref. the 300px reference really is a fitted row (so a stale row cannot match it)', async () => {
923
+ const ref = await reference(300);
924
+ expect(ref.chips).to.equal(5);
925
+ expect(ref.visible).to.be.within(1, 4);
926
+ expect(ref.more).to.equal(`+${5 - ref.visible}`);
927
+ });
928
+ it('R5a. hidden -> visible without reconnect recovers the fitted row, then the watch ends', async () => {
929
+ const ref = await reference(300);
930
+ const parent = box(300, 'none');
931
+ const el = await makeSelect(parent);
932
+ expect(watchActive(el), 'watch armed while hidden').to.be.true;
933
+ parent.style.display = 'block';
934
+ await settle(el);
935
+ expectFitted(fitOf(el), ref, el);
936
+ expect(watchActive(el)).to.be.false;
937
+ });
938
+ it('R5b. hidden -> reconnected into a visible 300px parent re-fits, twice, without extra observers', async () => {
939
+ const ref = await reference(300);
940
+ const el = await makeSelect(box(300, 'none'));
941
+ expect(watchActive(el)).to.be.true;
942
+ el.remove();
943
+ expect(watchActive(el), 'disconnect stops the watch').to.be.false;
944
+ box(300).appendChild(el);
945
+ await settle(el);
946
+ expectFitted(fitOf(el), ref, el);
947
+ expect(watchActive(el)).to.be.false;
948
+ const liveAfterFirst = liveCount();
949
+ el.remove();
950
+ box(300).appendChild(el);
951
+ await settle(el);
952
+ expectFitted(fitOf(el), ref, el);
953
+ expect(watchActive(el)).to.be.false;
954
+ expect(liveCount(), 'no observer growth across reconnects').to.equal(liveAfterFirst);
955
+ });
956
+ it('R5c. visible -> disconnect -> reconnect into a narrower parent re-fits to the new width', async () => {
957
+ const ref = await reference(300);
958
+ const el = await makeSelect(box(700));
959
+ expect(fitOf(el).visible).to.be.greaterThan(ref.visible);
960
+ el.remove();
961
+ box(300).appendChild(el);
962
+ await settle(el);
963
+ expectFitted(fitOf(el), ref, el);
964
+ expect(watchActive(el)).to.be.false;
965
+ });
966
+ it('R5d. value changed while hidden and detached, then reconnected, fits the new value', async () => {
967
+ const four = LABELS.slice(0, 4);
968
+ const ref = await reference(300, { value: four });
969
+ const el = await makeSelect(box(300, 'none'));
970
+ el.remove();
971
+ el.value = four;
972
+ await settle(el);
973
+ expect(watchActive(el), 'no watch left on a detached select').to.be.false;
974
+ box(300).appendChild(el);
975
+ await settle(el);
976
+ expectFitted(fitOf(el), ref, el);
977
+ expect(watchActive(el)).to.be.false;
978
+ });
979
+ it('R5e. reconnect into another hidden parent keeps one watch, and recovers when shown', async () => {
980
+ const ref = await reference(300);
981
+ const el = await makeSelect(box(300, 'none'));
982
+ const liveHidden = liveCount();
983
+ el.remove();
984
+ const second = box(300, 'none');
985
+ second.appendChild(el);
986
+ await settle(el);
987
+ expect(watchActive(el), 're-armed in the new hidden parent').to.be.true;
988
+ expect(liveCount(), 'the old watch was replaced, not duplicated').to.equal(liveHidden);
989
+ second.style.display = 'block';
990
+ await settle(el);
991
+ expectFitted(fitOf(el), ref, el);
992
+ expect(watchActive(el)).to.be.false;
993
+ });
994
+ it('R5f. showTooltip true -> false before reconnect gives the untouched 2.0.8 row', async () => {
995
+ const legacy = await makeSelect(box(300), { showTooltip: false });
996
+ const legacyText = legacy.shadowRoot.querySelector('.select__tags').textContent.replace(/\s+/g, ' ').trim();
997
+ const el = await makeSelect(box(300, 'none'));
998
+ el.remove();
999
+ el.showTooltip = false;
1000
+ box(300).appendChild(el);
1001
+ await settle(el);
1002
+ expect(watchActive(el)).to.be.false;
1003
+ expect(el.shadowRoot.querySelector('.select__tags--fitted')).to.equal(null);
1004
+ chipsOf(el).forEach(chip => expect(chip.style.width, 'no fitted width on the 2.0.8 row').to.equal(''));
1005
+ expect(el.shadowRoot.querySelector('.select__tags').textContent.replace(/\s+/g, ' ').trim()).to.equal(legacyText);
1006
+ });
1007
+ it('R5g. showtooltip=false: hidden -> shown and reconnect never arm a watch or write chip widths', async () => {
1008
+ const parent = box(300, 'none');
1009
+ const el = await makeSelect(parent, { showTooltip: false });
1010
+ expect(watchActive(el)).to.be.false;
1011
+ parent.style.display = 'block';
1012
+ await settle(el);
1013
+ el.remove();
1014
+ box(300).appendChild(el);
1015
+ await settle(el);
1016
+ expect(watchActive(el)).to.be.false;
1017
+ chipsOf(el).forEach(chip => expect(chip.style.width).to.equal(''));
1018
+ });
1019
+ it('R5h. a visible showtooltip select never arms the watch', async () => {
1020
+ const el = await makeSelect(box(300));
1021
+ expect(watchActive(el)).to.be.false;
1022
+ });
1023
+ it('R5i. repeated disconnect/reconnect cycles stay stable', async () => {
1024
+ const ref = await reference(300);
1025
+ const el = await makeSelect(box(300));
1026
+ const baseline = liveCount();
1027
+ for (let i = 0; i < 5; i++) {
1028
+ el.remove();
1029
+ box(300).appendChild(el);
1030
+ await settle(el);
1031
+ }
1032
+ expectFitted(fitOf(el), ref, el);
1033
+ expect(watchActive(el)).to.be.false;
1034
+ expect(liveCount(), 'no observer growth across cycles').to.equal(baseline);
1035
+ });
1036
+ async function updatesOver(el, ms) {
1037
+ let updates = 0;
1038
+ const update = el.update;
1039
+ el.update = function (changed) {
1040
+ updates++;
1041
+ return update.call(this, changed);
1042
+ };
1043
+ await aTimeout(ms);
1044
+ el.update = update;
1045
+ return updates;
1046
+ }
1047
+ it('R5j. autoResize + showtooltip: hidden -> reconnect fits, resizes refit, and it settles', async () => {
1048
+ const ref300 = await reference(300);
1049
+ const ref500 = await reference(500);
1050
+ const el = await makeSelect(box(300, 'none'), { autoResize: true });
1051
+ el.remove();
1052
+ const parent = box(300);
1053
+ parent.appendChild(el);
1054
+ await settle(el);
1055
+ expectFitted(fitOf(el), ref300, el);
1056
+ parent.style.width = '500px';
1057
+ await settle(el);
1058
+ expectFitted(fitOf(el), ref500, el);
1059
+ expect(await updatesOver(el, 300), 'no render loop with showtooltip').to.equal(0);
1060
+ });
1061
+ it('R5k. autoResize: turning showtooltip on over the 2.0.8 row gives the fitted row and settles', async () => {
1062
+ const ref = await reference(300);
1063
+ const el = await makeSelect(box(300), { showTooltip: false, autoResize: true });
1064
+ el.showTooltip = true;
1065
+ await settle(el);
1066
+ await aTimeout(20);
1067
+ await settle(el);
1068
+ expectFitted(fitOf(el), ref, el);
1069
+ expect(await updatesOver(el, 300), 'no render loop once showtooltip is on').to.equal(0);
1070
+ });
1071
+ });
1072
+ class R7PrefixProbe extends HTMLElement {
1073
+ constructor() {
1074
+ super();
1075
+ R7PrefixProbe.created++;
1076
+ }
1077
+ }
1078
+ R7PrefixProbe.created = 0;
1079
+ if (!customElements.get('r7-prefix-probe'))
1080
+ customElements.define('r7-prefix-probe', R7PrefixProbe);
1081
+ describe('NileSelect option prefix copies across re-renders', () => {
1082
+ async function settle(el) {
1083
+ await el.updateComplete;
1084
+ await nextFrame();
1085
+ await nextFrame();
1086
+ await el.updateComplete;
1087
+ }
1088
+ const prefixIn = (el, selector) => el.shadowRoot.querySelector(`${selector} r7-prefix-probe`);
1089
+ const withPrefix = (value) => html `<nile-option value=${value}><r7-prefix-probe slot="prefix" data-v=${value}></r7-prefix-probe>${value}</nile-option>`;
1090
+ it('R7a. single select: the trigger prefix node is reused across re-renders', async () => {
1091
+ const el = await fixture(html `<nile-select .value=${'a'}>${withPrefix('a')}${withPrefix('b')}</nile-select>`);
1092
+ await settle(el);
1093
+ const first = prefixIn(el, '.select__prefix--from-options');
1094
+ expect(first, 'prefix copied into the trigger').to.exist;
1095
+ const created = R7PrefixProbe.created;
1096
+ for (let i = 0; i < 3; i++) {
1097
+ el.requestUpdate();
1098
+ await settle(el);
1099
+ }
1100
+ expect(prefixIn(el, '.select__prefix--from-options')).to.equal(first);
1101
+ expect(R7PrefixProbe.created).to.equal(created);
1102
+ });
1103
+ for (const showTooltip of [false, true]) {
1104
+ it(`R7b. multiple (showtooltip=${showTooltip}): chip prefix nodes are reused across re-renders`, async () => {
1105
+ const el = await fixture(html `<nile-select multiple ?showtooltip=${showTooltip} .value=${['a', 'b']}>
1106
+ ${withPrefix('a')}${withPrefix('b')}
1107
+ </nile-select>`);
1108
+ await settle(el);
1109
+ const before = Array.from(el.shadowRoot.querySelectorAll('.select__tags r7-prefix-probe'));
1110
+ expect(before.length).to.equal(2);
1111
+ const created = R7PrefixProbe.created;
1112
+ el.requestUpdate();
1113
+ await settle(el);
1114
+ const after = Array.from(el.shadowRoot.querySelectorAll('.select__tags r7-prefix-probe'));
1115
+ expect(after.length).to.equal(2);
1116
+ after.forEach((node, i) => expect(node === before[i], `chip ${i} prefix replaced`).to.be.true);
1117
+ expect(R7PrefixProbe.created).to.equal(created);
1118
+ });
1119
+ }
1120
+ it('R7c. changing the selection or the option prefix still updates the copy', async () => {
1121
+ const el = await fixture(html `<nile-select .value=${'a'}>${withPrefix('a')}${withPrefix('b')}</nile-select>`);
1122
+ await settle(el);
1123
+ el.value = 'b';
1124
+ await settle(el);
1125
+ expect(prefixIn(el, '.select__prefix--from-options').dataset.v).to.equal('b');
1126
+ const source = el.querySelector('nile-option[value="b"] r7-prefix-probe');
1127
+ source.setAttribute('data-v', 'b2');
1128
+ el.requestUpdate();
1129
+ await settle(el);
1130
+ expect(prefixIn(el, '.select__prefix--from-options').dataset.v).to.equal('b2');
1131
+ });
1132
+ });
1133
+ describe('NileSelect 2.0.8 compatibility matrix', () => {
1134
+ const OPTS = ['a', 'b', 'c'];
1135
+ const options = () => OPTS.map(v => html `<nile-option value=${v}>L${v}</nile-option>`);
1136
+ const DATA = OPTS.map(v => ({ label: `L${v}`, value: v }));
1137
+ async function settle(el) {
1138
+ await el.updateComplete;
1139
+ await nextFrame();
1140
+ await nextFrame();
1141
+ await el.updateComplete;
1142
+ }
1143
+ const selectedValues = (el) => Array.from(el.querySelectorAll('nile-option'))
1144
+ .filter(o => o.selected)
1145
+ .map(o => o.value);
1146
+ const chipCount = (el) => el.shadowRoot.querySelectorAll('.select__tags nile-tag').length;
1147
+ function record(el, names) {
1148
+ const seen = [];
1149
+ names.forEach(name => el.addEventListener(name, e => seen.push([name, JSON.parse(JSON.stringify(e.detail ?? null))])));
1150
+ return seen;
1151
+ }
1152
+ it('C1. a value in the template is applied when the options arrive later (multiple and single)', async () => {
1153
+ const multi = await fixture(html `<nile-select multiple .value=${['a', 'b']}></nile-select>`);
1154
+ await settle(multi);
1155
+ OPTS.forEach(v => { const o = document.createElement('nile-option'); o.value = v; o.textContent = v; multi.appendChild(o); });
1156
+ await settle(multi);
1157
+ expect(multi.value).to.deep.equal(['a', 'b']);
1158
+ expect(selectedValues(multi)).to.deep.equal(['a', 'b']);
1159
+ expect(chipCount(multi)).to.equal(2);
1160
+ const single = await fixture(html `<nile-select .value=${'b'}></nile-select>`);
1161
+ await settle(single);
1162
+ OPTS.forEach(v => { const o = document.createElement('nile-option'); o.value = v; o.textContent = v; single.appendChild(o); });
1163
+ await settle(single);
1164
+ expect(single.value).to.equal('b');
1165
+ expect(selectedValues(single)).to.deep.equal(['b']);
1166
+ });
1167
+ it('C2. programmatic value updates reach options, chips and the form value', async () => {
1168
+ const el = await fixture(html `<nile-select multiple name="s">${options()}</nile-select>`);
1169
+ await settle(el);
1170
+ el.value = ['b', 'c'];
1171
+ await settle(el);
1172
+ expect(selectedValues(el)).to.deep.equal(['b', 'c']);
1173
+ expect(chipCount(el)).to.equal(2);
1174
+ el.value = [];
1175
+ await settle(el);
1176
+ expect(selectedValues(el)).to.deep.equal([]);
1177
+ expect(chipCount(el)).to.equal(0);
1178
+ });
1179
+ it('C3. removing a chip updates the value and emits input, change, tag-remove in order', async () => {
1180
+ const el = await fixture(html `<nile-select multiple .value=${['a', 'b']}>${options()}</nile-select>`);
1181
+ await settle(el);
1182
+ const seen = record(el, ['nile-input', 'nile-change', 'nile-tag-remove']);
1183
+ const chip = el.shadowRoot.querySelector('.select__tags nile-tag');
1184
+ expect(chip, 'select rendered no chips').to.exist;
1185
+ chip.dispatchEvent(new CustomEvent('nile-remove', { bubbles: true, composed: true }));
1186
+ await settle(el);
1187
+ expect(el.value).to.deep.equal(['b']);
1188
+ expect(seen.map(([name]) => name)).to.deep.equal(['nile-input', 'nile-change', 'nile-tag-remove']);
1189
+ expect(seen[2][1].removedtagvalue).to.equal('a');
1190
+ });
1191
+ it('C4. the clear button empties the value and emits clear, input, change', async () => {
1192
+ const el = await fixture(html `<nile-select multiple clearable .value=${['a', 'b']}>${options()}</nile-select>`);
1193
+ await settle(el);
1194
+ const seen = record(el, ['nile-clear', 'nile-input', 'nile-change']);
1195
+ const clear = el.shadowRoot.querySelector('[part~="clear-button"]');
1196
+ expect(clear, 'clear button rendered').to.exist;
1197
+ clear.click();
1198
+ await settle(el);
1199
+ expect(el.value).to.deep.equal([]);
1200
+ expect(chipCount(el)).to.equal(0);
1201
+ expect(seen.map(([name]) => name)).to.deep.equal(['nile-clear', 'nile-input', 'nile-change']);
1202
+ });
1203
+ it('C5. form reset restores the value attribute', async () => {
1204
+ const form = await fixture(html `<form>
1205
+ <nile-select name="s" multiple value="a b">${options()}</nile-select>
1206
+ </form>`);
1207
+ const el = form.querySelector('nile-select');
1208
+ await settle(el);
1209
+ el.value = ['c'];
1210
+ await settle(el);
1211
+ form.reset();
1212
+ await settle(el);
1213
+ expect(el.value).to.deep.equal(['a', 'b']);
1214
+ expect(selectedValues(el)).to.deep.equal(['a', 'b']);
1215
+ });
1216
+ it('C6. portal + search: typing filters without touching the value', async () => {
1217
+ const el = await fixture(html `<nile-select multiple portal searchEnabled .value=${['a']}>${options()}</nile-select>`);
1218
+ await settle(el);
1219
+ await el.show();
1220
+ await aTimeout(50);
1221
+ const search = Array.from(document.body.querySelectorAll('nile-input')).find(n => !el.shadowRoot.contains(n));
1222
+ expect(search, 'portal search box rendered').to.exist;
1223
+ await search.updateComplete;
1224
+ const input = search.shadowRoot.querySelector('input');
1225
+ input.value = 'Lb';
1226
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
1227
+ await settle(el);
1228
+ expect(el.value).to.deep.equal(['a']);
1229
+ await el.hide();
1230
+ });
1231
+ it('C7. reconnecting a multiple select without showtooltip keeps value and chips', async () => {
1232
+ const host = await fixture(html `<div style="width: 500px">
1233
+ <nile-select multiple .value=${['a', 'b']}>${options()}</nile-select>
1234
+ </div>`);
1235
+ const el = host.querySelector('nile-select');
1236
+ await settle(el);
1237
+ el.remove();
1238
+ host.appendChild(el);
1239
+ await settle(el);
1240
+ expect(el.value).to.deep.equal(['a', 'b']);
1241
+ expect(chipCount(el)).to.equal(2);
1242
+ });
1243
+ it('C8. enableVirtualScroll: a value set before data arrives is kept and shown', async () => {
1244
+ const el = await fixture(html `<nile-select multiple .enableVirtualScroll=${true} .value=${['b']}></nile-select>`);
1245
+ await settle(el);
1246
+ el.data = DATA;
1247
+ await settle(el);
1248
+ const inner = el.shadowRoot.querySelector('nile-virtual-select');
1249
+ await inner.updateComplete;
1250
+ expect(el.value).to.deep.equal(['b']);
1251
+ expect(inner.value).to.deep.equal(['b']);
1252
+ expect(inner.shadowRoot.querySelectorAll('.select__tags nile-tag').length).to.equal(1);
1253
+ });
1254
+ it('C9. enableVirtualScroll: programmatic value and clear-all reach both selects', async () => {
1255
+ const el = await fixture(html `<nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>`);
1256
+ await settle(el);
1257
+ const inner = el.shadowRoot.querySelector('nile-virtual-select');
1258
+ el.value = ['a', 'c'];
1259
+ await settle(el);
1260
+ await inner.updateComplete;
1261
+ expect(inner.value).to.deep.equal(['a', 'c']);
1262
+ inner.unSelectAll();
1263
+ await inner.updateComplete;
1264
+ await settle(el);
1265
+ expect(inner.value).to.deep.equal([]);
1266
+ expect(el.value).to.deep.equal([]);
1267
+ });
1268
+ });
1269
+ describe('NileSelect showtooltip tooltip on sub-pixel truncation', () => {
1270
+ const A = 'Engineering Operations';
1271
+ const B = 'Human Resources Department And Payroll';
1272
+ async function frames(n = 5) {
1273
+ for (let i = 0; i < n; i++)
1274
+ await nextFrame();
1275
+ }
1276
+ async function mount(width, chipMaxWidth) {
1277
+ const host = await fixture(html `<div style="width: ${width}px">
1278
+ <nile-select multiple showtooltip chipminwidth="40" chipmaxwidth=${chipMaxWidth} .value=${[A, B]}>
1279
+ <nile-option value=${A}>${A}</nile-option>
1280
+ <nile-option value=${B}>${B}</nile-option>
1281
+ </nile-select>
1282
+ </div>`);
1283
+ const el = host.querySelector('nile-select');
1284
+ await el.updateComplete;
1285
+ await frames();
1286
+ await el.updateComplete;
1287
+ await frames(2);
1288
+ return el;
1289
+ }
1290
+ function visiblyCut(chip) {
1291
+ const content = chip.shadowRoot.querySelector('[part="content"]');
1292
+ const text = content.assignedNodes({ flatten: true }).find(n => n.textContent?.trim());
1293
+ const range = document.createRange();
1294
+ range.selectNodeContents(text);
1295
+ const over = range.getBoundingClientRect().right - content.getBoundingClientRect().right;
1296
+ return { cut: over > 1 / 64, over, pxGap: content.scrollWidth - content.clientWidth };
1297
+ }
1298
+ function chipsAndTips(el) {
1299
+ const chips = Array.from(el.shadowRoot.querySelectorAll('.select__tags nile-tag'));
1300
+ const tips = Array.from(el.shadowRoot.querySelectorAll('.select__tags nile-lite-tooltip'));
1301
+ return chips
1302
+ .map((chip, i) => ({ chip, tipOn: !!tips[i] && !tips[i].hasAttribute('disabled') }))
1303
+ .filter(({ chip }) => chip.style.display !== 'none');
1304
+ }
1305
+ it('TT1. every visibly cut chip has its tooltip, every uncut chip has none (sweep across the edge)', async () => {
1306
+ const wide = await mount(900, 400);
1307
+ const natural = chipsAndTips(wide)[0].chip.offsetWidth;
1308
+ let subPixelCuts = 0;
1309
+ for (let max = natural - 4; max <= natural + 1; max += 0.25) {
1310
+ const el = await mount(330, max);
1311
+ for (const { chip, tipOn } of chipsAndTips(el)) {
1312
+ const { cut, pxGap } = visiblyCut(chip);
1313
+ expect(tipOn, `chipMaxWidth ${max}: tooltip ${tipOn} but cut ${cut}`).to.equal(cut);
1314
+ if (cut && pxGap <= 1)
1315
+ subPixelCuts++;
1316
+ }
1317
+ }
1318
+ expect(subPixelCuts, 'sweep reached the sub-pixel truncation window').to.be.greaterThan(0);
1319
+ });
1320
+ it('TT2. chips at their natural width get no tooltip', async () => {
1321
+ const el = await mount(900, 400);
1322
+ for (const { chip, tipOn } of chipsAndTips(el)) {
1323
+ expect(visiblyCut(chip).cut).to.be.false;
1324
+ expect(tipOn).to.be.false;
1325
+ }
1326
+ });
448
1327
  });
449
1328
  //# sourceMappingURL=nile-select.test.js.map