@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.
- package/README.md +4 -0
- package/dist/index.js +619 -252
- package/dist/nile-select/index.cjs.js +1 -1
- package/dist/nile-select/index.esm.js +1 -1
- package/dist/nile-select/nile-select.cjs.js +2 -1
- 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 +126 -6
- package/dist/nile-select/nile-select.esm.js +77 -11
- 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 +3 -1
- package/dist/nile-virtual-select/index.cjs.js +1 -1
- package/dist/nile-virtual-select/index.esm.js +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -3
- 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 +126 -6
- package/dist/nile-virtual-select/nile-virtual-select.esm.js +71 -12
- package/dist/src/nile-select/nile-select.css.js +126 -6
- package/dist/src/nile-select/nile-select.css.js.map +1 -1
- package/dist/src/nile-select/nile-select.d.ts +43 -5
- package/dist/src/nile-select/nile-select.js +445 -106
- 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 +342 -1
- package/dist/src/nile-select/nile-select.test.js.map +1 -1
- package/dist/src/nile-select/virtual-scroll-helper.js +3 -1
- package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.css.js +126 -6
- 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 +45 -4
- package/dist/src/nile-virtual-select/nile-virtual-select.js +385 -59
- package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js +253 -1
- 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-select/nile-select.css.ts +126 -6
- package/src/nile-select/nile-select.test.ts +478 -1
- package/src/nile-select/nile-select.ts +499 -91
- package/src/nile-select/virtual-scroll-helper.ts +3 -1
- package/src/nile-virtual-select/nile-virtual-select.css.ts +126 -6
- package/src/nile-virtual-select/nile-virtual-select.test.ts +352 -1
- package/src/nile-virtual-select/nile-virtual-select.ts +460 -55
- package/vscode-html-custom-data.json +40 -6
|
@@ -15,10 +15,12 @@ import { styles } from './nile-select.css';
|
|
|
15
15
|
import '../nile-icon';
|
|
16
16
|
import '../nile-popup/nile-popup';
|
|
17
17
|
import '../nile-tag/nile-tag';
|
|
18
|
+
import '../nile-lite-tooltip/nile-lite-tooltip';
|
|
18
19
|
import { animateTo, stopAnimations } from '../internal/animate';
|
|
19
20
|
import { classMap } from 'lit/directives/class-map.js';
|
|
20
21
|
import { query, state } from 'lit/decorators.js';
|
|
21
22
|
import { defaultValue } from '../internal/default-value';
|
|
23
|
+
import { KeyCode } from '../internal/enum';
|
|
22
24
|
import { FormControlController } from '../internal/form';
|
|
23
25
|
import {
|
|
24
26
|
getAnimation,
|
|
@@ -34,7 +36,6 @@ import type { NileFormControl } from '../internal/nile-element';
|
|
|
34
36
|
import type NileOption from '../nile-option/nile-option';
|
|
35
37
|
import type NilePopup from '../nile-popup/nile-popup';
|
|
36
38
|
import type { RenderItemConfig } from '../nile-virtual-select/types.js';
|
|
37
|
-
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
38
39
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
39
40
|
import '../nile-virtual-select/nile-virtual-select';
|
|
40
41
|
import { VirtualScrollHelper } from './virtual-scroll-helper';
|
|
@@ -127,8 +128,22 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
127
128
|
private scrollTimeout: number | undefined;
|
|
128
129
|
private scrolling = false;
|
|
129
130
|
private options: NileOption[] = [];
|
|
130
|
-
private
|
|
131
|
+
private observedTagsContainer?: Element;
|
|
132
|
+
|
|
133
|
+
private resizeController = new ResizeController(this, {
|
|
134
|
+
callback: (entries: ResizeObserverEntry[]) => {
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
if (entry.target.classList.contains('select__tags')) {
|
|
137
|
+
this.calculateTotalWidthOfTags();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
131
142
|
private visibilityManager?: VisibilityManager;
|
|
143
|
+
private chipFitFrame = 0;
|
|
144
|
+
private lastChipLayout = { hidden: -1, visible: -1 };
|
|
145
|
+
@state() private clippedChips: boolean[] = [];
|
|
146
|
+
private clipFrame = 0;
|
|
132
147
|
|
|
133
148
|
|
|
134
149
|
@query('.select') popup: NilePopup;
|
|
@@ -142,6 +157,10 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
142
157
|
@state() currentOption: NileOption;
|
|
143
158
|
@state() selectedOptions: NileOption[] = [];
|
|
144
159
|
@state() oldValue: string | string[] = '';
|
|
160
|
+
@state() private hiddenChipCount = 0;
|
|
161
|
+
|
|
162
|
+
/** Last measured width of the overflow button, in px. */
|
|
163
|
+
private lastOverflowWidth = 0;
|
|
145
164
|
|
|
146
165
|
/** The name of the select, submitted as a name/value pair with form data. */
|
|
147
166
|
@property() name = '';
|
|
@@ -214,6 +233,7 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
214
233
|
|
|
215
234
|
@property({ attribute: true, reflect: true }) helpText = '';
|
|
216
235
|
@property({ attribute: 'help-text', reflect: true }) help_text = '';
|
|
236
|
+
/** Re-runs the fitted-chip layout when the control's own width changes. */
|
|
217
237
|
@property({ type: Boolean, attribute: true, reflect: true }) autoResize = false;
|
|
218
238
|
|
|
219
239
|
@property({ attribute: 'error-message', reflect: true }) errorMessage = '';
|
|
@@ -231,8 +251,39 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
231
251
|
* The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
|
|
232
252
|
* indicate the number of additional items that are selected. Set to 0 to remove the limit.
|
|
233
253
|
*/
|
|
254
|
+
private _maxOptionsVisible = 3;
|
|
255
|
+
|
|
256
|
+
private maxOptionsVisibleExplicit = false;
|
|
257
|
+
|
|
234
258
|
@property({ attribute: 'max-options-visible', type: Number })
|
|
235
|
-
maxOptionsVisible
|
|
259
|
+
get maxOptionsVisible(): number {
|
|
260
|
+
return this._maxOptionsVisible;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
set maxOptionsVisible(value: number) {
|
|
264
|
+
const previous = this._maxOptionsVisible;
|
|
265
|
+
this._maxOptionsVisible = value;
|
|
266
|
+
// Any assignment opts this instance out of automatic chip fitting.
|
|
267
|
+
this.maxOptionsVisibleExplicit = true;
|
|
268
|
+
this.requestUpdate('maxOptionsVisible', previous);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private get chipFittingEnabled(): boolean {
|
|
272
|
+
return !!this.multiple && !this.maxOptionsVisibleExplicit;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Minimum width, in px, of a squeezed chip. Needs `showtooltip`. */
|
|
276
|
+
@property({ type: Number, reflect: true, attribute: true }) chipMinWidth = 110;
|
|
277
|
+
|
|
278
|
+
/** Maximum width, in px, of a squeezed chip. Needs `showtooltip`. */
|
|
279
|
+
@property({ type: Number, reflect: true, attribute: true }) chipMaxWidth = 180;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Shows a chip's full value in a tooltip when its text is clipped, and is
|
|
283
|
+
* what allows a chip to be truncated at all.
|
|
284
|
+
*/
|
|
285
|
+
@property({ type: Boolean, attribute: true, reflect: true }) showTooltip = false;
|
|
286
|
+
|
|
236
287
|
|
|
237
288
|
/** Disables the select control. */
|
|
238
289
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
@@ -278,9 +329,6 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
278
329
|
@property({ type: Boolean, reflect: true }) required = false;
|
|
279
330
|
|
|
280
331
|
@property({ type: Boolean }) showSelected = false;
|
|
281
|
-
|
|
282
|
-
@state() oldMaxOptionsVisible: number = 1;
|
|
283
|
-
|
|
284
332
|
|
|
285
333
|
@property({ type: Boolean, reflect: true, attribute: true }) enableTabClose = false;
|
|
286
334
|
|
|
@@ -364,23 +412,47 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
364
412
|
this.visibilityManager?.cleanup();
|
|
365
413
|
// Clean up body append elements
|
|
366
414
|
this.portalManager.cleanupPortalAppend();
|
|
415
|
+
this.destroyChipFit();
|
|
367
416
|
this.emit('nile-destroy');
|
|
368
417
|
}
|
|
369
418
|
|
|
370
|
-
private setupResizeObserver() {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
419
|
+
private setupResizeObserver(): void {
|
|
420
|
+
const wanted =
|
|
421
|
+
this.autoResize && this.chipFittingEnabled
|
|
422
|
+
? (this.shadowRoot?.querySelector('.select__tags') as HTMLElement | null) ??
|
|
423
|
+
undefined
|
|
424
|
+
: undefined;
|
|
425
|
+
|
|
426
|
+
if (wanted === this.observedTagsContainer) {
|
|
427
|
+
if (!this.chipFittingEnabled) this.destroyChipFit();
|
|
428
|
+
return;
|
|
378
429
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
430
|
+
|
|
431
|
+
if (this.observedTagsContainer) {
|
|
432
|
+
this.resizeController.unobserve(this.observedTagsContainer);
|
|
433
|
+
this.observedTagsContainer = undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (wanted) {
|
|
437
|
+
this.resizeController.observe(wanted);
|
|
438
|
+
this.observedTagsContainer = wanted;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!this.chipFittingEnabled) {
|
|
443
|
+
this.destroyChipFit();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private syncVirtualSelectMaxOptionsVisible() {
|
|
448
|
+
if (!this.enableVirtualScroll || !this.maxOptionsVisibleExplicit) return;
|
|
449
|
+
|
|
450
|
+
const inner = this.shadowRoot?.querySelector('nile-virtual-select') as
|
|
451
|
+
| { maxOptionsVisible: number }
|
|
452
|
+
| null;
|
|
453
|
+
|
|
454
|
+
if (inner && inner.maxOptionsVisible !== this._maxOptionsVisible) {
|
|
455
|
+
inner.maxOptionsVisible = this._maxOptionsVisible;
|
|
384
456
|
}
|
|
385
457
|
}
|
|
386
458
|
|
|
@@ -397,12 +469,29 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
397
469
|
if(_changedProperties.has('tickOnSelect')) {
|
|
398
470
|
this.setTickOnSelectInOptions(this.tickOnSelect);
|
|
399
471
|
}
|
|
400
|
-
if (
|
|
472
|
+
if (
|
|
473
|
+
_changedProperties.has('autoResize') ||
|
|
474
|
+
_changedProperties.has('multiple') ||
|
|
475
|
+
_changedProperties.has('maxOptionsVisible')
|
|
476
|
+
) {
|
|
401
477
|
this.setupResizeObserver();
|
|
402
478
|
}
|
|
479
|
+
this.syncVirtualSelectMaxOptionsVisible();
|
|
403
480
|
if(_changedProperties.has('enableDescription')) {
|
|
404
481
|
this.setEnableDescriptionInOptions();
|
|
405
482
|
}
|
|
483
|
+
if (
|
|
484
|
+
_changedProperties.has('chipMinWidth') ||
|
|
485
|
+
_changedProperties.has('chipMaxWidth')
|
|
486
|
+
) {
|
|
487
|
+
this.destroyChipFit();
|
|
488
|
+
}
|
|
489
|
+
if (this.chipFittingEnabled) {
|
|
490
|
+
this.calculateTotalWidthOfTags();
|
|
491
|
+
}
|
|
492
|
+
if (this.multiple && this.showTooltip) {
|
|
493
|
+
this.scheduleClipCheck();
|
|
494
|
+
}
|
|
406
495
|
}
|
|
407
496
|
|
|
408
497
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
|
@@ -751,9 +840,14 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
751
840
|
el =>
|
|
752
841
|
el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button'
|
|
753
842
|
);
|
|
843
|
+
const isOverflowButton = path.some(
|
|
844
|
+
el =>
|
|
845
|
+
el instanceof HTMLButtonElement &&
|
|
846
|
+
el.classList.contains('select__tags-count')
|
|
847
|
+
);
|
|
754
848
|
|
|
755
849
|
// Ignore disabled controls and clicks on tags (remove buttons)
|
|
756
|
-
if (this.disabled || isIconButton) {
|
|
850
|
+
if (this.disabled || isIconButton || isOverflowButton) {
|
|
757
851
|
return;
|
|
758
852
|
}
|
|
759
853
|
|
|
@@ -951,25 +1045,13 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
951
1045
|
return options;
|
|
952
1046
|
}
|
|
953
1047
|
|
|
954
|
-
private
|
|
1048
|
+
private getOptionPrefixNodes(option: NileOption): Node[] {
|
|
955
1049
|
const prefixSlot = option.shadowRoot?.querySelector(
|
|
956
1050
|
'slot[name="prefix"]'
|
|
957
|
-
) as HTMLSlotElement;
|
|
958
|
-
if (!prefixSlot) return
|
|
959
|
-
|
|
960
|
-
const assignedNodes = prefixSlot.assignedNodes();
|
|
961
|
-
const htmlStrings: string[] = [];
|
|
962
|
-
|
|
963
|
-
assignedNodes.forEach(node => {
|
|
964
|
-
if (node instanceof HTMLElement) {
|
|
965
|
-
htmlStrings.push(node.outerHTML);
|
|
966
|
-
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
967
|
-
// Text node
|
|
968
|
-
htmlStrings.push(node.textContent || '');
|
|
969
|
-
}
|
|
970
|
-
});
|
|
1051
|
+
) as HTMLSlotElement | null;
|
|
1052
|
+
if (!prefixSlot) return [];
|
|
971
1053
|
|
|
972
|
-
return
|
|
1054
|
+
return prefixSlot.assignedElements().map(el => el.cloneNode(true));
|
|
973
1055
|
}
|
|
974
1056
|
|
|
975
1057
|
// Gets the first <nile-option> element
|
|
@@ -1038,6 +1120,9 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1038
1120
|
|
|
1039
1121
|
// Update the value and display label
|
|
1040
1122
|
if (this.multiple) {
|
|
1123
|
+
const hasOptions = this.getAllOptions().length > 0;
|
|
1124
|
+
|
|
1125
|
+
if (hasOptions) {
|
|
1041
1126
|
this.value = this.selectedOptions.map(el => el.value);
|
|
1042
1127
|
|
|
1043
1128
|
if (this.placeholder && this.value.length === 0) {
|
|
@@ -1045,6 +1130,7 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1045
1130
|
this.displayLabel = '';
|
|
1046
1131
|
} else {
|
|
1047
1132
|
this.displayLabel = this.selectedOptions.length + ' selected';
|
|
1133
|
+
}
|
|
1048
1134
|
}
|
|
1049
1135
|
|
|
1050
1136
|
if (this.selectedOptions.length === 0) {
|
|
@@ -1371,58 +1457,363 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1371
1457
|
event.stopPropagation();
|
|
1372
1458
|
}
|
|
1373
1459
|
|
|
1374
|
-
|
|
1375
|
-
if (this.
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1460
|
+
calculateTotalWidthOfTags() {
|
|
1461
|
+
if (!this.chipFittingEnabled) return;
|
|
1462
|
+
|
|
1463
|
+
if (this.chipFitFrame) cancelAnimationFrame(this.chipFitFrame);
|
|
1464
|
+
this.chipFitFrame = requestAnimationFrame(() => {
|
|
1465
|
+
this.chipFitFrame = 0;
|
|
1466
|
+
this.layoutChips();
|
|
1467
|
+
});
|
|
1380
1468
|
}
|
|
1469
|
+
|
|
1470
|
+
private allocateChipWidths(
|
|
1471
|
+
natural: number[],
|
|
1472
|
+
available: number
|
|
1473
|
+
): number[] | null {
|
|
1474
|
+
const min = this.chipMinWidth;
|
|
1475
|
+
const max = this.chipMaxWidth;
|
|
1476
|
+
const total = natural.reduce((sum, w) => sum + w, 0);
|
|
1477
|
+
if (total <= available) {
|
|
1478
|
+
return natural.slice();
|
|
1479
|
+
}
|
|
1480
|
+
// Nothing reveals a truncated value without a tooltip, so chips keep their
|
|
1481
|
+
// natural width and the surplus moves into the +N button instead.
|
|
1482
|
+
if (!this.showTooltip) return null;
|
|
1483
|
+
const capped = natural.map(w => Math.min(w, max));
|
|
1484
|
+
const cappedTotal = capped.reduce((sum, w) => sum + w, 0);
|
|
1485
|
+
if (cappedTotal <= available) {
|
|
1486
|
+
return capped;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const widths = new Array<number>(natural.length).fill(0);
|
|
1490
|
+
let pool = capped.map((_, i) => i);
|
|
1491
|
+
let remaining = available;
|
|
1492
|
+
|
|
1493
|
+
for (;;) {
|
|
1494
|
+
if (pool.length === 0) break;
|
|
1495
|
+
const share = remaining / pool.length;
|
|
1496
|
+
const settled = pool.filter(i => capped[i] <= share);
|
|
1497
|
+
if (settled.length === 0) break;
|
|
1498
|
+
for (const i of settled) {
|
|
1499
|
+
widths[i] = capped[i];
|
|
1500
|
+
remaining -= capped[i];
|
|
1501
|
+
}
|
|
1502
|
+
pool = pool.filter(i => capped[i] > share);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if (pool.length > 0) {
|
|
1506
|
+
const share = remaining / pool.length;
|
|
1507
|
+
if (share < min) {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const width = Math.floor(share);
|
|
1512
|
+
for (const i of pool) {
|
|
1513
|
+
widths[i] = width;
|
|
1381
1514
|
}
|
|
1382
1515
|
}
|
|
1383
1516
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1517
|
+
return widths;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
private computeChipFit(
|
|
1521
|
+
natural: number[],
|
|
1522
|
+
available: number,
|
|
1523
|
+
overflowWidth: number,
|
|
1524
|
+
gap: number
|
|
1525
|
+
): { visibleCount: number; widths: number[]; hiddenCount: number } {
|
|
1526
|
+
const total = natural.length;
|
|
1527
|
+
|
|
1528
|
+
if (total === 0) {
|
|
1529
|
+
return { visibleCount: 0, widths: [], hiddenCount: 0 };
|
|
1397
1530
|
}
|
|
1398
|
-
|
|
1531
|
+
|
|
1532
|
+
for (let count = total; count >= 1; count--) {
|
|
1533
|
+
const hidden = total - count;
|
|
1534
|
+
const reserved = hidden > 0 ? overflowWidth + gap : 0;
|
|
1535
|
+
const row = available - reserved - gap * (count - 1);
|
|
1536
|
+
|
|
1537
|
+
if (row <= 0) continue;
|
|
1538
|
+
|
|
1539
|
+
const widths = this.allocateChipWidths(natural.slice(0, count), row);
|
|
1540
|
+
if (widths) {
|
|
1541
|
+
return { visibleCount: count, widths, hiddenCount: hidden };
|
|
1399
1542
|
}
|
|
1400
|
-
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const room = available - overflowWidth - gap;
|
|
1546
|
+
const fallback = Math.max(
|
|
1547
|
+
0,
|
|
1548
|
+
// A single chip too wide for the row has to be clipped either way; only
|
|
1549
|
+
// the tooltip variant squeezes it to chipMinWidth.
|
|
1550
|
+
Math.min(natural[0], this.showTooltip ? Math.max(this.chipMinWidth, room) : room)
|
|
1551
|
+
);
|
|
1552
|
+
return { visibleCount: 1, widths: [fallback], hiddenCount: total - 1 };
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
private layoutChips(): void {
|
|
1556
|
+
const root = this.shadowRoot;
|
|
1557
|
+
if (!root) return;
|
|
1558
|
+
|
|
1559
|
+
const container = root.querySelector('.select__tags') as HTMLElement | null;
|
|
1560
|
+
if (!container) return;
|
|
1561
|
+
|
|
1562
|
+
const chips = Array.from(
|
|
1563
|
+
container.querySelectorAll('nile-tag')
|
|
1564
|
+
) as HTMLElement[];
|
|
1565
|
+
if (chips.length === 0) {
|
|
1566
|
+
this.hiddenChipCount = 0;
|
|
1401
1567
|
return;
|
|
1402
1568
|
}
|
|
1403
1569
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1570
|
+
const overflowEl = container.querySelector(
|
|
1571
|
+
'.select__tags-count'
|
|
1572
|
+
) as HTMLElement | null;
|
|
1573
|
+
|
|
1574
|
+
for (const chip of chips) {
|
|
1575
|
+
chip.style.display = '';
|
|
1576
|
+
chip.style.width = 'auto';
|
|
1577
|
+
chip.style.maxWidth = 'none';
|
|
1578
|
+
chip.style.minWidth = '0';
|
|
1579
|
+
chip.style.flex = '0 0 auto';
|
|
1580
|
+
}
|
|
1581
|
+
const natural = chips.map(chip =>
|
|
1582
|
+
Math.ceil(chip.getBoundingClientRect().width)
|
|
1407
1583
|
);
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1584
|
+
// Not rendered yet on the pass that first finds an overflow; reuse the last width.
|
|
1585
|
+
const overflowWidth = overflowEl
|
|
1586
|
+
? Math.ceil(overflowEl.getBoundingClientRect().width)
|
|
1587
|
+
: this.lastOverflowWidth;
|
|
1588
|
+
if (overflowEl) this.lastOverflowWidth = overflowWidth;
|
|
1589
|
+
|
|
1590
|
+
const containerStyles = getComputedStyle(container);
|
|
1591
|
+
const gap =
|
|
1592
|
+
parseFloat(containerStyles.columnGap || containerStyles.gap || '0') || 0;
|
|
1593
|
+
|
|
1594
|
+
const fit = this.computeChipFit(
|
|
1595
|
+
natural,
|
|
1596
|
+
container.clientWidth,
|
|
1597
|
+
overflowWidth,
|
|
1598
|
+
gap
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
chips.forEach((chip, index) => {
|
|
1602
|
+
if (index < fit.visibleCount) {
|
|
1603
|
+
// Never squeeze a chip below chipMinWidth: narrower than that it is
|
|
1604
|
+
// also narrower than the tag's own box - the label is already at zero
|
|
1605
|
+
// while the remove button is fixed - so the box overflows the chip and
|
|
1606
|
+
// paints the X over the next chip or the +N button. A chip whose
|
|
1607
|
+
// natural width is already smaller is left alone, not padded out.
|
|
1608
|
+
const width = Math.min(
|
|
1609
|
+
natural[index],
|
|
1610
|
+
Math.max(fit.widths[index], this.chipMinWidth)
|
|
1611
|
+
);
|
|
1612
|
+
chip.style.display = '';
|
|
1613
|
+
chip.style.width = `${width}px`;
|
|
1614
|
+
chip.style.maxWidth = `${width}px`;
|
|
1615
|
+
chip.style.flex = '0 0 auto';
|
|
1616
|
+
} else {
|
|
1617
|
+
chip.style.display = 'none';
|
|
1411
1618
|
}
|
|
1619
|
+
});
|
|
1412
1620
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1621
|
+
if (
|
|
1622
|
+
this.lastChipLayout.hidden !== fit.hiddenCount ||
|
|
1623
|
+
this.lastChipLayout.visible !== fit.visibleCount
|
|
1624
|
+
) {
|
|
1625
|
+
this.lastChipLayout = {
|
|
1626
|
+
hidden: fit.hiddenCount,
|
|
1627
|
+
visible: fit.visibleCount
|
|
1628
|
+
};
|
|
1629
|
+
this.hiddenChipCount = fit.hiddenCount;
|
|
1630
|
+
}
|
|
1415
1631
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1632
|
+
this.refreshClippedChips();
|
|
1633
|
+
}
|
|
1418
1634
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1635
|
+
private isChipClipped(el: HTMLElement | null | undefined): boolean {
|
|
1636
|
+
if (!el) return false;
|
|
1637
|
+
|
|
1638
|
+
const root = el.shadowRoot;
|
|
1639
|
+
if (root) {
|
|
1640
|
+
const content = root.querySelector(
|
|
1641
|
+
'[part="content"]'
|
|
1642
|
+
) as HTMLElement | null;
|
|
1643
|
+
if (content && content.scrollWidth > content.clientWidth + 1) return true;
|
|
1644
|
+
|
|
1645
|
+
const inner = root.querySelector(
|
|
1646
|
+
'[part="prefix_content_wrapper"]'
|
|
1647
|
+
) as HTMLElement | null;
|
|
1648
|
+
if (inner && inner.scrollWidth > inner.clientWidth + 1) return true;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return el.scrollWidth > el.clientWidth + 1;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
private refreshClippedChips(): void {
|
|
1655
|
+
if (!this.multiple) return;
|
|
1656
|
+
|
|
1657
|
+
const root = this.shadowRoot;
|
|
1658
|
+
if (!root) return;
|
|
1659
|
+
|
|
1660
|
+
const chips = Array.from(
|
|
1661
|
+
root.querySelectorAll('.select__tags nile-tag')
|
|
1662
|
+
) as HTMLElement[];
|
|
1663
|
+
|
|
1664
|
+
const next = chips.map(chip => this.isChipClipped(chip));
|
|
1665
|
+
|
|
1666
|
+
const same =
|
|
1667
|
+
next.length === this.clippedChips.length &&
|
|
1668
|
+
next.every((v, i) => v === this.clippedChips[i]);
|
|
1669
|
+
|
|
1670
|
+
if (!same) {
|
|
1671
|
+
this.clippedChips = next;
|
|
1422
1672
|
}
|
|
1423
1673
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1674
|
+
|
|
1675
|
+
private scheduleClipCheck(): void {
|
|
1676
|
+
if (this.clipFrame) cancelAnimationFrame(this.clipFrame);
|
|
1677
|
+
this.clipFrame = requestAnimationFrame(() => {
|
|
1678
|
+
this.clipFrame = 0;
|
|
1679
|
+
this.refreshClippedChips();
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/** A chip gets a tooltip only while its text is measured as clipped. */
|
|
1684
|
+
private chipTooltipDisabled(index: number): boolean {
|
|
1685
|
+
return !this.showTooltip || !(this.clippedChips[index] ?? false);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
private destroyChipFit(): void {
|
|
1689
|
+
if (this.chipFitFrame) cancelAnimationFrame(this.chipFitFrame);
|
|
1690
|
+
this.chipFitFrame = 0;
|
|
1691
|
+
if (this.clipFrame) cancelAnimationFrame(this.clipFrame);
|
|
1692
|
+
this.clipFrame = 0;
|
|
1693
|
+
this.lastChipLayout = { hidden: -1, visible: -1 };
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private getChipLabel(option: NileOption): string {
|
|
1697
|
+
return (option.getTextLabel?.() ?? '').trim();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
private handleChipKeyDown(event: KeyboardEvent, option: NileOption): void {
|
|
1701
|
+
if (event.key !== KeyCode.BACKSPACE && event.key !== KeyCode.DELETE) return;
|
|
1702
|
+
if (this.disabled) return;
|
|
1703
|
+
|
|
1704
|
+
event.preventDefault();
|
|
1705
|
+
event.stopPropagation();
|
|
1706
|
+
|
|
1707
|
+
const removedIndex = this.selectedOptions.indexOf(option);
|
|
1708
|
+
|
|
1709
|
+
this.handleTagRemove(
|
|
1710
|
+
new CustomEvent('nile-remove') as unknown as NileRemoveEvent,
|
|
1711
|
+
option
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
this.restoreFocusAfterChipRemoval(removedIndex);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** Moves focus to the next chip, the last chip, or the field after a removal. */
|
|
1718
|
+
private async restoreFocusAfterChipRemoval(
|
|
1719
|
+
removedIndex: number
|
|
1720
|
+
): Promise<void> {
|
|
1721
|
+
if (removedIndex < 0) return;
|
|
1722
|
+
|
|
1723
|
+
await this.updateComplete;
|
|
1724
|
+
|
|
1725
|
+
// Wait for the fit pass so hidden chips are not focused.
|
|
1726
|
+
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
|
1727
|
+
|
|
1728
|
+
const visibleChips = Array.from(
|
|
1729
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.select__tags nile-tag') ??
|
|
1730
|
+
[]
|
|
1731
|
+
).filter(chip => chip.style.display !== 'none');
|
|
1732
|
+
|
|
1733
|
+
const target =
|
|
1734
|
+
visibleChips[removedIndex] ?? visibleChips[visibleChips.length - 1];
|
|
1735
|
+
|
|
1736
|
+
if (target) {
|
|
1737
|
+
target.focus({ preventScroll: true });
|
|
1738
|
+
} else {
|
|
1739
|
+
this.displayInput?.focus({ preventScroll: true });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
private handleOverflowActivate(event: Event): void {
|
|
1744
|
+
event.preventDefault();
|
|
1745
|
+
event.stopPropagation();
|
|
1746
|
+
if (this.disabled) return;
|
|
1747
|
+
if (this.open) {
|
|
1748
|
+
this.hide();
|
|
1749
|
+
} else {
|
|
1750
|
+
this.show();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
private handleOverflowKeyDown(event: KeyboardEvent): void {
|
|
1755
|
+
if (event.key !== KeyCode.ENTER && event.key !== KeyCode.SPACE) return;
|
|
1756
|
+
this.handleOverflowActivate(event);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
private renderFittedTags() {
|
|
1760
|
+
return html`
|
|
1761
|
+
<div part="tags" class="select__tags select__tags--fitted">
|
|
1762
|
+
${this.selectedOptions.map((option, index) => {
|
|
1763
|
+
const label = this.getChipLabel(option);
|
|
1764
|
+
const prefixNodes = this.getOptionPrefixNodes(option);
|
|
1765
|
+
return html`
|
|
1766
|
+
<nile-lite-tooltip
|
|
1767
|
+
class="select__tag-tip"
|
|
1768
|
+
.content=${label}
|
|
1769
|
+
?disabled=${this.chipTooltipDisabled(index)}
|
|
1770
|
+
>
|
|
1771
|
+
<nile-tag
|
|
1772
|
+
part="tag"
|
|
1773
|
+
exportparts="
|
|
1774
|
+
base:tag__base,
|
|
1775
|
+
content:tag__content,
|
|
1776
|
+
remove-button:tag__remove-button,
|
|
1777
|
+
remove-button__base:tag__remove-button__base,
|
|
1778
|
+
prefix:tag__prefix,
|
|
1779
|
+
prefix_content_wrapper:tag__prefix_content_wrapper
|
|
1780
|
+
"
|
|
1781
|
+
?pill=${this.pill}
|
|
1782
|
+
size=${this.size}
|
|
1783
|
+
removable
|
|
1784
|
+
tabindex="0"
|
|
1785
|
+
aria-label=${`${label}. Press Backspace or Delete to remove.`}
|
|
1786
|
+
@nile-remove=${(event: NileRemoveEvent) =>
|
|
1787
|
+
this.handleTagRemove(event, option)}
|
|
1788
|
+
@keydown=${(event: KeyboardEvent) =>
|
|
1789
|
+
this.handleChipKeyDown(event, option)}
|
|
1790
|
+
>
|
|
1791
|
+
${prefixNodes.length
|
|
1792
|
+
? html`<span class="tag__prefix" slot="prefix"
|
|
1793
|
+
>${prefixNodes}</span
|
|
1794
|
+
>`
|
|
1795
|
+
: ''}
|
|
1796
|
+
${label}
|
|
1797
|
+
</nile-tag>
|
|
1798
|
+
</nile-lite-tooltip>
|
|
1799
|
+
`;
|
|
1800
|
+
})}
|
|
1801
|
+
${this.hiddenChipCount > 0
|
|
1802
|
+
? html`<button
|
|
1803
|
+
type="button"
|
|
1804
|
+
class="select__tags-count"
|
|
1805
|
+
?disabled=${this.disabled}
|
|
1806
|
+
aria-controls="listbox"
|
|
1807
|
+
aria-expanded=${this.open ? 'true' : 'false'}
|
|
1808
|
+
aria-haspopup="listbox"
|
|
1809
|
+
aria-label=${`${this.hiddenChipCount} more selected`}
|
|
1810
|
+
@click=${this.handleOverflowActivate}
|
|
1811
|
+
@keydown=${this.handleOverflowKeyDown}
|
|
1812
|
+
>+${this.hiddenChipCount}</button
|
|
1813
|
+
>`
|
|
1814
|
+
: ''}
|
|
1815
|
+
</div>
|
|
1816
|
+
`;
|
|
1426
1817
|
}
|
|
1427
1818
|
|
|
1428
1819
|
handleInputAfterInit() {
|
|
@@ -1444,9 +1835,9 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1444
1835
|
const isPlaceholderVisible = this.placeholder && this.value.length === 0;
|
|
1445
1836
|
const hasHelpText = (this.helpText || this.help_text) ? true : false;
|
|
1446
1837
|
const hasErrorMessage = this.errorMessage ? true : false;
|
|
1447
|
-
const
|
|
1448
|
-
? this.
|
|
1449
|
-
:
|
|
1838
|
+
const prefixNodes = this.selectedOptions[0]
|
|
1839
|
+
? this.getOptionPrefixNodes(this.selectedOptions[0])
|
|
1840
|
+
: [];
|
|
1450
1841
|
|
|
1451
1842
|
return html`
|
|
1452
1843
|
<div
|
|
@@ -1521,9 +1912,9 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1521
1912
|
@keydown=${this.handleComboboxKeyDown}
|
|
1522
1913
|
@mousedown=${this.handleComboboxMouseDown}
|
|
1523
1914
|
>
|
|
1524
|
-
${
|
|
1915
|
+
${prefixNodes.length && !this.multiple
|
|
1525
1916
|
? html`<div class="select__prefix--from-options">
|
|
1526
|
-
${
|
|
1917
|
+
${prefixNodes}
|
|
1527
1918
|
</div>`
|
|
1528
1919
|
: html`<slot
|
|
1529
1920
|
part="prefix"
|
|
@@ -1558,21 +1949,24 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1558
1949
|
/>
|
|
1559
1950
|
|
|
1560
1951
|
${this.multiple
|
|
1561
|
-
?
|
|
1952
|
+
? this.chipFittingEnabled
|
|
1953
|
+
? this.renderFittedTags()
|
|
1954
|
+
: html`
|
|
1562
1955
|
<div part="tags" class="select__tags">
|
|
1563
1956
|
${this.selectedOptions.map((option, index) => {
|
|
1564
1957
|
if (
|
|
1565
1958
|
index < this.maxOptionsVisible ||
|
|
1566
1959
|
this.maxOptionsVisible <= 0
|
|
1567
1960
|
) {
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
index + 1 > this.oldMaxOptionsVisible &&
|
|
1571
|
-
this.maxOptionsVisible === Infinity,
|
|
1572
|
-
};
|
|
1961
|
+
const prefixNodes =
|
|
1962
|
+
this.getOptionPrefixNodes(option);
|
|
1573
1963
|
return html`
|
|
1964
|
+
<nile-lite-tooltip
|
|
1965
|
+
class="select__tag-tip"
|
|
1966
|
+
.content=${option.getTextLabel()}
|
|
1967
|
+
?disabled=${this.chipTooltipDisabled(index)}
|
|
1968
|
+
>
|
|
1574
1969
|
<nile-tag
|
|
1575
|
-
class=${classMap(classes)}
|
|
1576
1970
|
part="tag"
|
|
1577
1971
|
exportparts="
|
|
1578
1972
|
base:tag__base,
|
|
@@ -1582,21 +1976,21 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1582
1976
|
prefix:tag__prefix,
|
|
1583
1977
|
prefix_content_wrapper:tag__prefix_content_wrapper
|
|
1584
1978
|
"
|
|
1979
|
+
style="max-width:${this.chipMaxWidth}px"
|
|
1585
1980
|
?pill=${this.pill}
|
|
1586
1981
|
size=${this.size}
|
|
1587
1982
|
removable
|
|
1588
1983
|
@nile-remove=${(event: NileRemoveEvent) =>
|
|
1589
1984
|
this.handleTagRemove(event, option)}
|
|
1590
1985
|
>
|
|
1591
|
-
${
|
|
1986
|
+
${prefixNodes.length
|
|
1592
1987
|
? html`<span class="tag__prefix" slot="prefix"
|
|
1593
|
-
>${
|
|
1594
|
-
this.getOptionPrefix(option)
|
|
1595
|
-
)}</span
|
|
1988
|
+
>${prefixNodes}</span
|
|
1596
1989
|
>`
|
|
1597
1990
|
: ''}
|
|
1598
1991
|
${option.getTextLabel()}
|
|
1599
1992
|
</nile-tag>
|
|
1993
|
+
</nile-lite-tooltip>
|
|
1600
1994
|
`;
|
|
1601
1995
|
} else if (index === this.maxOptionsVisible) {
|
|
1602
1996
|
return html`
|
|
@@ -1737,21 +2131,35 @@ export class NileSelect extends NileElement implements NileFormControl{
|
|
|
1737
2131
|
@click="${this.handleFooterClick}"
|
|
1738
2132
|
>
|
|
1739
2133
|
<span
|
|
2134
|
+
class="select__footer-action"
|
|
1740
2135
|
@click="${this.toggleShowSelected}"
|
|
1741
2136
|
style="cursor: pointer;"
|
|
2137
|
+
title="Show selected"
|
|
1742
2138
|
>
|
|
1743
2139
|
<nile-checkbox
|
|
1744
2140
|
?disabled="${this.selectedOptions.length === 0}"
|
|
1745
2141
|
?checked="${this.showSelected}"
|
|
1746
2142
|
>
|
|
1747
|
-
|
|
2143
|
+
<span class="select__footer-label"
|
|
2144
|
+
>Selected</span
|
|
2145
|
+
></nile-checkbox
|
|
1748
2146
|
>
|
|
1749
2147
|
</span>
|
|
1750
2148
|
${this.selectedOptions.length !== 0
|
|
1751
2149
|
? html`<span
|
|
1752
2150
|
class="select__clear"
|
|
1753
2151
|
@click="${this.unSlectAll}"
|
|
1754
|
-
|
|
2152
|
+
title="Clear"
|
|
2153
|
+
><nile-glyph
|
|
2154
|
+
class="select__footer-glyph"
|
|
2155
|
+
name="ng-eraser"
|
|
2156
|
+
size="16"
|
|
2157
|
+
method="var(--nile-svg-method-stroke, var(--ng-svg-method-stroke))"
|
|
2158
|
+
color="var(--nile-colors-primary-600, var(--ng-colors-text-brand-secondary-700))"
|
|
2159
|
+
></nile-glyph
|
|
2160
|
+
><span class="select__footer-label"
|
|
2161
|
+
>Clear</span
|
|
2162
|
+
></span
|
|
1755
2163
|
>`
|
|
1756
2164
|
: ``}
|
|
1757
2165
|
</div>`
|