@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
@@ -7,6 +7,7 @@
7
7
  import '../nile-icon';
8
8
  import '../nile-popup/nile-popup';
9
9
  import '../nile-tag/nile-tag';
10
+ import '../nile-lite-tooltip/nile-lite-tooltip';
10
11
  import '../nile-checkbox/nile-checkbox';
11
12
  import '../nile-loader/nile-loader';
12
13
  import NileElement from '../internal/nile-element';
@@ -61,6 +62,10 @@ import type { VirtualOption, RenderItemConfig } from './types.js';
61
62
  * @csspart listbox - The listbox container where options are slotted.
62
63
  * @csspart tags - The container that houses option tags when `multiple` is used.
63
64
  * @csspart tag - The individual tags that represent each multiselect option.
65
+ * @csspart tag__base - The tag's base part.
66
+ * @csspart tag__content - The tag's content part.
67
+ * @csspart tag__remove-button - The tag's remove button.
68
+ * @csspart tag__remove-button__base - The tag's remove button base part.
64
69
  * @csspart clear-button - The clear button.
65
70
  * @csspart expand-icon - The container that wraps the expand icon.
66
71
  * @csspart footer - The footer container with show selected and clear all options.
@@ -98,7 +103,7 @@ export declare class NileVirtualSelect extends NileElement implements NileFormCo
98
103
  size: 'small' | 'medium' | 'large';
99
104
  /** Placeholder text to show as a hint when the select is empty. */
100
105
  placeholder: string;
101
- /** Enable automatic resizing of tags area */
106
+ /** Re-runs the fitted-chip layout when the control's own width changes. */
102
107
  autoResize: boolean;
103
108
  /** Current search value */
104
109
  searchValue: string;
@@ -176,8 +181,27 @@ export declare class NileVirtualSelect extends NileElement implements NileFormCo
176
181
  * The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
177
182
  * indicate the number of additional items that are selected. Set to 0 to remove the limit.
178
183
  */
179
- maxOptionsVisible: number;
180
- oldMaxOptionsVisible: number;
184
+ private _maxOptionsVisible;
185
+ private maxOptionsVisibleExplicit;
186
+ get maxOptionsVisible(): number;
187
+ set maxOptionsVisible(value: number);
188
+ private get chipFittingEnabled();
189
+ /** Minimum width, in px, of a squeezed chip. Needs `showtooltip`. */
190
+ chipMinWidth: number;
191
+ /** Maximum width, in px, of a squeezed chip. Needs `showtooltip`. */
192
+ chipMaxWidth: number;
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
+ showTooltip: boolean;
198
+ private hiddenChipCount;
199
+ /** Last measured width of the overflow button, in px. */
200
+ private lastOverflowWidth;
201
+ private chipFitFrame;
202
+ private lastChipLayout;
203
+ private clippedChips;
204
+ private clipFrame;
181
205
  enableTabClose: boolean;
182
206
  descriptionSearchEnabled: boolean;
183
207
  allowHtmlLabel: boolean;
@@ -247,6 +271,7 @@ export declare class NileVirtualSelect extends NileElement implements NileFormCo
247
271
  private unSelectAll;
248
272
  private handleLabelClick;
249
273
  private handleComboboxMouseDown;
274
+ private observedTagsContainer?;
250
275
  private resizeController;
251
276
  private shouldIgnoreComboboxClick;
252
277
  private handleComboboxKeyDown;
@@ -295,6 +320,7 @@ export declare class NileVirtualSelect extends NileElement implements NileFormCo
295
320
  private renderCombobox;
296
321
  private renderPrefix;
297
322
  private renderDisplayInput;
323
+ private renderFittedTags;
298
324
  private renderTags;
299
325
  private renderValueInput;
300
326
  private renderClearButton;
@@ -309,8 +335,23 @@ export declare class NileVirtualSelect extends NileElement implements NileFormCo
309
335
  nileInput(value: any): void;
310
336
  nileChange(value: any): void;
311
337
  private updateValidity;
312
- private calculateWidthOfSelectTagsDiv;
338
+ private setupResizeObserver;
313
339
  private calculateTotalWidthOfTags;
340
+ private allocateChipWidths;
341
+ private computeChipFit;
342
+ private layoutChips;
343
+ private isChipClipped;
344
+ private refreshClippedChips;
345
+ private scheduleClipCheck;
346
+ /** A chip gets a tooltip only while its text is measured as clipped. */
347
+ private chipTooltipDisabled;
348
+ private destroyChipFit;
349
+ private getChipLabel;
350
+ private handleChipKeyDown;
351
+ /** Moves focus to the next chip, the last chip, or the field after a removal. */
352
+ private restoreFocusAfterChipRemoval;
353
+ private handleOverflowActivate;
354
+ private handleOverflowKeyDown;
314
355
  private resetScrollPosition;
315
356
  }
316
357
  export default NileVirtualSelect;
@@ -11,12 +11,14 @@ import { styles } from './nile-virtual-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 '../nile-checkbox/nile-checkbox';
15
16
  import '../nile-loader/nile-loader';
16
17
  import { animateTo, stopAnimations } from '../internal/animate';
17
18
  import { classMap } from 'lit/directives/class-map.js';
18
19
  import { query, state } from 'lit/decorators.js';
19
20
  import { defaultValue } from '../internal/default-value';
21
+ import { KeyCode } from '../internal/enum';
20
22
  import { FormControlController } from '../internal/form';
21
23
  import { getAnimation, setDefaultAnimation, } from '../utilities/animation-registry';
22
24
  import { HasSlotController } from '../internal/slot';
@@ -77,6 +79,10 @@ import { VisibilityManager } from '../utilities/visibility-manager.js';
77
79
  * @csspart listbox - The listbox container where options are slotted.
78
80
  * @csspart tags - The container that houses option tags when `multiple` is used.
79
81
  * @csspart tag - The individual tags that represent each multiselect option.
82
+ * @csspart tag__base - The tag's base part.
83
+ * @csspart tag__content - The tag's content part.
84
+ * @csspart tag__remove-button - The tag's remove button.
85
+ * @csspart tag__remove-button__base - The tag's remove button base part.
80
86
  * @csspart clear-button - The clear button.
81
87
  * @csspart expand-icon - The container that wraps the expand icon.
82
88
  * @csspart footer - The footer container with show selected and clear all options.
@@ -110,7 +116,7 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
110
116
  this.size = 'medium';
111
117
  /** Placeholder text to show as a hint when the select is empty. */
112
118
  this.placeholder = 'Select...';
113
- /** Enable automatic resizing of tags area */
119
+ /** Re-runs the fitted-chip layout when the control's own width changes. */
114
120
  this.autoResize = false;
115
121
  /** Current search value */
116
122
  this.searchValue = '';
@@ -186,8 +192,24 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
186
192
  * The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
187
193
  * indicate the number of additional items that are selected. Set to 0 to remove the limit.
188
194
  */
189
- this.maxOptionsVisible = 3;
190
- this.oldMaxOptionsVisible = 1;
195
+ this._maxOptionsVisible = 3;
196
+ this.maxOptionsVisibleExplicit = false;
197
+ /** Minimum width, in px, of a squeezed chip. Needs `showtooltip`. */
198
+ this.chipMinWidth = 110;
199
+ /** Maximum width, in px, of a squeezed chip. Needs `showtooltip`. */
200
+ this.chipMaxWidth = 180;
201
+ /**
202
+ * Shows a chip's full value in a tooltip when its text is clipped, and is
203
+ * what allows a chip to be truncated at all.
204
+ */
205
+ this.showTooltip = false;
206
+ this.hiddenChipCount = 0;
207
+ /** Last measured width of the overflow button, in px. */
208
+ this.lastOverflowWidth = 0;
209
+ this.chipFitFrame = 0;
210
+ this.lastChipLayout = { hidden: -1, visible: -1 };
211
+ this.clippedChips = [];
212
+ this.clipFrame = 0;
191
213
  this.enableTabClose = false;
192
214
  this.descriptionSearchEnabled = false;
193
215
  this.allowHtmlLabel = true;
@@ -237,6 +259,19 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
237
259
  }
238
260
  });
239
261
  }
262
+ get maxOptionsVisible() {
263
+ return this._maxOptionsVisible;
264
+ }
265
+ set maxOptionsVisible(value) {
266
+ const previous = this._maxOptionsVisible;
267
+ this._maxOptionsVisible = value;
268
+ // Any assignment opts this instance out of automatic chip fitting.
269
+ this.maxOptionsVisibleExplicit = true;
270
+ this.requestUpdate('maxOptionsVisible', previous);
271
+ }
272
+ get chipFittingEnabled() {
273
+ return !!this.multiple && !this.maxOptionsVisibleExplicit;
274
+ }
240
275
  /** Gets the validity state object */
241
276
  get validity() {
242
277
  return this.valueInput?.validity;
@@ -265,19 +300,26 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
265
300
  }
266
301
  // Clean up body append elements
267
302
  this.portalManager.cleanupPortalAppend();
303
+ this.destroyChipFit();
268
304
  }
269
305
  updated(changedProperties) {
270
306
  if (changedProperties.has('value')) {
271
307
  this.selectionChanged();
272
308
  }
273
- if (changedProperties.has('autoResize')) {
274
- const tagsDiv = this.shadowRoot?.querySelector('.select__tags');
275
- if (this.autoResize && tagsDiv) {
276
- this.resizeController.observe(tagsDiv);
277
- }
278
- else if (tagsDiv) {
279
- this.resizeController.unobserve(tagsDiv);
280
- }
309
+ if (changedProperties.has('autoResize') ||
310
+ changedProperties.has('multiple') ||
311
+ changedProperties.has('maxOptionsVisible')) {
312
+ this.setupResizeObserver();
313
+ }
314
+ if (changedProperties.has('chipMinWidth') ||
315
+ changedProperties.has('chipMaxWidth')) {
316
+ this.destroyChipFit();
317
+ }
318
+ if (this.chipFittingEnabled) {
319
+ this.calculateTotalWidthOfTags();
320
+ }
321
+ if (this.multiple && this.showTooltip) {
322
+ this.scheduleClipCheck();
281
323
  }
282
324
  }
283
325
  initializeComponent() {
@@ -502,7 +544,9 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
502
544
  shouldIgnoreComboboxClick(event) {
503
545
  const path = event.composedPath();
504
546
  const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button');
505
- return this.disabled || isIconButton;
547
+ const isOverflowButton = path.some(el => el instanceof HTMLButtonElement &&
548
+ el.classList.contains('select__tags-count'));
549
+ return this.disabled || isIconButton || isOverflowButton;
506
550
  }
507
551
  handleComboboxKeyDown(event) {
508
552
  if (this.isEnterOrSpaceKey(event)) {
@@ -1048,20 +1092,74 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1048
1092
  />
1049
1093
  `;
1050
1094
  }
1095
+ renderFittedTags() {
1096
+ return html `
1097
+ <div part="tags" class="select__tags select__tags--fitted">
1098
+ ${this.selectedOptions.map((option, index) => {
1099
+ const label = this.getChipLabel(option);
1100
+ return html `
1101
+ <nile-lite-tooltip
1102
+ class="select__tag-tip"
1103
+ .content=${label}
1104
+ ?disabled=${this.chipTooltipDisabled(index)}
1105
+ >
1106
+ <nile-tag
1107
+ part="tag"
1108
+ exportparts="
1109
+ base:tag__base,
1110
+ content:tag__content,
1111
+ remove-button:tag__remove-button,
1112
+ remove-button__base:tag__remove-button__base,
1113
+ prefix:tag__prefix,
1114
+ prefix_content_wrapper:tag__prefix_content_wrapper
1115
+ "
1116
+ ?pill=${this.pill}
1117
+ size=${this.size}
1118
+ removable
1119
+ tabindex="0"
1120
+ aria-label=${`${label}. Press Backspace or Delete to remove.`}
1121
+ @nile-remove=${(event) => this.handleTagRemove(event, option)}
1122
+ @keydown=${(event) => this.handleChipKeyDown(event, option)}
1123
+ >
1124
+ ${label}
1125
+ </nile-tag>
1126
+ </nile-lite-tooltip>
1127
+ `;
1128
+ })}
1129
+ ${this.hiddenChipCount > 0
1130
+ ? html `<button
1131
+ type="button"
1132
+ class="select__tags-count"
1133
+ ?disabled=${this.disabled}
1134
+ aria-controls="listbox"
1135
+ aria-expanded=${this.open ? 'true' : 'false'}
1136
+ aria-haspopup="listbox"
1137
+ aria-label=${`${this.hiddenChipCount} more selected`}
1138
+ @click=${this.handleOverflowActivate}
1139
+ @keydown=${this.handleOverflowKeyDown}
1140
+ >+${this.hiddenChipCount}</button
1141
+ >`
1142
+ : ''}
1143
+ </div>
1144
+ `;
1145
+ }
1051
1146
  renderTags() {
1147
+ if (this.chipFittingEnabled) {
1148
+ return this.renderFittedTags();
1149
+ }
1052
1150
  return this.multiple
1053
1151
  ? html `
1054
1152
  <div part="tags" class="select__tags">
1055
1153
  ${this.selectedOptions.map((option, index) => {
1056
1154
  if (index < this.maxOptionsVisible ||
1057
1155
  this.maxOptionsVisible <= 0) {
1058
- const classes = {
1059
- select__invisible: index + 1 > this.oldMaxOptionsVisible &&
1060
- this.maxOptionsVisible === Infinity,
1061
- };
1062
1156
  return html `
1157
+ <nile-lite-tooltip
1158
+ class="select__tag-tip"
1159
+ .content=${option.getTextLabel()}
1160
+ ?disabled=${this.chipTooltipDisabled(index)}
1161
+ >
1063
1162
  <nile-tag
1064
- class=${classMap(classes)}
1065
1163
  part="tag"
1066
1164
  exportparts="
1067
1165
  base:tag__base,
@@ -1069,6 +1167,7 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1069
1167
  remove-button:tag__remove-button,
1070
1168
  remove-button__base:tag__remove-button__base
1071
1169
  "
1170
+ style="max-width:${this.chipMaxWidth}px"
1072
1171
  ?pill=${this.pill}
1073
1172
  size=${this.size}
1074
1173
  removable
@@ -1076,6 +1175,7 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1076
1175
  >
1077
1176
  ${option.getTextLabel()}
1078
1177
  </nile-tag>
1178
+ </nile-lite-tooltip>
1079
1179
  `;
1080
1180
  }
1081
1181
  else if (index === this.maxOptionsVisible) {
@@ -1222,14 +1322,16 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1222
1322
  @click="${this.handleFooterClick}"
1223
1323
  >
1224
1324
  <span
1325
+ class="select__footer-action"
1225
1326
  @click="${this.toggleShowSelected}"
1226
1327
  style="cursor: pointer;"
1328
+ title="Show selected"
1227
1329
  >
1228
1330
  <nile-checkbox
1229
1331
  ?disabled="${this.selectedOptions.length === 0}"
1230
1332
  ?checked="${this.showSelected}"
1231
1333
  >
1232
- &nbsp; Show Selected</nile-checkbox
1334
+ <span class="select__footer-label">Selected</span></nile-checkbox
1233
1335
  >
1234
1336
  </span>
1235
1337
  ${this.selectedOptions.length !== 0
@@ -1237,8 +1339,16 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1237
1339
  <span
1238
1340
  class="select__clear"
1239
1341
  @click="${this.unSelectAll}"
1342
+ title="Clear"
1240
1343
  >
1241
- Clear All
1344
+ <nile-glyph
1345
+ class="select__footer-glyph"
1346
+ name="ng-eraser"
1347
+ size="16"
1348
+ method="var(--nile-svg-method-stroke, var(--ng-svg-method-stroke))"
1349
+ color="var(--nile-colors-primary-600, var(--ng-colors-text-brand-secondary-700))"
1350
+ ></nile-glyph
1351
+ ><span class="select__footer-label">Clear</span>
1242
1352
  </span>
1243
1353
  `
1244
1354
  : ``}
@@ -1274,50 +1384,254 @@ let NileVirtualSelect = class NileVirtualSelect extends NileElement {
1274
1384
  this.formControlController.updateValidity();
1275
1385
  });
1276
1386
  }
1277
- calculateWidthOfSelectTagsDiv() {
1278
- if (this.shadowRoot) {
1279
- const selectTagsDiv = this.shadowRoot.querySelector('.select__tags');
1280
- if (selectTagsDiv) {
1281
- const width = selectTagsDiv.offsetWidth;
1282
- return width - 70;
1283
- }
1387
+ setupResizeObserver() {
1388
+ const wanted = this.autoResize && this.chipFittingEnabled
1389
+ ? this.shadowRoot?.querySelector('.select__tags') ??
1390
+ undefined
1391
+ : undefined;
1392
+ if (wanted === this.observedTagsContainer) {
1393
+ if (!this.chipFittingEnabled)
1394
+ this.destroyChipFit();
1395
+ return;
1396
+ }
1397
+ if (this.observedTagsContainer) {
1398
+ this.resizeController.unobserve(this.observedTagsContainer);
1399
+ this.observedTagsContainer = undefined;
1400
+ }
1401
+ if (wanted) {
1402
+ this.resizeController.observe(wanted);
1403
+ this.observedTagsContainer = wanted;
1404
+ return;
1405
+ }
1406
+ if (!this.chipFittingEnabled) {
1407
+ this.destroyChipFit();
1284
1408
  }
1285
- return null;
1286
1409
  }
1287
1410
  calculateTotalWidthOfTags() {
1288
- if (this.maxOptionsVisible !== Infinity) {
1289
- this.oldMaxOptionsVisible = this.maxOptionsVisible;
1290
- }
1291
- this.maxOptionsVisible = Infinity;
1292
- setTimeout(() => {
1293
- let widths = [];
1294
- if (this.shadowRoot) {
1295
- const tags = this.shadowRoot.querySelectorAll('nile-tag');
1296
- tags.forEach(tag => {
1297
- if (tag instanceof HTMLElement) {
1298
- widths.push(tag.offsetWidth);
1299
- }
1300
- });
1411
+ if (!this.chipFittingEnabled)
1412
+ return;
1413
+ if (this.chipFitFrame)
1414
+ cancelAnimationFrame(this.chipFitFrame);
1415
+ this.chipFitFrame = requestAnimationFrame(() => {
1416
+ this.chipFitFrame = 0;
1417
+ this.layoutChips();
1418
+ });
1419
+ }
1420
+ allocateChipWidths(natural, available) {
1421
+ const min = this.chipMinWidth;
1422
+ const max = this.chipMaxWidth;
1423
+ const total = natural.reduce((sum, w) => sum + w, 0);
1424
+ if (total <= available) {
1425
+ return natural.slice();
1426
+ }
1427
+ // Nothing reveals a truncated value without a tooltip, so chips keep their
1428
+ // natural width and the surplus moves into the +N button instead.
1429
+ if (!this.showTooltip)
1430
+ return null;
1431
+ const capped = natural.map(w => Math.min(w, max));
1432
+ const cappedTotal = capped.reduce((sum, w) => sum + w, 0);
1433
+ if (cappedTotal <= available) {
1434
+ return capped;
1435
+ }
1436
+ const widths = new Array(natural.length).fill(0);
1437
+ let pool = capped.map((_, i) => i);
1438
+ let remaining = available;
1439
+ for (;;) {
1440
+ if (pool.length === 0)
1441
+ break;
1442
+ const share = remaining / pool.length;
1443
+ const settled = pool.filter(i => capped[i] <= share);
1444
+ if (settled.length === 0)
1445
+ break;
1446
+ for (const i of settled) {
1447
+ widths[i] = capped[i];
1448
+ remaining -= capped[i];
1301
1449
  }
1302
- if (this.value.length !== widths.length) {
1303
- return;
1450
+ pool = pool.filter(i => capped[i] > share);
1451
+ }
1452
+ if (pool.length > 0) {
1453
+ const share = remaining / pool.length;
1454
+ if (share < min) {
1455
+ return null;
1304
1456
  }
1305
- let sum = widths.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
1306
- const widthOfSelectTagsDiv = this.calculateWidthOfSelectTagsDiv();
1307
- if (!widthOfSelectTagsDiv) {
1308
- return;
1457
+ const width = Math.floor(share);
1458
+ for (const i of pool) {
1459
+ widths[i] = width;
1309
1460
  }
1310
- let summ = 0;
1311
- let lastindex = 0;
1312
- for (let i = 0; i < widths.length; i++) {
1313
- summ += widths[i];
1314
- if (summ > widthOfSelectTagsDiv) {
1315
- lastindex = i;
1316
- break;
1317
- }
1461
+ }
1462
+ return widths;
1463
+ }
1464
+ computeChipFit(natural, available, overflowWidth, gap) {
1465
+ const total = natural.length;
1466
+ if (total === 0) {
1467
+ return { visibleCount: 0, widths: [], hiddenCount: 0 };
1468
+ }
1469
+ for (let count = total; count >= 1; count--) {
1470
+ const hidden = total - count;
1471
+ const reserved = hidden > 0 ? overflowWidth + gap : 0;
1472
+ const row = available - reserved - gap * (count - 1);
1473
+ if (row <= 0)
1474
+ continue;
1475
+ const widths = this.allocateChipWidths(natural.slice(0, count), row);
1476
+ if (widths) {
1477
+ return { visibleCount: count, widths, hiddenCount: hidden };
1318
1478
  }
1319
- this.maxOptionsVisible = lastindex;
1320
- }, 1);
1479
+ }
1480
+ const room = available - overflowWidth - gap;
1481
+ const fallback = Math.max(0,
1482
+ // A single chip too wide for the row has to be clipped either way; only
1483
+ // the tooltip variant squeezes it to chipMinWidth.
1484
+ Math.min(natural[0], this.showTooltip ? Math.max(this.chipMinWidth, room) : room));
1485
+ return { visibleCount: 1, widths: [fallback], hiddenCount: total - 1 };
1486
+ }
1487
+ layoutChips() {
1488
+ const root = this.shadowRoot;
1489
+ if (!root)
1490
+ return;
1491
+ const container = root.querySelector('.select__tags');
1492
+ if (!container)
1493
+ return;
1494
+ const chips = Array.from(container.querySelectorAll('nile-tag'));
1495
+ if (chips.length === 0) {
1496
+ this.hiddenChipCount = 0;
1497
+ return;
1498
+ }
1499
+ const overflowEl = container.querySelector('.select__tags-count');
1500
+ for (const chip of chips) {
1501
+ chip.style.display = '';
1502
+ chip.style.width = 'auto';
1503
+ chip.style.maxWidth = 'none';
1504
+ chip.style.minWidth = '0';
1505
+ chip.style.flex = '0 0 auto';
1506
+ }
1507
+ const natural = chips.map(chip => Math.ceil(chip.getBoundingClientRect().width));
1508
+ // Not rendered yet on the pass that first finds an overflow; reuse the last width.
1509
+ const overflowWidth = overflowEl
1510
+ ? Math.ceil(overflowEl.getBoundingClientRect().width)
1511
+ : this.lastOverflowWidth;
1512
+ if (overflowEl)
1513
+ this.lastOverflowWidth = overflowWidth;
1514
+ const containerStyles = getComputedStyle(container);
1515
+ const gap = parseFloat(containerStyles.columnGap || containerStyles.gap || '0') || 0;
1516
+ const fit = this.computeChipFit(natural, container.clientWidth, overflowWidth, gap);
1517
+ chips.forEach((chip, index) => {
1518
+ if (index < fit.visibleCount) {
1519
+ // Never squeeze a chip below chipMinWidth: narrower than that it is
1520
+ // also narrower than the tag's own box - the label is already at zero
1521
+ // while the remove button is fixed - so the box overflows the chip and
1522
+ // paints the X over the next chip or the +N button. A chip whose
1523
+ // natural width is already smaller is left alone, not padded out.
1524
+ const width = Math.min(natural[index], Math.max(fit.widths[index], this.chipMinWidth));
1525
+ chip.style.display = '';
1526
+ chip.style.width = `${width}px`;
1527
+ chip.style.maxWidth = `${width}px`;
1528
+ chip.style.flex = '0 0 auto';
1529
+ }
1530
+ else {
1531
+ chip.style.display = 'none';
1532
+ }
1533
+ });
1534
+ if (this.lastChipLayout.hidden !== fit.hiddenCount ||
1535
+ this.lastChipLayout.visible !== fit.visibleCount) {
1536
+ this.lastChipLayout = {
1537
+ hidden: fit.hiddenCount,
1538
+ visible: fit.visibleCount
1539
+ };
1540
+ this.hiddenChipCount = fit.hiddenCount;
1541
+ }
1542
+ this.refreshClippedChips();
1543
+ }
1544
+ isChipClipped(el) {
1545
+ if (!el)
1546
+ return false;
1547
+ const root = el.shadowRoot;
1548
+ if (root) {
1549
+ const content = root.querySelector('[part="content"]');
1550
+ if (content && content.scrollWidth > content.clientWidth + 1)
1551
+ return true;
1552
+ const inner = root.querySelector('[part="prefix_content_wrapper"]');
1553
+ if (inner && inner.scrollWidth > inner.clientWidth + 1)
1554
+ return true;
1555
+ }
1556
+ return el.scrollWidth > el.clientWidth + 1;
1557
+ }
1558
+ refreshClippedChips() {
1559
+ if (!this.multiple)
1560
+ return;
1561
+ const root = this.shadowRoot;
1562
+ if (!root)
1563
+ return;
1564
+ const chips = Array.from(root.querySelectorAll('.select__tags nile-tag'));
1565
+ const next = chips.map(chip => this.isChipClipped(chip));
1566
+ const same = next.length === this.clippedChips.length &&
1567
+ next.every((v, i) => v === this.clippedChips[i]);
1568
+ if (!same) {
1569
+ this.clippedChips = next;
1570
+ }
1571
+ }
1572
+ scheduleClipCheck() {
1573
+ if (this.clipFrame)
1574
+ cancelAnimationFrame(this.clipFrame);
1575
+ this.clipFrame = requestAnimationFrame(() => {
1576
+ this.clipFrame = 0;
1577
+ this.refreshClippedChips();
1578
+ });
1579
+ }
1580
+ /** A chip gets a tooltip only while its text is measured as clipped. */
1581
+ chipTooltipDisabled(index) {
1582
+ return !this.showTooltip || !(this.clippedChips[index] ?? false);
1583
+ }
1584
+ destroyChipFit() {
1585
+ if (this.chipFitFrame)
1586
+ cancelAnimationFrame(this.chipFitFrame);
1587
+ this.chipFitFrame = 0;
1588
+ if (this.clipFrame)
1589
+ cancelAnimationFrame(this.clipFrame);
1590
+ this.clipFrame = 0;
1591
+ this.lastChipLayout = { hidden: -1, visible: -1 };
1592
+ }
1593
+ getChipLabel(option) {
1594
+ return (option.getTextLabel?.() ?? '').trim();
1595
+ }
1596
+ handleChipKeyDown(event, option) {
1597
+ if (event.key !== KeyCode.BACKSPACE && event.key !== KeyCode.DELETE)
1598
+ return;
1599
+ if (this.disabled)
1600
+ return;
1601
+ event.preventDefault();
1602
+ event.stopPropagation();
1603
+ const removedIndex = this.selectedOptions.indexOf(option);
1604
+ this.handleTagRemove(new CustomEvent('nile-remove'), option);
1605
+ this.restoreFocusAfterChipRemoval(removedIndex);
1606
+ }
1607
+ /** Moves focus to the next chip, the last chip, or the field after a removal. */
1608
+ async restoreFocusAfterChipRemoval(removedIndex) {
1609
+ if (removedIndex < 0)
1610
+ return;
1611
+ await this.updateComplete;
1612
+ // Wait for the fit pass so hidden chips are not focused.
1613
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
1614
+ const visibleChips = Array.from(this.shadowRoot?.querySelectorAll('.select__tags nile-tag') ??
1615
+ []).filter(chip => chip.style.display !== 'none');
1616
+ const target = visibleChips[removedIndex] ?? visibleChips[visibleChips.length - 1];
1617
+ if (target) {
1618
+ target.focus({ preventScroll: true });
1619
+ }
1620
+ else {
1621
+ this.displayInput?.focus({ preventScroll: true });
1622
+ }
1623
+ }
1624
+ handleOverflowActivate(event) {
1625
+ event.preventDefault();
1626
+ event.stopPropagation();
1627
+ if (this.disabled)
1628
+ return;
1629
+ this.open = !this.open;
1630
+ }
1631
+ handleOverflowKeyDown(event) {
1632
+ if (event.key !== KeyCode.ENTER && event.key !== KeyCode.SPACE)
1633
+ return;
1634
+ this.handleOverflowActivate(event);
1321
1635
  }
1322
1636
  async resetScrollPosition() {
1323
1637
  await this.portalManager.resetScrollPosition();
@@ -1378,7 +1692,7 @@ __decorate([
1378
1692
  property()
1379
1693
  ], NileVirtualSelect.prototype, "placeholder", void 0);
1380
1694
  __decorate([
1381
- property({ type: Boolean })
1695
+ property({ type: Boolean, attribute: true, reflect: true })
1382
1696
  ], NileVirtualSelect.prototype, "autoResize", void 0);
1383
1697
  __decorate([
1384
1698
  state()
@@ -1472,10 +1786,22 @@ __decorate([
1472
1786
  ], NileVirtualSelect.prototype, "portal", void 0);
1473
1787
  __decorate([
1474
1788
  property({ attribute: 'max-options-visible', type: Number })
1475
- ], NileVirtualSelect.prototype, "maxOptionsVisible", void 0);
1789
+ ], NileVirtualSelect.prototype, "maxOptionsVisible", null);
1790
+ __decorate([
1791
+ property({ type: Number, reflect: true, attribute: true })
1792
+ ], NileVirtualSelect.prototype, "chipMinWidth", void 0);
1793
+ __decorate([
1794
+ property({ type: Number, reflect: true, attribute: true })
1795
+ ], NileVirtualSelect.prototype, "chipMaxWidth", void 0);
1796
+ __decorate([
1797
+ property({ type: Boolean, attribute: true, reflect: true })
1798
+ ], NileVirtualSelect.prototype, "showTooltip", void 0);
1799
+ __decorate([
1800
+ state()
1801
+ ], NileVirtualSelect.prototype, "hiddenChipCount", void 0);
1476
1802
  __decorate([
1477
1803
  state()
1478
- ], NileVirtualSelect.prototype, "oldMaxOptionsVisible", void 0);
1804
+ ], NileVirtualSelect.prototype, "clippedChips", void 0);
1479
1805
  __decorate([
1480
1806
  property({ type: Boolean, reflect: true, attribute: true })
1481
1807
  ], NileVirtualSelect.prototype, "enableTabClose", void 0);