@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.
- package/README.md +13 -0
- package/dist/index.js +132 -134
- package/dist/nile-auto-complete/nile-auto-complete.css.cjs.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.css.cjs.js.map +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.css.esm.js +5 -2
- package/dist/nile-chip/nile-chip.css.cjs.js +1 -1
- package/dist/nile-chip/nile-chip.css.cjs.js.map +1 -1
- package/dist/nile-chip/nile-chip.css.esm.js +2 -0
- package/dist/nile-combobox/nile-combobox.css.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.css.cjs.js.map +1 -1
- package/dist/nile-combobox/nile-combobox.css.esm.js +9 -9
- package/dist/nile-command-menu/nile-command-menu.css.cjs.js +1 -1
- package/dist/nile-command-menu/nile-command-menu.css.cjs.js.map +1 -1
- package/dist/nile-command-menu/nile-command-menu.css.esm.js +5 -5
- package/dist/nile-command-menu-item/nile-command-menu-item.css.cjs.js +1 -1
- package/dist/nile-command-menu-item/nile-command-menu-item.css.cjs.js.map +1 -1
- package/dist/nile-command-menu-item/nile-command-menu-item.css.esm.js +2 -2
- package/dist/nile-option/nile-option.css.cjs.js +1 -1
- package/dist/nile-option/nile-option.css.cjs.js.map +1 -1
- package/dist/nile-option/nile-option.css.esm.js +1 -1
- package/dist/nile-select/nile-select.cjs.js +2 -2
- package/dist/nile-select/nile-select.cjs.js.map +1 -1
- package/dist/nile-select/nile-select.css.cjs.js +1 -1
- package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
- package/dist/nile-select/nile-select.css.esm.js +19 -24
- package/dist/nile-select/nile-select.esm.js +43 -42
- package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
- package/dist/nile-select/virtual-scroll-helper.esm.js +2 -0
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +20 -25
- package/dist/nile-virtual-select/nile-virtual-select.esm.js +26 -26
- package/dist/src/nile-auto-complete/nile-auto-complete.css.js +3 -0
- package/dist/src/nile-auto-complete/nile-auto-complete.css.js.map +1 -1
- package/dist/src/nile-chip/nile-chip.css.js +2 -0
- package/dist/src/nile-chip/nile-chip.css.js.map +1 -1
- package/dist/src/nile-combobox/nile-combobox.css.js +9 -9
- package/dist/src/nile-combobox/nile-combobox.css.js.map +1 -1
- package/dist/src/nile-command-menu/nile-command-menu.css.js +5 -5
- package/dist/src/nile-command-menu/nile-command-menu.css.js.map +1 -1
- package/dist/src/nile-command-menu-item/nile-command-menu-item.css.js +2 -2
- package/dist/src/nile-command-menu-item/nile-command-menu-item.css.js.map +1 -1
- package/dist/src/nile-option/nile-option.css.js +1 -1
- package/dist/src/nile-option/nile-option.css.js.map +1 -1
- package/dist/src/nile-select/nile-select.css.js +19 -24
- package/dist/src/nile-select/nile-select.css.js.map +1 -1
- package/dist/src/nile-select/nile-select.d.ts +18 -2
- package/dist/src/nile-select/nile-select.js +226 -64
- package/dist/src/nile-select/nile-select.js.map +1 -1
- package/dist/src/nile-select/nile-select.test.d.ts +1 -0
- package/dist/src/nile-select/nile-select.test.js +896 -17
- package/dist/src/nile-select/nile-select.test.js.map +1 -1
- package/dist/src/nile-select/virtual-scroll-helper.js +2 -0
- package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.css.js +20 -25
- package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +10 -2
- package/dist/src/nile-virtual-select/nile-virtual-select.js +142 -30
- package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.test.d.ts +1 -0
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js +331 -16
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/nile-auto-complete/nile-auto-complete.css.ts +3 -0
- package/src/nile-chip/nile-chip.css.ts +2 -0
- package/src/nile-combobox/nile-combobox.css.ts +9 -9
- package/src/nile-command-menu/nile-command-menu.css.ts +5 -5
- package/src/nile-command-menu-item/nile-command-menu-item.css.ts +2 -2
- package/src/nile-option/nile-option.css.ts +1 -1
- package/src/nile-select/nile-select.css.ts +19 -24
- package/src/nile-select/nile-select.test.ts +1112 -17
- package/src/nile-select/nile-select.ts +237 -67
- package/src/nile-select/virtual-scroll-helper.ts +2 -0
- package/src/nile-virtual-select/nile-virtual-select.css.ts +20 -25
- package/src/nile-virtual-select/nile-virtual-select.test.ts +404 -16
- package/src/nile-virtual-select/nile-virtual-select.ts +200 -79
- 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
|
import type { NileSelect } from './nile-select';
|
|
5
6
|
|
|
6
7
|
describe('NileSelect', () => {
|
|
@@ -142,7 +143,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
142
143
|
it('M1. multiple + no max-options-visible + autoResize=false renders fitted chips', async () => {
|
|
143
144
|
const host = await fixture<HTMLElement>(
|
|
144
145
|
html`<div style="width: 320px">
|
|
145
|
-
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
146
|
+
<nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
|
|
146
147
|
</div>`
|
|
147
148
|
);
|
|
148
149
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
@@ -157,7 +158,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
157
158
|
it('M2. multiple + no max-options-visible + autoResize=true refits when the container resizes', async () => {
|
|
158
159
|
const host = await fixture<HTMLElement>(
|
|
159
160
|
html`<div style="width: 480px">
|
|
160
|
-
<nile-select multiple autoresize .value=${LABELS}>${options()}</nile-select>
|
|
161
|
+
<nile-select showtooltip multiple autoresize .value=${LABELS}>${options()}</nile-select>
|
|
161
162
|
</div>`
|
|
162
163
|
);
|
|
163
164
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
@@ -177,7 +178,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
177
178
|
it('M3. explicit max-options-visible switches to fixed-count layout', async () => {
|
|
178
179
|
const host = await fixture<HTMLElement>(
|
|
179
180
|
html`<div style="width: 320px">
|
|
180
|
-
<nile-select multiple max-options-visible="2" .value=${LABELS}>
|
|
181
|
+
<nile-select showtooltip multiple max-options-visible="2" .value=${LABELS}>
|
|
181
182
|
${options()}
|
|
182
183
|
</nile-select>
|
|
183
184
|
</div>`
|
|
@@ -194,7 +195,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
194
195
|
it('M3b. max-options-visible set as a property also switches to fixed-count layout', async () => {
|
|
195
196
|
const host = await fixture<HTMLElement>(
|
|
196
197
|
html`<div style="width: 320px">
|
|
197
|
-
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
198
|
+
<nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
|
|
198
199
|
</div>`
|
|
199
200
|
);
|
|
200
201
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
@@ -211,7 +212,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
211
212
|
it('M3c. the default max-options-visible is still 3 and reading it does not opt out', async () => {
|
|
212
213
|
const host = await fixture<HTMLElement>(
|
|
213
214
|
html`<div style="width: 320px">
|
|
214
|
-
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
215
|
+
<nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
|
|
215
216
|
</div>`
|
|
216
217
|
);
|
|
217
218
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
@@ -240,7 +241,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
240
241
|
it('M8. virtual-scroll mode keeps fitted chips when max-options-visible was never set', async () => {
|
|
241
242
|
const host = await fixture<HTMLElement>(
|
|
242
243
|
html`<div style="width: 320px">
|
|
243
|
-
<nile-select
|
|
244
|
+
<nile-select showtooltip
|
|
244
245
|
multiple
|
|
245
246
|
enable-virtual-scroll
|
|
246
247
|
.enableVirtualScroll=${true}
|
|
@@ -258,13 +259,14 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
258
259
|
await nextFrame();
|
|
259
260
|
|
|
260
261
|
const innerTags = inner.shadowRoot.querySelector('.select__tags');
|
|
262
|
+
expect(innerTags, 'inner .select__tags should render').to.exist;
|
|
261
263
|
expect(innerTags.classList.contains('select__tags--fitted')).to.be.true;
|
|
262
264
|
});
|
|
263
265
|
|
|
264
266
|
it('M9. virtual-scroll mode forwards an explicit max-options-visible', async () => {
|
|
265
267
|
const host = await fixture<HTMLElement>(
|
|
266
268
|
html`<div style="width: 320px">
|
|
267
|
-
<nile-select
|
|
269
|
+
<nile-select showtooltip
|
|
268
270
|
multiple
|
|
269
271
|
max-options-visible="2"
|
|
270
272
|
.enableVirtualScroll=${true}
|
|
@@ -307,7 +309,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
307
309
|
it('M11. toggling max-options-visible does not leak resize observers or lose the observed container', async () => {
|
|
308
310
|
const host = await fixture<HTMLElement>(
|
|
309
311
|
html`<div style="width: 480px">
|
|
310
|
-
<nile-select multiple autoresize .value=${LABELS}>
|
|
312
|
+
<nile-select showtooltip multiple autoresize .value=${LABELS}>
|
|
311
313
|
${options()}
|
|
312
314
|
</nile-select>
|
|
313
315
|
</div>`
|
|
@@ -360,7 +362,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
360
362
|
return tips(el).filter(tip => !tip.hasAttribute('disabled'));
|
|
361
363
|
}
|
|
362
364
|
|
|
363
|
-
it('M12. no showtooltip attribute
|
|
365
|
+
it('M12. no showtooltip attribute renders no chip tooltips', async () => {
|
|
364
366
|
const host = await fixture<HTMLElement>(
|
|
365
367
|
html`<div style="width: 200px">
|
|
366
368
|
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
@@ -371,8 +373,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
371
373
|
|
|
372
374
|
expect(el.showTooltip).to.be.false;
|
|
373
375
|
expect(el.hasAttribute('showtooltip')).to.be.false;
|
|
374
|
-
expect(tips(el).length).to.
|
|
375
|
-
expect(liveTips(el).length).to.equal(0);
|
|
376
|
+
expect(tips(el).length).to.equal(0);
|
|
376
377
|
});
|
|
377
378
|
|
|
378
379
|
it('M13. showtooltip shows the chip tooltip', async () => {
|
|
@@ -406,7 +407,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
406
407
|
}).length;
|
|
407
408
|
}
|
|
408
409
|
|
|
409
|
-
it('M16. without showtooltip
|
|
410
|
+
it('M16. without showtooltip the pre-2.0.9 chip row is used', async () => {
|
|
410
411
|
const host = await fixture<HTMLElement>(
|
|
411
412
|
html`<div style="width: 480px">
|
|
412
413
|
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
@@ -414,11 +415,16 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
414
415
|
);
|
|
415
416
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
416
417
|
await settle(el);
|
|
418
|
+
await aTimeout(20);
|
|
419
|
+
await settle(el);
|
|
417
420
|
|
|
418
421
|
expect(el.showTooltip).to.be.false;
|
|
419
|
-
expect(
|
|
420
|
-
|
|
421
|
-
expect(
|
|
422
|
+
expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
|
|
423
|
+
expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.not.exist;
|
|
424
|
+
expect(chips(el).every(chip => !chip.style.maxWidth)).to.be.true;
|
|
425
|
+
const overflowCount = el.shadowRoot!.querySelector('span.select__tags-count');
|
|
426
|
+
expect(overflowCount).to.exist;
|
|
427
|
+
expect(overflowCount!.textContent).to.contain('More');
|
|
422
428
|
});
|
|
423
429
|
|
|
424
430
|
it('M17. showtooltip is what allows a fitted chip to be truncated', async () => {
|
|
@@ -467,7 +473,7 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
467
473
|
it('M15. the overflow button label and text never disagree, including first paint', async () => {
|
|
468
474
|
const host = await fixture<HTMLElement>(
|
|
469
475
|
html`<div style="width: 220px">
|
|
470
|
-
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
476
|
+
<nile-select showtooltip multiple .value=${LABELS}>${options()}</nile-select>
|
|
471
477
|
</div>`
|
|
472
478
|
);
|
|
473
479
|
const el = host.querySelector('nile-select') as NileSelect;
|
|
@@ -580,4 +586,1093 @@ describe('NileSelect chip fitting behaviour matrix', () => {
|
|
|
580
586
|
expect(inner!.showTooltip).to.be.true;
|
|
581
587
|
expect(inner!.chipMaxWidth).to.equal(240);
|
|
582
588
|
});
|
|
589
|
+
|
|
590
|
+
it('G1. default chips are not capped or squeezed by the 2.0.9 chip styles', async () => {
|
|
591
|
+
const host = await fixture<HTMLElement>(
|
|
592
|
+
html`<div style="width: 480px">
|
|
593
|
+
<nile-select multiple .value=${LABELS.slice(0, 2)}>${options()}</nile-select>
|
|
594
|
+
</div>`
|
|
595
|
+
);
|
|
596
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
597
|
+
await settle(el);
|
|
598
|
+
|
|
599
|
+
const [chip] = chips(el);
|
|
600
|
+
expect(chip, 'select rendered no chips').to.exist;
|
|
601
|
+
expect(getComputedStyle(chip).maxWidth).to.equal('none');
|
|
602
|
+
expect(getComputedStyle(chip).minWidth).to.not.equal('0px');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('G2. showtooltip caps chips at the chip max width', async () => {
|
|
606
|
+
const host = await fixture<HTMLElement>(
|
|
607
|
+
html`<div style="width: 480px">
|
|
608
|
+
<nile-select multiple showtooltip .value=${LABELS.slice(0, 2)}>${options()}</nile-select>
|
|
609
|
+
</div>`
|
|
610
|
+
);
|
|
611
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
612
|
+
await settle(el);
|
|
613
|
+
|
|
614
|
+
const [firstChip] = chips(el);
|
|
615
|
+
expect(firstChip, 'select rendered no chips').to.exist;
|
|
616
|
+
expect(getComputedStyle(firstChip).maxWidth).to.not.equal('none');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('G3. autoresize without showtooltip refits with the 2.0.8 width fit', async () => {
|
|
620
|
+
const host = await fixture<HTMLElement>(
|
|
621
|
+
html`<div style="width: 1200px">
|
|
622
|
+
<nile-select multiple autoresize .value=${LABELS}>${options()}</nile-select>
|
|
623
|
+
</div>`
|
|
624
|
+
);
|
|
625
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
626
|
+
await settle(el);
|
|
627
|
+
await aTimeout(20);
|
|
628
|
+
const wide = el.maxOptionsVisible;
|
|
629
|
+
|
|
630
|
+
host.style.width = '300px';
|
|
631
|
+
await aTimeout(200);
|
|
632
|
+
await settle(el);
|
|
633
|
+
|
|
634
|
+
expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
|
|
635
|
+
expect(wide).to.equal(0);
|
|
636
|
+
expect(el.maxOptionsVisible).to.be.greaterThan(0);
|
|
637
|
+
expect(el.shadowRoot!.querySelector('span.select__tags-count')).to.exist;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('G4. toggling showTooltip switches between the fitted and the 2.0.8 chip row', async () => {
|
|
641
|
+
const host = await fixture<HTMLElement>(
|
|
642
|
+
html`<div style="width: 320px">
|
|
643
|
+
<nile-select multiple .value=${LABELS}>${options()}</nile-select>
|
|
644
|
+
</div>`
|
|
645
|
+
);
|
|
646
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
647
|
+
await settle(el);
|
|
648
|
+
expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
|
|
649
|
+
|
|
650
|
+
el.showTooltip = true;
|
|
651
|
+
await settle(el);
|
|
652
|
+
expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.true;
|
|
653
|
+
expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.exist;
|
|
654
|
+
|
|
655
|
+
el.showTooltip = false;
|
|
656
|
+
await settle(el);
|
|
657
|
+
await aTimeout(20);
|
|
658
|
+
await settle(el);
|
|
659
|
+
expect(tagsRoot(el)!.classList.contains('select__tags--fitted')).to.be.false;
|
|
660
|
+
expect(el.shadowRoot!.querySelector('button.select__tags-count')).to.not.exist;
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe('NileSelect enableVirtualScroll value sync', () => {
|
|
665
|
+
const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
|
|
666
|
+
const DATA = ROLES.map(role => ({ label: role, value: role }));
|
|
667
|
+
|
|
668
|
+
type InnerVirtual = HTMLElement & {
|
|
669
|
+
value: string | string[];
|
|
670
|
+
updateComplete: Promise<boolean>;
|
|
671
|
+
handleOptionSelection(option: { value: string }): void;
|
|
672
|
+
handleTagRemove(event: Event, option: { value: string }): void;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
async function virtualSelect(tpl = html`<nile-select
|
|
676
|
+
multiple
|
|
677
|
+
.enableVirtualScroll=${true}
|
|
678
|
+
.data=${DATA}
|
|
679
|
+
.value=${[]}
|
|
680
|
+
></nile-select>`) {
|
|
681
|
+
const el = await fixture<NileSelect>(tpl);
|
|
682
|
+
await el.updateComplete;
|
|
683
|
+
const inner = el.shadowRoot!.querySelector('nile-virtual-select') as unknown as InnerVirtual;
|
|
684
|
+
await inner.updateComplete;
|
|
685
|
+
return { el, inner };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function settle(el: NileSelect, inner: InnerVirtual) {
|
|
689
|
+
await inner.updateComplete;
|
|
690
|
+
await el.updateComplete;
|
|
691
|
+
await inner.updateComplete;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
it('V1. picking an option updates the wrapper value', async () => {
|
|
695
|
+
const { el, inner } = await virtualSelect();
|
|
696
|
+
|
|
697
|
+
inner.handleOptionSelection({ value: 'Administrators' });
|
|
698
|
+
await settle(el, inner);
|
|
699
|
+
|
|
700
|
+
expect(el.value).to.deep.equal(['Administrators']);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('V2. the first pick survives a wrapper re-render', async () => {
|
|
704
|
+
const { el, inner } = await virtualSelect();
|
|
705
|
+
|
|
706
|
+
inner.handleOptionSelection({ value: 'Administrators' });
|
|
707
|
+
await settle(el, inner);
|
|
708
|
+
|
|
709
|
+
el.requestUpdate();
|
|
710
|
+
await settle(el, inner);
|
|
711
|
+
|
|
712
|
+
expect(inner.value).to.deep.equal(['Administrators']);
|
|
713
|
+
expect(el.value).to.deep.equal(['Administrators']);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('V3. a removed chip stays removed after a wrapper re-render', async () => {
|
|
717
|
+
const { el, inner } = await virtualSelect(html`<nile-select
|
|
718
|
+
multiple
|
|
719
|
+
.enableVirtualScroll=${true}
|
|
720
|
+
.data=${DATA}
|
|
721
|
+
.value=${['Administrators', 'Users', 'Guests']}
|
|
722
|
+
></nile-select>`);
|
|
723
|
+
|
|
724
|
+
inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Administrators' });
|
|
725
|
+
await settle(el, inner);
|
|
726
|
+
inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Users' });
|
|
727
|
+
await settle(el, inner);
|
|
728
|
+
|
|
729
|
+
el.requestUpdate();
|
|
730
|
+
await settle(el, inner);
|
|
731
|
+
|
|
732
|
+
expect(el.value).to.deep.equal(['Guests']);
|
|
733
|
+
expect(inner.value).to.deep.equal(['Guests']);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('V4. nile-change listeners on the wrapper already see the new value', async () => {
|
|
737
|
+
const { el, inner } = await virtualSelect();
|
|
738
|
+
const seen: unknown[] = [];
|
|
739
|
+
el.addEventListener('nile-change', () => seen.push(el.value));
|
|
740
|
+
|
|
741
|
+
inner.handleOptionSelection({ value: 'Guests' });
|
|
742
|
+
await settle(el, inner);
|
|
743
|
+
|
|
744
|
+
expect(seen).to.deep.equal([['Guests']]);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('V5. single-select virtual mode keeps the wrapper value in sync', async () => {
|
|
748
|
+
const { el, inner } = await virtualSelect(html`<nile-select
|
|
749
|
+
.enableVirtualScroll=${true}
|
|
750
|
+
.data=${DATA}
|
|
751
|
+
></nile-select>`);
|
|
752
|
+
|
|
753
|
+
inner.handleOptionSelection({ value: 'Users' });
|
|
754
|
+
await settle(el, inner);
|
|
755
|
+
el.requestUpdate();
|
|
756
|
+
await settle(el, inner);
|
|
757
|
+
|
|
758
|
+
expect(el.value).to.equal('Users');
|
|
759
|
+
expect(inner.value).to.equal('Users');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('V6. resizing the host does not re-render the wrapper without autoResize', async () => {
|
|
763
|
+
const host = await fixture<HTMLElement>(html`<div style="width: 400px">
|
|
764
|
+
<nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>
|
|
765
|
+
</div>`);
|
|
766
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
767
|
+
await el.updateComplete;
|
|
768
|
+
await nextFrame();
|
|
769
|
+
await nextFrame();
|
|
770
|
+
|
|
771
|
+
let updates = 0;
|
|
772
|
+
const update = (el as any).update;
|
|
773
|
+
(el as any).update = function (this: NileSelect, changed: Map<string, unknown>) {
|
|
774
|
+
updates++;
|
|
775
|
+
return update.call(this, changed);
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
host.style.width = '250px';
|
|
779
|
+
await nextFrame();
|
|
780
|
+
await nextFrame();
|
|
781
|
+
await aTimeout(50);
|
|
782
|
+
|
|
783
|
+
expect(updates).to.equal(0);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
describe('NileSelect enableVirtualScroll + searchEnabled value sync', () => {
|
|
788
|
+
const ROLES = ['Administrators', 'Users', 'Guests', 'Print Operators'];
|
|
789
|
+
const DATA = ROLES.map(role => ({ label: role, value: role }));
|
|
790
|
+
|
|
791
|
+
type InnerVirtual = HTMLElement & {
|
|
792
|
+
value: string | string[];
|
|
793
|
+
updateComplete: Promise<boolean>;
|
|
794
|
+
handleOptionSelection(option: { value: string }): void;
|
|
795
|
+
handleTagRemove(event: Event, option: { value: string }): void;
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
async function settle(el: NileSelect, inner: InnerVirtual) {
|
|
799
|
+
await inner.updateComplete;
|
|
800
|
+
await el.updateComplete;
|
|
801
|
+
await inner.updateComplete;
|
|
802
|
+
await nextFrame();
|
|
803
|
+
await el.updateComplete;
|
|
804
|
+
await inner.updateComplete;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function searchableVirtual({ multiple = true, portal = false, value = ['Users'] as string | string[] } = {}) {
|
|
808
|
+
const el = await fixture<NileSelect>(html`<nile-select
|
|
809
|
+
?multiple=${multiple}
|
|
810
|
+
.searchEnabled=${true}
|
|
811
|
+
.enableVirtualScroll=${true}
|
|
812
|
+
.portal=${portal}
|
|
813
|
+
.data=${DATA}
|
|
814
|
+
.value=${value}
|
|
815
|
+
></nile-select>`);
|
|
816
|
+
await el.updateComplete;
|
|
817
|
+
const inner = el.shadowRoot!.querySelector('nile-virtual-select') as unknown as InnerVirtual;
|
|
818
|
+
await inner.updateComplete;
|
|
819
|
+
(inner as any).open = true;
|
|
820
|
+
await settle(el, inner);
|
|
821
|
+
if (portal) {
|
|
822
|
+
await waitUntil(() => document.body.querySelector('.nile-virtual-select-portal-append #listbox'), 'listbox never portalled');
|
|
823
|
+
}
|
|
824
|
+
await waitUntil(() => searchInput(inner)?.shadowRoot?.querySelector('input'), 'search box never rendered');
|
|
825
|
+
return { el, inner };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function searchInput(inner: InnerVirtual): HTMLElement {
|
|
829
|
+
const inShadow = inner.shadowRoot!.querySelector('.select__search nile-input');
|
|
830
|
+
const inPortal = document.body.querySelector('.nile-virtual-select-portal-append .select__search nile-input');
|
|
831
|
+
return (inShadow ?? inPortal) as HTMLElement;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function typeSearch(el: NileSelect, inner: InnerVirtual, text: string) {
|
|
835
|
+
const input = searchInput(inner).shadowRoot!.querySelector('input') as HTMLInputElement;
|
|
836
|
+
input.value = text;
|
|
837
|
+
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
838
|
+
await settle(el, inner);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
afterEach(() => {
|
|
842
|
+
document.body.querySelectorAll('.nile-virtual-select-portal-append').forEach(n => n.remove());
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('R1a. typing in the search box keeps the selected values (multiple)', async () => {
|
|
846
|
+
const { el, inner } = await searchableVirtual();
|
|
847
|
+
expect(searchInput(inner), 'search box rendered').to.exist;
|
|
848
|
+
|
|
849
|
+
await typeSearch(el, inner, 'Adm');
|
|
850
|
+
|
|
851
|
+
expect(el.value).to.deep.equal(['Users']);
|
|
852
|
+
expect(inner.value).to.deep.equal(['Users']);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('R1b. typing in the search box keeps the selected value (single)', async () => {
|
|
856
|
+
const { el, inner } = await searchableVirtual({ multiple: false, value: 'Users' });
|
|
857
|
+
|
|
858
|
+
await typeSearch(el, inner, 'zz');
|
|
859
|
+
|
|
860
|
+
expect(el.value).to.equal('Users');
|
|
861
|
+
expect(inner.value).to.equal('Users');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('R1c. clearing the search text does not clear the selection', async () => {
|
|
865
|
+
const { el, inner } = await searchableVirtual({ value: ['Users', 'Guests'] });
|
|
866
|
+
|
|
867
|
+
await typeSearch(el, inner, 'Gu');
|
|
868
|
+
await typeSearch(el, inner, '');
|
|
869
|
+
expect(el.value).to.deep.equal(['Users', 'Guests']);
|
|
870
|
+
|
|
871
|
+
await typeSearch(el, inner, 'Gu');
|
|
872
|
+
const clear = searchInput(inner).shadowRoot!.querySelector('.input__clear') as HTMLElement;
|
|
873
|
+
expect(clear, 'search clear button rendered').to.exist;
|
|
874
|
+
clear.click();
|
|
875
|
+
await settle(el, inner);
|
|
876
|
+
|
|
877
|
+
expect(el.value).to.deep.equal(['Users', 'Guests']);
|
|
878
|
+
expect(inner.value).to.deep.equal(['Users', 'Guests']);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('R1d. a real selection while searching still updates the wrapper value', async () => {
|
|
882
|
+
const { el, inner } = await searchableVirtual();
|
|
883
|
+
|
|
884
|
+
await typeSearch(el, inner, 'Gu');
|
|
885
|
+
inner.handleOptionSelection({ value: 'Guests' });
|
|
886
|
+
await settle(el, inner);
|
|
887
|
+
|
|
888
|
+
expect(el.value).to.deep.equal(['Users', 'Guests']);
|
|
889
|
+
el.requestUpdate();
|
|
890
|
+
await settle(el, inner);
|
|
891
|
+
expect(inner.value).to.deep.equal(['Users', 'Guests']);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('R1e. removing a chip still updates the wrapper value', async () => {
|
|
895
|
+
const { el, inner } = await searchableVirtual({ value: ['Users', 'Guests'] });
|
|
896
|
+
|
|
897
|
+
await typeSearch(el, inner, 'U');
|
|
898
|
+
inner.handleTagRemove(new CustomEvent('nile-remove'), { value: 'Users' });
|
|
899
|
+
await settle(el, inner);
|
|
900
|
+
|
|
901
|
+
expect(el.value).to.deep.equal(['Guests']);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('R1f. nile-input and nile-change from the inner select sync; nested ones do not', async () => {
|
|
905
|
+
const { el, inner } = await searchableVirtual();
|
|
906
|
+
const nested = inner.shadowRoot!.querySelector('.select__search') as HTMLElement;
|
|
907
|
+
const fire = (target: EventTarget, name: string, value: unknown) =>
|
|
908
|
+
target.dispatchEvent(new CustomEvent(name, { detail: { value }, bubbles: true, composed: true }));
|
|
909
|
+
|
|
910
|
+
fire(nested, 'nile-input', 'nested-input');
|
|
911
|
+
fire(nested, 'nile-change', 'nested-change');
|
|
912
|
+
expect(el.value).to.deep.equal(['Users']);
|
|
913
|
+
|
|
914
|
+
fire(inner, 'nile-input', ['Guests']);
|
|
915
|
+
expect(el.value).to.deep.equal(['Guests']);
|
|
916
|
+
fire(inner, 'nile-change', ['Administrators']);
|
|
917
|
+
expect(el.value).to.deep.equal(['Administrators']);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('R1g. portal mode: list is portalled, search keeps the value, selection syncs', async () => {
|
|
921
|
+
const { el, inner } = await searchableVirtual({ portal: true });
|
|
922
|
+
|
|
923
|
+
const listbox = document.body.querySelector('.nile-virtual-select-portal-append #listbox');
|
|
924
|
+
expect(listbox, 'listbox moved under document.body').to.exist;
|
|
925
|
+
expect(inner.shadowRoot!.querySelector('#listbox')).to.equal(null);
|
|
926
|
+
|
|
927
|
+
await typeSearch(el, inner, 'Adm');
|
|
928
|
+
expect(el.value).to.deep.equal(['Users']);
|
|
929
|
+
|
|
930
|
+
inner.handleOptionSelection({ value: 'Administrators' });
|
|
931
|
+
await settle(el, inner);
|
|
932
|
+
expect(el.value).to.deep.equal(['Users', 'Administrators']);
|
|
933
|
+
|
|
934
|
+
(inner as any).open = false;
|
|
935
|
+
await settle(el, inner);
|
|
936
|
+
await waitUntil(() => !document.body.querySelector('.nile-virtual-select-portal-append'), 'portal not cleaned up');
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
describe('NileSelect chipMinWidth / chipMaxWidth attributes', () => {
|
|
941
|
+
const LABELS = ['Engineering Operations Team', 'Human Resources Department'];
|
|
942
|
+
const options = () => LABELS.map(label => html`<nile-option value=${label}>${label}</nile-option>`);
|
|
943
|
+
|
|
944
|
+
async function settle(el: NileSelect) {
|
|
945
|
+
await el.updateComplete;
|
|
946
|
+
await nextFrame();
|
|
947
|
+
await nextFrame();
|
|
948
|
+
await el.updateComplete;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const firstChip = (el: NileSelect) =>
|
|
952
|
+
el.shadowRoot!.querySelector('.select__tags nile-tag') as HTMLElement;
|
|
953
|
+
|
|
954
|
+
it('R4a. defaults are 110 / 180 and are not written to the host', async () => {
|
|
955
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple>${options()}</nile-select>`);
|
|
956
|
+
await settle(el);
|
|
957
|
+
|
|
958
|
+
expect(el.chipMinWidth).to.equal(110);
|
|
959
|
+
expect(el.chipMaxWidth).to.equal(180);
|
|
960
|
+
expect(el.hasAttribute('chipminwidth')).to.be.false;
|
|
961
|
+
expect(el.hasAttribute('chipmaxwidth')).to.be.false;
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('R4b. the chipminwidth / chipmaxwidth attributes still set the properties', async () => {
|
|
965
|
+
const el = await fixture<NileSelect>(
|
|
966
|
+
html`<nile-select multiple chipminwidth="90" chipmaxwidth="150">${options()}</nile-select>`
|
|
967
|
+
);
|
|
968
|
+
await settle(el);
|
|
969
|
+
|
|
970
|
+
expect(el.chipMinWidth).to.equal(90);
|
|
971
|
+
expect(el.chipMaxWidth).to.equal(150);
|
|
972
|
+
|
|
973
|
+
el.setAttribute('chipmaxwidth', '160');
|
|
974
|
+
await settle(el);
|
|
975
|
+
expect(el.chipMaxWidth).to.equal(160);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('R4c. setting the properties programmatically works and does not add attributes', async () => {
|
|
979
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple>${options()}</nile-select>`);
|
|
980
|
+
await settle(el);
|
|
981
|
+
|
|
982
|
+
el.chipMinWidth = 95;
|
|
983
|
+
el.chipMaxWidth = 140;
|
|
984
|
+
await settle(el);
|
|
985
|
+
|
|
986
|
+
expect(el.chipMinWidth).to.equal(95);
|
|
987
|
+
expect(el.chipMaxWidth).to.equal(140);
|
|
988
|
+
expect(el.hasAttribute('chipmaxwidth')).to.be.false;
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('R4d. showtooltip still caps squeezed chips at chipMaxWidth (attribute and property)', async () => {
|
|
992
|
+
const host = await fixture<HTMLElement>(html`<div style="width: 300px">
|
|
993
|
+
<nile-select multiple showtooltip chipmaxwidth="150" .value=${LABELS}>${options()}</nile-select>
|
|
994
|
+
</div>`);
|
|
995
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
996
|
+
await settle(el);
|
|
997
|
+
|
|
998
|
+
const chip = firstChip(el);
|
|
999
|
+
expect(chip, 'select rendered no chips').to.exist;
|
|
1000
|
+
expect(chip.getBoundingClientRect().width).to.be.within(1, 150.5);
|
|
1001
|
+
|
|
1002
|
+
el.chipMaxWidth = 120;
|
|
1003
|
+
await settle(el);
|
|
1004
|
+
await nextFrame();
|
|
1005
|
+
expect(firstChip(el).getBoundingClientRect().width).to.be.within(1, 120.5);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('R4e. virtual mode forwards explicit values without reflecting defaults on either host', async () => {
|
|
1009
|
+
const data = LABELS.map(label => ({ label, value: label }));
|
|
1010
|
+
const el = await fixture<NileSelect>(
|
|
1011
|
+
html`<nile-select multiple .enableVirtualScroll=${true} .data=${data} chipmaxwidth="150"></nile-select>`
|
|
1012
|
+
);
|
|
1013
|
+
await settle(el);
|
|
1014
|
+
const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
|
|
1015
|
+
await inner.updateComplete;
|
|
1016
|
+
|
|
1017
|
+
expect(inner.chipMaxWidth).to.equal(150);
|
|
1018
|
+
expect(inner.chipMinWidth).to.equal(110);
|
|
1019
|
+
expect(el.hasAttribute('chipminwidth')).to.be.false;
|
|
1020
|
+
expect(inner.hasAttribute('chipminwidth')).to.be.false;
|
|
1021
|
+
expect(inner.hasAttribute('chipmaxwidth')).to.be.false;
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
describe('NileSelect footer width with and without noWidthSync', () => {
|
|
1026
|
+
async function openSelect(width: number, noWidthSync: boolean) {
|
|
1027
|
+
const host = await fixture<HTMLElement>(html`<div style="width: ${width}px">
|
|
1028
|
+
<nile-select multiple .noWidthSync=${noWidthSync} .value=${['a']}>
|
|
1029
|
+
<nile-option value="a">a</nile-option>
|
|
1030
|
+
<nile-option value="b">b</nile-option>
|
|
1031
|
+
</nile-select>
|
|
1032
|
+
</div>`);
|
|
1033
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
1034
|
+
await el.updateComplete;
|
|
1035
|
+
await el.show();
|
|
1036
|
+
await nextFrame();
|
|
1037
|
+
const listbox = el.shadowRoot!.querySelector('#listbox') as HTMLElement;
|
|
1038
|
+
const footer = el.shadowRoot!.querySelector('.select__footer') as HTMLElement;
|
|
1039
|
+
const combobox = el.shadowRoot!.querySelector('[part~="combobox"]') as HTMLElement;
|
|
1040
|
+
return { el, listbox, footer, combobox };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const labelShown = (footer: HTMLElement, selector: string) =>
|
|
1044
|
+
getComputedStyle(footer.querySelector(selector) as HTMLElement).display !== 'none';
|
|
1045
|
+
|
|
1046
|
+
before(() => document.documentElement.style.setProperty('--nile-display-flex', 'flex'));
|
|
1047
|
+
after(() => document.documentElement.style.removeProperty('--nile-display-flex'));
|
|
1048
|
+
|
|
1049
|
+
for (const width of [120, 400]) {
|
|
1050
|
+
it(`R3a. noWidthSync + multiple at ${width}px does not collapse and shows the whole footer`, async () => {
|
|
1051
|
+
const { listbox, footer } = await openSelect(width, true);
|
|
1052
|
+
|
|
1053
|
+
expect(listbox.offsetWidth).to.be.greaterThan(60);
|
|
1054
|
+
expect(listbox.offsetWidth).to.be.at.least(footer.offsetWidth);
|
|
1055
|
+
expect(footer.scrollWidth).to.be.at.most(footer.clientWidth + 1);
|
|
1056
|
+
expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.true;
|
|
1057
|
+
expect(labelShown(footer, '.select__footer-action .select__footer-label')).to.be.true;
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
it('R3b. width sync at normal width keeps the dropdown as wide as the trigger', async () => {
|
|
1062
|
+
const { listbox, combobox, footer } = await openSelect(400, false);
|
|
1063
|
+
|
|
1064
|
+
expect(Math.abs(listbox.offsetWidth - combobox.offsetWidth)).to.be.below(2);
|
|
1065
|
+
expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.true;
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it('R3c. width sync at a narrow width still collapses the footer labels (container queries kept)', async () => {
|
|
1069
|
+
const { listbox, combobox, footer } = await openSelect(130, false);
|
|
1070
|
+
|
|
1071
|
+
expect(Math.abs(listbox.offsetWidth - combobox.offsetWidth)).to.be.below(2);
|
|
1072
|
+
expect(labelShown(footer, '.select__clear .select__footer-label')).to.be.false;
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
describe('NileSelect showtooltip chip fit recovery (hidden, reconnect, re-parent)', () => {
|
|
1077
|
+
const LABELS = [
|
|
1078
|
+
'Engineering Operations Team',
|
|
1079
|
+
'Human Resources Department',
|
|
1080
|
+
'Finance And Accounting Group',
|
|
1081
|
+
'Customer Success Organisation',
|
|
1082
|
+
'Product Design Studio Unit'
|
|
1083
|
+
];
|
|
1084
|
+
const options = () => LABELS.map(label => html`<nile-option value=${label}>${label}</nile-option>`);
|
|
1085
|
+
|
|
1086
|
+
const NativeRO = window.ResizeObserver;
|
|
1087
|
+
const active = new Map<ResizeObserver, Set<Element>>();
|
|
1088
|
+
const liveCount = () => Array.from(active.values()).filter(targets => targets.size > 0).length;
|
|
1089
|
+
before(() => {
|
|
1090
|
+
(window as any).ResizeObserver = class extends NativeRO {
|
|
1091
|
+
observe(target: Element, options?: ResizeObserverOptions) {
|
|
1092
|
+
if (!active.has(this)) active.set(this, new Set());
|
|
1093
|
+
active.get(this)!.add(target);
|
|
1094
|
+
super.observe(target, options);
|
|
1095
|
+
}
|
|
1096
|
+
unobserve(target: Element) {
|
|
1097
|
+
active.get(this)?.delete(target);
|
|
1098
|
+
super.unobserve(target);
|
|
1099
|
+
}
|
|
1100
|
+
disconnect() {
|
|
1101
|
+
active.get(this)?.clear();
|
|
1102
|
+
super.disconnect();
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
});
|
|
1106
|
+
after(() => {
|
|
1107
|
+
(window as any).ResizeObserver = NativeRO;
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
async function frames(n = 4) {
|
|
1111
|
+
for (let i = 0; i < n; i++) await nextFrame();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function settle(el: NileSelect) {
|
|
1115
|
+
await el.updateComplete;
|
|
1116
|
+
await frames();
|
|
1117
|
+
await el.updateComplete;
|
|
1118
|
+
await frames(2);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const chipsOf = (el: NileSelect) =>
|
|
1122
|
+
Array.from(el.shadowRoot!.querySelectorAll('.select__tags nile-tag')) as HTMLElement[];
|
|
1123
|
+
|
|
1124
|
+
const watchActive = (el: NileSelect) => !!(el as any).measurableWatch;
|
|
1125
|
+
|
|
1126
|
+
function fitOf(el: NileSelect) {
|
|
1127
|
+
const chips = chipsOf(el);
|
|
1128
|
+
const visible = chips.filter(chip => chip.style.display !== 'none');
|
|
1129
|
+
const button = el.shadowRoot!.querySelector('button.select__tags-count') as HTMLElement | null;
|
|
1130
|
+
return {
|
|
1131
|
+
chips: chips.length,
|
|
1132
|
+
visible: visible.length,
|
|
1133
|
+
more: button ? button.textContent!.trim() : '',
|
|
1134
|
+
widths: visible.map(chip => chip.offsetWidth)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function box(width: number, display = 'block') {
|
|
1139
|
+
const div = document.createElement('div');
|
|
1140
|
+
div.style.width = `${width}px`;
|
|
1141
|
+
div.style.display = display;
|
|
1142
|
+
document.body.appendChild(div);
|
|
1143
|
+
boxes.push(div);
|
|
1144
|
+
return div;
|
|
1145
|
+
}
|
|
1146
|
+
let boxes: HTMLElement[] = [];
|
|
1147
|
+
afterEach(() => {
|
|
1148
|
+
boxes.forEach(b => b.remove());
|
|
1149
|
+
boxes = [];
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
async function makeSelect(parent: HTMLElement, { showTooltip = true, autoResize = false, value = LABELS } = {}) {
|
|
1153
|
+
const el = document.createElement('nile-select') as NileSelect;
|
|
1154
|
+
el.multiple = true;
|
|
1155
|
+
el.showTooltip = showTooltip;
|
|
1156
|
+
el.autoResize = autoResize;
|
|
1157
|
+
for (const label of LABELS) {
|
|
1158
|
+
const option = document.createElement('nile-option') as any;
|
|
1159
|
+
option.value = label;
|
|
1160
|
+
option.textContent = label;
|
|
1161
|
+
el.appendChild(option);
|
|
1162
|
+
}
|
|
1163
|
+
el.value = value;
|
|
1164
|
+
parent.appendChild(el);
|
|
1165
|
+
await settle(el);
|
|
1166
|
+
return el;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function reference(width: number, opts: { showTooltip?: boolean; value?: string[] } = {}) {
|
|
1170
|
+
const el = await makeSelect(box(width), opts);
|
|
1171
|
+
const fit = fitOf(el);
|
|
1172
|
+
el.parentElement!.remove();
|
|
1173
|
+
return fit;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function expectFitted(actual: ReturnType<typeof fitOf>, expected: ReturnType<typeof fitOf>, el: NileSelect) {
|
|
1177
|
+
expect(actual.chips).to.equal(expected.chips);
|
|
1178
|
+
expect(actual.visible, 'visible chips').to.equal(expected.visible);
|
|
1179
|
+
expect(actual.more, '+N button').to.equal(expected.more);
|
|
1180
|
+
actual.widths.forEach((w, i) => {
|
|
1181
|
+
expect(w, `chip ${i} width`).to.be.within(el.chipMinWidth - 1, el.chipMaxWidth + 1);
|
|
1182
|
+
expect(Math.abs(w - expected.widths[i]), `chip ${i} matches reference`).to.be.at.most(2);
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
it('R5-ref. the 300px reference really is a fitted row (so a stale row cannot match it)', async () => {
|
|
1187
|
+
const ref = await reference(300);
|
|
1188
|
+
expect(ref.chips).to.equal(5);
|
|
1189
|
+
expect(ref.visible).to.be.within(1, 4);
|
|
1190
|
+
expect(ref.more).to.equal(`+${5 - ref.visible}`);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('R5a. hidden -> visible without reconnect recovers the fitted row, then the watch ends', async () => {
|
|
1194
|
+
const ref = await reference(300);
|
|
1195
|
+
const parent = box(300, 'none');
|
|
1196
|
+
const el = await makeSelect(parent);
|
|
1197
|
+
expect(watchActive(el), 'watch armed while hidden').to.be.true;
|
|
1198
|
+
|
|
1199
|
+
parent.style.display = 'block';
|
|
1200
|
+
await settle(el);
|
|
1201
|
+
|
|
1202
|
+
expectFitted(fitOf(el), ref, el);
|
|
1203
|
+
expect(watchActive(el)).to.be.false;
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('R5b. hidden -> reconnected into a visible 300px parent re-fits, twice, without extra observers', async () => {
|
|
1207
|
+
const ref = await reference(300);
|
|
1208
|
+
const el = await makeSelect(box(300, 'none'));
|
|
1209
|
+
expect(watchActive(el)).to.be.true;
|
|
1210
|
+
|
|
1211
|
+
el.remove();
|
|
1212
|
+
expect(watchActive(el), 'disconnect stops the watch').to.be.false;
|
|
1213
|
+
box(300).appendChild(el);
|
|
1214
|
+
await settle(el);
|
|
1215
|
+
expectFitted(fitOf(el), ref, el);
|
|
1216
|
+
expect(watchActive(el)).to.be.false;
|
|
1217
|
+
const liveAfterFirst = liveCount();
|
|
1218
|
+
|
|
1219
|
+
el.remove();
|
|
1220
|
+
box(300).appendChild(el);
|
|
1221
|
+
await settle(el);
|
|
1222
|
+
expectFitted(fitOf(el), ref, el);
|
|
1223
|
+
expect(watchActive(el)).to.be.false;
|
|
1224
|
+
expect(liveCount(), 'no observer growth across reconnects').to.equal(liveAfterFirst);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('R5c. visible -> disconnect -> reconnect into a narrower parent re-fits to the new width', async () => {
|
|
1228
|
+
const ref = await reference(300);
|
|
1229
|
+
const el = await makeSelect(box(700));
|
|
1230
|
+
expect(fitOf(el).visible).to.be.greaterThan(ref.visible);
|
|
1231
|
+
|
|
1232
|
+
el.remove();
|
|
1233
|
+
box(300).appendChild(el);
|
|
1234
|
+
await settle(el);
|
|
1235
|
+
|
|
1236
|
+
expectFitted(fitOf(el), ref, el);
|
|
1237
|
+
expect(watchActive(el)).to.be.false;
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
it('R5d. value changed while hidden and detached, then reconnected, fits the new value', async () => {
|
|
1241
|
+
const four = LABELS.slice(0, 4);
|
|
1242
|
+
const ref = await reference(300, { value: four });
|
|
1243
|
+
const el = await makeSelect(box(300, 'none'));
|
|
1244
|
+
|
|
1245
|
+
el.remove();
|
|
1246
|
+
el.value = four;
|
|
1247
|
+
await settle(el);
|
|
1248
|
+
expect(watchActive(el), 'no watch left on a detached select').to.be.false;
|
|
1249
|
+
|
|
1250
|
+
box(300).appendChild(el);
|
|
1251
|
+
await settle(el);
|
|
1252
|
+
expectFitted(fitOf(el), ref, el);
|
|
1253
|
+
expect(watchActive(el)).to.be.false;
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it('R5e. reconnect into another hidden parent keeps one watch, and recovers when shown', async () => {
|
|
1257
|
+
const ref = await reference(300);
|
|
1258
|
+
const el = await makeSelect(box(300, 'none'));
|
|
1259
|
+
const liveHidden = liveCount();
|
|
1260
|
+
|
|
1261
|
+
el.remove();
|
|
1262
|
+
const second = box(300, 'none');
|
|
1263
|
+
second.appendChild(el);
|
|
1264
|
+
await settle(el);
|
|
1265
|
+
expect(watchActive(el), 're-armed in the new hidden parent').to.be.true;
|
|
1266
|
+
expect(liveCount(), 'the old watch was replaced, not duplicated').to.equal(liveHidden);
|
|
1267
|
+
|
|
1268
|
+
second.style.display = 'block';
|
|
1269
|
+
await settle(el);
|
|
1270
|
+
expectFitted(fitOf(el), ref, el);
|
|
1271
|
+
expect(watchActive(el)).to.be.false;
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it('R5f. showTooltip true -> false before reconnect gives the untouched 2.0.8 row', async () => {
|
|
1275
|
+
const legacy = await makeSelect(box(300), { showTooltip: false });
|
|
1276
|
+
const legacyText = legacy.shadowRoot!.querySelector('.select__tags')!.textContent!.replace(/\s+/g, ' ').trim();
|
|
1277
|
+
|
|
1278
|
+
const el = await makeSelect(box(300, 'none'));
|
|
1279
|
+
el.remove();
|
|
1280
|
+
el.showTooltip = false;
|
|
1281
|
+
box(300).appendChild(el);
|
|
1282
|
+
await settle(el);
|
|
1283
|
+
|
|
1284
|
+
expect(watchActive(el)).to.be.false;
|
|
1285
|
+
expect(el.shadowRoot!.querySelector('.select__tags--fitted')).to.equal(null);
|
|
1286
|
+
chipsOf(el).forEach(chip => expect(chip.style.width, 'no fitted width on the 2.0.8 row').to.equal(''));
|
|
1287
|
+
expect(el.shadowRoot!.querySelector('.select__tags')!.textContent!.replace(/\s+/g, ' ').trim()).to.equal(legacyText);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
it('R5g. showtooltip=false: hidden -> shown and reconnect never arm a watch or write chip widths', async () => {
|
|
1291
|
+
const parent = box(300, 'none');
|
|
1292
|
+
const el = await makeSelect(parent, { showTooltip: false });
|
|
1293
|
+
expect(watchActive(el)).to.be.false;
|
|
1294
|
+
|
|
1295
|
+
parent.style.display = 'block';
|
|
1296
|
+
await settle(el);
|
|
1297
|
+
el.remove();
|
|
1298
|
+
box(300).appendChild(el);
|
|
1299
|
+
await settle(el);
|
|
1300
|
+
|
|
1301
|
+
expect(watchActive(el)).to.be.false;
|
|
1302
|
+
chipsOf(el).forEach(chip => expect(chip.style.width).to.equal(''));
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
it('R5h. a visible showtooltip select never arms the watch', async () => {
|
|
1306
|
+
const el = await makeSelect(box(300));
|
|
1307
|
+
expect(watchActive(el)).to.be.false;
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
it('R5i. repeated disconnect/reconnect cycles stay stable', async () => {
|
|
1311
|
+
const ref = await reference(300);
|
|
1312
|
+
const el = await makeSelect(box(300));
|
|
1313
|
+
const baseline = liveCount();
|
|
1314
|
+
|
|
1315
|
+
for (let i = 0; i < 5; i++) {
|
|
1316
|
+
el.remove();
|
|
1317
|
+
box(300).appendChild(el);
|
|
1318
|
+
await settle(el);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
expectFitted(fitOf(el), ref, el);
|
|
1322
|
+
expect(watchActive(el)).to.be.false;
|
|
1323
|
+
expect(liveCount(), 'no observer growth across cycles').to.equal(baseline);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
async function updatesOver(el: NileSelect, ms: number) {
|
|
1327
|
+
let updates = 0;
|
|
1328
|
+
const update = (el as any).update;
|
|
1329
|
+
(el as any).update = function (this: NileSelect, changed: Map<string, unknown>) {
|
|
1330
|
+
updates++;
|
|
1331
|
+
return update.call(this, changed);
|
|
1332
|
+
};
|
|
1333
|
+
await aTimeout(ms);
|
|
1334
|
+
(el as any).update = update;
|
|
1335
|
+
return updates;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
it('R5j. autoResize + showtooltip: hidden -> reconnect fits, resizes refit, and it settles', async () => {
|
|
1339
|
+
const ref300 = await reference(300);
|
|
1340
|
+
const ref500 = await reference(500);
|
|
1341
|
+
const el = await makeSelect(box(300, 'none'), { autoResize: true });
|
|
1342
|
+
|
|
1343
|
+
el.remove();
|
|
1344
|
+
const parent = box(300);
|
|
1345
|
+
parent.appendChild(el);
|
|
1346
|
+
await settle(el);
|
|
1347
|
+
expectFitted(fitOf(el), ref300, el);
|
|
1348
|
+
|
|
1349
|
+
parent.style.width = '500px';
|
|
1350
|
+
await settle(el);
|
|
1351
|
+
expectFitted(fitOf(el), ref500, el);
|
|
1352
|
+
|
|
1353
|
+
expect(await updatesOver(el, 300), 'no render loop with showtooltip').to.equal(0);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('R5k. autoResize: turning showtooltip on over the 2.0.8 row gives the fitted row and settles', async () => {
|
|
1357
|
+
const ref = await reference(300);
|
|
1358
|
+
const el = await makeSelect(box(300), { showTooltip: false, autoResize: true });
|
|
1359
|
+
|
|
1360
|
+
el.showTooltip = true;
|
|
1361
|
+
await settle(el);
|
|
1362
|
+
await aTimeout(20);
|
|
1363
|
+
await settle(el);
|
|
1364
|
+
|
|
1365
|
+
expectFitted(fitOf(el), ref, el);
|
|
1366
|
+
expect(await updatesOver(el, 300), 'no render loop once showtooltip is on').to.equal(0);
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
class R7PrefixProbe extends HTMLElement {
|
|
1371
|
+
static created = 0;
|
|
1372
|
+
constructor() {
|
|
1373
|
+
super();
|
|
1374
|
+
R7PrefixProbe.created++;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (!customElements.get('r7-prefix-probe')) customElements.define('r7-prefix-probe', R7PrefixProbe);
|
|
1378
|
+
|
|
1379
|
+
describe('NileSelect option prefix copies across re-renders', () => {
|
|
1380
|
+
async function settle(el: NileSelect) {
|
|
1381
|
+
await el.updateComplete;
|
|
1382
|
+
await nextFrame();
|
|
1383
|
+
await nextFrame();
|
|
1384
|
+
await el.updateComplete;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const prefixIn = (el: NileSelect, selector: string) =>
|
|
1388
|
+
el.shadowRoot!.querySelector(`${selector} r7-prefix-probe`) as HTMLElement | null;
|
|
1389
|
+
|
|
1390
|
+
const withPrefix = (value: string) =>
|
|
1391
|
+
html`<nile-option value=${value}><r7-prefix-probe slot="prefix" data-v=${value}></r7-prefix-probe>${value}</nile-option>`;
|
|
1392
|
+
|
|
1393
|
+
it('R7a. single select: the trigger prefix node is reused across re-renders', async () => {
|
|
1394
|
+
const el = await fixture<NileSelect>(html`<nile-select .value=${'a'}>${withPrefix('a')}${withPrefix('b')}</nile-select>`);
|
|
1395
|
+
await settle(el);
|
|
1396
|
+
|
|
1397
|
+
const first = prefixIn(el, '.select__prefix--from-options');
|
|
1398
|
+
expect(first, 'prefix copied into the trigger').to.exist;
|
|
1399
|
+
const created = R7PrefixProbe.created;
|
|
1400
|
+
|
|
1401
|
+
for (let i = 0; i < 3; i++) {
|
|
1402
|
+
el.requestUpdate();
|
|
1403
|
+
await settle(el);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
expect(prefixIn(el, '.select__prefix--from-options')).to.equal(first);
|
|
1407
|
+
expect(R7PrefixProbe.created).to.equal(created);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
for (const showTooltip of [false, true]) {
|
|
1411
|
+
it(`R7b. multiple (showtooltip=${showTooltip}): chip prefix nodes are reused across re-renders`, async () => {
|
|
1412
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple ?showtooltip=${showTooltip} .value=${['a', 'b']}>
|
|
1413
|
+
${withPrefix('a')}${withPrefix('b')}
|
|
1414
|
+
</nile-select>`);
|
|
1415
|
+
await settle(el);
|
|
1416
|
+
|
|
1417
|
+
const before = Array.from(el.shadowRoot!.querySelectorAll('.select__tags r7-prefix-probe'));
|
|
1418
|
+
expect(before.length).to.equal(2);
|
|
1419
|
+
const created = R7PrefixProbe.created;
|
|
1420
|
+
|
|
1421
|
+
el.requestUpdate();
|
|
1422
|
+
await settle(el);
|
|
1423
|
+
|
|
1424
|
+
const after = Array.from(el.shadowRoot!.querySelectorAll('.select__tags r7-prefix-probe'));
|
|
1425
|
+
expect(after.length).to.equal(2);
|
|
1426
|
+
after.forEach((node, i) => expect(node === before[i], `chip ${i} prefix replaced`).to.be.true);
|
|
1427
|
+
expect(R7PrefixProbe.created).to.equal(created);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
it('R7c. changing the selection or the option prefix still updates the copy', async () => {
|
|
1432
|
+
const el = await fixture<NileSelect>(html`<nile-select .value=${'a'}>${withPrefix('a')}${withPrefix('b')}</nile-select>`);
|
|
1433
|
+
await settle(el);
|
|
1434
|
+
|
|
1435
|
+
el.value = 'b';
|
|
1436
|
+
await settle(el);
|
|
1437
|
+
expect(prefixIn(el, '.select__prefix--from-options')!.dataset.v).to.equal('b');
|
|
1438
|
+
|
|
1439
|
+
const source = el.querySelector('nile-option[value="b"] r7-prefix-probe') as HTMLElement;
|
|
1440
|
+
source.setAttribute('data-v', 'b2');
|
|
1441
|
+
el.requestUpdate();
|
|
1442
|
+
await settle(el);
|
|
1443
|
+
expect(prefixIn(el, '.select__prefix--from-options')!.dataset.v).to.equal('b2');
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
describe('NileSelect 2.0.8 compatibility matrix', () => {
|
|
1448
|
+
const OPTS = ['a', 'b', 'c'];
|
|
1449
|
+
const options = () => OPTS.map(v => html`<nile-option value=${v}>L${v}</nile-option>`);
|
|
1450
|
+
const DATA = OPTS.map(v => ({ label: `L${v}`, value: v }));
|
|
1451
|
+
|
|
1452
|
+
async function settle(el: NileSelect) {
|
|
1453
|
+
await el.updateComplete;
|
|
1454
|
+
await nextFrame();
|
|
1455
|
+
await nextFrame();
|
|
1456
|
+
await el.updateComplete;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const selectedValues = (el: NileSelect) =>
|
|
1460
|
+
Array.from(el.querySelectorAll('nile-option'))
|
|
1461
|
+
.filter(o => (o as any).selected)
|
|
1462
|
+
.map(o => (o as any).value);
|
|
1463
|
+
const chipCount = (el: NileSelect) => el.shadowRoot!.querySelectorAll('.select__tags nile-tag').length;
|
|
1464
|
+
|
|
1465
|
+
function record(el: EventTarget, names: string[]) {
|
|
1466
|
+
const seen: [string, unknown][] = [];
|
|
1467
|
+
names.forEach(name =>
|
|
1468
|
+
el.addEventListener(name, e => seen.push([name, JSON.parse(JSON.stringify((e as CustomEvent).detail ?? null))]))
|
|
1469
|
+
);
|
|
1470
|
+
return seen;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
it('C1. a value in the template is applied when the options arrive later (multiple and single)', async () => {
|
|
1474
|
+
const multi = await fixture<NileSelect>(html`<nile-select multiple .value=${['a', 'b']}></nile-select>`);
|
|
1475
|
+
await settle(multi);
|
|
1476
|
+
OPTS.forEach(v => { const o = document.createElement('nile-option'); (o as any).value = v; o.textContent = v; multi.appendChild(o); });
|
|
1477
|
+
await settle(multi);
|
|
1478
|
+
expect(multi.value).to.deep.equal(['a', 'b']);
|
|
1479
|
+
expect(selectedValues(multi)).to.deep.equal(['a', 'b']);
|
|
1480
|
+
expect(chipCount(multi)).to.equal(2);
|
|
1481
|
+
|
|
1482
|
+
const single = await fixture<NileSelect>(html`<nile-select .value=${'b'}></nile-select>`);
|
|
1483
|
+
await settle(single);
|
|
1484
|
+
OPTS.forEach(v => { const o = document.createElement('nile-option'); (o as any).value = v; o.textContent = v; single.appendChild(o); });
|
|
1485
|
+
await settle(single);
|
|
1486
|
+
expect(single.value).to.equal('b');
|
|
1487
|
+
expect(selectedValues(single)).to.deep.equal(['b']);
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
it('C2. programmatic value updates reach options, chips and the form value', async () => {
|
|
1491
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple name="s">${options()}</nile-select>`);
|
|
1492
|
+
await settle(el);
|
|
1493
|
+
|
|
1494
|
+
el.value = ['b', 'c'];
|
|
1495
|
+
await settle(el);
|
|
1496
|
+
expect(selectedValues(el)).to.deep.equal(['b', 'c']);
|
|
1497
|
+
expect(chipCount(el)).to.equal(2);
|
|
1498
|
+
|
|
1499
|
+
el.value = [];
|
|
1500
|
+
await settle(el);
|
|
1501
|
+
expect(selectedValues(el)).to.deep.equal([]);
|
|
1502
|
+
expect(chipCount(el)).to.equal(0);
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it('C3. removing a chip updates the value and emits input, change, tag-remove in order', async () => {
|
|
1506
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple .value=${['a', 'b']}>${options()}</nile-select>`);
|
|
1507
|
+
await settle(el);
|
|
1508
|
+
const seen = record(el, ['nile-input', 'nile-change', 'nile-tag-remove']);
|
|
1509
|
+
|
|
1510
|
+
const chip = el.shadowRoot!.querySelector('.select__tags nile-tag') as HTMLElement;
|
|
1511
|
+
expect(chip, 'select rendered no chips').to.exist;
|
|
1512
|
+
chip.dispatchEvent(new CustomEvent('nile-remove', { bubbles: true, composed: true }));
|
|
1513
|
+
await settle(el);
|
|
1514
|
+
|
|
1515
|
+
expect(el.value).to.deep.equal(['b']);
|
|
1516
|
+
expect(seen.map(([name]) => name)).to.deep.equal(['nile-input', 'nile-change', 'nile-tag-remove']);
|
|
1517
|
+
expect((seen[2][1] as any).removedtagvalue).to.equal('a');
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it('C4. the clear button empties the value and emits clear, input, change', async () => {
|
|
1521
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple clearable .value=${['a', 'b']}>${options()}</nile-select>`);
|
|
1522
|
+
await settle(el);
|
|
1523
|
+
const seen = record(el, ['nile-clear', 'nile-input', 'nile-change']);
|
|
1524
|
+
|
|
1525
|
+
const clear = el.shadowRoot!.querySelector('[part~="clear-button"]') as HTMLElement;
|
|
1526
|
+
expect(clear, 'clear button rendered').to.exist;
|
|
1527
|
+
clear.click();
|
|
1528
|
+
await settle(el);
|
|
1529
|
+
|
|
1530
|
+
expect(el.value).to.deep.equal([]);
|
|
1531
|
+
expect(chipCount(el)).to.equal(0);
|
|
1532
|
+
expect(seen.map(([name]) => name)).to.deep.equal(['nile-clear', 'nile-input', 'nile-change']);
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
it('C5. form reset restores the value attribute', async () => {
|
|
1536
|
+
const form = await fixture<HTMLFormElement>(html`<form>
|
|
1537
|
+
<nile-select name="s" multiple value="a b">${options()}</nile-select>
|
|
1538
|
+
</form>`);
|
|
1539
|
+
const el = form.querySelector('nile-select') as NileSelect;
|
|
1540
|
+
await settle(el);
|
|
1541
|
+
|
|
1542
|
+
el.value = ['c'];
|
|
1543
|
+
await settle(el);
|
|
1544
|
+
form.reset();
|
|
1545
|
+
await settle(el);
|
|
1546
|
+
|
|
1547
|
+
expect(el.value).to.deep.equal(['a', 'b']);
|
|
1548
|
+
expect(selectedValues(el)).to.deep.equal(['a', 'b']);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
it('C6. portal + search: typing filters without touching the value', async () => {
|
|
1552
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple portal searchEnabled .value=${['a']}>${options()}</nile-select>`);
|
|
1553
|
+
await settle(el);
|
|
1554
|
+
await el.show();
|
|
1555
|
+
await aTimeout(50);
|
|
1556
|
+
|
|
1557
|
+
const search = Array.from(document.body.querySelectorAll('nile-input')).find(n => !el.shadowRoot!.contains(n)) as HTMLElement | undefined;
|
|
1558
|
+
expect(search, 'portal search box rendered').to.exist;
|
|
1559
|
+
await (search as any).updateComplete;
|
|
1560
|
+
const input = search!.shadowRoot!.querySelector('input') as HTMLInputElement;
|
|
1561
|
+
input.value = 'Lb';
|
|
1562
|
+
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
1563
|
+
await settle(el);
|
|
1564
|
+
|
|
1565
|
+
expect(el.value).to.deep.equal(['a']);
|
|
1566
|
+
await el.hide();
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
it('C7. reconnecting a multiple select without showtooltip keeps value and chips', async () => {
|
|
1570
|
+
const host = await fixture<HTMLElement>(html`<div style="width: 500px">
|
|
1571
|
+
<nile-select multiple .value=${['a', 'b']}>${options()}</nile-select>
|
|
1572
|
+
</div>`);
|
|
1573
|
+
const el = host.querySelector('nile-select') as NileSelect;
|
|
1574
|
+
await settle(el);
|
|
1575
|
+
|
|
1576
|
+
el.remove();
|
|
1577
|
+
host.appendChild(el);
|
|
1578
|
+
await settle(el);
|
|
1579
|
+
|
|
1580
|
+
expect(el.value).to.deep.equal(['a', 'b']);
|
|
1581
|
+
expect(chipCount(el)).to.equal(2);
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
it('C8. enableVirtualScroll: a value set before data arrives is kept and shown', async () => {
|
|
1585
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple .enableVirtualScroll=${true} .value=${['b']}></nile-select>`);
|
|
1586
|
+
await settle(el);
|
|
1587
|
+
el.data = DATA;
|
|
1588
|
+
await settle(el);
|
|
1589
|
+
const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
|
|
1590
|
+
await inner.updateComplete;
|
|
1591
|
+
|
|
1592
|
+
expect(el.value).to.deep.equal(['b']);
|
|
1593
|
+
expect(inner.value).to.deep.equal(['b']);
|
|
1594
|
+
expect(inner.shadowRoot.querySelectorAll('.select__tags nile-tag').length).to.equal(1);
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it('C9. enableVirtualScroll: programmatic value and clear-all reach both selects', async () => {
|
|
1598
|
+
const el = await fixture<NileSelect>(html`<nile-select multiple .enableVirtualScroll=${true} .data=${DATA}></nile-select>`);
|
|
1599
|
+
await settle(el);
|
|
1600
|
+
const inner = el.shadowRoot!.querySelector('nile-virtual-select') as any;
|
|
1601
|
+
|
|
1602
|
+
el.value = ['a', 'c'];
|
|
1603
|
+
await settle(el);
|
|
1604
|
+
await inner.updateComplete;
|
|
1605
|
+
expect(inner.value).to.deep.equal(['a', 'c']);
|
|
1606
|
+
|
|
1607
|
+
inner.unSelectAll();
|
|
1608
|
+
await inner.updateComplete;
|
|
1609
|
+
await settle(el);
|
|
1610
|
+
expect(inner.value).to.deep.equal([]);
|
|
1611
|
+
expect(el.value).to.deep.equal([]);
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
describe('NileSelect showtooltip tooltip on sub-pixel truncation', () => {
|
|
1616
|
+
const A = 'Engineering Operations';
|
|
1617
|
+
const B = 'Human Resources Department And Payroll';
|
|
1618
|
+
|
|
1619
|
+
async function frames(n = 5) {
|
|
1620
|
+
for (let i = 0; i < n; i++) await nextFrame();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function mount(width: number, chipMaxWidth: number) {
|
|
1624
|
+
const host = await fixture<HTMLElement>(html`<div style="width: ${width}px">
|
|
1625
|
+
<nile-select multiple showtooltip chipminwidth="40" chipmaxwidth=${chipMaxWidth} .value=${[A, B]}>
|
|
1626
|
+
<nile-option value=${A}>${A}</nile-option>
|
|
1627
|
+
<nile-option value=${B}>${B}</nile-option>
|
|
1628
|
+
</nile-select>
|
|
1629
|
+
</div>`);
|
|
1630
|
+
const el = host.querySelector('nile-select') as any;
|
|
1631
|
+
await el.updateComplete;
|
|
1632
|
+
await frames();
|
|
1633
|
+
await el.updateComplete;
|
|
1634
|
+
await frames(2);
|
|
1635
|
+
return el;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function visiblyCut(chip: HTMLElement) {
|
|
1639
|
+
const content = chip.shadowRoot!.querySelector('[part="content"]') as HTMLSlotElement;
|
|
1640
|
+
const text = content.assignedNodes({ flatten: true }).find(n => n.textContent?.trim())!;
|
|
1641
|
+
const range = document.createRange();
|
|
1642
|
+
range.selectNodeContents(text);
|
|
1643
|
+
const over = range.getBoundingClientRect().right - content.getBoundingClientRect().right;
|
|
1644
|
+
return { cut: over > 1 / 64, over, pxGap: content.scrollWidth - content.clientWidth };
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function chipsAndTips(el: any) {
|
|
1648
|
+
const chips = Array.from(el.shadowRoot.querySelectorAll('.select__tags nile-tag')) as HTMLElement[];
|
|
1649
|
+
const tips = Array.from(el.shadowRoot.querySelectorAll('.select__tags nile-lite-tooltip')) as HTMLElement[];
|
|
1650
|
+
return chips
|
|
1651
|
+
.map((chip, i) => ({ chip, tipOn: !!tips[i] && !tips[i].hasAttribute('disabled') }))
|
|
1652
|
+
.filter(({ chip }) => chip.style.display !== 'none');
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
it('TT1. every visibly cut chip has its tooltip, every uncut chip has none (sweep across the edge)', async () => {
|
|
1656
|
+
const wide = await mount(900, 400);
|
|
1657
|
+
const natural = chipsAndTips(wide)[0].chip.offsetWidth;
|
|
1658
|
+
|
|
1659
|
+
let subPixelCuts = 0;
|
|
1660
|
+
for (let max = natural - 4; max <= natural + 1; max += 0.25) {
|
|
1661
|
+
const el = await mount(330, max);
|
|
1662
|
+
for (const { chip, tipOn } of chipsAndTips(el)) {
|
|
1663
|
+
const { cut, pxGap } = visiblyCut(chip);
|
|
1664
|
+
expect(tipOn, `chipMaxWidth ${max}: tooltip ${tipOn} but cut ${cut}`).to.equal(cut);
|
|
1665
|
+
if (cut && pxGap <= 1) subPixelCuts++;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
expect(subPixelCuts, 'sweep reached the sub-pixel truncation window').to.be.greaterThan(0);
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
it('TT2. chips at their natural width get no tooltip', async () => {
|
|
1672
|
+
const el = await mount(900, 400);
|
|
1673
|
+
for (const { chip, tipOn } of chipsAndTips(el)) {
|
|
1674
|
+
expect(visiblyCut(chip).cut).to.be.false;
|
|
1675
|
+
expect(tipOn).to.be.false;
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
583
1678
|
});
|