@aquera/nile-elements 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +619 -252
  3. package/dist/nile-select/index.cjs.js +1 -1
  4. package/dist/nile-select/index.esm.js +1 -1
  5. package/dist/nile-select/nile-select.cjs.js +2 -1
  6. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  7. package/dist/nile-select/nile-select.css.cjs.js +1 -1
  8. package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
  9. package/dist/nile-select/nile-select.css.esm.js +126 -6
  10. package/dist/nile-select/nile-select.esm.js +77 -11
  11. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  12. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  13. package/dist/nile-select/virtual-scroll-helper.esm.js +3 -1
  14. package/dist/nile-virtual-select/index.cjs.js +1 -1
  15. package/dist/nile-virtual-select/index.esm.js +1 -1
  16. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -3
  17. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  18. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
  19. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
  20. package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +126 -6
  21. package/dist/nile-virtual-select/nile-virtual-select.esm.js +71 -12
  22. package/dist/src/nile-select/nile-select.css.js +126 -6
  23. package/dist/src/nile-select/nile-select.css.js.map +1 -1
  24. package/dist/src/nile-select/nile-select.d.ts +43 -5
  25. package/dist/src/nile-select/nile-select.js +445 -106
  26. package/dist/src/nile-select/nile-select.js.map +1 -1
  27. package/dist/src/nile-select/nile-select.test.d.ts +1 -0
  28. package/dist/src/nile-select/nile-select.test.js +342 -1
  29. package/dist/src/nile-select/nile-select.test.js.map +1 -1
  30. package/dist/src/nile-select/virtual-scroll-helper.js +3 -1
  31. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  32. package/dist/src/nile-virtual-select/nile-virtual-select.css.js +126 -6
  33. package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
  34. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +45 -4
  35. package/dist/src/nile-virtual-select/nile-virtual-select.js +385 -59
  36. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  37. package/dist/src/nile-virtual-select/nile-virtual-select.test.js +253 -1
  38. package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/version.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +1 -1
  43. package/src/nile-select/nile-select.css.ts +126 -6
  44. package/src/nile-select/nile-select.test.ts +478 -1
  45. package/src/nile-select/nile-select.ts +499 -91
  46. package/src/nile-select/virtual-scroll-helper.ts +3 -1
  47. package/src/nile-virtual-select/nile-virtual-select.css.ts +126 -6
  48. package/src/nile-virtual-select/nile-virtual-select.test.ts +352 -1
  49. package/src/nile-virtual-select/nile-virtual-select.ts +460 -55
  50. package/vscode-html-custom-data.json +40 -6
@@ -15,12 +15,14 @@ import { styles } from './nile-virtual-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 '../nile-checkbox/nile-checkbox';
19
20
  import '../nile-loader/nile-loader';
20
21
  import { animateTo, stopAnimations } from '../internal/animate';
21
22
  import { classMap } from 'lit/directives/class-map.js';
22
23
  import { query, state } from 'lit/decorators.js';
23
24
  import { defaultValue } from '../internal/default-value';
25
+ import { KeyCode } from '../internal/enum';
24
26
  import { FormControlController } from '../internal/form';
25
27
  import {
26
28
  getAnimation,
@@ -92,6 +94,10 @@ import { VisibilityManager } from '../utilities/visibility-manager.js';
92
94
  * @csspart listbox - The listbox container where options are slotted.
93
95
  * @csspart tags - The container that houses option tags when `multiple` is used.
94
96
  * @csspart tag - The individual tags that represent each multiselect option.
97
+ * @csspart tag__base - The tag's base part.
98
+ * @csspart tag__content - The tag's content part.
99
+ * @csspart tag__remove-button - The tag's remove button.
100
+ * @csspart tag__remove-button__base - The tag's remove button base part.
95
101
  * @csspart clear-button - The clear button.
96
102
  * @csspart expand-icon - The container that wraps the expand icon.
97
103
  * @csspart footer - The footer container with show selected and clear all options.
@@ -155,8 +161,8 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
155
161
  /** Placeholder text to show as a hint when the select is empty. */
156
162
  @property() placeholder = 'Select...';
157
163
 
158
- /** Enable automatic resizing of tags area */
159
- @property({ type: Boolean }) autoResize = false;
164
+ /** Re-runs the fitted-chip layout when the control's own width changes. */
165
+ @property({ type: Boolean, attribute: true, reflect: true }) autoResize = false;
160
166
 
161
167
  /** Current search value */
162
168
  @state() searchValue: string = '';
@@ -264,10 +270,49 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
264
270
  * The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
265
271
  * indicate the number of additional items that are selected. Set to 0 to remove the limit.
266
272
  */
273
+ private _maxOptionsVisible = 3;
274
+
275
+ private maxOptionsVisibleExplicit = false;
276
+
267
277
  @property({ attribute: 'max-options-visible', type: Number })
268
- maxOptionsVisible = 3;
278
+ get maxOptionsVisible(): number {
279
+ return this._maxOptionsVisible;
280
+ }
281
+
282
+ set maxOptionsVisible(value: number) {
283
+ const previous = this._maxOptionsVisible;
284
+ this._maxOptionsVisible = value;
285
+ // Any assignment opts this instance out of automatic chip fitting.
286
+ this.maxOptionsVisibleExplicit = true;
287
+ this.requestUpdate('maxOptionsVisible', previous);
288
+ }
289
+
290
+ private get chipFittingEnabled(): boolean {
291
+ return !!this.multiple && !this.maxOptionsVisibleExplicit;
292
+ }
293
+
294
+ /** Minimum width, in px, of a squeezed chip. Needs `showtooltip`. */
295
+ @property({ type: Number, reflect: true, attribute: true }) chipMinWidth = 110;
269
296
 
270
- @state() oldMaxOptionsVisible: number = 1;
297
+ /** Maximum width, in px, of a squeezed chip. Needs `showtooltip`. */
298
+ @property({ type: Number, reflect: true, attribute: true }) chipMaxWidth = 180;
299
+
300
+ /**
301
+ * Shows a chip's full value in a tooltip when its text is clipped, and is
302
+ * what allows a chip to be truncated at all.
303
+ */
304
+ @property({ type: Boolean, attribute: true, reflect: true }) showTooltip = false;
305
+
306
+
307
+ @state() private hiddenChipCount = 0;
308
+
309
+ /** Last measured width of the overflow button, in px. */
310
+ private lastOverflowWidth = 0;
311
+
312
+ private chipFitFrame = 0;
313
+ private lastChipLayout = { hidden: -1, visible: -1 };
314
+ @state() private clippedChips: boolean[] = [];
315
+ private clipFrame = 0;
271
316
 
272
317
  @property({ type: Boolean, reflect: true, attribute: true }) enableTabClose = false;
273
318
 
@@ -326,19 +371,31 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
326
371
  }
327
372
  // Clean up body append elements
328
373
  this.portalManager.cleanupPortalAppend();
374
+ this.destroyChipFit();
329
375
  }
330
376
 
331
377
  protected updated(changedProperties: PropertyValues): void {
332
378
  if(changedProperties.has('value')) {
333
379
  this.selectionChanged();
334
380
  }
335
- if (changedProperties.has('autoResize')) {
336
- const tagsDiv = this.shadowRoot?.querySelector('.select__tags') as HTMLElement;
337
- if (this.autoResize && tagsDiv) {
338
- this.resizeController.observe(tagsDiv);
339
- } else if (tagsDiv) {
340
- this.resizeController.unobserve(tagsDiv);
381
+ if (
382
+ changedProperties.has('autoResize') ||
383
+ changedProperties.has('multiple') ||
384
+ changedProperties.has('maxOptionsVisible')
385
+ ) {
386
+ this.setupResizeObserver();
387
+ }
388
+ if (
389
+ changedProperties.has('chipMinWidth') ||
390
+ changedProperties.has('chipMaxWidth')
391
+ ) {
392
+ this.destroyChipFit();
341
393
  }
394
+ if (this.chipFittingEnabled) {
395
+ this.calculateTotalWidthOfTags();
396
+ }
397
+ if (this.multiple && this.showTooltip) {
398
+ this.scheduleClipCheck();
342
399
  }
343
400
  }
344
401
 
@@ -632,6 +689,8 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
632
689
  this.open = !this.open;
633
690
  }
634
691
 
692
+ private observedTagsContainer?: Element;
693
+
635
694
  private resizeController = new ResizeController(this, {
636
695
  callback: (entries: ResizeObserverEntry[]) => {
637
696
  for (const entry of entries) {
@@ -648,7 +707,12 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
648
707
  el =>
649
708
  el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button'
650
709
  );
651
- return this.disabled || isIconButton;
710
+ const isOverflowButton = path.some(
711
+ el =>
712
+ el instanceof HTMLButtonElement &&
713
+ el.classList.contains('select__tags-count')
714
+ );
715
+ return this.disabled || isIconButton || isOverflowButton;
652
716
  }
653
717
 
654
718
  private handleComboboxKeyDown(event: KeyboardEvent): void {
@@ -1310,7 +1374,65 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1310
1374
  `;
1311
1375
  }
1312
1376
 
1377
+ private renderFittedTags(): TemplateResult {
1378
+ return html`
1379
+ <div part="tags" class="select__tags select__tags--fitted">
1380
+ ${this.selectedOptions.map((option, index) => {
1381
+ const label = this.getChipLabel(option);
1382
+ return html`
1383
+ <nile-lite-tooltip
1384
+ class="select__tag-tip"
1385
+ .content=${label}
1386
+ ?disabled=${this.chipTooltipDisabled(index)}
1387
+ >
1388
+ <nile-tag
1389
+ part="tag"
1390
+ exportparts="
1391
+ base:tag__base,
1392
+ content:tag__content,
1393
+ remove-button:tag__remove-button,
1394
+ remove-button__base:tag__remove-button__base,
1395
+ prefix:tag__prefix,
1396
+ prefix_content_wrapper:tag__prefix_content_wrapper
1397
+ "
1398
+ ?pill=${this.pill}
1399
+ size=${this.size}
1400
+ removable
1401
+ tabindex="0"
1402
+ aria-label=${`${label}. Press Backspace or Delete to remove.`}
1403
+ @nile-remove=${(event: NileRemoveEvent) =>
1404
+ this.handleTagRemove(event, option)}
1405
+ @keydown=${(event: KeyboardEvent) =>
1406
+ this.handleChipKeyDown(event, option)}
1407
+ >
1408
+ ${label}
1409
+ </nile-tag>
1410
+ </nile-lite-tooltip>
1411
+ `;
1412
+ })}
1413
+ ${this.hiddenChipCount > 0
1414
+ ? html`<button
1415
+ type="button"
1416
+ class="select__tags-count"
1417
+ ?disabled=${this.disabled}
1418
+ aria-controls="listbox"
1419
+ aria-expanded=${this.open ? 'true' : 'false'}
1420
+ aria-haspopup="listbox"
1421
+ aria-label=${`${this.hiddenChipCount} more selected`}
1422
+ @click=${this.handleOverflowActivate}
1423
+ @keydown=${this.handleOverflowKeyDown}
1424
+ >+${this.hiddenChipCount}</button
1425
+ >`
1426
+ : ''}
1427
+ </div>
1428
+ `;
1429
+ }
1430
+
1313
1431
  private renderTags(): TemplateResult {
1432
+ if (this.chipFittingEnabled) {
1433
+ return this.renderFittedTags();
1434
+ }
1435
+
1314
1436
  return this.multiple
1315
1437
  ? html`
1316
1438
  <div part="tags" class="select__tags">
@@ -1319,14 +1441,13 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1319
1441
  index < this.maxOptionsVisible ||
1320
1442
  this.maxOptionsVisible <= 0
1321
1443
  ) {
1322
- const classes = {
1323
- select__invisible:
1324
- index + 1 > this.oldMaxOptionsVisible &&
1325
- this.maxOptionsVisible === Infinity,
1326
- };
1327
1444
  return html`
1445
+ <nile-lite-tooltip
1446
+ class="select__tag-tip"
1447
+ .content=${option.getTextLabel()}
1448
+ ?disabled=${this.chipTooltipDisabled(index)}
1449
+ >
1328
1450
  <nile-tag
1329
- class=${classMap(classes)}
1330
1451
  part="tag"
1331
1452
  exportparts="
1332
1453
  base:tag__base,
@@ -1334,6 +1455,7 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1334
1455
  remove-button:tag__remove-button,
1335
1456
  remove-button__base:tag__remove-button__base
1336
1457
  "
1458
+ style="max-width:${this.chipMaxWidth}px"
1337
1459
  ?pill=${this.pill}
1338
1460
  size=${this.size}
1339
1461
  removable
@@ -1342,6 +1464,7 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1342
1464
  >
1343
1465
  ${option.getTextLabel()}
1344
1466
  </nile-tag>
1467
+ </nile-lite-tooltip>
1345
1468
  `;
1346
1469
  } else if (index === this.maxOptionsVisible) {
1347
1470
  return html`
@@ -1496,14 +1619,16 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1496
1619
  @click="${this.handleFooterClick}"
1497
1620
  >
1498
1621
  <span
1622
+ class="select__footer-action"
1499
1623
  @click="${this.toggleShowSelected}"
1500
1624
  style="cursor: pointer;"
1625
+ title="Show selected"
1501
1626
  >
1502
1627
  <nile-checkbox
1503
1628
  ?disabled="${this.selectedOptions.length === 0}"
1504
1629
  ?checked="${this.showSelected}"
1505
1630
  >
1506
- &nbsp; Show Selected</nile-checkbox
1631
+ <span class="select__footer-label">Selected</span></nile-checkbox
1507
1632
  >
1508
1633
  </span>
1509
1634
  ${this.selectedOptions.length !== 0
@@ -1511,8 +1636,16 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1511
1636
  <span
1512
1637
  class="select__clear"
1513
1638
  @click="${this.unSelectAll}"
1639
+ title="Clear"
1514
1640
  >
1515
- Clear All
1641
+ <nile-glyph
1642
+ class="select__footer-glyph"
1643
+ name="ng-eraser"
1644
+ size="16"
1645
+ method="var(--nile-svg-method-stroke, var(--ng-svg-method-stroke))"
1646
+ color="var(--nile-colors-primary-600, var(--ng-colors-text-brand-secondary-700))"
1647
+ ></nile-glyph
1648
+ ><span class="select__footer-label">Clear</span>
1516
1649
  </span>
1517
1650
  `
1518
1651
  : ``}
@@ -1573,59 +1706,331 @@ export class NileVirtualSelect extends NileElement implements NileFormControl {
1573
1706
  });
1574
1707
  }
1575
1708
 
1576
- private calculateWidthOfSelectTagsDiv(): number | null {
1577
- if (this.shadowRoot) {
1578
- const selectTagsDiv = this.shadowRoot.querySelector('.select__tags') as HTMLElement;
1579
- if (selectTagsDiv) {
1580
- const width = selectTagsDiv.offsetWidth;
1581
- return width - 70;
1709
+ private setupResizeObserver(): void {
1710
+ const wanted =
1711
+ this.autoResize && this.chipFittingEnabled
1712
+ ? (this.shadowRoot?.querySelector('.select__tags') as HTMLElement | null) ??
1713
+ undefined
1714
+ : undefined;
1715
+
1716
+ if (wanted === this.observedTagsContainer) {
1717
+ if (!this.chipFittingEnabled) this.destroyChipFit();
1718
+ return;
1719
+ }
1720
+
1721
+ if (this.observedTagsContainer) {
1722
+ this.resizeController.unobserve(this.observedTagsContainer);
1723
+ this.observedTagsContainer = undefined;
1582
1724
  }
1725
+
1726
+ if (wanted) {
1727
+ this.resizeController.observe(wanted);
1728
+ this.observedTagsContainer = wanted;
1729
+ return;
1730
+ }
1731
+
1732
+ if (!this.chipFittingEnabled) {
1733
+ this.destroyChipFit();
1583
1734
  }
1584
- return null;
1585
1735
  }
1586
1736
 
1587
1737
  private calculateTotalWidthOfTags(): void {
1588
- if (this.maxOptionsVisible !== Infinity) {
1589
- this.oldMaxOptionsVisible = this.maxOptionsVisible;
1590
- }
1591
-
1592
- this.maxOptionsVisible = Infinity;
1593
- setTimeout(() => {
1594
- let widths: number[] = [];
1595
- if (this.shadowRoot) {
1596
- const tags = this.shadowRoot.querySelectorAll('nile-tag');
1597
- tags.forEach(tag => {
1598
- if (tag instanceof HTMLElement) {
1599
- widths.push(tag.offsetWidth);
1738
+ if (!this.chipFittingEnabled) return;
1739
+
1740
+ if (this.chipFitFrame) cancelAnimationFrame(this.chipFitFrame);
1741
+ this.chipFitFrame = requestAnimationFrame(() => {
1742
+ this.chipFitFrame = 0;
1743
+ this.layoutChips();
1744
+ });
1745
+ }
1746
+
1747
+ private allocateChipWidths(
1748
+ natural: number[],
1749
+ available: number
1750
+ ): number[] | null {
1751
+ const min = this.chipMinWidth;
1752
+ const max = this.chipMaxWidth;
1753
+ const total = natural.reduce((sum, w) => sum + w, 0);
1754
+
1755
+ if (total <= available) {
1756
+ return natural.slice();
1757
+ }
1758
+
1759
+ // Nothing reveals a truncated value without a tooltip, so chips keep their
1760
+ // natural width and the surplus moves into the +N button instead.
1761
+ if (!this.showTooltip) return null;
1762
+
1763
+ const capped = natural.map(w => Math.min(w, max));
1764
+ const cappedTotal = capped.reduce((sum, w) => sum + w, 0);
1765
+ if (cappedTotal <= available) {
1766
+ return capped;
1600
1767
  }
1601
- });
1768
+
1769
+ const widths = new Array<number>(natural.length).fill(0);
1770
+ let pool = capped.map((_, i) => i);
1771
+ let remaining = available;
1772
+
1773
+ for (;;) {
1774
+ if (pool.length === 0) break;
1775
+ const share = remaining / pool.length;
1776
+ const settled = pool.filter(i => capped[i] <= share);
1777
+ if (settled.length === 0) break;
1778
+ for (const i of settled) {
1779
+ widths[i] = capped[i];
1780
+ remaining -= capped[i];
1602
1781
  }
1603
- if (this.value.length !== widths.length) {
1604
- return;
1782
+ pool = pool.filter(i => capped[i] > share);
1783
+ }
1784
+
1785
+ if (pool.length > 0) {
1786
+ const share = remaining / pool.length;
1787
+
1788
+ if (share < min) {
1789
+ return null;
1790
+ }
1791
+
1792
+ const width = Math.floor(share);
1793
+ for (const i of pool) {
1794
+ widths[i] = width;
1605
1795
  }
1796
+ }
1797
+
1798
+ return widths;
1799
+ }
1800
+
1801
+ private computeChipFit(
1802
+ natural: number[],
1803
+ available: number,
1804
+ overflowWidth: number,
1805
+ gap: number
1806
+ ): { visibleCount: number; widths: number[]; hiddenCount: number } {
1807
+ const total = natural.length;
1808
+
1809
+ if (total === 0) {
1810
+ return { visibleCount: 0, widths: [], hiddenCount: 0 };
1811
+ }
1812
+
1813
+ for (let count = total; count >= 1; count--) {
1814
+ const hidden = total - count;
1815
+ const reserved = hidden > 0 ? overflowWidth + gap : 0;
1816
+ const row = available - reserved - gap * (count - 1);
1817
+
1818
+ if (row <= 0) continue;
1606
1819
 
1607
- let sum = widths.reduce(
1608
- (accumulator, currentValue) => accumulator + currentValue,
1609
- 0
1820
+ const widths = this.allocateChipWidths(natural.slice(0, count), row);
1821
+ if (widths) {
1822
+ return { visibleCount: count, widths, hiddenCount: hidden };
1823
+ }
1824
+ }
1825
+
1826
+ const room = available - overflowWidth - gap;
1827
+ const fallback = Math.max(
1828
+ 0,
1829
+ // A single chip too wide for the row has to be clipped either way; only
1830
+ // the tooltip variant squeezes it to chipMinWidth.
1831
+ Math.min(natural[0], this.showTooltip ? Math.max(this.chipMinWidth, room) : room)
1610
1832
  );
1611
- const widthOfSelectTagsDiv = this.calculateWidthOfSelectTagsDiv();
1612
- if (!widthOfSelectTagsDiv) {
1833
+ return { visibleCount: 1, widths: [fallback], hiddenCount: total - 1 };
1834
+ }
1835
+
1836
+ private layoutChips(): void {
1837
+ const root = this.shadowRoot;
1838
+ if (!root) return;
1839
+
1840
+ const container = root.querySelector('.select__tags') as HTMLElement | null;
1841
+ if (!container) return;
1842
+
1843
+ const chips = Array.from(
1844
+ container.querySelectorAll('nile-tag')
1845
+ ) as HTMLElement[];
1846
+ if (chips.length === 0) {
1847
+ this.hiddenChipCount = 0;
1613
1848
  return;
1614
1849
  }
1615
1850
 
1616
- let summ = 0;
1617
- let lastindex = 0;
1851
+ const overflowEl = container.querySelector(
1852
+ '.select__tags-count'
1853
+ ) as HTMLElement | null;
1618
1854
 
1619
- for (let i = 0; i < widths.length; i++) {
1620
- summ += widths[i];
1855
+ for (const chip of chips) {
1856
+ chip.style.display = '';
1857
+ chip.style.width = 'auto';
1858
+ chip.style.maxWidth = 'none';
1859
+ chip.style.minWidth = '0';
1860
+ chip.style.flex = '0 0 auto';
1861
+ }
1862
+ const natural = chips.map(chip =>
1863
+ Math.ceil(chip.getBoundingClientRect().width)
1864
+ );
1865
+ // Not rendered yet on the pass that first finds an overflow; reuse the last width.
1866
+ const overflowWidth = overflowEl
1867
+ ? Math.ceil(overflowEl.getBoundingClientRect().width)
1868
+ : this.lastOverflowWidth;
1869
+ if (overflowEl) this.lastOverflowWidth = overflowWidth;
1870
+
1871
+ const containerStyles = getComputedStyle(container);
1872
+ const gap =
1873
+ parseFloat(containerStyles.columnGap || containerStyles.gap || '0') || 0;
1874
+
1875
+ const fit = this.computeChipFit(
1876
+ natural,
1877
+ container.clientWidth,
1878
+ overflowWidth,
1879
+ gap
1880
+ );
1621
1881
 
1622
- if (summ > widthOfSelectTagsDiv) {
1623
- lastindex = i;
1624
- break;
1882
+ chips.forEach((chip, index) => {
1883
+ if (index < fit.visibleCount) {
1884
+ // Never squeeze a chip below chipMinWidth: narrower than that it is
1885
+ // also narrower than the tag's own box - the label is already at zero
1886
+ // while the remove button is fixed - so the box overflows the chip and
1887
+ // paints the X over the next chip or the +N button. A chip whose
1888
+ // natural width is already smaller is left alone, not padded out.
1889
+ const width = Math.min(
1890
+ natural[index],
1891
+ Math.max(fit.widths[index], this.chipMinWidth)
1892
+ );
1893
+ chip.style.display = '';
1894
+ chip.style.width = `${width}px`;
1895
+ chip.style.maxWidth = `${width}px`;
1896
+ chip.style.flex = '0 0 auto';
1897
+ } else {
1898
+ chip.style.display = 'none';
1625
1899
  }
1900
+ });
1901
+
1902
+ if (
1903
+ this.lastChipLayout.hidden !== fit.hiddenCount ||
1904
+ this.lastChipLayout.visible !== fit.visibleCount
1905
+ ) {
1906
+ this.lastChipLayout = {
1907
+ hidden: fit.hiddenCount,
1908
+ visible: fit.visibleCount
1909
+ };
1910
+ this.hiddenChipCount = fit.hiddenCount;
1911
+ }
1912
+
1913
+ this.refreshClippedChips();
1914
+ }
1915
+
1916
+ private isChipClipped(el: HTMLElement | null | undefined): boolean {
1917
+ if (!el) return false;
1918
+
1919
+ const root = el.shadowRoot;
1920
+ if (root) {
1921
+ const content = root.querySelector(
1922
+ '[part="content"]'
1923
+ ) as HTMLElement | null;
1924
+ if (content && content.scrollWidth > content.clientWidth + 1) return true;
1925
+
1926
+ const inner = root.querySelector(
1927
+ '[part="prefix_content_wrapper"]'
1928
+ ) as HTMLElement | null;
1929
+ if (inner && inner.scrollWidth > inner.clientWidth + 1) return true;
1930
+ }
1931
+
1932
+ return el.scrollWidth > el.clientWidth + 1;
1933
+ }
1934
+
1935
+ private refreshClippedChips(): void {
1936
+ if (!this.multiple) return;
1937
+
1938
+ const root = this.shadowRoot;
1939
+ if (!root) return;
1940
+
1941
+ const chips = Array.from(
1942
+ root.querySelectorAll('.select__tags nile-tag')
1943
+ ) as HTMLElement[];
1944
+
1945
+ const next = chips.map(chip => this.isChipClipped(chip));
1946
+
1947
+ const same =
1948
+ next.length === this.clippedChips.length &&
1949
+ next.every((v, i) => v === this.clippedChips[i]);
1950
+
1951
+ if (!same) {
1952
+ this.clippedChips = next;
1626
1953
  }
1627
- this.maxOptionsVisible = lastindex;
1628
- }, 1);
1954
+ }
1955
+
1956
+ private scheduleClipCheck(): void {
1957
+ if (this.clipFrame) cancelAnimationFrame(this.clipFrame);
1958
+ this.clipFrame = requestAnimationFrame(() => {
1959
+ this.clipFrame = 0;
1960
+ this.refreshClippedChips();
1961
+ });
1962
+ }
1963
+
1964
+ /** A chip gets a tooltip only while its text is measured as clipped. */
1965
+ private chipTooltipDisabled(index: number): boolean {
1966
+ return !this.showTooltip || !(this.clippedChips[index] ?? false);
1967
+ }
1968
+
1969
+ private destroyChipFit(): void {
1970
+ if (this.chipFitFrame) cancelAnimationFrame(this.chipFitFrame);
1971
+ this.chipFitFrame = 0;
1972
+ if (this.clipFrame) cancelAnimationFrame(this.clipFrame);
1973
+ this.clipFrame = 0;
1974
+ this.lastChipLayout = { hidden: -1, visible: -1 };
1975
+ }
1976
+
1977
+ private getChipLabel(option: VirtualOption): string {
1978
+ return (option.getTextLabel?.() ?? '').trim();
1979
+ }
1980
+
1981
+ private handleChipKeyDown(event: KeyboardEvent, option: VirtualOption): void {
1982
+ if (event.key !== KeyCode.BACKSPACE && event.key !== KeyCode.DELETE) return;
1983
+ if (this.disabled) return;
1984
+
1985
+ event.preventDefault();
1986
+ event.stopPropagation();
1987
+
1988
+ const removedIndex = this.selectedOptions.indexOf(option);
1989
+
1990
+ this.handleTagRemove(
1991
+ new CustomEvent('nile-remove') as unknown as NileRemoveEvent,
1992
+ option
1993
+ );
1994
+
1995
+ this.restoreFocusAfterChipRemoval(removedIndex);
1996
+ }
1997
+
1998
+ /** Moves focus to the next chip, the last chip, or the field after a removal. */
1999
+ private async restoreFocusAfterChipRemoval(
2000
+ removedIndex: number
2001
+ ): Promise<void> {
2002
+ if (removedIndex < 0) return;
2003
+
2004
+ await this.updateComplete;
2005
+
2006
+ // Wait for the fit pass so hidden chips are not focused.
2007
+ await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
2008
+
2009
+ const visibleChips = Array.from(
2010
+ this.shadowRoot?.querySelectorAll<HTMLElement>('.select__tags nile-tag') ??
2011
+ []
2012
+ ).filter(chip => chip.style.display !== 'none');
2013
+
2014
+ const target =
2015
+ visibleChips[removedIndex] ?? visibleChips[visibleChips.length - 1];
2016
+
2017
+ if (target) {
2018
+ target.focus({ preventScroll: true });
2019
+ } else {
2020
+ this.displayInput?.focus({ preventScroll: true });
2021
+ }
2022
+ }
2023
+
2024
+ private handleOverflowActivate(event: Event): void {
2025
+ event.preventDefault();
2026
+ event.stopPropagation();
2027
+ if (this.disabled) return;
2028
+ this.open = !this.open;
2029
+ }
2030
+
2031
+ private handleOverflowKeyDown(event: KeyboardEvent): void {
2032
+ if (event.key !== KeyCode.ENTER && event.key !== KeyCode.SPACE) return;
2033
+ this.handleOverflowActivate(event);
1629
2034
  }
1630
2035
 
1631
2036
  private async resetScrollPosition(): Promise<void> {