@vaadin/combo-box 25.0.0-alpha3 → 25.0.0-alpha4

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.
@@ -3,16 +3,9 @@
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
6
  import { get } from '@vaadin/component-base/src/path-utils.js';
13
- import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
14
7
  import { ValidateMixin } from '@vaadin/field-base/src/validate-mixin.js';
15
- import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
8
+ import { ComboBoxBaseMixin } from './vaadin-combo-box-base-mixin.js';
16
9
  import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
17
10
 
18
11
  /**
@@ -45,52 +38,14 @@ function findItemIndex(items, callback) {
45
38
 
46
39
  /**
47
40
  * @polymerMixin
41
+ * @mixes ComboBoxBaseMixin
48
42
  * @mixes ValidateMixin
49
- * @mixes DisabledMixin
50
- * @mixes InputMixin
51
- * @mixes KeyboardMixin
52
- * @mixes FocusMixin
53
- * @mixes OverlayClassMixin
54
- * @param {function(new:HTMLElement)} subclass
43
+ * @param {function(new:HTMLElement)} superClass
55
44
  */
56
- export const ComboBoxMixin = (subclass) =>
57
- class ComboBoxMixinClass extends OverlayClassMixin(
58
- ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass))))),
59
- ) {
45
+ export const ComboBoxMixin = (superClass) =>
46
+ class ComboBoxMixinClass extends ValidateMixin(ComboBoxBaseMixin(superClass)) {
60
47
  static get properties() {
61
48
  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
49
  /**
95
50
  * Custom function for rendering the content of every item.
96
51
  * Receives three arguments:
@@ -144,12 +99,6 @@ export const ComboBoxMixin = (subclass) =>
144
99
  sync: true,
145
100
  },
146
101
 
147
- /**
148
- * Used to detect user value changes and fire `change` events.
149
- * @private
150
- */
151
- _lastCommittedValue: String,
152
-
153
102
  /**
154
103
  * When set to `true`, "loading" attribute is added to host and the overlay element.
155
104
  * @type {boolean}
@@ -161,17 +110,6 @@ export const ComboBoxMixin = (subclass) =>
161
110
  sync: true,
162
111
  },
163
112
 
164
- /**
165
- * @type {number}
166
- * @protected
167
- */
168
- _focusedIndex: {
169
- type: Number,
170
- observer: '_focusedIndexChanged',
171
- value: -1,
172
- sync: true,
173
- },
174
-
175
113
  /**
176
114
  * Filtering string the user has typed into the input field.
177
115
  * @type {string}
@@ -248,40 +186,6 @@ export const ComboBoxMixin = (subclass) =>
248
186
  sync: true,
249
187
  },
250
188
 
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
189
  /** @private */
286
190
  __keepOverlayOpened: {
287
191
  type: Boolean,
@@ -292,91 +196,23 @@ export const ComboBoxMixin = (subclass) =>
292
196
 
293
197
  static get observers() {
294
198
  return [
295
- '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
296
199
  '_openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
297
- '_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme, itemClassNameGenerator)',
200
+ '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
201
+ '_updateScroller(opened, _dropdownItems, _focusedIndex, _theme)',
298
202
  ];
299
203
  }
300
204
 
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
205
  /** @protected */
347
206
  ready() {
348
207
  super.ready();
349
208
 
350
- this._initOverlay();
351
- this._initScroller();
352
-
209
+ /**
210
+ * Used to detect user value changes and fire `change` events.
211
+ * Do not define in `properties` to avoid triggering updates.
212
+ * @type {string}
213
+ * @protected
214
+ */
353
215
  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
216
  }
381
217
 
382
218
  /**
@@ -398,116 +234,36 @@ export const ComboBoxMixin = (subclass) =>
398
234
  }
399
235
 
400
236
  /**
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
237
  * @param {Object} props
420
238
  * @protected
421
239
  */
422
240
  updated(props) {
423
241
  super.updated(props);
424
242
 
243
+ ['loading', 'itemIdPath', 'itemClassNameGenerator', 'renderer', 'selectedItem'].forEach((prop) => {
244
+ if (props.has(prop)) {
245
+ this._scroller[prop] = this[prop];
246
+ }
247
+ });
248
+
425
249
  if (props.has('filter')) {
426
250
  this._filterChanged(this.filter);
427
251
  }
428
252
  }
429
253
 
430
254
  /** @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());
255
+ _updateScroller(opened, items, focusedIndex, theme) {
256
+ if (opened) {
257
+ this._scroller.style.maxHeight =
258
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
259
+ }
442
260
 
443
- // Manual two-way binding for the overlay "opened" property
444
- overlay.addEventListener('opened-changed', (e) => {
445
- this._overlayOpened = e.detail.value;
261
+ this._scroller.setProperties({
262
+ items: opened ? items : [],
263
+ opened,
264
+ focusedIndex,
265
+ theme,
446
266
  });
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);
469
- }
470
- };
471
-
472
- // Ensure the scroller is rendered
473
- overlay.requestContentUpdate();
474
-
475
- // Trigger the observer to set properties
476
- this._scroller = scroller;
477
- }
478
-
479
- /** @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
- });
510
- }
511
267
  }
512
268
 
513
269
  /** @private */
@@ -517,174 +273,39 @@ export const ComboBoxMixin = (subclass) =>
517
273
  this._overlayOpened = opened && (keepOverlayOpened || loading || !!(items && items.length));
518
274
  }
519
275
 
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
276
  /**
277
+ * Override method from `ComboBoxBaseMixin` to deselect
278
+ * dropdown item by requesting content update on clear.
613
279
  * @param {Event} event
614
280
  * @protected
615
281
  */
616
282
  _onClearButtonClick(event) {
617
- event.preventDefault();
618
- this._onClearAction();
283
+ super._onClearButtonClick(event);
619
284
 
620
- // De-select dropdown item
621
285
  if (this.opened) {
622
286
  this.requestContentUpdate();
623
287
  }
624
288
  }
625
289
 
626
290
  /**
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
291
+ * Override method inherited from `InputMixin`
292
+ * to revert the input value to value.
644
293
  * @protected
294
+ * @override
645
295
  */
646
- _onHostClick(event) {
647
- if (!this.autoOpenDisabled) {
648
- event.preventDefault();
649
- this.open();
650
- }
651
- }
296
+ _inputElementChanged(input) {
297
+ super._inputElementChanged(input);
652
298
 
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);
299
+ if (input) {
300
+ this._revertInputValueToValue();
661
301
  }
662
302
  }
663
303
 
664
304
  /**
665
- * Override an event listener from `KeyboardMixin`.
666
- *
667
- * @param {KeyboardEvent} e
305
+ * Override method from `ComboBoxBaseMixin` to handle item label path.
668
306
  * @protected
669
307
  * @override
670
308
  */
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
309
  _getItemLabel(item) {
689
310
  let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
690
311
  if (label === undefined || label === null) {
@@ -702,73 +323,11 @@ export const ComboBoxMixin = (subclass) =>
702
323
  return value;
703
324
  }
704
325
 
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 */
326
+ /**
327
+ * Override method from `ComboBoxBaseMixin` to handle loading.
328
+ * @protected
329
+ * @override
330
+ */
772
331
  _closeOrCommit() {
773
332
  if (!this.opened && !this.loading) {
774
333
  this._commitValue();
@@ -778,39 +337,10 @@ export const ComboBoxMixin = (subclass) =>
778
337
  }
779
338
 
780
339
  /**
781
- * Override an event listener from `KeyboardMixin`.
782
- *
783
- * @param {KeyboardEvent} e
340
+ * Override method from `ComboBoxBaseMixin` to handle valid value.
784
341
  * @protected
785
342
  * @override
786
343
  */
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
344
  _hasValidInputValue() {
815
345
  const hasInvalidOption =
816
346
  this._focusedIndex < 0 &&
@@ -821,62 +351,18 @@ export const ComboBoxMixin = (subclass) =>
821
351
  }
822
352
 
823
353
  /**
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
354
+ * Override method from `ComboBoxBaseMixin`.
829
355
  * @protected
830
356
  * @override
831
357
  */
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
- }
358
+ _onEscapeCancel() {
359
+ this.cancel();
875
360
  }
876
361
 
877
362
  /**
878
- * Clears the current value.
363
+ * Override method from `ComboBoxBaseMixin` to reset selected item.
879
364
  * @protected
365
+ * @override
880
366
  */
881
367
  _onClearAction() {
882
368
  this.selectedItem = null;
@@ -907,20 +393,43 @@ export const ComboBoxMixin = (subclass) =>
907
393
  this._closeOrCommit();
908
394
  }
909
395
 
910
- /** @private */
396
+ /**
397
+ * Override method from `ComboBoxBaseMixin` to store last committed value.
398
+ * @protected
399
+ * @override
400
+ */
911
401
  _onOpened() {
402
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
403
+
912
404
  // _detectAndDispatchChange() should not consider value changes done before opening
913
405
  this._lastCommittedValue = this.value;
914
406
  }
915
407
 
916
- /** @private */
408
+ /**
409
+ * Override method from `ComboBoxBaseMixin` to dispatch an event.
410
+ * @protected
411
+ * @override
412
+ */
413
+ _onOverlayClosed() {
414
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
415
+ }
416
+
417
+ /**
418
+ * Override method from `ComboBoxBaseMixin` to commit value on overlay closing.
419
+ * @protected
420
+ * @override
421
+ */
917
422
  _onClosed() {
918
423
  if (!this.loading || this.allowCustomValue) {
919
424
  this._commitValue();
920
425
  }
921
426
  }
922
427
 
923
- /** @private */
428
+ /**
429
+ * Override method from `ComboBoxBaseMixin` to implement value commit logic.
430
+ * @protected
431
+ * @override
432
+ */
924
433
  _commitValue() {
925
434
  if (this._focusedIndex > -1) {
926
435
  const focusedItem = this._dropdownItems[this._focusedIndex];
@@ -980,7 +489,8 @@ export const ComboBoxMixin = (subclass) =>
980
489
  }
981
490
 
982
491
  /**
983
- * Override an event listener from `InputMixin`.
492
+ * Override an event listener from `ComboBoxBaseMixin` to handle
493
+ * batched setting of both `opened` and `filter` properties.
984
494
  * @param {!Event} event
985
495
  * @protected
986
496
  * @override
@@ -1045,7 +555,11 @@ export const ComboBoxMixin = (subclass) =>
1045
555
  }
1046
556
  }
1047
557
 
1048
- /** @protected */
558
+ /**
559
+ * Override method from `ComboBoxBaseMixin` to handle reverting value.
560
+ * @protected
561
+ * @override
562
+ */
1049
563
  _revertInputValue() {
1050
564
  if (this.filter !== '') {
1051
565
  this._inputElementValue = this.filter;
@@ -1226,19 +740,6 @@ export const ComboBoxMixin = (subclass) =>
1226
740
  }
1227
741
  }
1228
742
 
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
743
  /**
1243
744
  * Returns the first item that matches the provided value.
1244
745
  *
@@ -1270,91 +771,20 @@ export const ComboBoxMixin = (subclass) =>
1270
771
  });
1271
772
  }
1272
773
 
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
774
  /**
1290
- * Override method inherited from `FocusMixin`
1291
- * to close the overlay on blur and commit the value.
1292
- *
1293
- * @param {boolean} focused
775
+ * Override method from `ComboBoxBaseMixin`.
1294
776
  * @protected
1295
777
  * @override
1296
778
  */
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) {
779
+ _handleFocusOut() {
780
+ // User's logic in `custom-value-set` event listener might cause input to blur,
781
+ // which will result in attempting to commit the same custom value once again.
782
+ if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
783
+ delete this._lastCustomValue;
1353
784
  return;
1354
785
  }
1355
786
 
1356
- event.preventDefault();
1357
- this._onClearAction();
787
+ super._handleFocusOut();
1358
788
  }
1359
789
 
1360
790
  /**