@aquera/nile-elements 2.0.8 → 2.0.9

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 (50) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +619 -252
  3. package/dist/nile-select/index.cjs.js +1 -1
  4. package/dist/nile-select/index.esm.js +1 -1
  5. package/dist/nile-select/nile-select.cjs.js +2 -1
  6. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  7. package/dist/nile-select/nile-select.css.cjs.js +1 -1
  8. package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
  9. package/dist/nile-select/nile-select.css.esm.js +126 -6
  10. package/dist/nile-select/nile-select.esm.js +77 -11
  11. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  12. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  13. package/dist/nile-select/virtual-scroll-helper.esm.js +3 -1
  14. package/dist/nile-virtual-select/index.cjs.js +1 -1
  15. package/dist/nile-virtual-select/index.esm.js +1 -1
  16. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -3
  17. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  18. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
  19. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
  20. package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +126 -6
  21. package/dist/nile-virtual-select/nile-virtual-select.esm.js +71 -12
  22. package/dist/src/nile-select/nile-select.css.js +126 -6
  23. package/dist/src/nile-select/nile-select.css.js.map +1 -1
  24. package/dist/src/nile-select/nile-select.d.ts +43 -5
  25. package/dist/src/nile-select/nile-select.js +445 -106
  26. package/dist/src/nile-select/nile-select.js.map +1 -1
  27. package/dist/src/nile-select/nile-select.test.d.ts +1 -0
  28. package/dist/src/nile-select/nile-select.test.js +342 -1
  29. package/dist/src/nile-select/nile-select.test.js.map +1 -1
  30. package/dist/src/nile-select/virtual-scroll-helper.js +3 -1
  31. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  32. package/dist/src/nile-virtual-select/nile-virtual-select.css.js +126 -6
  33. package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
  34. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +45 -4
  35. package/dist/src/nile-virtual-select/nile-virtual-select.js +385 -59
  36. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  37. package/dist/src/nile-virtual-select/nile-virtual-select.test.js +253 -1
  38. package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/version.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +1 -1
  43. package/src/nile-select/nile-select.css.ts +126 -6
  44. package/src/nile-select/nile-select.test.ts +478 -1
  45. package/src/nile-select/nile-select.ts +499 -91
  46. package/src/nile-select/virtual-scroll-helper.ts +3 -1
  47. package/src/nile-virtual-select/nile-virtual-select.css.ts +126 -6
  48. package/src/nile-virtual-select/nile-virtual-select.test.ts +352 -1
  49. package/src/nile-virtual-select/nile-virtual-select.ts +460 -55
  50. package/vscode-html-custom-data.json +40 -6
@@ -20,6 +20,9 @@ export class VirtualScrollHelper {
20
20
  .name=${component.name}
21
21
  .value=${component.value}
22
22
  .autoResize=${component.autoResize}
23
+ .showTooltip=${component.showTooltip}
24
+ .chipMinWidth=${component.chipMinWidth}
25
+ .chipMaxWidth=${component.chipMaxWidth}
23
26
  .placeholder=${component.placeholder}
24
27
  .size=${component.size}
25
28
  .searchEnabled=${component.searchEnabled}
@@ -47,7 +50,6 @@ export class VirtualScrollHelper {
47
50
  .renderItemConfig=${component.renderItemConfig}
48
51
  .blockValueChange=${component.blockValueChange}
49
52
  .noWidthSync=${component.noWidthSync}
50
- .maxOptionsVisible=${component.maxOptionsVisible}
51
53
  .enableTabClose=${component.enableTabClose}
52
54
  .data=${component.data}
53
55
  .open=${component.open}
@@ -206,7 +206,7 @@ export const styles = css`
206
206
  display: flex;
207
207
  flex: 1;
208
208
  align-items: center;
209
- flex-wrap: no-wrap;
209
+ flex-wrap: nowrap;
210
210
  margin-inline-start: var(--nile-spacing-7px, var(--ng-spacing-md));
211
211
  width: 100%;
212
212
  overflow: hidden;
@@ -227,6 +227,98 @@ export const styles = css`
227
227
  margin-right: var(--nile-spacing-5xl, var(--ng-spacing-5xl));
228
228
  }
229
229
 
230
+ button.select__tags-count {
231
+ appearance: none;
232
+ -webkit-appearance: none;
233
+ background: none;
234
+ text-align: inherit;
235
+ }
236
+
237
+ button.select__tags-count:disabled {
238
+ cursor: not-allowed;
239
+ }
240
+
241
+ .select__tags--fitted {
242
+ min-width: 0;
243
+ }
244
+
245
+ .select__tags nile-lite-tooltip.select__tag-tip {
246
+ display: contents;
247
+ }
248
+
249
+ .select__tags nile-tag {
250
+ flex: 0 1 auto;
251
+ cursor: pointer;
252
+ min-width: 0;
253
+ }
254
+
255
+ /* A chip is capped - and so truncated - only when a tooltip can reveal the
256
+ full value. Without one it keeps its natural width and the fit pass moves
257
+ the surplus into the +N button instead. */
258
+ :host([showtooltip]) .select__tags nile-tag {
259
+ max-width: var(--nile-width-180px, var(--ng-height-180px));
260
+ }
261
+
262
+ /* Only the fitted layout can move a chip into the +N button, so only there
263
+ can a chip refuse to shrink. */
264
+ :host(:not([showtooltip])) .select__tags--fitted nile-tag {
265
+ flex-shrink: 0;
266
+ }
267
+
268
+ .select__tags nile-tag::part(base) {
269
+ max-width: 100%;
270
+ }
271
+
272
+ .select__tags nile-tag::part(prefix_content_wrapper) {
273
+ flex: 1 1 auto;
274
+ min-width: 0;
275
+ }
276
+
277
+ .select__tags nile-tag::part(content) {
278
+ display: block;
279
+ flex: 1 1 auto;
280
+ min-width: 0;
281
+ /* The tag's line-height of 1 leaves the clip box shorter than the glyph
282
+ box, so descenders are cut. Pad the clip box, negate it in layout. */
283
+ padding-block: 3px;
284
+ margin-block: -3px;
285
+ overflow: hidden;
286
+ white-space: nowrap;
287
+ }
288
+
289
+ :host([showtooltip]) .select__tags nile-tag::part(content) {
290
+ text-overflow: ellipsis;
291
+ }
292
+
293
+ .select__tags nile-tag::part(remove-button) {
294
+ flex: 0 0 auto;
295
+ }
296
+
297
+ .select__tags--fitted nile-tag:focus-visible {
298
+ outline: 2px solid var(--nile-colors-primary-600, var(--ng-colors-border-brand));
299
+ outline-offset: 1px;
300
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
301
+ }
302
+
303
+ .select__tags--fitted .select__tags-count {
304
+ flex: 0 0 auto;
305
+ margin: 0;
306
+ padding: 0 var(--nile-spacing-xsmall, var(--ng-spacing-md));
307
+ height: var(--nile-height-26px, var(--ng-height-26px));
308
+ display: inline-flex;
309
+ align-items: center;
310
+ white-space: nowrap;
311
+ cursor: pointer;
312
+ border: 1px dashed
313
+ var(--nile-colors-neutral-500, var(--ng-colors-border-secondary));
314
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
315
+ }
316
+
317
+ .select__tags--fitted .select__tags-count:focus-visible {
318
+ outline: 2px solid var(--nile-colors-primary-600, var(--ng-colors-border-brand));
319
+ outline-offset: 1px;
320
+ }
321
+
230
322
  .select__tags::slotted(nile-tag) {
231
323
  cursor: pointer !important;
232
324
  }
@@ -457,9 +549,41 @@ export const styles = css`
457
549
  flex-direction: row;
458
550
  align-items: center;
459
551
  padding: var(--nile-spacing-md, var(--ng-spacing-md)) var(--nile-spacing-lg, var(--ng-spacing-lg));
460
- gap: var(--nile-spacing-lg, var(--ng-spacing-lg));
552
+ gap: var(--nile-spacing-none, var(--ng-spacing-none));
461
553
  justify-content: space-between;
462
554
  box-sizing: unset;
555
+ container-type: inline-size;
556
+ container-name: nile-select-footer;
557
+ }
558
+
559
+ .select__footer-action,
560
+ .select__footer .select__clear {
561
+ display: inline-flex;
562
+ align-items: center;
563
+ gap: var(--nile-spacing-sm, var(--ng-spacing-sm));
564
+ min-width: 0;
565
+ }
566
+
567
+ .select__footer-glyph {
568
+ flex: 0 0 auto;
569
+ display: inline-flex;
570
+ vertical-align: middle;
571
+ }
572
+
573
+ .select__footer-label {
574
+ white-space: nowrap;
575
+ }
576
+
577
+ @container nile-select-footer (max-width: 148px) {
578
+ .select__clear .select__footer-label {
579
+ display: none;
580
+ }
581
+ }
582
+
583
+ @container nile-select-footer (max-width: 113px) {
584
+ .select__footer-label {
585
+ display: none;
586
+ }
463
587
  }
464
588
 
465
589
  .tag__prefix[slot='prefix'] {
@@ -496,10 +620,6 @@ export const styles = css`
496
620
  }
497
621
  }
498
622
 
499
- .select__invisible{
500
- opacity: 0;
501
- }
502
-
503
623
  .select__prefix--from-options {
504
624
  margin-inline-end: var(--nile-spacing-lg, var(--ng-spacing-md));
505
625
  }
@@ -1,4 +1,4 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { aTimeout, expect, fixture, html, nextFrame } from '@open-wc/testing';
2
2
  import './nile-virtual-select';
3
3
  import type { NileVirtualSelect } from './nile-virtual-select';
4
4
 
@@ -104,3 +104,354 @@ describe('NileVirtualSelect', () => {
104
104
  it('99. no nile-calendar', async () => { const el = await fixture<NileVirtualSelect>(html`<nile-virtual-select></nile-virtual-select>`); expect(el.shadowRoot!.querySelector('nile-calendar')).to.be.null; });
105
105
  it('100. full integration', async () => { const el = await fixture<NileVirtualSelect>(html`<nile-virtual-select class="vs" id="vs1"></nile-virtual-select>`); expect(el.id).to.equal('vs1'); expect(el.shadowRoot).to.not.be.null; expect(el.shadowRoot!.querySelector('nile-popup')).to.exist; expect(el.shadowRoot!.querySelector('.select__combobox')).to.exist; });
106
106
  });
107
+
108
+ describe('NileVirtualSelect chip fitting behaviour matrix', () => {
109
+ const LABELS = [
110
+ 'Engineering Operations Team',
111
+ 'Human Resources Department',
112
+ 'Finance And Accounting Group',
113
+ 'Customer Success Organisation',
114
+ 'Product Design Studio Unit'
115
+ ];
116
+
117
+ const DATA = LABELS.map(label => ({ label, value: label }));
118
+
119
+ async function settle(el: NileVirtualSelect) {
120
+ await el.updateComplete;
121
+ await nextFrame();
122
+ await nextFrame();
123
+ await el.updateComplete;
124
+ }
125
+
126
+ function tagsRoot(el: NileVirtualSelect) {
127
+ return el.shadowRoot!.querySelector('.select__tags');
128
+ }
129
+
130
+ function chips(el: NileVirtualSelect) {
131
+ return Array.from(
132
+ el.shadowRoot!.querySelectorAll('.select__tags nile-tag')
133
+ ) as HTMLElement[];
134
+ }
135
+
136
+ function hiddenChips(el: NileVirtualSelect) {
137
+ return chips(el).filter(chip => chip.style.display === 'none');
138
+ }
139
+
140
+ it('V1. multiple + no max-options-visible + autoResize=false renders fitted chips', async () => {
141
+ const host = await fixture<HTMLElement>(
142
+ html`<div style="width: 320px">
143
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
144
+ </nile-virtual-select>
145
+ </div>`
146
+ );
147
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
148
+ await settle(el);
149
+
150
+ expect(el.autoResize).to.be.false;
151
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
152
+ expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.exist;
153
+ expect(chips(el).length).to.equal(LABELS.length);
154
+ });
155
+
156
+ it('V2. multiple + no max-options-visible + autoResize=true refits when the container resizes', async () => {
157
+ const host = await fixture<HTMLElement>(
158
+ html`<div style="width: 480px">
159
+ <nile-virtual-select multiple autoresize .data=${DATA} .value=${LABELS}>
160
+ </nile-virtual-select>
161
+ </div>`
162
+ );
163
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
164
+ await settle(el);
165
+
166
+ expect(el.autoResize).to.be.true;
167
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
168
+ const wideHidden = hiddenChips(el).length;
169
+
170
+ host.style.width = '180px';
171
+ await aTimeout(200);
172
+ await settle(el);
173
+
174
+ expect(hiddenChips(el).length).to.be.at.least(wideHidden);
175
+ });
176
+
177
+ it('V3. explicit max-options-visible switches to fixed-count layout', async () => {
178
+ const host = await fixture<HTMLElement>(
179
+ html`<div style="width: 320px">
180
+ <nile-virtual-select
181
+ multiple
182
+ max-options-visible="2"
183
+ .data=${DATA}
184
+ .value=${LABELS}
185
+ ></nile-virtual-select>
186
+ </div>`
187
+ );
188
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
189
+ await settle(el);
190
+
191
+ expect(el.maxOptionsVisible).to.equal(2);
192
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
193
+ expect(chips(el).length).to.equal(2);
194
+ expect(el.shadowRoot!.querySelector('span.select__tags-count')).to.exist;
195
+ });
196
+
197
+ it('V3b. max-options-visible set as a property also switches to fixed-count layout', async () => {
198
+ const host = await fixture<HTMLElement>(
199
+ html`<div style="width: 320px">
200
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
201
+ </nile-virtual-select>
202
+ </div>`
203
+ );
204
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
205
+ await settle(el);
206
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
207
+
208
+ el.maxOptionsVisible = 1;
209
+ await settle(el);
210
+
211
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
212
+ expect(chips(el).length).to.equal(1);
213
+ });
214
+
215
+ it('V3c. the default max-options-visible is still 3 and reading it does not opt out', async () => {
216
+ const host = await fixture<HTMLElement>(
217
+ html`<div style="width: 320px">
218
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
219
+ </nile-virtual-select>
220
+ </div>`
221
+ );
222
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
223
+ await settle(el);
224
+
225
+ expect(el.maxOptionsVisible).to.equal(3);
226
+ expect(el.maxOptionsVisible).to.equal(3);
227
+ await settle(el);
228
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
229
+ });
230
+
231
+ it('V5. autoresize works as a markup attribute', async () => {
232
+ const b = await fixture<NileVirtualSelect>(
233
+ html`<nile-virtual-select multiple autoresize></nile-virtual-select>`
234
+ );
235
+ await b.updateComplete;
236
+ expect(b.autoResize).to.be.true;
237
+
238
+ const c = await fixture<NileVirtualSelect>(
239
+ html`<nile-virtual-select multiple></nile-virtual-select>`
240
+ );
241
+ await c.updateComplete;
242
+ expect(c.autoResize).to.be.false;
243
+ });
244
+
245
+ it('V6. the +N button count matches the number of hidden chips', async () => {
246
+ const host = await fixture<HTMLElement>(
247
+ html`<div style="width: 220px">
248
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
249
+ </nile-virtual-select>
250
+ </div>`
251
+ );
252
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
253
+ await settle(el);
254
+
255
+ const overflow = el.shadowRoot!.querySelector(
256
+ 'button.select__tags-count'
257
+ ) as HTMLElement | null;
258
+ const hidden = hiddenChips(el).length;
259
+
260
+ if (hidden > 0) {
261
+ expect(overflow).to.exist;
262
+ expect(overflow!.textContent!.trim()).to.equal(`+${hidden}`);
263
+ expect(overflow!.getAttribute('aria-label')).to.equal(
264
+ `${hidden} more selected`
265
+ );
266
+ } else {
267
+ // Nothing overflows, so the button is not rendered at all.
268
+ expect(overflow).to.not.exist;
269
+ }
270
+ });
271
+
272
+ it('V7. nile-select and nile-virtual-select agree on when fitting is active', async () => {
273
+ const host = await fixture<HTMLElement>(
274
+ html`<div style="width: 320px">
275
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
276
+ </nile-virtual-select>
277
+ <nile-virtual-select
278
+ multiple
279
+ max-options-visible="2"
280
+ .data=${DATA}
281
+ .value=${LABELS}
282
+ ></nile-virtual-select>
283
+ </div>`
284
+ );
285
+ const [fitted, fixed] = Array.from(
286
+ host.querySelectorAll('nile-virtual-select')
287
+ ) as NileVirtualSelect[];
288
+ await settle(fitted);
289
+ await settle(fixed);
290
+
291
+ expect(tagsRoot(fitted)!.classList.contains('select__tags--fitted')).to.be
292
+ .true;
293
+ expect(tagsRoot(fixed)!.classList.contains('select__tags--fitted')).to.be
294
+ .false;
295
+ });
296
+
297
+ // The tooltip is opt-in: `showtooltip` present, and the chip's text clipped.
298
+ function tips(el: NileVirtualSelect) {
299
+ return Array.from(
300
+ el.shadowRoot!.querySelectorAll('.select__tag-tip')
301
+ ) as HTMLElement[];
302
+ }
303
+
304
+ function liveTips(el: NileVirtualSelect) {
305
+ return tips(el).filter(tip => !tip.hasAttribute('disabled'));
306
+ }
307
+
308
+ // Truncation is bound to the tooltip: a clipped chip is only acceptable when
309
+ // something can reveal the value it hid. A chip wider than the whole field is
310
+ // the one exception - there is nowhere for it to go.
311
+ function chipContent(chip: HTMLElement) {
312
+ return chip.shadowRoot!.querySelector('[part~="content"]') as HTMLElement;
313
+ }
314
+
315
+ function clippedChipCount(el: NileVirtualSelect) {
316
+ return chips(el)
317
+ .filter(chip => chip.style.display !== 'none')
318
+ .filter(chip => {
319
+ const content = chipContent(chip);
320
+ return content.scrollWidth > content.clientWidth + 1;
321
+ }).length;
322
+ }
323
+
324
+ it('V12. without showtooltip a fitted chip is never truncated', async () => {
325
+ const host = await fixture<HTMLElement>(
326
+ html`<div style="width: 480px">
327
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}></nile-virtual-select>
328
+ </div>`
329
+ );
330
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
331
+ await settle(el);
332
+
333
+ 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
+ });
338
+
339
+ it('V13. showtooltip is what allows a fitted chip to be truncated', async () => {
340
+ const host = await fixture<HTMLElement>(
341
+ html`<div style="width: 480px">
342
+ <nile-virtual-select
343
+ multiple
344
+ showtooltip
345
+ .data=${DATA}
346
+ .value=${LABELS}
347
+ ></nile-virtual-select>
348
+ </div>`
349
+ );
350
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
351
+ await settle(el);
352
+
353
+ expect(el.showTooltip).to.be.true;
354
+ expect(clippedChipCount(el)).to.be.greaterThan(0);
355
+ });
356
+
357
+ it('V8. no showtooltip attribute hides the chip tooltip', async () => {
358
+ const host = await fixture<HTMLElement>(
359
+ html`<div style="width: 200px">
360
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}></nile-virtual-select>
361
+ </div>`
362
+ );
363
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
364
+ await settle(el);
365
+
366
+ expect(el.showTooltip).to.be.false;
367
+ expect(el.hasAttribute('showtooltip')).to.be.false;
368
+ expect(tips(el).length).to.be.greaterThan(0);
369
+ expect(liveTips(el).length).to.equal(0);
370
+ });
371
+
372
+ it('V9. showtooltip shows the chip tooltip', async () => {
373
+ const host = await fixture<HTMLElement>(
374
+ html`<div style="width: 200px">
375
+ <nile-virtual-select multiple showtooltip .data=${DATA} .value=${LABELS}></nile-virtual-select>
376
+ </div>`
377
+ );
378
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
379
+ await settle(el);
380
+
381
+ expect(el.showTooltip).to.be.true;
382
+ expect(liveTips(el).length).to.be.greaterThan(0);
383
+ });
384
+
385
+ it('V10. removing showtooltip hides the chip tooltip again', async () => {
386
+ const host = await fixture<HTMLElement>(
387
+ html`<div style="width: 200px">
388
+ <nile-virtual-select multiple showtooltip .data=${DATA} .value=${LABELS}></nile-virtual-select>
389
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}></nile-virtual-select>
390
+ </div>`
391
+ );
392
+ const [el, control] = Array.from(
393
+ host.querySelectorAll('nile-virtual-select')
394
+ ) as NileVirtualSelect[];
395
+ await settle(el);
396
+ await settle(control);
397
+
398
+ expect(liveTips(el).length).to.be.greaterThan(0);
399
+
400
+ el.removeAttribute('showtooltip');
401
+ await settle(el);
402
+
403
+ expect(el.showTooltip).to.be.false;
404
+ expect(liveTips(el).length).to.equal(0);
405
+ // Removing the attribute must land back on the default.
406
+ expect(tips(el).map(tip => tip.hasAttribute('disabled'))).to.deep.equal(
407
+ tips(control).map(tip => tip.hasAttribute('disabled'))
408
+ );
409
+ });
410
+
411
+ it('V11. the overflow button label and text never disagree, including first paint', async () => {
412
+ const host = await fixture<HTMLElement>(
413
+ html`<div style="width: 220px">
414
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS}>
415
+ </nile-virtual-select>
416
+ </div>`
417
+ );
418
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
419
+
420
+ // Before the layout frame runs, no button may claim a count it cannot back up.
421
+ await el.updateComplete;
422
+ const early = el.shadowRoot!.querySelector('button.select__tags-count');
423
+ if (early) {
424
+ const shown = early.textContent!.trim().replace('+', '');
425
+ expect(early.getAttribute('aria-label')).to.equal(
426
+ `${shown} more selected`
427
+ );
428
+ }
429
+
430
+ await settle(el);
431
+
432
+ const overflow = el.shadowRoot!.querySelector(
433
+ 'button.select__tags-count'
434
+ ) as HTMLElement | null;
435
+ expect(overflow).to.exist;
436
+ const shown = overflow!.textContent!.trim().replace('+', '');
437
+ expect(overflow!.getAttribute('aria-label')).to.equal(
438
+ `${shown} more selected`
439
+ );
440
+ expect(Number(shown)).to.equal(hiddenChips(el).length);
441
+ });
442
+
443
+ it('V12. no overflow button is rendered when every chip fits', async () => {
444
+ const host = await fixture<HTMLElement>(
445
+ html`<div style="width: 1200px">
446
+ <nile-virtual-select multiple .data=${DATA} .value=${LABELS.slice(0, 1)}>
447
+ </nile-virtual-select>
448
+ </div>`
449
+ );
450
+ const el = host.querySelector('nile-virtual-select') as NileVirtualSelect;
451
+ await settle(el);
452
+
453
+ expect(hiddenChips(el).length).to.equal(0);
454
+ expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.not
455
+ .exist;
456
+ });
457
+ });