@vaadin/combo-box 24.8.4 → 25.0.0-alpha10

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