@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
@@ -11,10 +11,12 @@ import { styles } from './nile-select.css';
11
11
  import '../nile-icon';
12
12
  import '../nile-popup/nile-popup';
13
13
  import '../nile-tag/nile-tag';
14
+ import '../nile-lite-tooltip/nile-lite-tooltip';
14
15
  import { animateTo, stopAnimations } from '../internal/animate';
15
16
  import { classMap } from 'lit/directives/class-map.js';
16
17
  import { query, state } from 'lit/decorators.js';
17
18
  import { defaultValue } from '../internal/default-value';
19
+ import { KeyCode } from '../internal/enum';
18
20
  import { FormControlController } from '../internal/form';
19
21
  import { getAnimation, setDefaultAnimation, } from '../utilities/animation-registry';
20
22
  import { HasSlotController } from '../internal/slot';
@@ -22,7 +24,6 @@ import { scrollIntoView } from '../internal/scroll';
22
24
  import { waitForEvent } from '../internal/event';
23
25
  import { watch } from '../internal/watch';
24
26
  import NileElement from '../internal/nile-element';
25
- import { unsafeHTML } from 'lit/directives/unsafe-html.js';
26
27
  import { ifDefined } from 'lit/directives/if-defined.js';
27
28
  import '../nile-virtual-select/nile-virtual-select';
28
29
  import { VirtualScrollHelper } from './virtual-scroll-helper';
@@ -88,6 +89,19 @@ import { VisibilityManager } from '../utilities/visibility-manager';
88
89
  * @csspart footer-area - The sticky wrapper around the custom footer and the built-in multi-select footer.
89
90
  */
90
91
  let NileSelect = class NileSelect extends NileElement {
92
+ get maxOptionsVisible() {
93
+ return this._maxOptionsVisible;
94
+ }
95
+ set maxOptionsVisible(value) {
96
+ const previous = this._maxOptionsVisible;
97
+ this._maxOptionsVisible = value;
98
+ // Any assignment opts this instance out of automatic chip fitting.
99
+ this.maxOptionsVisibleExplicit = true;
100
+ this.requestUpdate('maxOptionsVisible', previous);
101
+ }
102
+ get chipFittingEnabled() {
103
+ return !!this.multiple && !this.maxOptionsVisibleExplicit;
104
+ }
91
105
  /** Gets the validity state object */
92
106
  get validity() {
93
107
  return this.valueInput?.validity;
@@ -103,10 +117,26 @@ let NileSelect = class NileSelect extends NileElement {
103
117
  this.typeToSelectString = '';
104
118
  this.scrolling = false;
105
119
  this.options = [];
120
+ this.resizeController = new ResizeController(this, {
121
+ callback: (entries) => {
122
+ for (const entry of entries) {
123
+ if (entry.target.classList.contains('select__tags')) {
124
+ this.calculateTotalWidthOfTags();
125
+ }
126
+ }
127
+ }
128
+ });
129
+ this.chipFitFrame = 0;
130
+ this.lastChipLayout = { hidden: -1, visible: -1 };
131
+ this.clippedChips = [];
132
+ this.clipFrame = 0;
106
133
  this.hasFocus = false;
107
134
  this.displayLabel = '';
108
135
  this.selectedOptions = [];
109
136
  this.oldValue = '';
137
+ this.hiddenChipCount = 0;
138
+ /** Last measured width of the overflow button, in px. */
139
+ this.lastOverflowWidth = 0;
110
140
  /** The name of the select, submitted as a name/value pair with form data. */
111
141
  this.name = '';
112
142
  /**
@@ -141,6 +171,7 @@ let NileSelect = class NileSelect extends NileElement {
141
171
  this.multiple = false;
142
172
  this.helpText = '';
143
173
  this.help_text = '';
174
+ /** Re-runs the fitted-chip layout when the control's own width changes. */
144
175
  this.autoResize = false;
145
176
  this.errorMessage = '';
146
177
  /** Sets the input to a warning state, changing its visual appearance. */
@@ -153,7 +184,17 @@ let NileSelect = class NileSelect extends NileElement {
153
184
  * The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
154
185
  * indicate the number of additional items that are selected. Set to 0 to remove the limit.
155
186
  */
156
- this.maxOptionsVisible = 3;
187
+ this._maxOptionsVisible = 3;
188
+ this.maxOptionsVisibleExplicit = false;
189
+ /** Minimum width, in px, of a squeezed chip. Needs `showtooltip`. */
190
+ this.chipMinWidth = 110;
191
+ /** Maximum width, in px, of a squeezed chip. Needs `showtooltip`. */
192
+ this.chipMaxWidth = 180;
193
+ /**
194
+ * Shows a chip's full value in a tooltip when its text is clipped, and is
195
+ * what allows a chip to be truncated at all.
196
+ */
197
+ this.showTooltip = false;
157
198
  /** Disables the select control. */
158
199
  this.disabled = false;
159
200
  /** Adds a clear button when the select is not empty. */
@@ -188,7 +229,6 @@ let NileSelect = class NileSelect extends NileElement {
188
229
  /** The select's required attribute. */
189
230
  this.required = false;
190
231
  this.showSelected = false;
191
- this.oldMaxOptionsVisible = 1;
192
232
  this.enableTabClose = false;
193
233
  this.showNoResults = false;
194
234
  this.noResultsMessage = 'No results found';
@@ -238,21 +278,38 @@ let NileSelect = class NileSelect extends NileElement {
238
278
  this.visibilityManager?.cleanup();
239
279
  // Clean up body append elements
240
280
  this.portalManager.cleanupPortalAppend();
281
+ this.destroyChipFit();
241
282
  this.emit('nile-destroy');
242
283
  }
243
284
  setupResizeObserver() {
244
- if (this.autoResize) {
245
- const tagsContainer = this.shadowRoot?.querySelector('.select__tags');
246
- if (tagsContainer) {
247
- this.resizeController = new ResizeController(this, {
248
- callback: () => this.calculateTotalWidthOfTags()
249
- });
250
- this.resizeController.observe(tagsContainer);
251
- }
285
+ const wanted = this.autoResize && this.chipFittingEnabled
286
+ ? this.shadowRoot?.querySelector('.select__tags') ??
287
+ undefined
288
+ : undefined;
289
+ if (wanted === this.observedTagsContainer) {
290
+ if (!this.chipFittingEnabled)
291
+ this.destroyChipFit();
292
+ return;
252
293
  }
253
- else {
254
- this.resizeController?.unobserve?.(this.shadowRoot?.querySelector('.select__tags'));
255
- this.resizeController = undefined;
294
+ if (this.observedTagsContainer) {
295
+ this.resizeController.unobserve(this.observedTagsContainer);
296
+ this.observedTagsContainer = undefined;
297
+ }
298
+ if (wanted) {
299
+ this.resizeController.observe(wanted);
300
+ this.observedTagsContainer = wanted;
301
+ return;
302
+ }
303
+ if (!this.chipFittingEnabled) {
304
+ this.destroyChipFit();
305
+ }
306
+ }
307
+ syncVirtualSelectMaxOptionsVisible() {
308
+ if (!this.enableVirtualScroll || !this.maxOptionsVisibleExplicit)
309
+ return;
310
+ const inner = this.shadowRoot?.querySelector('nile-virtual-select');
311
+ if (inner && inner.maxOptionsVisible !== this._maxOptionsVisible) {
312
+ inner.maxOptionsVisible = this._maxOptionsVisible;
256
313
  }
257
314
  }
258
315
  updated(_changedProperties) {
@@ -268,12 +325,25 @@ let NileSelect = class NileSelect extends NileElement {
268
325
  if (_changedProperties.has('tickOnSelect')) {
269
326
  this.setTickOnSelectInOptions(this.tickOnSelect);
270
327
  }
271
- if (_changedProperties.has('autoResize')) {
328
+ if (_changedProperties.has('autoResize') ||
329
+ _changedProperties.has('multiple') ||
330
+ _changedProperties.has('maxOptionsVisible')) {
272
331
  this.setupResizeObserver();
273
332
  }
333
+ this.syncVirtualSelectMaxOptionsVisible();
274
334
  if (_changedProperties.has('enableDescription')) {
275
335
  this.setEnableDescriptionInOptions();
276
336
  }
337
+ if (_changedProperties.has('chipMinWidth') ||
338
+ _changedProperties.has('chipMaxWidth')) {
339
+ this.destroyChipFit();
340
+ }
341
+ if (this.chipFittingEnabled) {
342
+ this.calculateTotalWidthOfTags();
343
+ }
344
+ if (this.multiple && this.showTooltip) {
345
+ this.scheduleClipCheck();
346
+ }
277
347
  }
278
348
  firstUpdated(_changedProperties) {
279
349
  if (this.enableGroupHeader) {
@@ -560,8 +630,10 @@ let NileSelect = class NileSelect extends NileElement {
560
630
  handleComboboxMouseDown(event) {
561
631
  const path = event.composedPath();
562
632
  const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button');
633
+ const isOverflowButton = path.some(el => el instanceof HTMLButtonElement &&
634
+ el.classList.contains('select__tags-count'));
563
635
  // Ignore disabled controls and clicks on tags (remove buttons)
564
- if (this.disabled || isIconButton) {
636
+ if (this.disabled || isIconButton || isOverflowButton) {
565
637
  return;
566
638
  }
567
639
  event.preventDefault();
@@ -727,22 +799,11 @@ let NileSelect = class NileSelect extends NileElement {
727
799
  }
728
800
  return options;
729
801
  }
730
- getOptionPrefix(option) {
802
+ getOptionPrefixNodes(option) {
731
803
  const prefixSlot = option.shadowRoot?.querySelector('slot[name="prefix"]');
732
804
  if (!prefixSlot)
733
- return '';
734
- const assignedNodes = prefixSlot.assignedNodes();
735
- const htmlStrings = [];
736
- assignedNodes.forEach(node => {
737
- if (node instanceof HTMLElement) {
738
- htmlStrings.push(node.outerHTML);
739
- }
740
- else if (node.nodeType === Node.TEXT_NODE) {
741
- // Text node
742
- htmlStrings.push(node.textContent || '');
743
- }
744
- });
745
- return htmlStrings.join('');
805
+ return [];
806
+ return prefixSlot.assignedElements().map(el => el.cloneNode(true));
746
807
  }
747
808
  // Gets the first <nile-option> element
748
809
  getFirstOption() {
@@ -799,13 +860,16 @@ let NileSelect = class NileSelect extends NileElement {
799
860
  this.selectedOptions = this.getAllOptions().filter(el => el.selected);
800
861
  // Update the value and display label
801
862
  if (this.multiple) {
802
- this.value = this.selectedOptions.map(el => el.value);
803
- if (this.placeholder && this.value.length === 0) {
804
- // When no items are selected, keep the value empty so the placeholder shows
805
- this.displayLabel = '';
806
- }
807
- else {
808
- this.displayLabel = this.selectedOptions.length + ' selected';
863
+ const hasOptions = this.getAllOptions().length > 0;
864
+ if (hasOptions) {
865
+ this.value = this.selectedOptions.map(el => el.value);
866
+ if (this.placeholder && this.value.length === 0) {
867
+ // When no items are selected, keep the value empty so the placeholder shows
868
+ this.displayLabel = '';
869
+ }
870
+ else {
871
+ this.displayLabel = this.selectedOptions.length + ' selected';
872
+ }
809
873
  }
810
874
  if (this.selectedOptions.length === 0) {
811
875
  this.showSelected = false;
@@ -1070,49 +1134,293 @@ let NileSelect = class NileSelect extends NileElement {
1070
1134
  // This will stop the event from bubbling up to the parent `nile-select` component
1071
1135
  event.stopPropagation();
1072
1136
  }
1073
- calculateWidthOfSelectTagsDiv() {
1074
- if (this.shadowRoot) {
1075
- const selectTagsDiv = this.shadowRoot.querySelector('div.select__tags');
1076
- if (selectTagsDiv instanceof HTMLElement) {
1077
- const width = selectTagsDiv.offsetWidth;
1078
- return width - 70;
1137
+ calculateTotalWidthOfTags() {
1138
+ if (!this.chipFittingEnabled)
1139
+ return;
1140
+ if (this.chipFitFrame)
1141
+ cancelAnimationFrame(this.chipFitFrame);
1142
+ this.chipFitFrame = requestAnimationFrame(() => {
1143
+ this.chipFitFrame = 0;
1144
+ this.layoutChips();
1145
+ });
1146
+ }
1147
+ allocateChipWidths(natural, available) {
1148
+ const min = this.chipMinWidth;
1149
+ const max = this.chipMaxWidth;
1150
+ const total = natural.reduce((sum, w) => sum + w, 0);
1151
+ if (total <= available) {
1152
+ return natural.slice();
1153
+ }
1154
+ // Nothing reveals a truncated value without a tooltip, so chips keep their
1155
+ // natural width and the surplus moves into the +N button instead.
1156
+ if (!this.showTooltip)
1157
+ return null;
1158
+ const capped = natural.map(w => Math.min(w, max));
1159
+ const cappedTotal = capped.reduce((sum, w) => sum + w, 0);
1160
+ if (cappedTotal <= available) {
1161
+ return capped;
1162
+ }
1163
+ const widths = new Array(natural.length).fill(0);
1164
+ let pool = capped.map((_, i) => i);
1165
+ let remaining = available;
1166
+ for (;;) {
1167
+ if (pool.length === 0)
1168
+ break;
1169
+ const share = remaining / pool.length;
1170
+ const settled = pool.filter(i => capped[i] <= share);
1171
+ if (settled.length === 0)
1172
+ break;
1173
+ for (const i of settled) {
1174
+ widths[i] = capped[i];
1175
+ remaining -= capped[i];
1079
1176
  }
1177
+ pool = pool.filter(i => capped[i] > share);
1080
1178
  }
1081
- }
1082
- calculateTotalWidthOfTags() {
1083
- if (this.maxOptionsVisible !== Infinity) {
1084
- this.oldMaxOptionsVisible = this.maxOptionsVisible;
1085
- }
1086
- this.maxOptionsVisible = Infinity;
1087
- setTimeout(() => {
1088
- let widths = [];
1089
- if (this.shadowRoot) {
1090
- const tags = this.shadowRoot.querySelectorAll('nile-tag');
1091
- tags.forEach(tag => {
1092
- if (tag instanceof HTMLElement) {
1093
- widths.push(tag.offsetWidth);
1094
- }
1095
- });
1179
+ if (pool.length > 0) {
1180
+ const share = remaining / pool.length;
1181
+ if (share < min) {
1182
+ return null;
1096
1183
  }
1097
- if (this.value.length !== widths.length) {
1098
- return;
1184
+ const width = Math.floor(share);
1185
+ for (const i of pool) {
1186
+ widths[i] = width;
1099
1187
  }
1100
- let sum = widths.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
1101
- const widthOfSelectTagsDiv = this.calculateWidthOfSelectTagsDiv();
1102
- if (!widthOfSelectTagsDiv) {
1103
- return;
1188
+ }
1189
+ return widths;
1190
+ }
1191
+ computeChipFit(natural, available, overflowWidth, gap) {
1192
+ const total = natural.length;
1193
+ if (total === 0) {
1194
+ return { visibleCount: 0, widths: [], hiddenCount: 0 };
1195
+ }
1196
+ for (let count = total; count >= 1; count--) {
1197
+ const hidden = total - count;
1198
+ const reserved = hidden > 0 ? overflowWidth + gap : 0;
1199
+ const row = available - reserved - gap * (count - 1);
1200
+ if (row <= 0)
1201
+ continue;
1202
+ const widths = this.allocateChipWidths(natural.slice(0, count), row);
1203
+ if (widths) {
1204
+ return { visibleCount: count, widths, hiddenCount: hidden };
1104
1205
  }
1105
- let summ = 0;
1106
- let lastindex = 0;
1107
- for (let i = 0; i < widths.length; i++) {
1108
- summ += widths[i];
1109
- if (summ > widthOfSelectTagsDiv) {
1110
- lastindex = i;
1111
- break;
1112
- }
1206
+ }
1207
+ const room = available - overflowWidth - gap;
1208
+ const fallback = Math.max(0,
1209
+ // A single chip too wide for the row has to be clipped either way; only
1210
+ // the tooltip variant squeezes it to chipMinWidth.
1211
+ Math.min(natural[0], this.showTooltip ? Math.max(this.chipMinWidth, room) : room));
1212
+ return { visibleCount: 1, widths: [fallback], hiddenCount: total - 1 };
1213
+ }
1214
+ layoutChips() {
1215
+ const root = this.shadowRoot;
1216
+ if (!root)
1217
+ return;
1218
+ const container = root.querySelector('.select__tags');
1219
+ if (!container)
1220
+ return;
1221
+ const chips = Array.from(container.querySelectorAll('nile-tag'));
1222
+ if (chips.length === 0) {
1223
+ this.hiddenChipCount = 0;
1224
+ return;
1225
+ }
1226
+ const overflowEl = container.querySelector('.select__tags-count');
1227
+ for (const chip of chips) {
1228
+ chip.style.display = '';
1229
+ chip.style.width = 'auto';
1230
+ chip.style.maxWidth = 'none';
1231
+ chip.style.minWidth = '0';
1232
+ chip.style.flex = '0 0 auto';
1233
+ }
1234
+ const natural = chips.map(chip => Math.ceil(chip.getBoundingClientRect().width));
1235
+ // Not rendered yet on the pass that first finds an overflow; reuse the last width.
1236
+ const overflowWidth = overflowEl
1237
+ ? Math.ceil(overflowEl.getBoundingClientRect().width)
1238
+ : this.lastOverflowWidth;
1239
+ if (overflowEl)
1240
+ this.lastOverflowWidth = overflowWidth;
1241
+ const containerStyles = getComputedStyle(container);
1242
+ const gap = parseFloat(containerStyles.columnGap || containerStyles.gap || '0') || 0;
1243
+ const fit = this.computeChipFit(natural, container.clientWidth, overflowWidth, gap);
1244
+ chips.forEach((chip, index) => {
1245
+ if (index < fit.visibleCount) {
1246
+ // Never squeeze a chip below chipMinWidth: narrower than that it is
1247
+ // also narrower than the tag's own box - the label is already at zero
1248
+ // while the remove button is fixed - so the box overflows the chip and
1249
+ // paints the X over the next chip or the +N button. A chip whose
1250
+ // natural width is already smaller is left alone, not padded out.
1251
+ const width = Math.min(natural[index], Math.max(fit.widths[index], this.chipMinWidth));
1252
+ chip.style.display = '';
1253
+ chip.style.width = `${width}px`;
1254
+ chip.style.maxWidth = `${width}px`;
1255
+ chip.style.flex = '0 0 auto';
1256
+ }
1257
+ else {
1258
+ chip.style.display = 'none';
1113
1259
  }
1114
- this.maxOptionsVisible = lastindex;
1115
- }, 1);
1260
+ });
1261
+ if (this.lastChipLayout.hidden !== fit.hiddenCount ||
1262
+ this.lastChipLayout.visible !== fit.visibleCount) {
1263
+ this.lastChipLayout = {
1264
+ hidden: fit.hiddenCount,
1265
+ visible: fit.visibleCount
1266
+ };
1267
+ this.hiddenChipCount = fit.hiddenCount;
1268
+ }
1269
+ this.refreshClippedChips();
1270
+ }
1271
+ isChipClipped(el) {
1272
+ if (!el)
1273
+ return false;
1274
+ const root = el.shadowRoot;
1275
+ if (root) {
1276
+ const content = root.querySelector('[part="content"]');
1277
+ if (content && content.scrollWidth > content.clientWidth + 1)
1278
+ return true;
1279
+ const inner = root.querySelector('[part="prefix_content_wrapper"]');
1280
+ if (inner && inner.scrollWidth > inner.clientWidth + 1)
1281
+ return true;
1282
+ }
1283
+ return el.scrollWidth > el.clientWidth + 1;
1284
+ }
1285
+ refreshClippedChips() {
1286
+ if (!this.multiple)
1287
+ return;
1288
+ const root = this.shadowRoot;
1289
+ if (!root)
1290
+ return;
1291
+ const chips = Array.from(root.querySelectorAll('.select__tags nile-tag'));
1292
+ const next = chips.map(chip => this.isChipClipped(chip));
1293
+ const same = next.length === this.clippedChips.length &&
1294
+ next.every((v, i) => v === this.clippedChips[i]);
1295
+ if (!same) {
1296
+ this.clippedChips = next;
1297
+ }
1298
+ }
1299
+ scheduleClipCheck() {
1300
+ if (this.clipFrame)
1301
+ cancelAnimationFrame(this.clipFrame);
1302
+ this.clipFrame = requestAnimationFrame(() => {
1303
+ this.clipFrame = 0;
1304
+ this.refreshClippedChips();
1305
+ });
1306
+ }
1307
+ /** A chip gets a tooltip only while its text is measured as clipped. */
1308
+ chipTooltipDisabled(index) {
1309
+ return !this.showTooltip || !(this.clippedChips[index] ?? false);
1310
+ }
1311
+ destroyChipFit() {
1312
+ if (this.chipFitFrame)
1313
+ cancelAnimationFrame(this.chipFitFrame);
1314
+ this.chipFitFrame = 0;
1315
+ if (this.clipFrame)
1316
+ cancelAnimationFrame(this.clipFrame);
1317
+ this.clipFrame = 0;
1318
+ this.lastChipLayout = { hidden: -1, visible: -1 };
1319
+ }
1320
+ getChipLabel(option) {
1321
+ return (option.getTextLabel?.() ?? '').trim();
1322
+ }
1323
+ handleChipKeyDown(event, option) {
1324
+ if (event.key !== KeyCode.BACKSPACE && event.key !== KeyCode.DELETE)
1325
+ return;
1326
+ if (this.disabled)
1327
+ return;
1328
+ event.preventDefault();
1329
+ event.stopPropagation();
1330
+ const removedIndex = this.selectedOptions.indexOf(option);
1331
+ this.handleTagRemove(new CustomEvent('nile-remove'), option);
1332
+ this.restoreFocusAfterChipRemoval(removedIndex);
1333
+ }
1334
+ /** Moves focus to the next chip, the last chip, or the field after a removal. */
1335
+ async restoreFocusAfterChipRemoval(removedIndex) {
1336
+ if (removedIndex < 0)
1337
+ return;
1338
+ await this.updateComplete;
1339
+ // Wait for the fit pass so hidden chips are not focused.
1340
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
1341
+ const visibleChips = Array.from(this.shadowRoot?.querySelectorAll('.select__tags nile-tag') ??
1342
+ []).filter(chip => chip.style.display !== 'none');
1343
+ const target = visibleChips[removedIndex] ?? visibleChips[visibleChips.length - 1];
1344
+ if (target) {
1345
+ target.focus({ preventScroll: true });
1346
+ }
1347
+ else {
1348
+ this.displayInput?.focus({ preventScroll: true });
1349
+ }
1350
+ }
1351
+ handleOverflowActivate(event) {
1352
+ event.preventDefault();
1353
+ event.stopPropagation();
1354
+ if (this.disabled)
1355
+ return;
1356
+ if (this.open) {
1357
+ this.hide();
1358
+ }
1359
+ else {
1360
+ this.show();
1361
+ }
1362
+ }
1363
+ handleOverflowKeyDown(event) {
1364
+ if (event.key !== KeyCode.ENTER && event.key !== KeyCode.SPACE)
1365
+ return;
1366
+ this.handleOverflowActivate(event);
1367
+ }
1368
+ renderFittedTags() {
1369
+ return html `
1370
+ <div part="tags" class="select__tags select__tags--fitted">
1371
+ ${this.selectedOptions.map((option, index) => {
1372
+ const label = this.getChipLabel(option);
1373
+ const prefixNodes = this.getOptionPrefixNodes(option);
1374
+ return html `
1375
+ <nile-lite-tooltip
1376
+ class="select__tag-tip"
1377
+ .content=${label}
1378
+ ?disabled=${this.chipTooltipDisabled(index)}
1379
+ >
1380
+ <nile-tag
1381
+ part="tag"
1382
+ exportparts="
1383
+ base:tag__base,
1384
+ content:tag__content,
1385
+ remove-button:tag__remove-button,
1386
+ remove-button__base:tag__remove-button__base,
1387
+ prefix:tag__prefix,
1388
+ prefix_content_wrapper:tag__prefix_content_wrapper
1389
+ "
1390
+ ?pill=${this.pill}
1391
+ size=${this.size}
1392
+ removable
1393
+ tabindex="0"
1394
+ aria-label=${`${label}. Press Backspace or Delete to remove.`}
1395
+ @nile-remove=${(event) => this.handleTagRemove(event, option)}
1396
+ @keydown=${(event) => this.handleChipKeyDown(event, option)}
1397
+ >
1398
+ ${prefixNodes.length
1399
+ ? html `<span class="tag__prefix" slot="prefix"
1400
+ >${prefixNodes}</span
1401
+ >`
1402
+ : ''}
1403
+ ${label}
1404
+ </nile-tag>
1405
+ </nile-lite-tooltip>
1406
+ `;
1407
+ })}
1408
+ ${this.hiddenChipCount > 0
1409
+ ? html `<button
1410
+ type="button"
1411
+ class="select__tags-count"
1412
+ ?disabled=${this.disabled}
1413
+ aria-controls="listbox"
1414
+ aria-expanded=${this.open ? 'true' : 'false'}
1415
+ aria-haspopup="listbox"
1416
+ aria-label=${`${this.hiddenChipCount} more selected`}
1417
+ @click=${this.handleOverflowActivate}
1418
+ @keydown=${this.handleOverflowKeyDown}
1419
+ >+${this.hiddenChipCount}</button
1420
+ >`
1421
+ : ''}
1422
+ </div>
1423
+ `;
1116
1424
  }
1117
1425
  handleInputAfterInit() {
1118
1426
  this.shadowRoot?.querySelector('nile-input')?.inputFocus();
@@ -1130,9 +1438,9 @@ let NileSelect = class NileSelect extends NileElement {
1130
1438
  const isPlaceholderVisible = this.placeholder && this.value.length === 0;
1131
1439
  const hasHelpText = (this.helpText || this.help_text) ? true : false;
1132
1440
  const hasErrorMessage = this.errorMessage ? true : false;
1133
- const prefixContent = this.selectedOptions[0]
1134
- ? this.getOptionPrefix(this.selectedOptions[0])
1135
- : '';
1441
+ const prefixNodes = this.selectedOptions[0]
1442
+ ? this.getOptionPrefixNodes(this.selectedOptions[0])
1443
+ : [];
1136
1444
  return html `
1137
1445
  <div
1138
1446
  part="form-control"
@@ -1206,9 +1514,9 @@ let NileSelect = class NileSelect extends NileElement {
1206
1514
  @keydown=${this.handleComboboxKeyDown}
1207
1515
  @mousedown=${this.handleComboboxMouseDown}
1208
1516
  >
1209
- ${prefixContent && !this.multiple
1517
+ ${prefixNodes.length && !this.multiple
1210
1518
  ? html `<div class="select__prefix--from-options">
1211
- ${unsafeHTML(prefixContent)}
1519
+ ${prefixNodes}
1212
1520
  </div>`
1213
1521
  : html `<slot
1214
1522
  part="prefix"
@@ -1243,18 +1551,21 @@ let NileSelect = class NileSelect extends NileElement {
1243
1551
  />
1244
1552
 
1245
1553
  ${this.multiple
1246
- ? html `
1554
+ ? this.chipFittingEnabled
1555
+ ? this.renderFittedTags()
1556
+ : html `
1247
1557
  <div part="tags" class="select__tags">
1248
1558
  ${this.selectedOptions.map((option, index) => {
1249
- if (index < this.maxOptionsVisible ||
1250
- this.maxOptionsVisible <= 0) {
1251
- const classes = {
1252
- select__invisible: index + 1 > this.oldMaxOptionsVisible &&
1253
- this.maxOptionsVisible === Infinity,
1254
- };
1255
- return html `
1559
+ if (index < this.maxOptionsVisible ||
1560
+ this.maxOptionsVisible <= 0) {
1561
+ const prefixNodes = this.getOptionPrefixNodes(option);
1562
+ return html `
1563
+ <nile-lite-tooltip
1564
+ class="select__tag-tip"
1565
+ .content=${option.getTextLabel()}
1566
+ ?disabled=${this.chipTooltipDisabled(index)}
1567
+ >
1256
1568
  <nile-tag
1257
- class=${classMap(classes)}
1258
1569
  part="tag"
1259
1570
  exportparts="
1260
1571
  base:tag__base,
@@ -1264,31 +1575,33 @@ let NileSelect = class NileSelect extends NileElement {
1264
1575
  prefix:tag__prefix,
1265
1576
  prefix_content_wrapper:tag__prefix_content_wrapper
1266
1577
  "
1578
+ style="max-width:${this.chipMaxWidth}px"
1267
1579
  ?pill=${this.pill}
1268
1580
  size=${this.size}
1269
1581
  removable
1270
1582
  @nile-remove=${(event) => this.handleTagRemove(event, option)}
1271
1583
  >
1272
- ${this.getOptionPrefix(option)
1273
- ? html `<span class="tag__prefix" slot="prefix"
1274
- >${unsafeHTML(this.getOptionPrefix(option))}</span
1584
+ ${prefixNodes.length
1585
+ ? html `<span class="tag__prefix" slot="prefix"
1586
+ >${prefixNodes}</span
1275
1587
  >`
1276
- : ''}
1588
+ : ''}
1277
1589
  ${option.getTextLabel()}
1278
1590
  </nile-tag>
1591
+ </nile-lite-tooltip>
1279
1592
  `;
1280
- }
1281
- else if (index === this.maxOptionsVisible) {
1282
- return html `
1593
+ }
1594
+ else if (index === this.maxOptionsVisible) {
1595
+ return html `
1283
1596
  <span class="select__tags-count"
1284
1597
  >+${this.selectedOptions.length - index} More
1285
1598
  </span>
1286
1599
  `;
1287
- }
1288
- else {
1289
- return null;
1290
- }
1291
- })}
1600
+ }
1601
+ else {
1602
+ return null;
1603
+ }
1604
+ })}
1292
1605
  </div>
1293
1606
  `
1294
1607
  : ''}
@@ -1418,21 +1731,35 @@ let NileSelect = class NileSelect extends NileElement {
1418
1731
  @click="${this.handleFooterClick}"
1419
1732
  >
1420
1733
  <span
1734
+ class="select__footer-action"
1421
1735
  @click="${this.toggleShowSelected}"
1422
1736
  style="cursor: pointer;"
1737
+ title="Show selected"
1423
1738
  >
1424
1739
  <nile-checkbox
1425
1740
  ?disabled="${this.selectedOptions.length === 0}"
1426
1741
  ?checked="${this.showSelected}"
1427
1742
  >
1428
- &nbsp; Show Selected</nile-checkbox
1743
+ <span class="select__footer-label"
1744
+ >Selected</span
1745
+ ></nile-checkbox
1429
1746
  >
1430
1747
  </span>
1431
1748
  ${this.selectedOptions.length !== 0
1432
1749
  ? html `<span
1433
1750
  class="select__clear"
1434
1751
  @click="${this.unSlectAll}"
1435
- >Clear All</span
1752
+ title="Clear"
1753
+ ><nile-glyph
1754
+ class="select__footer-glyph"
1755
+ name="ng-eraser"
1756
+ size="16"
1757
+ method="var(--nile-svg-method-stroke, var(--ng-svg-method-stroke))"
1758
+ color="var(--nile-colors-primary-600, var(--ng-colors-text-brand-secondary-700))"
1759
+ ></nile-glyph
1760
+ ><span class="select__footer-label"
1761
+ >Clear</span
1762
+ ></span
1436
1763
  >`
1437
1764
  : ``}
1438
1765
  </div>`
@@ -1464,6 +1791,9 @@ let NileSelect = class NileSelect extends NileElement {
1464
1791
  }
1465
1792
  };
1466
1793
  NileSelect.styles = styles;
1794
+ __decorate([
1795
+ state()
1796
+ ], NileSelect.prototype, "clippedChips", void 0);
1467
1797
  __decorate([
1468
1798
  query('.select')
1469
1799
  ], NileSelect.prototype, "popup", void 0);
@@ -1494,6 +1824,9 @@ __decorate([
1494
1824
  __decorate([
1495
1825
  state()
1496
1826
  ], NileSelect.prototype, "oldValue", void 0);
1827
+ __decorate([
1828
+ state()
1829
+ ], NileSelect.prototype, "hiddenChipCount", void 0);
1497
1830
  __decorate([
1498
1831
  property()
1499
1832
  ], NileSelect.prototype, "name", void 0);
@@ -1587,7 +1920,16 @@ __decorate([
1587
1920
  ], NileSelect.prototype, "success", void 0);
1588
1921
  __decorate([
1589
1922
  property({ attribute: 'max-options-visible', type: Number })
1590
- ], NileSelect.prototype, "maxOptionsVisible", void 0);
1923
+ ], NileSelect.prototype, "maxOptionsVisible", null);
1924
+ __decorate([
1925
+ property({ type: Number, reflect: true, attribute: true })
1926
+ ], NileSelect.prototype, "chipMinWidth", void 0);
1927
+ __decorate([
1928
+ property({ type: Number, reflect: true, attribute: true })
1929
+ ], NileSelect.prototype, "chipMaxWidth", void 0);
1930
+ __decorate([
1931
+ property({ type: Boolean, attribute: true, reflect: true })
1932
+ ], NileSelect.prototype, "showTooltip", void 0);
1591
1933
  __decorate([
1592
1934
  property({ type: Boolean, reflect: true })
1593
1935
  ], NileSelect.prototype, "disabled", void 0);
@@ -1621,9 +1963,6 @@ __decorate([
1621
1963
  __decorate([
1622
1964
  property({ type: Boolean })
1623
1965
  ], NileSelect.prototype, "showSelected", void 0);
1624
- __decorate([
1625
- state()
1626
- ], NileSelect.prototype, "oldMaxOptionsVisible", void 0);
1627
1966
  __decorate([
1628
1967
  property({ type: Boolean, reflect: true, attribute: true })
1629
1968
  ], NileSelect.prototype, "enableTabClose", void 0);