@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
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "2.0.8",
6
+ "version": "2.0.9",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -195,7 +195,7 @@ export const styles = css`
195
195
  display: flex;
196
196
  flex: 1;
197
197
  align-items: center;
198
- flex-wrap: no-wrap;
198
+ flex-wrap: nowrap;
199
199
  margin-inline-start: var(--nile-spacing-7px, var(--ng-spacing-md));
200
200
  width: 100%;
201
201
  overflow: hidden;
@@ -216,6 +216,98 @@ export const styles = css`
216
216
  margin-right: var(--nile-spacing-5xl, var(--ng-spacing-5xl));
217
217
  }
218
218
 
219
+ button.select__tags-count {
220
+ appearance: none;
221
+ -webkit-appearance: none;
222
+ background: none;
223
+ text-align: inherit;
224
+ }
225
+
226
+ button.select__tags-count:disabled {
227
+ cursor: not-allowed;
228
+ }
229
+
230
+ .select__tags--fitted {
231
+ min-width: 0;
232
+ }
233
+
234
+ .select__tags nile-lite-tooltip.select__tag-tip {
235
+ display: contents;
236
+ }
237
+
238
+ .select__tags nile-tag {
239
+ flex: 0 1 auto;
240
+ cursor: pointer;
241
+ min-width: 0;
242
+ }
243
+
244
+ /* A chip is capped - and so truncated - only when a tooltip can reveal the
245
+ full value. Without one it keeps its natural width and the fit pass moves
246
+ the surplus into the +N button instead. */
247
+ :host([showtooltip]) .select__tags nile-tag {
248
+ max-width: var(--nile-width-180px, var(--ng-height-180px));
249
+ }
250
+
251
+ /* Only the fitted layout can move a chip into the +N button, so only there
252
+ can a chip refuse to shrink. */
253
+ :host(:not([showtooltip])) .select__tags--fitted nile-tag {
254
+ flex-shrink: 0;
255
+ }
256
+
257
+ .select__tags nile-tag::part(base) {
258
+ max-width: 100%;
259
+ }
260
+
261
+ .select__tags nile-tag::part(prefix_content_wrapper) {
262
+ flex: 1 1 auto;
263
+ min-width: 0;
264
+ }
265
+
266
+ .select__tags nile-tag::part(content) {
267
+ display: block;
268
+ flex: 1 1 auto;
269
+ min-width: 0;
270
+ /* The tag's line-height of 1 leaves the clip box shorter than the glyph
271
+ box, so descenders are cut. Pad the clip box, negate it in layout. */
272
+ padding-block: 3px;
273
+ margin-block: -3px;
274
+ overflow: hidden;
275
+ white-space: nowrap;
276
+ }
277
+
278
+ :host([showtooltip]) .select__tags nile-tag::part(content) {
279
+ text-overflow: ellipsis;
280
+ }
281
+
282
+ .select__tags nile-tag::part(remove-button) {
283
+ flex: 0 0 auto;
284
+ }
285
+
286
+ .select__tags--fitted nile-tag:focus-visible {
287
+ outline: 2px solid var(--nile-colors-primary-600, var(--ng-colors-border-brand));
288
+ outline-offset: 1px;
289
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
290
+ }
291
+
292
+ .select__tags--fitted .select__tags-count {
293
+ flex: 0 0 auto;
294
+ margin: 0;
295
+ padding: 0 var(--nile-spacing-xsmall, var(--ng-spacing-md));
296
+ height: var(--nile-height-26px, var(--ng-height-26px));
297
+ display: inline-flex;
298
+ align-items: center;
299
+ white-space: nowrap;
300
+ cursor: pointer;
301
+ border: 1px dashed
302
+ var(--nile-colors-neutral-500, var(--ng-colors-border-secondary));
303
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
304
+ }
305
+
306
+ .select__tags--fitted .select__tags-count:focus-visible {
307
+ outline: 2px solid var(--nile-colors-primary-600, var(--ng-colors-border-brand));
308
+ outline-offset: 1px;
309
+ }
310
+
219
311
  .select__tags::slotted(nile-tag) {
220
312
  cursor: pointer !important;
221
313
  }
@@ -443,6 +535,8 @@ export const styles = css`
443
535
  bottom: 0;
444
536
  z-index: 1;
445
537
  background: var(--nile-colors-white-base, var(--ng-colors-bg-primary));
538
+ container-type: inline-size;
539
+ container-name: nile-select-footer;
446
540
  }
447
541
 
448
542
  .select__custom-footer {
@@ -469,10 +563,40 @@ export const styles = css`
469
563
  flex-direction: row;
470
564
  align-items: center;
471
565
  padding: var(--nile-spacing-md, var(--ng-spacing-md)) var(--nile-spacing-lg, var(--ng-spacing-lg));
472
- gap: var(--nile-spacing-lg, var(--ng-spacing-lg));
566
+ gap: var(--nile-spacing-none, var(--ng-spacing-none));
473
567
  justify-content: space-between;
474
568
  }
475
569
 
570
+ .select__footer-action,
571
+ .select__footer .select__clear {
572
+ display: inline-flex;
573
+ align-items: center;
574
+ gap: var(--nile-spacing-sm, var(--ng-spacing-sm));
575
+ min-width: 0;
576
+ }
577
+
578
+ .select__footer-glyph {
579
+ flex: 0 0 auto;
580
+ display: inline-flex;
581
+ vertical-align: middle;
582
+ }
583
+
584
+ .select__footer-label {
585
+ white-space: nowrap;
586
+ }
587
+
588
+ @container nile-select-footer (max-width: 178px) {
589
+ .select__clear .select__footer-label {
590
+ display: none;
591
+ }
592
+ }
593
+
594
+ @container nile-select-footer (max-width: 137px) {
595
+ .select__footer-label {
596
+ display: none;
597
+ }
598
+ }
599
+
476
600
  :host([legacy]) .select__footer {
477
601
  background: var(--nile-colors-neutral-100);
478
602
  border-top: 1px solid var(--nile-colors-neutral-400);
@@ -516,10 +640,6 @@ export const styles = css`
516
640
  }
517
641
  }
518
642
 
519
- .select__invisible {
520
- opacity: 0;
521
- }
522
-
523
643
  .select__prefix--from-options {
524
644
  margin-inline-end: var(--nile-spacing-lg, var(--ng-spacing-md));
525
645
  }
@@ -1,5 +1,6 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { aTimeout, expect, fixture, html, nextFrame } from '@open-wc/testing';
2
2
  import './nile-select';
3
+ import '../nile-option/nile-option';
3
4
  import type { NileSelect } from './nile-select';
4
5
 
5
6
  describe('NileSelect', () => {
@@ -104,3 +105,479 @@ describe('NileSelect', () => {
104
105
  it('99. no nile-card', async () => { const el = await fixture<NileSelect>(html`<nile-select></nile-select>`); expect(el.shadowRoot!.querySelector('nile-card')).to.be.null; });
105
106
  it('100. full integration', async () => { const el = await fixture<NileSelect>(html`<nile-select class="sl" id="sl1"></nile-select>`); expect(el.id).to.equal('sl1'); expect(el.shadowRoot).to.not.be.null; expect(el.shadowRoot!.querySelector('slot:not([name])')).to.exist; });
106
107
  });
108
+
109
+ describe('NileSelect chip fitting behaviour matrix', () => {
110
+ const LABELS = [
111
+ 'Engineering Operations Team',
112
+ 'Human Resources Department',
113
+ 'Finance And Accounting Group',
114
+ 'Customer Success Organisation',
115
+ 'Product Design Studio Unit'
116
+ ];
117
+
118
+ const options = () =>
119
+ LABELS.map(label => html`<nile-option value=${label}>${label}</nile-option>`);
120
+
121
+ async function settle(el: NileSelect) {
122
+ await el.updateComplete;
123
+ await nextFrame();
124
+ await nextFrame();
125
+ await el.updateComplete;
126
+ }
127
+
128
+ function tagsRoot(el: NileSelect) {
129
+ return el.shadowRoot!.querySelector('.select__tags');
130
+ }
131
+
132
+ function chips(el: NileSelect) {
133
+ return Array.from(
134
+ el.shadowRoot!.querySelectorAll('.select__tags nile-tag')
135
+ ) as HTMLElement[];
136
+ }
137
+
138
+ function hiddenChips(el: NileSelect) {
139
+ return chips(el).filter(chip => chip.style.display === 'none');
140
+ }
141
+
142
+ it('M1. multiple + no max-options-visible + autoResize=false renders fitted chips', async () => {
143
+ const host = await fixture<HTMLElement>(
144
+ html`<div style="width: 320px">
145
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
146
+ </div>`
147
+ );
148
+ const el = host.querySelector('nile-select') as NileSelect;
149
+ await settle(el);
150
+
151
+ expect(el.autoResize).to.be.false;
152
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
153
+ expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.exist;
154
+ expect(chips(el).length).to.equal(LABELS.length);
155
+ });
156
+
157
+ it('M2. multiple + no max-options-visible + autoResize=true refits when the container resizes', async () => {
158
+ const host = await fixture<HTMLElement>(
159
+ html`<div style="width: 480px">
160
+ <nile-select multiple autoresize .value=${LABELS}>${options()}</nile-select>
161
+ </div>`
162
+ );
163
+ const el = host.querySelector('nile-select') as NileSelect;
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('M3. explicit max-options-visible switches to fixed-count layout', async () => {
178
+ const host = await fixture<HTMLElement>(
179
+ html`<div style="width: 320px">
180
+ <nile-select multiple max-options-visible="2" .value=${LABELS}>
181
+ ${options()}
182
+ </nile-select>
183
+ </div>`
184
+ );
185
+ const el = host.querySelector('nile-select') as NileSelect;
186
+ await settle(el);
187
+
188
+ expect(el.maxOptionsVisible).to.equal(2);
189
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
190
+ expect(chips(el).length).to.equal(2);
191
+ expect(el.shadowRoot!.querySelector('span.select__tags-count')).to.exist;
192
+ });
193
+
194
+ it('M3b. max-options-visible set as a property also switches to fixed-count layout', async () => {
195
+ const host = await fixture<HTMLElement>(
196
+ html`<div style="width: 320px">
197
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
198
+ </div>`
199
+ );
200
+ const el = host.querySelector('nile-select') as NileSelect;
201
+ await settle(el);
202
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
203
+
204
+ el.maxOptionsVisible = 1;
205
+ await settle(el);
206
+
207
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
208
+ expect(chips(el).length).to.equal(1);
209
+ });
210
+
211
+ it('M3c. the default max-options-visible is still 3 and reading it does not opt out', async () => {
212
+ const host = await fixture<HTMLElement>(
213
+ html`<div style="width: 320px">
214
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
215
+ </div>`
216
+ );
217
+ const el = host.querySelector('nile-select') as NileSelect;
218
+ await settle(el);
219
+
220
+ expect(el.maxOptionsVisible).to.equal(3);
221
+ expect(el.maxOptionsVisible).to.equal(3);
222
+ await settle(el);
223
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
224
+ });
225
+
226
+ it('M5. autoresize works as a markup attribute', async () => {
227
+ const b = await fixture<NileSelect>(
228
+ html`<nile-select multiple autoresize></nile-select>`
229
+ );
230
+ await b.updateComplete;
231
+ expect(b.autoResize).to.be.true;
232
+
233
+ const c = await fixture<NileSelect>(
234
+ html`<nile-select multiple></nile-select>`
235
+ );
236
+ await c.updateComplete;
237
+ expect(c.autoResize).to.be.false;
238
+ });
239
+
240
+ it('M8. virtual-scroll mode keeps fitted chips when max-options-visible was never set', async () => {
241
+ const host = await fixture<HTMLElement>(
242
+ html`<div style="width: 320px">
243
+ <nile-select
244
+ multiple
245
+ enable-virtual-scroll
246
+ .enableVirtualScroll=${true}
247
+ .data=${LABELS.map(label => ({ label, value: label }))}
248
+ .value=${LABELS}
249
+ ></nile-select>
250
+ </div>`
251
+ );
252
+ const el = host.querySelector('nile-select') as NileSelect;
253
+ await settle(el);
254
+
255
+ const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
256
+ expect(inner, 'inner nile-virtual-select should render').to.exist;
257
+ await inner.updateComplete;
258
+ await nextFrame();
259
+
260
+ const innerTags = inner.shadowRoot.querySelector('.select__tags');
261
+ expect(innerTags.classList.contains('select__tags--fitted')).to.be.true;
262
+ });
263
+
264
+ it('M9. virtual-scroll mode forwards an explicit max-options-visible', async () => {
265
+ const host = await fixture<HTMLElement>(
266
+ html`<div style="width: 320px">
267
+ <nile-select
268
+ multiple
269
+ max-options-visible="2"
270
+ .enableVirtualScroll=${true}
271
+ .data=${LABELS.map(label => ({ label, value: label }))}
272
+ .value=${LABELS}
273
+ ></nile-select>
274
+ </div>`
275
+ );
276
+ const el = host.querySelector('nile-select') as NileSelect;
277
+ await settle(el);
278
+
279
+ const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
280
+ await inner.updateComplete;
281
+ await nextFrame();
282
+
283
+ expect(inner.maxOptionsVisible).to.equal(2);
284
+ expect(
285
+ inner.shadowRoot
286
+ .querySelector('.select__tags')
287
+ .classList.contains('select__tags--fitted')
288
+ ).to.be.false;
289
+ });
290
+
291
+ it('M10. autoResize reflects to the autoresize attribute', async () => {
292
+ const el = await fixture<NileSelect>(
293
+ html`<nile-select multiple></nile-select>`
294
+ );
295
+ await el.updateComplete;
296
+ expect(el.hasAttribute('autoresize')).to.be.false;
297
+
298
+ el.autoResize = true;
299
+ await el.updateComplete;
300
+ expect(el.hasAttribute('autoresize')).to.be.true;
301
+
302
+ el.autoResize = false;
303
+ await el.updateComplete;
304
+ expect(el.hasAttribute('autoresize')).to.be.false;
305
+ });
306
+
307
+ it('M11. toggling max-options-visible does not leak resize observers or lose the observed container', async () => {
308
+ const host = await fixture<HTMLElement>(
309
+ html`<div style="width: 480px">
310
+ <nile-select multiple autoresize .value=${LABELS}>
311
+ ${options()}
312
+ </nile-select>
313
+ </div>`
314
+ );
315
+ const el = host.querySelector('nile-select') as NileSelect;
316
+ await settle(el);
317
+
318
+ el.maxOptionsVisible = 2;
319
+ await settle(el);
320
+ expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
321
+
322
+ const observed = (el as any).observedTagsContainer;
323
+ expect(observed, 'observer detached in fixed-count mode').to.be.undefined;
324
+ });
325
+
326
+ it('M6. the +N button count matches the number of hidden chips', async () => {
327
+ const host = await fixture<HTMLElement>(
328
+ html`<div style="width: 220px">
329
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
330
+ </div>`
331
+ );
332
+ const el = host.querySelector('nile-select') as NileSelect;
333
+ await settle(el);
334
+
335
+ const overflow = el.shadowRoot!.querySelector(
336
+ 'button.select__tags-count'
337
+ ) as HTMLElement | null;
338
+ const hidden = hiddenChips(el).length;
339
+
340
+ if (hidden > 0) {
341
+ expect(overflow).to.exist;
342
+ expect(overflow!.textContent!.trim()).to.equal(`+${hidden}`);
343
+ expect(overflow!.getAttribute('aria-label')).to.equal(
344
+ `${hidden} more selected`
345
+ );
346
+ } else {
347
+ // Nothing overflows, so the button is not rendered at all.
348
+ expect(overflow).to.not.exist;
349
+ }
350
+ });
351
+
352
+ // The tooltip is opt-in: `showtooltip` present, and the chip's text clipped.
353
+ function tips(el: NileSelect) {
354
+ return Array.from(
355
+ el.shadowRoot!.querySelectorAll('.select__tag-tip')
356
+ ) as HTMLElement[];
357
+ }
358
+
359
+ function liveTips(el: NileSelect) {
360
+ return tips(el).filter(tip => !tip.hasAttribute('disabled'));
361
+ }
362
+
363
+ it('M12. no showtooltip attribute hides the chip tooltip', async () => {
364
+ const host = await fixture<HTMLElement>(
365
+ html`<div style="width: 200px">
366
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
367
+ </div>`
368
+ );
369
+ const el = host.querySelector('nile-select') as NileSelect;
370
+ await settle(el);
371
+
372
+ expect(el.showTooltip).to.be.false;
373
+ expect(el.hasAttribute('showtooltip')).to.be.false;
374
+ expect(tips(el).length).to.be.greaterThan(0);
375
+ expect(liveTips(el).length).to.equal(0);
376
+ });
377
+
378
+ it('M13. showtooltip shows the chip tooltip', async () => {
379
+ const host = await fixture<HTMLElement>(
380
+ html`<div style="width: 200px">
381
+ <nile-select multiple showtooltip .value=${LABELS}>
382
+ ${options()}
383
+ </nile-select>
384
+ </div>`
385
+ );
386
+ const el = host.querySelector('nile-select') as NileSelect;
387
+ await settle(el);
388
+
389
+ expect(el.showTooltip).to.be.true;
390
+ expect(liveTips(el).length).to.be.greaterThan(0);
391
+ });
392
+
393
+ // Truncation is bound to the tooltip: a clipped chip is only acceptable when
394
+ // something can reveal the value it hid. A chip wider than the whole field is
395
+ // the one exception - there is nowhere for it to go.
396
+ function chipContent(chip: HTMLElement) {
397
+ return chip.shadowRoot!.querySelector('[part~="content"]') as HTMLElement;
398
+ }
399
+
400
+ function clippedChipCount(el: NileSelect) {
401
+ return chips(el)
402
+ .filter(chip => chip.style.display !== 'none')
403
+ .filter(chip => {
404
+ const content = chipContent(chip);
405
+ return content.scrollWidth > content.clientWidth + 1;
406
+ }).length;
407
+ }
408
+
409
+ it('M16. without showtooltip a fitted chip is never truncated', async () => {
410
+ const host = await fixture<HTMLElement>(
411
+ html`<div style="width: 480px">
412
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
413
+ </div>`
414
+ );
415
+ const el = host.querySelector('nile-select') as NileSelect;
416
+ await settle(el);
417
+
418
+ expect(el.showTooltip).to.be.false;
419
+ expect(clippedChipCount(el)).to.equal(0);
420
+ // The chips that could not fit went into the +N button, not into an ellipsis.
421
+ expect(hiddenChips(el).length).to.be.greaterThan(0);
422
+ });
423
+
424
+ it('M17. showtooltip is what allows a fitted chip to be truncated', async () => {
425
+ const host = await fixture<HTMLElement>(
426
+ html`<div style="width: 480px">
427
+ <nile-select multiple showtooltip .value=${LABELS}>
428
+ ${options()}
429
+ </nile-select>
430
+ </div>`
431
+ );
432
+ const el = host.querySelector('nile-select') as NileSelect;
433
+ await settle(el);
434
+
435
+ expect(el.showTooltip).to.be.true;
436
+ expect(clippedChipCount(el)).to.be.greaterThan(0);
437
+ });
438
+
439
+ it('M14. removing showtooltip hides the chip tooltip again', async () => {
440
+ const host = await fixture<HTMLElement>(
441
+ html`<div style="width: 200px">
442
+ <nile-select multiple showtooltip .value=${LABELS}>
443
+ ${options()}
444
+ </nile-select>
445
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
446
+ </div>`
447
+ );
448
+ const [el, control] = Array.from(
449
+ host.querySelectorAll('nile-select')
450
+ ) as NileSelect[];
451
+ await settle(el);
452
+ await settle(control);
453
+
454
+ expect(liveTips(el).length).to.be.greaterThan(0);
455
+
456
+ el.removeAttribute('showtooltip');
457
+ await settle(el);
458
+
459
+ expect(el.showTooltip).to.be.false;
460
+ expect(liveTips(el).length).to.equal(0);
461
+ // Removing the attribute must land back on the default.
462
+ expect(tips(el).map(tip => tip.hasAttribute('disabled'))).to.deep.equal(
463
+ tips(control).map(tip => tip.hasAttribute('disabled'))
464
+ );
465
+ });
466
+
467
+ it('M15. the overflow button label and text never disagree, including first paint', async () => {
468
+ const host = await fixture<HTMLElement>(
469
+ html`<div style="width: 220px">
470
+ <nile-select multiple .value=${LABELS}>${options()}</nile-select>
471
+ </div>`
472
+ );
473
+ const el = host.querySelector('nile-select') as NileSelect;
474
+
475
+ // Before the layout frame runs, no button may claim a count it cannot back up.
476
+ await el.updateComplete;
477
+ const early = el.shadowRoot!.querySelector('button.select__tags-count');
478
+ if (early) {
479
+ const shown = early.textContent!.trim().replace('+', '');
480
+ expect(early.getAttribute('aria-label')).to.equal(
481
+ `${shown} more selected`
482
+ );
483
+ }
484
+
485
+ await settle(el);
486
+
487
+ const overflow = el.shadowRoot!.querySelector(
488
+ 'button.select__tags-count'
489
+ ) as HTMLElement | null;
490
+ expect(overflow).to.exist;
491
+ const shown = overflow!.textContent!.trim().replace('+', '');
492
+ expect(overflow!.getAttribute('aria-label')).to.equal(
493
+ `${shown} more selected`
494
+ );
495
+ expect(Number(shown)).to.equal(hiddenChips(el).length);
496
+ });
497
+
498
+ it('M16. no overflow button is rendered when every chip fits', async () => {
499
+ const host = await fixture<HTMLElement>(
500
+ html`<div style="width: 1200px">
501
+ <nile-select multiple .value=${LABELS.slice(0, 1)}>
502
+ ${options()}
503
+ </nile-select>
504
+ </div>`
505
+ );
506
+ const el = host.querySelector('nile-select') as NileSelect;
507
+ await settle(el);
508
+
509
+ expect(hiddenChips(el).length).to.equal(0);
510
+ expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.not
511
+ .exist;
512
+ });
513
+
514
+ // `<nile-select enableVirtualScroll>` renders a nile-virtual-select in its own
515
+ // shadow root and hands it the public API by hand. Anything added to one and
516
+ // not to that list is silently dead on the wrapper form, which is how
517
+ // showTooltip / chipMinWidth / chipMaxWidth were missed - so assert the whole
518
+ // chip-fitting surface crosses over, defaults included.
519
+ function innerVirtual(el: NileSelect) {
520
+ return el.shadowRoot!.querySelector('nile-virtual-select') as unknown as {
521
+ showTooltip: boolean;
522
+ chipMinWidth: number;
523
+ chipMaxWidth: number;
524
+ autoResize: boolean;
525
+ } | null;
526
+ }
527
+
528
+ it('M17. enableVirtualScroll forwards the chip-fitting properties inward', async () => {
529
+ const el = await fixture<NileSelect>(
530
+ html`<nile-select
531
+ multiple
532
+ enableVirtualScroll
533
+ showtooltip
534
+ autoresize
535
+ chipminwidth="90"
536
+ chipmaxwidth="160"
537
+ ></nile-select>`
538
+ );
539
+ await el.updateComplete;
540
+
541
+ const inner = innerVirtual(el);
542
+ expect(inner).to.exist;
543
+ expect(el.showTooltip).to.be.true;
544
+ expect(el.chipMinWidth).to.equal(90);
545
+ expect(el.chipMaxWidth).to.equal(160);
546
+ expect(el.autoResize).to.be.true;
547
+
548
+ expect(inner!.showTooltip).to.equal(el.showTooltip);
549
+ expect(inner!.chipMinWidth).to.equal(el.chipMinWidth);
550
+ expect(inner!.chipMaxWidth).to.equal(el.chipMaxWidth);
551
+ expect(inner!.autoResize).to.equal(el.autoResize);
552
+ });
553
+
554
+ it('M18. enableVirtualScroll forwards the chip-fitting defaults too', async () => {
555
+ const el = await fixture<NileSelect>(
556
+ html`<nile-select multiple enableVirtualScroll></nile-select>`
557
+ );
558
+ await el.updateComplete;
559
+
560
+ const inner = innerVirtual(el);
561
+ expect(inner).to.exist;
562
+ // chipMinWidth defaults to 110 on both sides, so a missing forward looks
563
+ // correct here - the other two are what actually catch a dropped property.
564
+ expect(inner!.showTooltip).to.be.false;
565
+ expect(inner!.chipMinWidth).to.equal(110);
566
+ expect(inner!.chipMaxWidth).to.equal(180);
567
+ });
568
+
569
+ it('M19. changing a chip property after first render reaches the inner select', async () => {
570
+ const el = await fixture<NileSelect>(
571
+ html`<nile-select multiple enableVirtualScroll></nile-select>`
572
+ );
573
+ await el.updateComplete;
574
+
575
+ el.showTooltip = true;
576
+ el.chipMaxWidth = 240;
577
+ await el.updateComplete;
578
+
579
+ const inner = innerVirtual(el);
580
+ expect(inner!.showTooltip).to.be.true;
581
+ expect(inner!.chipMaxWidth).to.equal(240);
582
+ });
583
+ });