@vaadin/multi-select-combo-box 24.6.0-alpha9 → 24.6.0-beta1

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 (26) hide show
  1. package/package.json +16 -15
  2. package/src/vaadin-lit-multi-select-combo-box-chip.js +79 -0
  3. package/src/vaadin-lit-multi-select-combo-box-container.js +66 -0
  4. package/src/vaadin-lit-multi-select-combo-box-internal.js +56 -0
  5. package/src/vaadin-lit-multi-select-combo-box-item.js +50 -0
  6. package/src/vaadin-lit-multi-select-combo-box-overlay.js +64 -0
  7. package/src/vaadin-lit-multi-select-combo-box-scroller.js +96 -0
  8. package/src/vaadin-lit-multi-select-combo-box.d.ts +1 -0
  9. package/src/vaadin-lit-multi-select-combo-box.js +146 -0
  10. package/src/vaadin-multi-select-combo-box-chip.js +6 -27
  11. package/src/vaadin-multi-select-combo-box-internal-mixin.js +425 -0
  12. package/src/vaadin-multi-select-combo-box-internal.js +3 -399
  13. package/src/vaadin-multi-select-combo-box-mixin.d.ts +253 -0
  14. package/src/vaadin-multi-select-combo-box-mixin.js +1150 -0
  15. package/src/vaadin-multi-select-combo-box-styles.d.ts +10 -0
  16. package/src/vaadin-multi-select-combo-box-styles.js +73 -0
  17. package/src/vaadin-multi-select-combo-box.d.ts +5 -213
  18. package/src/vaadin-multi-select-combo-box.js +5 -1139
  19. package/theme/lumo/vaadin-lit-multi-select-combo-box.d.ts +3 -0
  20. package/theme/lumo/vaadin-lit-multi-select-combo-box.js +3 -0
  21. package/theme/material/vaadin-lit-multi-select-combo-box.d.ts +3 -0
  22. package/theme/material/vaadin-lit-multi-select-combo-box.js +3 -0
  23. package/vaadin-lit-multi-select-combo-box.d.ts +1 -0
  24. package/vaadin-lit-multi-select-combo-box.js +2 -0
  25. package/web-types.json +5 -5
  26. package/web-types.lit.json +8 -8
@@ -7,58 +7,12 @@ import './vaadin-multi-select-combo-box-chip.js';
7
7
  import './vaadin-multi-select-combo-box-container.js';
8
8
  import './vaadin-multi-select-combo-box-internal.js';
9
9
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
10
- import { announce } from '@vaadin/a11y-base/src/announce.js';
11
10
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
12
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
13
- import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
14
- import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
15
- import { processTemplates } from '@vaadin/component-base/src/templates.js';
16
- import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
17
- import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
18
- import { InputController } from '@vaadin/field-base/src/input-controller.js';
19
- import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
20
12
  import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
21
- import { css, registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
22
-
23
- const multiSelectComboBox = css`
24
- :host {
25
- --input-min-width: var(--vaadin-multi-select-combo-box-input-min-width, 4em);
26
- --_chip-min-width: var(--vaadin-multi-select-combo-box-chip-min-width, 50px);
27
- }
28
-
29
- #chips {
30
- display: flex;
31
- align-items: center;
32
- }
33
-
34
- ::slotted(input) {
35
- box-sizing: border-box;
36
- flex: 1 0 var(--input-min-width);
37
- }
38
-
39
- ::slotted([slot='chip']),
40
- ::slotted([slot='overflow']) {
41
- flex: 0 1 auto;
42
- }
43
-
44
- ::slotted([slot='chip']) {
45
- overflow: hidden;
46
- }
47
-
48
- :host(:is([readonly], [disabled])) ::slotted(input) {
49
- flex-grow: 0;
50
- flex-basis: 0;
51
- padding: 0;
52
- }
53
-
54
- :host([auto-expand-vertically]) #chips {
55
- display: contents;
56
- }
57
-
58
- :host([auto-expand-horizontally]) [class$='container'] {
59
- width: auto;
60
- }
61
- `;
13
+ import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14
+ import { MultiSelectComboBoxMixin } from './vaadin-multi-select-combo-box-mixin.js';
15
+ import { multiSelectComboBox } from './vaadin-multi-select-combo-box-styles.js';
62
16
 
63
17
  registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
64
18
  moduleId: 'vaadin-multi-select-combo-box-styles',
@@ -145,10 +99,9 @@ registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectCo
145
99
  * @extends HTMLElement
146
100
  * @mixes ElementMixin
147
101
  * @mixes ThemableMixin
148
- * @mixes InputControlMixin
149
- * @mixes ResizeMixin
102
+ * @mixes MultiSelectComboBoxMixin
150
103
  */
151
- class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(ElementMixin(PolymerElement)))) {
104
+ class MultiSelectComboBox extends MultiSelectComboBoxMixin(ThemableMixin(ElementMixin(PolymerElement))) {
152
105
  static get is() {
153
106
  return 'vaadin-multi-select-combo-box';
154
107
  }
@@ -227,1093 +180,6 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
227
180
  <slot name="tooltip"></slot>
228
181
  `;
229
182
  }
230
-
231
- static get properties() {
232
- return {
233
- /**
234
- * Set to true to auto expand horizontally, causing input field to
235
- * grow until max width is reached.
236
- * @attr {boolean} auto-expand-horizontally
237
- */
238
- autoExpandHorizontally: {
239
- type: Boolean,
240
- value: false,
241
- reflectToAttribute: true,
242
- observer: '_autoExpandHorizontallyChanged',
243
- },
244
-
245
- /**
246
- * Set to true to not collapse selected items chips into the overflow
247
- * chip and instead always expand vertically, causing input field to
248
- * wrap into multiple lines when width is limited.
249
- * @attr {boolean} auto-expand-vertically
250
- */
251
- autoExpandVertically: {
252
- type: Boolean,
253
- value: false,
254
- reflectToAttribute: true,
255
- observer: '_autoExpandVerticallyChanged',
256
- },
257
-
258
- /**
259
- * Set true to prevent the overlay from opening automatically.
260
- * @attr {boolean} auto-open-disabled
261
- */
262
- autoOpenDisabled: Boolean,
263
-
264
- /**
265
- * Set to true to display the clear icon which clears the input.
266
- * @attr {boolean} clear-button-visible
267
- */
268
- clearButtonVisible: {
269
- type: Boolean,
270
- reflectToAttribute: true,
271
- observer: '_clearButtonVisibleChanged',
272
- value: false,
273
- },
274
-
275
- /**
276
- * A full set of items to filter the visible options from.
277
- * The items can be of either `String` or `Object` type.
278
- */
279
- items: {
280
- type: Array,
281
- },
282
-
283
- /**
284
- * A function used to generate CSS class names for dropdown
285
- * items and selected chips based on the item. The return
286
- * value should be the generated class name as a string, or
287
- * multiple class names separated by whitespace characters.
288
- */
289
- itemClassNameGenerator: {
290
- type: Object,
291
- observer: '__itemClassNameGeneratorChanged',
292
- },
293
-
294
- /**
295
- * The item property used for a visual representation of the item.
296
- * @attr {string} item-label-path
297
- */
298
- itemLabelPath: {
299
- type: String,
300
- value: 'label',
301
- },
302
-
303
- /**
304
- * Path for the value of the item. If `items` is an array of objects,
305
- * this property is used as a string value for the selected item.
306
- * @attr {string} item-value-path
307
- */
308
- itemValuePath: {
309
- type: String,
310
- value: 'value',
311
- },
312
-
313
- /**
314
- * Path for the id of the item, used to detect whether the item is selected.
315
- * @attr {string} item-id-path
316
- */
317
- itemIdPath: {
318
- type: String,
319
- },
320
-
321
- /**
322
- * The object used to localize this component.
323
- * To change the default localization, replace the entire
324
- * _i18n_ object or just the property you want to modify.
325
- *
326
- * The object has the following JSON structure and default values:
327
- * ```
328
- * {
329
- * // Screen reader announcement on clear button click.
330
- * cleared: 'Selection cleared',
331
- * // Screen reader announcement when a chip is focused.
332
- * focused: ' focused. Press Backspace to remove',
333
- * // Screen reader announcement when item is selected.
334
- * selected: 'added to selection',
335
- * // Screen reader announcement when item is deselected.
336
- * deselected: 'removed from selection',
337
- * // Screen reader announcement of the selected items count.
338
- * // {count} is replaced with the actual count of items.
339
- * total: '{count} items selected',
340
- * }
341
- * ```
342
- * @type {!MultiSelectComboBoxI18n}
343
- * @default {English/US}
344
- */
345
- i18n: {
346
- type: Object,
347
- value: () => {
348
- return {
349
- cleared: 'Selection cleared',
350
- focused: 'focused. Press Backspace to remove',
351
- selected: 'added to selection',
352
- deselected: 'removed from selection',
353
- total: '{count} items selected',
354
- };
355
- },
356
- },
357
-
358
- /**
359
- * When true, filter string isn't cleared after selecting an item.
360
- */
361
- keepFilter: {
362
- type: Boolean,
363
- value: false,
364
- },
365
-
366
- /**
367
- * True when loading items from the data provider, false otherwise.
368
- */
369
- loading: {
370
- type: Boolean,
371
- value: false,
372
- reflectToAttribute: true,
373
- },
374
-
375
- /**
376
- * A space-delimited list of CSS class names to set on the overlay element.
377
- *
378
- * @attr {string} overlay-class
379
- */
380
- overlayClass: {
381
- type: String,
382
- },
383
-
384
- /**
385
- * When present, it specifies that the field is read-only.
386
- */
387
- readonly: {
388
- type: Boolean,
389
- value: false,
390
- observer: '_readonlyChanged',
391
- reflectToAttribute: true,
392
- },
393
-
394
- /**
395
- * The list of selected items.
396
- * Note: modifying the selected items creates a new array each time.
397
- */
398
- selectedItems: {
399
- type: Array,
400
- value: () => [],
401
- notify: true,
402
- },
403
-
404
- /**
405
- * True if the dropdown is open, false otherwise.
406
- */
407
- opened: {
408
- type: Boolean,
409
- notify: true,
410
- value: false,
411
- reflectToAttribute: true,
412
- },
413
-
414
- /**
415
- * Total number of items.
416
- */
417
- size: {
418
- type: Number,
419
- },
420
-
421
- /**
422
- * Number of items fetched at a time from the data provider.
423
- * @attr {number} page-size
424
- */
425
- pageSize: {
426
- type: Number,
427
- value: 50,
428
- observer: '_pageSizeChanged',
429
- },
430
-
431
- /**
432
- * Function that provides items lazily. Receives two arguments:
433
- *
434
- * - `params` - Object with the following properties:
435
- * - `params.page` Requested page index
436
- * - `params.pageSize` Current page size
437
- * - `params.filter` Currently applied filter
438
- *
439
- * - `callback(items, size)` - Callback function with arguments:
440
- * - `items` Current page of items
441
- * - `size` Total number of items.
442
- */
443
- dataProvider: {
444
- type: Object,
445
- },
446
-
447
- /**
448
- * When true, the user can input a value that is not present in the items list.
449
- * @attr {boolean} allow-custom-value
450
- */
451
- allowCustomValue: {
452
- type: Boolean,
453
- value: false,
454
- },
455
-
456
- /**
457
- * A hint to the user of what can be entered in the control.
458
- * The placeholder will be only displayed in the case when
459
- * there is no item selected.
460
- */
461
- placeholder: {
462
- type: String,
463
- observer: '_placeholderChanged',
464
- },
465
-
466
- /**
467
- * Custom function for rendering the content of every item.
468
- * Receives three arguments:
469
- *
470
- * - `root` The `<vaadin-multi-select-combo-box-item>` internal container DOM element.
471
- * - `comboBox` The reference to the `<vaadin-multi-select-combo-box>` element.
472
- * - `model` The object with the properties related with the rendered
473
- * item, contains:
474
- * - `model.index` The index of the rendered item.
475
- * - `model.item` The item.
476
- */
477
- renderer: Function,
478
-
479
- /**
480
- * Filtering string the user has typed into the input field.
481
- */
482
- filter: {
483
- type: String,
484
- value: '',
485
- notify: true,
486
- },
487
-
488
- /**
489
- * A subset of items, filtered based on the user input. Filtered items
490
- * can be assigned directly to omit the internal filtering functionality.
491
- * The items can be of either `String` or `Object` type.
492
- */
493
- filteredItems: Array,
494
-
495
- /**
496
- * Set to true to group selected items at the top of the overlay.
497
- * @attr {boolean} selected-items-on-top
498
- */
499
- selectedItemsOnTop: {
500
- type: Boolean,
501
- value: false,
502
- },
503
-
504
- /** @private */
505
- value: {
506
- type: String,
507
- },
508
-
509
- /** @private */
510
- _overflowItems: {
511
- type: Array,
512
- value: () => [],
513
- },
514
-
515
- /** @private */
516
- _focusedChipIndex: {
517
- type: Number,
518
- value: -1,
519
- observer: '_focusedChipIndexChanged',
520
- },
521
-
522
- /** @private */
523
- _lastFilter: {
524
- type: String,
525
- },
526
-
527
- /** @private */
528
- _topGroup: {
529
- type: Array,
530
- },
531
- };
532
- }
533
-
534
- static get observers() {
535
- return [
536
- '_selectedItemsChanged(selectedItems, selectedItems.*)',
537
- '__updateOverflowChip(_overflow, _overflowItems, disabled, readonly)',
538
- '__updateTopGroup(selectedItemsOnTop, selectedItems, opened)',
539
- ];
540
- }
541
-
542
- /** @protected */
543
- get slotStyles() {
544
- const tag = this.localName;
545
- return [
546
- ...super.slotStyles,
547
- `
548
- ${tag}[has-value] input::placeholder {
549
- color: transparent !important;
550
- forced-color-adjust: none;
551
- }
552
- `,
553
- ];
554
- }
555
-
556
- /**
557
- * Used by `InputControlMixin` as a reference to the clear button element.
558
- * @protected
559
- * @return {!HTMLElement}
560
- */
561
- get clearElement() {
562
- return this.$.clearButton;
563
- }
564
-
565
- /** @protected */
566
- get _chips() {
567
- return [...this.querySelectorAll('[slot="chip"]')];
568
- }
569
-
570
- /**
571
- * Override a getter from `InputMixin` to compute
572
- * the presence of value based on `selectedItems`.
573
- *
574
- * @protected
575
- * @override
576
- */
577
- get _hasValue() {
578
- return this.selectedItems && this.selectedItems.length > 0;
579
- }
580
-
581
- /** @protected */
582
- ready() {
583
- super.ready();
584
-
585
- this.addController(
586
- new InputController(this, (input) => {
587
- this._setInputElement(input);
588
- this._setFocusElement(input);
589
- this.stateTarget = input;
590
- this.ariaTarget = input;
591
- }),
592
- );
593
- this.addController(new LabelledInputController(this.inputElement, this._labelController));
594
-
595
- this._tooltipController = new TooltipController(this);
596
- this.addController(this._tooltipController);
597
- this._tooltipController.setPosition('top');
598
- this._tooltipController.setAriaTarget(this.inputElement);
599
- this._tooltipController.setShouldShow((target) => !target.opened);
600
-
601
- this._inputField = this.shadowRoot.querySelector('[part="input-field"]');
602
-
603
- this._overflowController = new SlotController(this, 'overflow', 'vaadin-multi-select-combo-box-chip', {
604
- initializer: (chip) => {
605
- chip.addEventListener('mousedown', (e) => this._preventBlur(e));
606
- this._overflow = chip;
607
- },
608
- });
609
- this.addController(this._overflowController);
610
-
611
- this.__updateChips();
612
-
613
- processTemplates(this);
614
- }
615
-
616
- /**
617
- * Returns true if the current input value satisfies all constraints (if any).
618
- * @return {boolean}
619
- */
620
- checkValidity() {
621
- return this.required && !this.readonly ? this._hasValue : true;
622
- }
623
-
624
- /**
625
- * Clears the selected items.
626
- */
627
- clear() {
628
- this.__updateSelection([]);
629
-
630
- announce(this.i18n.cleared);
631
- }
632
-
633
- /**
634
- * Clears the cached pages and reloads data from data provider when needed.
635
- */
636
- clearCache() {
637
- if (this.$ && this.$.comboBox) {
638
- this.$.comboBox.clearCache();
639
- }
640
- }
641
-
642
- /**
643
- * Requests an update for the content of items.
644
- * While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
645
- *
646
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
647
- */
648
- requestContentUpdate() {
649
- if (this.$ && this.$.comboBox) {
650
- this.$.comboBox.requestContentUpdate();
651
- }
652
- }
653
-
654
- /**
655
- * Override method inherited from `DisabledMixin` to forward disabled to chips.
656
- * @protected
657
- * @override
658
- */
659
- _disabledChanged(disabled, oldDisabled) {
660
- super._disabledChanged(disabled, oldDisabled);
661
-
662
- if (disabled || oldDisabled) {
663
- this.__updateChips();
664
- }
665
- }
666
-
667
- /**
668
- * Override method inherited from `InputMixin` to forward the input to combo-box.
669
- * @protected
670
- * @override
671
- */
672
- _inputElementChanged(input) {
673
- super._inputElementChanged(input);
674
-
675
- if (input) {
676
- this.$.comboBox._setInputElement(input);
677
- }
678
- }
679
-
680
- /**
681
- * Override method inherited from `FocusMixin` to validate on blur.
682
- * @param {boolean} focused
683
- * @protected
684
- */
685
- _setFocused(focused) {
686
- super._setFocused(focused);
687
-
688
- // Do not validate when focusout is caused by document
689
- // losing focus, which happens on browser tab switch.
690
- if (!focused && document.hasFocus()) {
691
- this._focusedChipIndex = -1;
692
- this._requestValidation();
693
- }
694
- }
695
-
696
- /**
697
- * Implement callback from `ResizeMixin` to update chips.
698
- * @protected
699
- * @override
700
- */
701
- _onResize() {
702
- this.__updateChips();
703
- }
704
-
705
- /**
706
- * Override method from `DelegateStateMixin` to set required state
707
- * using `aria-required` attribute instead of `required`, in order
708
- * to prevent screen readers from announcing "invalid entry".
709
- * @protected
710
- * @override
711
- */
712
- _delegateAttribute(name, value) {
713
- if (!this.stateTarget) {
714
- return;
715
- }
716
-
717
- if (name === 'required') {
718
- this._delegateAttribute('aria-required', value ? 'true' : false);
719
- return;
720
- }
721
-
722
- super._delegateAttribute(name, value);
723
- }
724
-
725
- /** @private */
726
- _autoExpandHorizontallyChanged(autoExpand, oldAutoExpand) {
727
- if (autoExpand || oldAutoExpand) {
728
- this.__updateChips();
729
- }
730
- }
731
-
732
- /** @private */
733
- _autoExpandVerticallyChanged(autoExpand, oldAutoExpand) {
734
- if (autoExpand || oldAutoExpand) {
735
- this.__updateChips();
736
- }
737
- }
738
-
739
- /**
740
- * Setting clear button visible reduces total space available
741
- * for rendering chips, and making it hidden increases it.
742
- * @private
743
- */
744
- _clearButtonVisibleChanged(visible, oldVisible) {
745
- if (visible || oldVisible) {
746
- this.__updateChips();
747
- }
748
- }
749
-
750
- /**
751
- * Implement two-way binding for the `filteredItems` property
752
- * that can be set on the internal combo-box element.
753
- *
754
- * @param {CustomEvent} event
755
- * @private
756
- */
757
- _onFilteredItemsChanged(event) {
758
- const { value } = event.detail;
759
- if (Array.isArray(value) || value == null) {
760
- this.filteredItems = value;
761
- }
762
- }
763
-
764
- /** @private */
765
- _readonlyChanged(readonly, oldReadonly) {
766
- if (readonly || oldReadonly) {
767
- this.__updateChips();
768
- }
769
-
770
- if (this.dataProvider) {
771
- this.clearCache();
772
- }
773
- }
774
-
775
- /** @private */
776
- __itemClassNameGeneratorChanged(generator, oldGenerator) {
777
- if (generator || oldGenerator) {
778
- this.__updateChips();
779
- }
780
- }
781
-
782
- /** @private */
783
- _pageSizeChanged(pageSize, oldPageSize) {
784
- if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
785
- this.pageSize = oldPageSize;
786
- console.error('"pageSize" value must be an integer > 0');
787
- }
788
-
789
- this.$.comboBox.pageSize = this.pageSize;
790
- }
791
-
792
- /** @private */
793
- _placeholderChanged(placeholder) {
794
- const tmpPlaceholder = this.__tmpA11yPlaceholder;
795
- // Do not store temporary placeholder
796
- if (tmpPlaceholder !== placeholder) {
797
- this.__savedPlaceholder = placeholder;
798
-
799
- if (tmpPlaceholder) {
800
- this.placeholder = tmpPlaceholder;
801
- }
802
- }
803
- }
804
-
805
- /** @private */
806
- _selectedItemsChanged(selectedItems) {
807
- this._toggleHasValue(this._hasValue);
808
-
809
- // Use placeholder for announcing items
810
- if (this._hasValue) {
811
- const tmpPlaceholder = this._mergeItemLabels(selectedItems);
812
- if (this.__tmpA11yPlaceholder === undefined) {
813
- this.__savedPlaceholder = this.placeholder;
814
- }
815
- this.__tmpA11yPlaceholder = tmpPlaceholder;
816
- this.placeholder = tmpPlaceholder;
817
- } else if (this.__tmpA11yPlaceholder !== undefined) {
818
- delete this.__tmpA11yPlaceholder;
819
- this.placeholder = this.__savedPlaceholder;
820
- }
821
-
822
- // Re-render chips
823
- this.__updateChips();
824
-
825
- // Update selected for dropdown items
826
- this.requestContentUpdate();
827
-
828
- if (this.opened) {
829
- this.$.comboBox.$.overlay._updateOverlayWidth();
830
- }
831
- }
832
-
833
- /** @private */
834
- _getItemLabel(item) {
835
- return this.$.comboBox._getItemLabel(item);
836
- }
837
-
838
- /** @private */
839
- _mergeItemLabels(items) {
840
- return items.map((item) => this._getItemLabel(item)).join(', ');
841
- }
842
-
843
- /** @private */
844
- _findIndex(item, selectedItems, itemIdPath) {
845
- if (itemIdPath && item) {
846
- for (let index = 0; index < selectedItems.length; index++) {
847
- if (selectedItems[index] && selectedItems[index][itemIdPath] === item[itemIdPath]) {
848
- return index;
849
- }
850
- }
851
- return -1;
852
- }
853
-
854
- return selectedItems.indexOf(item);
855
- }
856
-
857
- /**
858
- * Clear the internal combo box value and filter. Filter will not be cleared
859
- * when the `keepFilter` option is enabled. Using `force` can enforce clearing
860
- * the filter.
861
- * @param {boolean} force overrides the keepFilter option
862
- * @private
863
- */
864
- __clearInternalValue(force = false) {
865
- if (!this.keepFilter || force) {
866
- // Clear both combo box value and filter.
867
- this.filter = '';
868
- this.$.comboBox.clear();
869
- } else {
870
- // Only clear combo box value. This effectively resets _lastCommittedValue
871
- // which allows toggling the same item multiple times via keyboard.
872
- this.$.comboBox.clear();
873
- // Restore input to the filter value. Needed when items are
874
- // navigated with keyboard, which overrides the input value
875
- // with the item label.
876
- this._inputElementValue = this.filter;
877
- }
878
- }
879
-
880
- /** @private */
881
- __announceItem(itemLabel, isSelected, itemCount) {
882
- const state = isSelected ? 'selected' : 'deselected';
883
- const total = this.i18n.total.replace('{count}', itemCount || 0);
884
- announce(`${itemLabel} ${this.i18n[state]} ${total}`);
885
- }
886
-
887
- /** @private */
888
- __removeItem(item) {
889
- const itemsCopy = [...this.selectedItems];
890
- itemsCopy.splice(itemsCopy.indexOf(item), 1);
891
- this.__updateSelection(itemsCopy);
892
- const itemLabel = this._getItemLabel(item);
893
- this.__announceItem(itemLabel, false, itemsCopy.length);
894
- }
895
-
896
- /** @private */
897
- __selectItem(item) {
898
- const itemsCopy = [...this.selectedItems];
899
-
900
- const index = this._findIndex(item, itemsCopy, this.itemIdPath);
901
- const itemLabel = this._getItemLabel(item);
902
-
903
- let isSelected = false;
904
-
905
- if (index !== -1) {
906
- const lastFilter = this._lastFilter;
907
- // Do not unselect when manually typing and committing an already selected item.
908
- if (lastFilter && lastFilter.toLowerCase() === itemLabel.toLowerCase()) {
909
- this.__clearInternalValue();
910
- return;
911
- }
912
-
913
- itemsCopy.splice(index, 1);
914
- } else {
915
- itemsCopy.push(item);
916
- isSelected = true;
917
- }
918
-
919
- this.__updateSelection(itemsCopy);
920
-
921
- // Suppress `value-changed` event.
922
- this.__clearInternalValue();
923
-
924
- this.__announceItem(itemLabel, isSelected, itemsCopy.length);
925
- }
926
-
927
- /** @private */
928
- __updateSelection(selectedItems) {
929
- this.selectedItems = selectedItems;
930
-
931
- this._requestValidation();
932
-
933
- this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
934
- }
935
-
936
- /** @private */
937
- __updateTopGroup(selectedItemsOnTop, selectedItems, opened) {
938
- if (!selectedItemsOnTop) {
939
- this._topGroup = [];
940
- } else if (!opened) {
941
- this._topGroup = [...selectedItems];
942
- }
943
- }
944
-
945
- /** @private */
946
- __createChip(item) {
947
- const chip = document.createElement('vaadin-multi-select-combo-box-chip');
948
- chip.setAttribute('slot', 'chip');
949
-
950
- chip.item = item;
951
- chip.disabled = this.disabled;
952
- chip.readonly = this.readonly;
953
-
954
- const label = this._getItemLabel(item);
955
- chip.label = label;
956
- chip.setAttribute('title', label);
957
-
958
- if (typeof this.itemClassNameGenerator === 'function') {
959
- chip.className = this.itemClassNameGenerator(item);
960
- }
961
-
962
- chip.addEventListener('item-removed', (e) => this._onItemRemoved(e));
963
- chip.addEventListener('mousedown', (e) => this._preventBlur(e));
964
-
965
- return chip;
966
- }
967
-
968
- /** @private */
969
- __getOverflowWidth() {
970
- const chip = this._overflow;
971
-
972
- chip.style.visibility = 'hidden';
973
- chip.removeAttribute('hidden');
974
-
975
- const count = chip.getAttribute('count');
976
-
977
- // Detect max possible width of the overflow chip
978
- // by measuring it with widest number (2 digits)
979
- chip.setAttribute('count', '99');
980
- const overflowStyle = getComputedStyle(chip);
981
- const overflowWidth = chip.clientWidth + parseInt(overflowStyle.marginInlineStart);
982
-
983
- chip.setAttribute('count', count);
984
- chip.setAttribute('hidden', '');
985
- chip.style.visibility = '';
986
-
987
- return overflowWidth;
988
- }
989
-
990
- /** @private */
991
- __updateChips() {
992
- if (!this._inputField || !this.inputElement) {
993
- return;
994
- }
995
-
996
- // Clear all chips except the overflow
997
- this._chips.forEach((chip) => {
998
- chip.remove();
999
- });
1000
-
1001
- const items = [...this.selectedItems];
1002
-
1003
- // Detect available remaining width for chips
1004
- const totalWidth = this._inputField.$.wrapper.clientWidth;
1005
- const inputWidth = parseInt(getComputedStyle(this.inputElement).flexBasis);
1006
-
1007
- let remainingWidth = totalWidth - inputWidth;
1008
-
1009
- if (items.length > 1) {
1010
- remainingWidth -= this.__getOverflowWidth();
1011
- }
1012
-
1013
- const chipMinWidth = parseInt(getComputedStyle(this).getPropertyValue('--_chip-min-width'));
1014
-
1015
- if (this.autoExpandHorizontally) {
1016
- const chips = [];
1017
-
1018
- // First, add all chips to make the field fully expand
1019
- for (let i = items.length - 1, refNode = null; i >= 0; i--) {
1020
- const chip = this.__createChip(items[i]);
1021
- this.insertBefore(chip, refNode);
1022
- refNode = chip;
1023
- chips.unshift(chip);
1024
- }
1025
-
1026
- const overflowItems = [];
1027
- const availableWidth = this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth;
1028
-
1029
- // When auto expanding vertically, no need to measure width
1030
- if (!this.autoExpandVertically && availableWidth < inputWidth) {
1031
- // Always show at least last item as a chip
1032
- while (chips.length > 1) {
1033
- const lastChip = chips.pop();
1034
- lastChip.remove();
1035
- overflowItems.unshift(items.pop());
1036
-
1037
- // Remove chips until there is enough width for the input element to fit
1038
- const neededWidth = overflowItems.length > 0 ? inputWidth + this.__getOverflowWidth() : inputWidth;
1039
- if (this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth >= neededWidth) {
1040
- break;
1041
- }
1042
- }
1043
-
1044
- if (chips.length === 1) {
1045
- chips[0].style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`;
1046
- }
1047
- }
1048
-
1049
- this._overflowItems = overflowItems;
1050
- return;
1051
- }
1052
-
1053
- // Add chips until remaining width is exceeded
1054
- for (let i = items.length - 1, refNode = null; i >= 0; i--) {
1055
- const chip = this.__createChip(items[i]);
1056
- this.insertBefore(chip, refNode);
1057
-
1058
- // When auto expanding vertically, no need to measure remaining width
1059
- if (!this.autoExpandVertically && this.$.chips.clientWidth > remainingWidth) {
1060
- // Always show at least last selected item as a chip
1061
- if (refNode === null) {
1062
- chip.style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`;
1063
- } else {
1064
- chip.remove();
1065
- break;
1066
- }
1067
- }
1068
-
1069
- items.pop();
1070
- refNode = chip;
1071
- }
1072
-
1073
- this._overflowItems = items;
1074
- }
1075
-
1076
- /** @private */
1077
- __updateOverflowChip(overflow, items, disabled, readonly) {
1078
- if (overflow) {
1079
- const count = items.length;
1080
-
1081
- overflow.label = `${count}`;
1082
- overflow.setAttribute('count', `${count}`);
1083
- overflow.setAttribute('title', this._mergeItemLabels(items));
1084
- overflow.toggleAttribute('hidden', count === 0);
1085
-
1086
- overflow.disabled = disabled;
1087
- overflow.readonly = readonly;
1088
- }
1089
- }
1090
-
1091
- /** @private */
1092
- _onClearButtonTouchend(event) {
1093
- // Cancel the following click and focus events
1094
- event.preventDefault();
1095
- // Prevent default combo box behavior which can otherwise unnecessarily
1096
- // clear the input and filter
1097
- event.stopPropagation();
1098
-
1099
- this.clear();
1100
- }
1101
-
1102
- /**
1103
- * Override method inherited from `InputControlMixin` and clear items.
1104
- * @protected
1105
- * @override
1106
- */
1107
- _onClearButtonClick(event) {
1108
- event.stopPropagation();
1109
-
1110
- this.clear();
1111
- }
1112
-
1113
- /**
1114
- * Override an event listener from `InputControlMixin` to
1115
- * stop the change event re-targeted from the input.
1116
- *
1117
- * @param {!Event} event
1118
- * @protected
1119
- * @override
1120
- */
1121
- _onChange(event) {
1122
- event.stopPropagation();
1123
- }
1124
-
1125
- /**
1126
- * Override an event listener from `KeyboardMixin`.
1127
- * Do not call `super` in order to override clear
1128
- * button logic defined in `InputControlMixin`.
1129
- *
1130
- * @param {!KeyboardEvent} event
1131
- * @protected
1132
- * @override
1133
- */
1134
- _onEscape(event) {
1135
- if (this.clearButtonVisible && this.selectedItems && this.selectedItems.length) {
1136
- event.stopPropagation();
1137
- this.selectedItems = [];
1138
- }
1139
- }
1140
-
1141
- /**
1142
- * Override an event listener from `KeyboardMixin`.
1143
- * @param {KeyboardEvent} event
1144
- * @protected
1145
- * @override
1146
- */
1147
- _onKeyDown(event) {
1148
- super._onKeyDown(event);
1149
-
1150
- const chips = this._chips;
1151
-
1152
- if (!this.readonly && chips.length > 0) {
1153
- switch (event.key) {
1154
- case 'Backspace':
1155
- this._onBackSpace(chips);
1156
- break;
1157
- case 'ArrowLeft':
1158
- this._onArrowLeft(chips, event);
1159
- break;
1160
- case 'ArrowRight':
1161
- this._onArrowRight(chips, event);
1162
- break;
1163
- default:
1164
- this._focusedChipIndex = -1;
1165
- break;
1166
- }
1167
- }
1168
- }
1169
-
1170
- /** @private */
1171
- _onArrowLeft(chips, event) {
1172
- if (this.inputElement.selectionStart !== 0) {
1173
- return;
1174
- }
1175
-
1176
- const idx = this._focusedChipIndex;
1177
- if (idx !== -1) {
1178
- event.preventDefault();
1179
- }
1180
- let newIdx;
1181
-
1182
- if (!this.__isRTL) {
1183
- if (idx === -1) {
1184
- // Focus last chip
1185
- newIdx = chips.length - 1;
1186
- } else if (idx > 0) {
1187
- // Focus prev chip
1188
- newIdx = idx - 1;
1189
- }
1190
- } else if (idx === chips.length - 1) {
1191
- // Blur last chip
1192
- newIdx = -1;
1193
- } else if (idx > -1) {
1194
- // Focus next chip
1195
- newIdx = idx + 1;
1196
- }
1197
-
1198
- if (newIdx !== undefined) {
1199
- this._focusedChipIndex = newIdx;
1200
- }
1201
- }
1202
-
1203
- /** @private */
1204
- _onArrowRight(chips, event) {
1205
- if (this.inputElement.selectionStart !== 0) {
1206
- return;
1207
- }
1208
-
1209
- const idx = this._focusedChipIndex;
1210
- if (idx !== -1) {
1211
- event.preventDefault();
1212
- }
1213
- let newIdx;
1214
-
1215
- if (this.__isRTL) {
1216
- if (idx === -1) {
1217
- // Focus last chip
1218
- newIdx = chips.length - 1;
1219
- } else if (idx > 0) {
1220
- // Focus prev chip
1221
- newIdx = idx - 1;
1222
- }
1223
- } else if (idx === chips.length - 1) {
1224
- // Blur last chip
1225
- newIdx = -1;
1226
- } else if (idx > -1) {
1227
- // Focus next chip
1228
- newIdx = idx + 1;
1229
- }
1230
-
1231
- if (newIdx !== undefined) {
1232
- this._focusedChipIndex = newIdx;
1233
- }
1234
- }
1235
-
1236
- /** @private */
1237
- _onBackSpace(chips) {
1238
- if (this.inputElement.selectionStart !== 0) {
1239
- return;
1240
- }
1241
-
1242
- const idx = this._focusedChipIndex;
1243
- if (idx === -1) {
1244
- this._focusedChipIndex = chips.length - 1;
1245
- } else {
1246
- this.__removeItem(chips[idx].item);
1247
- this._focusedChipIndex = -1;
1248
- }
1249
- }
1250
-
1251
- /** @private */
1252
- _focusedChipIndexChanged(focusedIndex, oldFocusedIndex) {
1253
- if (focusedIndex > -1 || oldFocusedIndex > -1) {
1254
- const chips = this._chips;
1255
- chips.forEach((chip, index) => {
1256
- chip.toggleAttribute('focused', index === focusedIndex);
1257
- });
1258
-
1259
- // Announce focused chip
1260
- if (focusedIndex > -1) {
1261
- const item = chips[focusedIndex].item;
1262
- const itemLabel = this._getItemLabel(item);
1263
- announce(`${itemLabel} ${this.i18n.focused}`);
1264
- }
1265
- }
1266
- }
1267
-
1268
- /** @private */
1269
- _onComboBoxChange() {
1270
- const item = this.$.comboBox.selectedItem;
1271
- if (item) {
1272
- this.__selectItem(item);
1273
- }
1274
- }
1275
-
1276
- /** @private */
1277
- _onComboBoxItemSelected(event) {
1278
- this.__selectItem(event.detail.item);
1279
- }
1280
-
1281
- /** @private */
1282
- _onCustomValueSet(event) {
1283
- // Do not set combo-box value
1284
- event.preventDefault();
1285
-
1286
- // Stop the original event
1287
- event.stopPropagation();
1288
-
1289
- this.__clearInternalValue(true);
1290
-
1291
- this.dispatchEvent(
1292
- new CustomEvent('custom-value-set', {
1293
- detail: event.detail,
1294
- composed: true,
1295
- bubbles: true,
1296
- }),
1297
- );
1298
- }
1299
-
1300
- /** @private */
1301
- _onItemRemoved(event) {
1302
- this.__removeItem(event.detail.item);
1303
- }
1304
-
1305
- /** @private */
1306
- _preventBlur(event) {
1307
- // Prevent mousedown event to keep the input focused
1308
- // and keep the overlay opened when clicking a chip.
1309
- event.preventDefault();
1310
- }
1311
-
1312
- /**
1313
- * Fired when the user sets a custom value.
1314
- * @event custom-value-set
1315
- * @param {string} detail the custom value
1316
- */
1317
183
  }
1318
184
 
1319
185
  defineCustomElement(MultiSelectComboBox);