@vaadin/combo-box 25.0.0-alpha2 → 25.0.0-alpha20

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 (34) hide show
  1. package/package.json +14 -17
  2. package/src/styles/vaadin-combo-box-base-styles.js +2 -2
  3. package/src/styles/vaadin-combo-box-overlay-base-styles.js +31 -48
  4. package/src/vaadin-combo-box-base-mixin.d.ts +54 -0
  5. package/src/vaadin-combo-box-base-mixin.js +772 -0
  6. package/src/vaadin-combo-box-data-provider-mixin.js +17 -32
  7. package/src/vaadin-combo-box-item-mixin.js +6 -1
  8. package/src/vaadin-combo-box-item.js +5 -2
  9. package/src/vaadin-combo-box-items-mixin.d.ts +53 -0
  10. package/src/vaadin-combo-box-items-mixin.js +275 -0
  11. package/src/vaadin-combo-box-mixin.d.ts +3 -74
  12. package/src/vaadin-combo-box-mixin.js +84 -886
  13. package/src/vaadin-combo-box-overlay-mixin.js +1 -22
  14. package/src/vaadin-combo-box-overlay.js +4 -4
  15. package/src/vaadin-combo-box-scroller-mixin.d.ts +1 -2
  16. package/src/vaadin-combo-box-scroller-mixin.js +5 -0
  17. package/src/vaadin-combo-box-scroller.js +1 -1
  18. package/src/vaadin-combo-box.d.ts +33 -19
  19. package/src/vaadin-combo-box.js +67 -22
  20. package/vaadin-combo-box.js +1 -1
  21. package/web-types.json +51 -73
  22. package/web-types.lit.json +17 -24
  23. package/src/styles/vaadin-combo-box-core-styles.d.ts +0 -8
  24. package/src/styles/vaadin-combo-box-core-styles.js +0 -12
  25. package/src/styles/vaadin-combo-box-overlay-core-styles.js +0 -18
  26. package/src/styles/vaadin-combo-box-scroller-core-styles.js +0 -30
  27. package/theme/lumo/vaadin-combo-box-item-styles.d.ts +0 -5
  28. package/theme/lumo/vaadin-combo-box-item-styles.js +0 -25
  29. package/theme/lumo/vaadin-combo-box-overlay-styles.d.ts +0 -6
  30. package/theme/lumo/vaadin-combo-box-overlay-styles.js +0 -60
  31. package/theme/lumo/vaadin-combo-box-styles.d.ts +0 -2
  32. package/theme/lumo/vaadin-combo-box-styles.js +0 -12
  33. package/theme/lumo/vaadin-combo-box.d.ts +0 -4
  34. package/theme/lumo/vaadin-combo-box.js +0 -4
@@ -3,17 +3,8 @@
3
3
  * Copyright (c) 2015 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
7
- import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
8
- import { isElementFocused, isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
9
- import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
10
- import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
11
- import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
12
- import { get } from '@vaadin/component-base/src/path-utils.js';
13
- import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
14
6
  import { ValidateMixin } from '@vaadin/field-base/src/validate-mixin.js';
15
- import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
16
- import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
7
+ import { ComboBoxItemsMixin } from './vaadin-combo-box-items-mixin.js';
17
8
 
18
9
  /**
19
10
  * Checks if the value is supported as an item value in this control.
@@ -25,72 +16,16 @@ function isValidValue(value) {
25
16
  return value !== undefined && value !== null;
26
17
  }
27
18
 
28
- /**
29
- * Returns the index of the first item that satisfies the provided testing function
30
- * ignoring placeholder items.
31
- *
32
- * @param {Array<ComboBoxItem | string>} items
33
- * @param {Function} callback
34
- * @return {number}
35
- */
36
- function findItemIndex(items, callback) {
37
- return items.findIndex((item) => {
38
- if (item instanceof ComboBoxPlaceholder) {
39
- return false;
40
- }
41
-
42
- return callback(item);
43
- });
44
- }
45
-
46
19
  /**
47
20
  * @polymerMixin
21
+ * @mixes ComboBoxItemsMixin
48
22
  * @mixes ValidateMixin
49
- * @mixes DisabledMixin
50
- * @mixes InputMixin
51
- * @mixes KeyboardMixin
52
- * @mixes FocusMixin
53
- * @mixes OverlayClassMixin
54
- * @param {function(new:HTMLElement)} subclass
23
+ * @param {function(new:HTMLElement)} superClass
55
24
  */
56
- export const ComboBoxMixin = (subclass) =>
57
- class ComboBoxMixinClass extends OverlayClassMixin(
58
- ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass))))),
59
- ) {
25
+ export const ComboBoxMixin = (superClass) =>
26
+ class ComboBoxMixinClass extends ValidateMixin(ComboBoxItemsMixin(superClass)) {
60
27
  static get properties() {
61
28
  return {
62
- /**
63
- * True if the dropdown is open, false otherwise.
64
- * @type {boolean}
65
- */
66
- opened: {
67
- type: Boolean,
68
- notify: true,
69
- value: false,
70
- reflectToAttribute: true,
71
- sync: true,
72
- observer: '_openedChanged',
73
- },
74
-
75
- /**
76
- * Set true to prevent the overlay from opening automatically.
77
- * @attr {boolean} auto-open-disabled
78
- */
79
- autoOpenDisabled: {
80
- type: Boolean,
81
- sync: true,
82
- },
83
-
84
- /**
85
- * When present, it specifies that the field is read-only.
86
- * @type {boolean}
87
- */
88
- readonly: {
89
- type: Boolean,
90
- value: false,
91
- reflectToAttribute: true,
92
- },
93
-
94
29
  /**
95
30
  * Custom function for rendering the content of every item.
96
31
  * Receives three arguments:
@@ -108,17 +43,6 @@ export const ComboBoxMixin = (subclass) =>
108
43
  sync: true,
109
44
  },
110
45
 
111
- /**
112
- * A full set of items to filter the visible options from.
113
- * The items can be of either `String` or `Object` type.
114
- * @type {!Array<!ComboBoxItem | string> | undefined}
115
- */
116
- items: {
117
- type: Array,
118
- sync: true,
119
- observer: '_itemsChanged',
120
- },
121
-
122
46
  /**
123
47
  * If `true`, the user can input a value that is not present in the items list.
124
48
  * `value` property will be set to the input value in this case.
@@ -132,24 +56,6 @@ export const ComboBoxMixin = (subclass) =>
132
56
  value: false,
133
57
  },
134
58
 
135
- /**
136
- * A subset of items, filtered based on the user input. Filtered items
137
- * can be assigned directly to omit the internal filtering functionality.
138
- * The items can be of either `String` or `Object` type.
139
- * @type {!Array<!ComboBoxItem | string> | undefined}
140
- */
141
- filteredItems: {
142
- type: Array,
143
- observer: '_filteredItemsChanged',
144
- sync: true,
145
- },
146
-
147
- /**
148
- * Used to detect user value changes and fire `change` events.
149
- * @private
150
- */
151
- _lastCommittedValue: String,
152
-
153
59
  /**
154
60
  * When set to `true`, "loading" attribute is added to host and the overlay element.
155
61
  * @type {boolean}
@@ -161,28 +67,6 @@ export const ComboBoxMixin = (subclass) =>
161
67
  sync: true,
162
68
  },
163
69
 
164
- /**
165
- * @type {number}
166
- * @protected
167
- */
168
- _focusedIndex: {
169
- type: Number,
170
- observer: '_focusedIndexChanged',
171
- value: -1,
172
- sync: true,
173
- },
174
-
175
- /**
176
- * Filtering string the user has typed into the input field.
177
- * @type {string}
178
- */
179
- filter: {
180
- type: String,
181
- value: '',
182
- notify: true,
183
- sync: true,
184
- },
185
-
186
70
  /**
187
71
  * The selected item from the `items` array.
188
72
  * @type {ComboBoxItem | string | undefined}
@@ -203,39 +87,6 @@ export const ComboBoxMixin = (subclass) =>
203
87
  type: Object,
204
88
  },
205
89
 
206
- /**
207
- * Path for label of the item. If `items` is an array of objects, the
208
- * `itemLabelPath` is used to fetch the displayed string label for each
209
- * item.
210
- *
211
- * The item label is also used for matching items when processing user
212
- * input, i.e., for filtering and selecting items.
213
- * @attr {string} item-label-path
214
- * @type {string}
215
- */
216
- itemLabelPath: {
217
- type: String,
218
- value: 'label',
219
- observer: '_itemLabelPathChanged',
220
- sync: true,
221
- },
222
-
223
- /**
224
- * Path for the value of the item. If `items` is an array of objects, the
225
- * `itemValuePath:` is used to fetch the string value for the selected
226
- * item.
227
- *
228
- * The item value is used in the `value` property of the combo box,
229
- * to provide the form value.
230
- * @attr {string} item-value-path
231
- * @type {string}
232
- */
233
- itemValuePath: {
234
- type: String,
235
- value: 'value',
236
- sync: true,
237
- },
238
-
239
90
  /**
240
91
  * Path for the id of the item. If `items` is an array of objects,
241
92
  * the `itemIdPath` is used to compare and identify the same item
@@ -248,40 +99,6 @@ export const ComboBoxMixin = (subclass) =>
248
99
  sync: true,
249
100
  },
250
101
 
251
- /**
252
- * @type {!HTMLElement | undefined}
253
- * @protected
254
- */
255
- _toggleElement: {
256
- type: Object,
257
- observer: '_toggleElementChanged',
258
- },
259
-
260
- /**
261
- * Set of items to be rendered in the dropdown.
262
- * @protected
263
- */
264
- _dropdownItems: {
265
- type: Array,
266
- sync: true,
267
- },
268
-
269
- /** @private */
270
- _closeOnBlurIsPrevented: Boolean,
271
-
272
- /** @private */
273
- _scroller: {
274
- type: Object,
275
- sync: true,
276
- },
277
-
278
- /** @private */
279
- _overlayOpened: {
280
- type: Boolean,
281
- sync: true,
282
- observer: '_overlayOpenedChanged',
283
- },
284
-
285
102
  /** @private */
286
103
  __keepOverlayOpened: {
287
104
  type: Boolean,
@@ -292,91 +109,23 @@ export const ComboBoxMixin = (subclass) =>
292
109
 
293
110
  static get observers() {
294
111
  return [
295
- '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
296
112
  '_openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
297
- '_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme, itemClassNameGenerator)',
113
+ '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
114
+ '_updateScroller(opened, _dropdownItems, _focusedIndex, _theme)',
298
115
  ];
299
116
  }
300
117
 
301
- constructor() {
302
- super();
303
- this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
304
- this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
305
- this._boundOnClick = this._onClick.bind(this);
306
- this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
307
- this._boundOnTouchend = this._onTouchend.bind(this);
308
- }
309
-
310
- /**
311
- * Tag name prefix used by scroller and items.
312
- * @protected
313
- * @return {string}
314
- */
315
- get _tagNamePrefix() {
316
- return 'vaadin-combo-box';
317
- }
318
-
319
- /**
320
- * Override method inherited from `InputMixin`
321
- * to customize the input element.
322
- * @protected
323
- * @override
324
- */
325
- _inputElementChanged(input) {
326
- super._inputElementChanged(input);
327
-
328
- if (input) {
329
- input.autocomplete = 'off';
330
- input.autocapitalize = 'off';
331
-
332
- input.setAttribute('role', 'combobox');
333
- input.setAttribute('aria-autocomplete', 'list');
334
- input.setAttribute('aria-expanded', !!this.opened);
335
-
336
- // Disable the macOS Safari spell check auto corrections.
337
- input.setAttribute('spellcheck', 'false');
338
-
339
- // Disable iOS autocorrect suggestions.
340
- input.setAttribute('autocorrect', 'off');
341
-
342
- this._revertInputValueToValue();
343
- }
344
- }
345
-
346
118
  /** @protected */
347
119
  ready() {
348
120
  super.ready();
349
121
 
350
- this._initOverlay();
351
- this._initScroller();
352
-
122
+ /**
123
+ * Used to detect user value changes and fire `change` events.
124
+ * Do not define in `properties` to avoid triggering updates.
125
+ * @type {string}
126
+ * @protected
127
+ */
353
128
  this._lastCommittedValue = this.value;
354
-
355
- this.addEventListener('click', this._boundOnClick);
356
- this.addEventListener('touchend', this._boundOnTouchend);
357
-
358
- if (this.clearElement) {
359
- this.clearElement.addEventListener('mousedown', this._boundOnClearButtonMouseDown);
360
- }
361
-
362
- const bringToFrontListener = () => {
363
- requestAnimationFrame(() => {
364
- this._overlayElement.bringToFront();
365
- });
366
- };
367
-
368
- this.addEventListener('mousedown', bringToFrontListener);
369
- this.addEventListener('touchstart', bringToFrontListener);
370
-
371
- this.addController(new VirtualKeyboardController(this));
372
- }
373
-
374
- /** @protected */
375
- disconnectedCallback() {
376
- super.disconnectedCallback();
377
-
378
- // Close the overlay on detach
379
- this.close();
380
129
  }
381
130
 
382
131
  /**
@@ -398,116 +147,32 @@ export const ComboBoxMixin = (subclass) =>
398
147
  }
399
148
 
400
149
  /**
401
- * Opens the dropdown list.
402
- */
403
- open() {
404
- // Prevent _open() being called when input is disabled or read-only
405
- if (!this.disabled && !this.readonly) {
406
- this.opened = true;
407
- }
408
- }
409
-
410
- /**
411
- * Closes the dropdown list.
412
- */
413
- close() {
414
- this.opened = false;
415
- }
416
-
417
- /**
418
- * Override LitElement lifecycle callback to handle filter property change.
419
150
  * @param {Object} props
420
151
  * @protected
421
152
  */
422
153
  updated(props) {
423
154
  super.updated(props);
424
155
 
425
- if (props.has('filter')) {
426
- this._filterChanged(this.filter);
427
- }
428
- }
429
-
430
- /** @private */
431
- _initOverlay() {
432
- const overlay = this.$.overlay;
433
-
434
- // Store instance for detecting "dir" attribute on opening
435
- overlay._comboBox = this;
436
-
437
- overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
438
- overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
439
-
440
- // Prevent blurring the input when clicking inside the overlay
441
- overlay.addEventListener('mousedown', (e) => e.preventDefault());
442
-
443
- // Manual two-way binding for the overlay "opened" property
444
- overlay.addEventListener('opened-changed', (e) => {
445
- this._overlayOpened = e.detail.value;
446
- });
447
-
448
- this._overlayElement = overlay;
449
- }
450
-
451
- /**
452
- * Create and initialize the scroller element.
453
- * Override to provide custom host reference.
454
- *
455
- * @protected
456
- */
457
- _initScroller(host) {
458
- const scroller = document.createElement(`${this._tagNamePrefix}-scroller`);
459
-
460
- scroller.owner = host || this;
461
- scroller.getItemLabel = this._getItemLabel.bind(this);
462
- scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
463
-
464
- const overlay = this._overlayElement;
465
-
466
- overlay.renderer = (root) => {
467
- if (!root.innerHTML) {
468
- root.appendChild(scroller);
156
+ ['loading', 'itemIdPath', 'itemClassNameGenerator', 'renderer', 'selectedItem'].forEach((prop) => {
157
+ if (props.has(prop)) {
158
+ this._scroller[prop] = this[prop];
469
159
  }
470
- };
471
-
472
- // Ensure the scroller is rendered
473
- overlay.requestContentUpdate();
474
-
475
- // Trigger the observer to set properties
476
- this._scroller = scroller;
160
+ });
477
161
  }
478
162
 
479
163
  /** @private */
480
- // eslint-disable-next-line @typescript-eslint/max-params
481
- _updateScroller(
482
- scroller,
483
- items,
484
- opened,
485
- loading,
486
- selectedItem,
487
- itemIdPath,
488
- focusedIndex,
489
- renderer,
490
- theme,
491
- itemClassNameGenerator,
492
- ) {
493
- if (scroller) {
494
- if (opened) {
495
- scroller.style.maxHeight =
496
- getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
497
- }
498
-
499
- scroller.setProperties({
500
- items: opened ? items : [],
501
- opened,
502
- loading,
503
- selectedItem,
504
- itemIdPath,
505
- focusedIndex,
506
- renderer,
507
- theme,
508
- itemClassNameGenerator,
509
- });
164
+ _updateScroller(opened, items, focusedIndex, theme) {
165
+ if (opened) {
166
+ this._scroller.style.maxHeight =
167
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
510
168
  }
169
+
170
+ this._scroller.setProperties({
171
+ items: opened ? items : [],
172
+ opened,
173
+ focusedIndex,
174
+ theme,
175
+ });
511
176
  }
512
177
 
513
178
  /** @private */
@@ -517,258 +182,39 @@ export const ComboBoxMixin = (subclass) =>
517
182
  this._overlayOpened = opened && (keepOverlayOpened || loading || !!(items && items.length));
518
183
  }
519
184
 
520
- /** @private */
521
- _overlayOpenedChanged(opened, wasOpened) {
522
- if (opened) {
523
- this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
524
-
525
- this._onOpened();
526
- } else if (wasOpened && this._dropdownItems && this._dropdownItems.length) {
527
- this.close();
528
-
529
- this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
530
- }
531
- }
532
-
533
- /** @private */
534
- _focusedIndexChanged(index, oldIndex) {
535
- if (oldIndex === undefined) {
536
- return;
537
- }
538
- this._updateActiveDescendant(index);
539
- }
540
-
541
- /** @protected */
542
- _isInputFocused() {
543
- return this.inputElement && isElementFocused(this.inputElement);
544
- }
545
-
546
- /** @private */
547
- _updateActiveDescendant(index) {
548
- const input = this.inputElement;
549
- if (!input) {
550
- return;
551
- }
552
-
553
- const item = this._getItemElements().find((el) => el.index === index);
554
- if (item) {
555
- input.setAttribute('aria-activedescendant', item.id);
556
- } else {
557
- input.removeAttribute('aria-activedescendant');
558
- }
559
- }
560
-
561
- /** @private */
562
- _openedChanged(opened, wasOpened) {
563
- // Prevent _close() being called when opened is set to its default value (false).
564
- if (wasOpened === undefined) {
565
- return;
566
- }
567
-
568
- if (opened) {
569
- // For touch devices, we don't want to popup virtual keyboard
570
- // unless input element is explicitly focused by the user.
571
- if (!this._isInputFocused() && !isTouch) {
572
- if (this.inputElement) {
573
- this.inputElement.focus();
574
- }
575
- }
576
- } else {
577
- this._onClosed();
578
- }
579
-
580
- const input = this.inputElement;
581
- if (input) {
582
- input.setAttribute('aria-expanded', !!opened);
583
-
584
- if (opened) {
585
- input.setAttribute('aria-controls', this._scroller.id);
586
- } else {
587
- input.removeAttribute('aria-controls');
588
- }
589
- }
590
- }
591
-
592
- /** @private */
593
- _onOverlayTouchAction() {
594
- // On touch devices, blur the input on touch start inside the overlay, in order to hide
595
- // the virtual keyboard. But don't close the overlay on this blur.
596
- this._closeOnBlurIsPrevented = true;
597
- this.inputElement.blur();
598
- this._closeOnBlurIsPrevented = false;
599
- }
600
-
601
- /** @protected */
602
- _isClearButton(event) {
603
- return event.composedPath()[0] === this.clearElement;
604
- }
605
-
606
- /** @private */
607
- __onClearButtonMouseDown(event) {
608
- event.preventDefault(); // Prevent native focusout event
609
- this.inputElement.focus();
610
- }
611
-
612
185
  /**
186
+ * Override method from `ComboBoxBaseMixin` to deselect
187
+ * dropdown item by requesting content update on clear.
613
188
  * @param {Event} event
614
189
  * @protected
615
190
  */
616
191
  _onClearButtonClick(event) {
617
- event.preventDefault();
618
- this._onClearAction();
192
+ super._onClearButtonClick(event);
619
193
 
620
- // De-select dropdown item
621
194
  if (this.opened) {
622
195
  this.requestContentUpdate();
623
196
  }
624
197
  }
625
198
 
626
199
  /**
627
- * @param {Event} event
628
- * @private
629
- */
630
- _onToggleButtonClick(event) {
631
- // Prevent parent components such as `vaadin-grid`
632
- // from handling the click event after it bubbles.
633
- event.preventDefault();
634
-
635
- if (this.opened) {
636
- this.close();
637
- } else {
638
- this.open();
639
- }
640
- }
641
-
642
- /**
643
- * @param {Event} event
200
+ * Override method inherited from `InputMixin`
201
+ * to revert the input value to value.
644
202
  * @protected
203
+ * @override
645
204
  */
646
- _onHostClick(event) {
647
- if (!this.autoOpenDisabled) {
648
- event.preventDefault();
649
- this.open();
650
- }
651
- }
205
+ _inputElementChanged(input) {
206
+ super._inputElementChanged(input);
652
207
 
653
- /** @private */
654
- _onClick(event) {
655
- if (this._isClearButton(event)) {
656
- this._onClearButtonClick(event);
657
- } else if (event.composedPath().includes(this._toggleElement)) {
658
- this._onToggleButtonClick(event);
659
- } else {
660
- this._onHostClick(event);
208
+ if (input) {
209
+ this._revertInputValueToValue();
661
210
  }
662
211
  }
663
212
 
664
213
  /**
665
- * Override an event listener from `KeyboardMixin`.
666
- *
667
- * @param {KeyboardEvent} e
214
+ * Override method from `ComboBoxBaseMixin` to handle loading.
668
215
  * @protected
669
216
  * @override
670
217
  */
671
- _onKeyDown(e) {
672
- super._onKeyDown(e);
673
-
674
- if (e.key === 'ArrowDown') {
675
- this._onArrowDown();
676
-
677
- // Prevent caret from moving
678
- e.preventDefault();
679
- } else if (e.key === 'ArrowUp') {
680
- this._onArrowUp();
681
-
682
- // Prevent caret from moving
683
- e.preventDefault();
684
- }
685
- }
686
-
687
- /** @private */
688
- _getItemLabel(item) {
689
- let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
690
- if (label === undefined || label === null) {
691
- label = item ? item.toString() : '';
692
- }
693
- return label;
694
- }
695
-
696
- /** @private */
697
- _getItemValue(item) {
698
- let value = item && this.itemValuePath ? get(this.itemValuePath, item) : undefined;
699
- if (value === undefined) {
700
- value = item ? item.toString() : '';
701
- }
702
- return value;
703
- }
704
-
705
- /** @private */
706
- _onArrowDown() {
707
- if (this.opened) {
708
- const items = this._dropdownItems;
709
- if (items) {
710
- this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
711
- this._prefillFocusedItemLabel();
712
- }
713
- } else {
714
- this.open();
715
- }
716
- }
717
-
718
- /** @private */
719
- _onArrowUp() {
720
- if (this.opened) {
721
- if (this._focusedIndex > -1) {
722
- this._focusedIndex = Math.max(0, this._focusedIndex - 1);
723
- } else {
724
- const items = this._dropdownItems;
725
- if (items) {
726
- this._focusedIndex = items.length - 1;
727
- }
728
- }
729
-
730
- this._prefillFocusedItemLabel();
731
- } else {
732
- this.open();
733
- }
734
- }
735
-
736
- /** @private */
737
- _prefillFocusedItemLabel() {
738
- if (this._focusedIndex > -1) {
739
- const focusedItem = this._dropdownItems[this._focusedIndex];
740
- this._inputElementValue = this._getItemLabel(focusedItem);
741
- this._markAllSelectionRange();
742
- }
743
- }
744
-
745
- /** @private */
746
- _setSelectionRange(start, end) {
747
- // Setting selection range focuses and/or moves the caret in some browsers,
748
- // and there's no need to modify the selection range if the input isn't focused anyway.
749
- // This affects Safari. When the overlay is open, and then hitting tab, browser should focus
750
- // the next focusable element instead of the combo-box itself.
751
- if (this._isInputFocused() && this.inputElement.setSelectionRange) {
752
- this.inputElement.setSelectionRange(start, end);
753
- }
754
- }
755
-
756
- /** @private */
757
- _markAllSelectionRange() {
758
- if (this._inputElementValue !== undefined) {
759
- this._setSelectionRange(0, this._inputElementValue.length);
760
- }
761
- }
762
-
763
- /** @private */
764
- _clearSelectionRange() {
765
- if (this._inputElementValue !== undefined) {
766
- const pos = this._inputElementValue ? this._inputElementValue.length : 0;
767
- this._setSelectionRange(pos, pos);
768
- }
769
- }
770
-
771
- /** @private */
772
218
  _closeOrCommit() {
773
219
  if (!this.opened && !this.loading) {
774
220
  this._commitValue();
@@ -778,39 +224,10 @@ export const ComboBoxMixin = (subclass) =>
778
224
  }
779
225
 
780
226
  /**
781
- * Override an event listener from `KeyboardMixin`.
782
- *
783
- * @param {KeyboardEvent} e
227
+ * Override method from `ComboBoxBaseMixin` to handle valid value.
784
228
  * @protected
785
229
  * @override
786
230
  */
787
- _onEnter(e) {
788
- // Do not commit value when custom values are disallowed and input value is not a valid option
789
- // also stop propagation of the event, otherwise the user could submit a form while the input
790
- // still contains an invalid value
791
- if (!this._hasValidInputValue()) {
792
- // Do not submit the surrounding form.
793
- e.preventDefault();
794
- // Do not trigger global listeners
795
- e.stopPropagation();
796
- return;
797
- }
798
-
799
- // Stop propagation of the enter event only if the dropdown is opened, this
800
- // "consumes" the enter event for the action of closing the dropdown
801
- if (this.opened) {
802
- // Do not submit the surrounding form.
803
- e.preventDefault();
804
- // Do not trigger global listeners
805
- e.stopPropagation();
806
- }
807
-
808
- this._closeOrCommit();
809
- }
810
-
811
- /**
812
- * @protected
813
- */
814
231
  _hasValidInputValue() {
815
232
  const hasInvalidOption =
816
233
  this._focusedIndex < 0 &&
@@ -821,62 +238,18 @@ export const ComboBoxMixin = (subclass) =>
821
238
  }
822
239
 
823
240
  /**
824
- * Override an event listener from `KeyboardMixin`.
825
- * Do not call `super` in order to override clear
826
- * button logic defined in `InputControlMixin`.
827
- *
828
- * @param {!KeyboardEvent} e
241
+ * Override method from `ComboBoxBaseMixin`.
829
242
  * @protected
830
243
  * @override
831
244
  */
832
- _onEscape(e) {
833
- if (
834
- this.autoOpenDisabled &&
835
- (this.opened || (this.value !== this._inputElementValue && this._inputElementValue.length > 0))
836
- ) {
837
- // Auto-open is disabled
838
- // The overlay is open or
839
- // The input value has changed but the change hasn't been committed, so cancel it.
840
- e.stopPropagation();
841
- this._focusedIndex = -1;
842
- this.cancel();
843
- } else if (this.opened) {
844
- // Auto-open is enabled
845
- // The overlay is open
846
- e.stopPropagation();
847
-
848
- if (this._focusedIndex > -1) {
849
- // An item is focused, revert the input to the filtered value
850
- this._focusedIndex = -1;
851
- this._revertInputValue();
852
- } else {
853
- // No item is focused, cancel the change and close the overlay
854
- this.cancel();
855
- }
856
- } else if (this.clearButtonVisible && !!this.value && !this.readonly) {
857
- e.stopPropagation();
858
- // The clear button is visible and the overlay is closed, so clear the value.
859
- this._onClearAction();
860
- }
861
- }
862
-
863
- /** @private */
864
- _toggleElementChanged(toggleElement) {
865
- if (toggleElement) {
866
- // Don't blur the input on toggle mousedown
867
- toggleElement.addEventListener('mousedown', (e) => e.preventDefault());
868
- // Unfocus previously focused element if focus is not inside combo box (on touch devices)
869
- toggleElement.addEventListener('click', () => {
870
- if (isTouch && !this._isInputFocused()) {
871
- document.activeElement.blur();
872
- }
873
- });
874
- }
245
+ _onEscapeCancel() {
246
+ this.cancel();
875
247
  }
876
248
 
877
249
  /**
878
- * Clears the current value.
250
+ * Override method from `ComboBoxBaseMixin` to reset selected item.
879
251
  * @protected
252
+ * @override
880
253
  */
881
254
  _onClearAction() {
882
255
  this.selectedItem = null;
@@ -907,20 +280,43 @@ export const ComboBoxMixin = (subclass) =>
907
280
  this._closeOrCommit();
908
281
  }
909
282
 
910
- /** @private */
283
+ /**
284
+ * Override method from `ComboBoxBaseMixin` to store last committed value.
285
+ * @protected
286
+ * @override
287
+ */
911
288
  _onOpened() {
289
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
290
+
912
291
  // _detectAndDispatchChange() should not consider value changes done before opening
913
292
  this._lastCommittedValue = this.value;
914
293
  }
915
294
 
916
- /** @private */
295
+ /**
296
+ * Override method from `ComboBoxBaseMixin` to dispatch an event.
297
+ * @protected
298
+ * @override
299
+ */
300
+ _onOverlayClosed() {
301
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
302
+ }
303
+
304
+ /**
305
+ * Override method from `ComboBoxBaseMixin` to commit value on overlay closing.
306
+ * @protected
307
+ * @override
308
+ */
917
309
  _onClosed() {
918
310
  if (!this.loading || this.allowCustomValue) {
919
311
  this._commitValue();
920
312
  }
921
313
  }
922
314
 
923
- /** @private */
315
+ /**
316
+ * Override method from `ComboBoxBaseMixin` to implement value commit logic.
317
+ * @protected
318
+ * @override
319
+ */
924
320
  _commitValue() {
925
321
  if (this._focusedIndex > -1) {
926
322
  const focusedItem = this._dropdownItems[this._focusedIndex];
@@ -979,36 +375,6 @@ export const ComboBoxMixin = (subclass) =>
979
375
  this._clearFilter();
980
376
  }
981
377
 
982
- /**
983
- * Override an event listener from `InputMixin`.
984
- * @param {!Event} event
985
- * @protected
986
- * @override
987
- */
988
- _onInput(event) {
989
- const filter = this._inputElementValue;
990
-
991
- // When opening dropdown on user input, both `opened` and `filter` properties are set.
992
- // Perform a batched property update instead of relying on sync property observers.
993
- // This is necessary to avoid an extra data-provider request for loading first page.
994
- const props = {};
995
-
996
- if (this.filter === filter) {
997
- // Filter and input value might get out of sync, while keyboard navigating for example.
998
- // Afterwards, input value might be changed to the same value as used in filtering.
999
- // In situation like these, we need to make sure all the filter changes handlers are run.
1000
- this._filterChanged(this.filter);
1001
- } else {
1002
- props.filter = filter;
1003
- }
1004
-
1005
- if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
1006
- props.opened = true;
1007
- }
1008
-
1009
- this.setProperties(props);
1010
- }
1011
-
1012
378
  /**
1013
379
  * Override an event listener from `InputMixin`.
1014
380
  * @param {!Event} event
@@ -1021,31 +387,11 @@ export const ComboBoxMixin = (subclass) =>
1021
387
  event.stopPropagation();
1022
388
  }
1023
389
 
1024
- /** @private */
1025
- _itemLabelPathChanged(itemLabelPath) {
1026
- if (typeof itemLabelPath !== 'string') {
1027
- console.error('You should set itemLabelPath to a valid string');
1028
- }
1029
- }
1030
-
1031
- /** @private */
1032
- _filterChanged(filter) {
1033
- // Scroll to the top of the list whenever the filter changes.
1034
- this._scrollIntoView(0);
1035
-
1036
- this._focusedIndex = -1;
1037
-
1038
- if (this.items) {
1039
- this.filteredItems = this._filterItems(this.items, filter);
1040
- } else {
1041
- // With certain use cases (e. g., external filtering), `items` are
1042
- // undefined. Filtering is unnecessary per se, but the filteredItems
1043
- // observer should still be invoked to update focused item.
1044
- this._filteredItemsChanged(this.filteredItems);
1045
- }
1046
- }
1047
-
1048
- /** @protected */
390
+ /**
391
+ * Override method from `ComboBoxBaseMixin` to handle reverting value.
392
+ * @protected
393
+ * @override
394
+ */
1049
395
  _revertInputValue() {
1050
396
  if (this.filter !== '') {
1051
397
  this._inputElementValue = this.filter;
@@ -1137,40 +483,6 @@ export const ComboBoxMixin = (subclass) =>
1137
483
  }
1138
484
  }
1139
485
 
1140
- /** @private */
1141
- _itemsChanged(items, oldItems) {
1142
- this._ensureItemsOrDataProvider(() => {
1143
- this.items = oldItems;
1144
- });
1145
-
1146
- if (items) {
1147
- this.filteredItems = items.slice(0);
1148
- } else if (oldItems) {
1149
- // Only clear filteredItems if the component had items previously but got cleared
1150
- this.filteredItems = null;
1151
- }
1152
- }
1153
-
1154
- /** @private */
1155
- _filteredItemsChanged(filteredItems) {
1156
- this._setDropdownItems(filteredItems);
1157
- }
1158
-
1159
- /** @private */
1160
- _filterItems(arr, filter) {
1161
- if (!arr) {
1162
- return arr;
1163
- }
1164
-
1165
- const filteredItems = arr.filter((item) => {
1166
- filter = filter ? filter.toString().toLowerCase() : '';
1167
- // Check if item contains input value.
1168
- return this._getItemLabel(item).toString().toLowerCase().indexOf(filter) > -1;
1169
- });
1170
-
1171
- return filteredItems;
1172
- }
1173
-
1174
486
  /** @private */
1175
487
  _selectItemForValue(value) {
1176
488
  const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
@@ -1194,6 +506,7 @@ export const ComboBoxMixin = (subclass) =>
1194
506
  * Override this method to show custom items.
1195
507
  *
1196
508
  * @protected
509
+ * @override
1197
510
  */
1198
511
  _setDropdownItems(newItems) {
1199
512
  const oldItems = this._dropdownItems;
@@ -1226,135 +539,20 @@ export const ComboBoxMixin = (subclass) =>
1226
539
  }
1227
540
  }
1228
541
 
1229
- /** @private */
1230
- _getItemElements() {
1231
- return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
1232
- }
1233
-
1234
- /** @private */
1235
- _scrollIntoView(index) {
1236
- if (!this._scroller) {
1237
- return;
1238
- }
1239
- this._scroller.scrollIntoView(index);
1240
- }
1241
-
1242
- /**
1243
- * Returns the first item that matches the provided value.
1244
- *
1245
- * @private
1246
- */
1247
- __getItemIndexByValue(items, value) {
1248
- if (!items || !isValidValue(value)) {
1249
- return -1;
1250
- }
1251
-
1252
- return findItemIndex(items, (item) => {
1253
- return this._getItemValue(item) === value;
1254
- });
1255
- }
1256
-
1257
- /**
1258
- * Returns the first item that matches the provided label.
1259
- * Labels are matched against each other case insensitively.
1260
- *
1261
- * @private
1262
- */
1263
- __getItemIndexByLabel(items, label) {
1264
- if (!items || !label) {
1265
- return -1;
1266
- }
1267
-
1268
- return findItemIndex(items, (item) => {
1269
- return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
1270
- });
1271
- }
1272
-
1273
- /** @private */
1274
- _overlaySelectedItemChanged(e) {
1275
- // Stop this private event from leaking outside.
1276
- e.stopPropagation();
1277
-
1278
- if (e.detail.item instanceof ComboBoxPlaceholder) {
1279
- // Placeholder items should not be selectable.
1280
- return;
1281
- }
1282
-
1283
- if (this.opened) {
1284
- this._focusedIndex = this.filteredItems.indexOf(e.detail.item);
1285
- this.close();
1286
- }
1287
- }
1288
-
1289
542
  /**
1290
- * Override method inherited from `FocusMixin`
1291
- * to close the overlay on blur and commit the value.
1292
- *
1293
- * @param {boolean} focused
543
+ * Override method from `ComboBoxBaseMixin`.
1294
544
  * @protected
1295
545
  * @override
1296
546
  */
1297
- _setFocused(focused) {
1298
- super._setFocused(focused);
1299
-
1300
- if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
1301
- // User's logic in `custom-value-set` event listener might cause input to blur,
1302
- // which will result in attempting to commit the same custom value once again.
1303
- if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
1304
- delete this._lastCustomValue;
1305
- return;
1306
- }
1307
-
1308
- if (isKeyboardActive()) {
1309
- // Close on Tab key causing blur. With mouse, close on outside click instead.
1310
- this._closeOrCommit();
1311
- return;
1312
- }
1313
-
1314
- if (!this.opened) {
1315
- this._commitValue();
1316
- } else if (!this._overlayOpened) {
1317
- // Combo-box is opened, but overlay is not visible -> custom value was entered.
1318
- // Make sure we close here as there won't be an "outside click" in this case.
1319
- this.close();
1320
- }
1321
- }
1322
- }
1323
-
1324
- /**
1325
- * Override method inherited from `FocusMixin` to not remove focused
1326
- * state when focus moves to the overlay.
1327
- *
1328
- * @param {FocusEvent} event
1329
- * @return {boolean}
1330
- * @protected
1331
- * @override
1332
- */
1333
- _shouldRemoveFocus(event) {
1334
- // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
1335
- // Do not focus the input in this case, because it would break announcement for the item.
1336
- if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
1337
- return false;
1338
- }
1339
-
1340
- // Do not blur when focus moves to the overlay
1341
- // Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
1342
- if (event.relatedTarget === this._overlayElement) {
1343
- event.composedPath()[0].focus();
1344
- return false;
1345
- }
1346
-
1347
- return true;
1348
- }
1349
-
1350
- /** @private */
1351
- _onTouchend(event) {
1352
- if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
547
+ _handleFocusOut() {
548
+ // User's logic in `custom-value-set` event listener might cause input to blur,
549
+ // which will result in attempting to commit the same custom value once again.
550
+ if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
551
+ delete this._lastCustomValue;
1353
552
  return;
1354
553
  }
1355
554
 
1356
- event.preventDefault();
1357
- this._onClearAction();
555
+ super._handleFocusOut();
1358
556
  }
1359
557
 
1360
558
  /**