@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
@@ -0,0 +1,776 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2015 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
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 { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
13
+ import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
14
+ import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
15
+
16
+ /**
17
+ * @polymerMixin
18
+ * @mixes DisabledMixin
19
+ * @mixes FocusMixin
20
+ * @mixes InputMixin
21
+ * @mixes KeyboardMixin
22
+ * @mixes OverlayClassMixin
23
+ * @param {function(new:HTMLElement)} superClass
24
+ */
25
+ export const ComboBoxBaseMixin = (superClass) =>
26
+ class ComboBoxMixinBaseClass extends OverlayClassMixin(
27
+ KeyboardMixin(InputMixin(DisabledMixin(FocusMixin(superClass)))),
28
+ ) {
29
+ static get properties() {
30
+ return {
31
+ /**
32
+ * True if the dropdown is open, false otherwise.
33
+ * @type {boolean}
34
+ */
35
+ opened: {
36
+ type: Boolean,
37
+ notify: true,
38
+ value: false,
39
+ reflectToAttribute: true,
40
+ sync: true,
41
+ observer: '_openedChanged',
42
+ },
43
+
44
+ /**
45
+ * Set true to prevent the overlay from opening automatically.
46
+ * @attr {boolean} auto-open-disabled
47
+ */
48
+ autoOpenDisabled: {
49
+ type: Boolean,
50
+ sync: true,
51
+ },
52
+
53
+ /**
54
+ * When present, it specifies that the field is read-only.
55
+ * @type {boolean}
56
+ */
57
+ readonly: {
58
+ type: Boolean,
59
+ value: false,
60
+ reflectToAttribute: true,
61
+ },
62
+
63
+ /**
64
+ * @type {number}
65
+ * @protected
66
+ */
67
+ _focusedIndex: {
68
+ type: Number,
69
+ observer: '_focusedIndexChanged',
70
+ value: -1,
71
+ sync: true,
72
+ },
73
+
74
+ /**
75
+ * @type {!HTMLElement | undefined}
76
+ * @protected
77
+ */
78
+ _toggleElement: {
79
+ type: Object,
80
+ observer: '_toggleElementChanged',
81
+ },
82
+
83
+ /**
84
+ * Set of items to be rendered in the dropdown.
85
+ * @protected
86
+ */
87
+ _dropdownItems: {
88
+ type: Array,
89
+ sync: true,
90
+ },
91
+
92
+ /**
93
+ * Whether the overlay should be opened.
94
+ * @protected
95
+ */
96
+ _overlayOpened: {
97
+ type: Boolean,
98
+ sync: true,
99
+ observer: '_overlayOpenedChanged',
100
+ },
101
+ };
102
+ }
103
+
104
+ constructor() {
105
+ super();
106
+
107
+ /**
108
+ * Reference to the `vaadin-combo-box-scroller` element instance.
109
+ * Do not define in `properties` to avoid triggering updates.
110
+ * @type {HTMLElement}
111
+ * @protected
112
+ */
113
+ this._scroller;
114
+
115
+ /**
116
+ * Used to detect if focusout should be ignored due to touch.
117
+ * Do not define in `properties` to avoid triggering updates.
118
+ * @type {boolean}
119
+ * @protected
120
+ */
121
+ this._closeOnBlurIsPrevented;
122
+
123
+ this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
124
+ this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
125
+ this._boundOnClick = this._onClick.bind(this);
126
+ this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
127
+ this._boundOnTouchend = this._onTouchend.bind(this);
128
+ }
129
+
130
+ /**
131
+ * Tag name prefix used by scroller and items.
132
+ * @protected
133
+ * @return {string}
134
+ */
135
+ get _tagNamePrefix() {
136
+ return 'vaadin-combo-box';
137
+ }
138
+
139
+ /**
140
+ * Override method inherited from `InputMixin`
141
+ * to customize the input element.
142
+ * @protected
143
+ * @override
144
+ */
145
+ _inputElementChanged(input) {
146
+ super._inputElementChanged(input);
147
+
148
+ if (input) {
149
+ input.autocomplete = 'off';
150
+ input.autocapitalize = 'off';
151
+
152
+ input.setAttribute('role', 'combobox');
153
+ input.setAttribute('aria-autocomplete', 'list');
154
+ input.setAttribute('aria-expanded', !!this.opened);
155
+
156
+ // Disable the macOS Safari spell check auto corrections.
157
+ input.setAttribute('spellcheck', 'false');
158
+
159
+ // Disable iOS autocorrect suggestions.
160
+ input.setAttribute('autocorrect', 'off');
161
+ }
162
+ }
163
+
164
+ /** @protected */
165
+ firstUpdated() {
166
+ super.firstUpdated();
167
+
168
+ // Init scroller in `firstUpdated()` to ensure the `_scroller` reference
169
+ // is available by the time property observer runs. Also, do not store it
170
+ // in a reactive property to avoid triggering another unnecessary update.
171
+ this._initScroller();
172
+ }
173
+
174
+ /** @protected */
175
+ ready() {
176
+ super.ready();
177
+
178
+ this._initOverlay();
179
+
180
+ this.addEventListener('click', this._boundOnClick);
181
+ this.addEventListener('touchend', this._boundOnTouchend);
182
+
183
+ if (this.clearElement) {
184
+ this.clearElement.addEventListener('mousedown', this._boundOnClearButtonMouseDown);
185
+ }
186
+
187
+ this.addController(new VirtualKeyboardController(this));
188
+ }
189
+
190
+ /** @protected */
191
+ disconnectedCallback() {
192
+ super.disconnectedCallback();
193
+
194
+ // Close the overlay on detach
195
+ this.close();
196
+ }
197
+
198
+ /**
199
+ * Opens the dropdown list.
200
+ */
201
+ open() {
202
+ // Prevent _open() being called when input is disabled or read-only
203
+ if (!this.disabled && !this.readonly) {
204
+ this.opened = true;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Closes the dropdown list.
210
+ */
211
+ close() {
212
+ this.opened = false;
213
+ }
214
+
215
+ /** @private */
216
+ _initOverlay() {
217
+ const overlay = this.$.overlay;
218
+
219
+ overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
220
+ overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
221
+
222
+ // Prevent blurring the input when clicking inside the overlay
223
+ overlay.addEventListener('mousedown', (e) => e.preventDefault());
224
+
225
+ // Manual two-way binding for the overlay "opened" property
226
+ overlay.addEventListener('opened-changed', (e) => {
227
+ this._overlayOpened = e.detail.value;
228
+ });
229
+
230
+ this._overlayElement = overlay;
231
+ }
232
+
233
+ /**
234
+ * Create and initialize the scroller element.
235
+ *
236
+ * @private
237
+ */
238
+ _initScroller() {
239
+ const scroller = document.createElement(`${this._tagNamePrefix}-scroller`);
240
+
241
+ scroller.owner = this;
242
+ scroller.getItemLabel = this._getItemLabel.bind(this);
243
+ scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
244
+
245
+ this._renderScroller(scroller);
246
+
247
+ this._scroller = scroller;
248
+ }
249
+
250
+ /**
251
+ * Render the scroller element to the overlay.
252
+ *
253
+ * @private
254
+ */
255
+ _renderScroller(scroller) {
256
+ scroller.setAttribute('slot', 'overlay');
257
+ // Prevent focusing scroller on input Tab
258
+ scroller.setAttribute('tabindex', '-1');
259
+ this.appendChild(scroller);
260
+ }
261
+
262
+ /**
263
+ * @type {boolean}
264
+ * @protected
265
+ */
266
+ get _hasDropdownItems() {
267
+ return !!(this._dropdownItems && this._dropdownItems.length);
268
+ }
269
+
270
+ /** @private */
271
+ _overlayOpenedChanged(opened, wasOpened) {
272
+ if (opened) {
273
+ this._onOpened();
274
+ } else if (wasOpened && this._hasDropdownItems) {
275
+ this.close();
276
+ this._onOverlayClosed();
277
+ }
278
+ }
279
+
280
+ /** @private */
281
+ _focusedIndexChanged(index, oldIndex) {
282
+ if (oldIndex === undefined) {
283
+ return;
284
+ }
285
+ this._updateActiveDescendant(index);
286
+ }
287
+
288
+ /** @protected */
289
+ _isInputFocused() {
290
+ return this.inputElement && isElementFocused(this.inputElement);
291
+ }
292
+
293
+ /** @private */
294
+ _updateActiveDescendant(index) {
295
+ const input = this.inputElement;
296
+ if (!input) {
297
+ return;
298
+ }
299
+
300
+ const item = this._getItemElements().find((el) => el.index === index);
301
+ if (item) {
302
+ input.setAttribute('aria-activedescendant', item.id);
303
+ } else {
304
+ input.removeAttribute('aria-activedescendant');
305
+ }
306
+ }
307
+
308
+ /** @private */
309
+ _openedChanged(opened, wasOpened) {
310
+ // Prevent _close() being called when opened is set to its default value (false).
311
+ if (wasOpened === undefined) {
312
+ return;
313
+ }
314
+
315
+ if (opened) {
316
+ // For touch devices, we don't want to popup virtual keyboard
317
+ // unless input element is explicitly focused by the user.
318
+ if (!this._isInputFocused() && !isTouch) {
319
+ if (this.inputElement) {
320
+ this.inputElement.focus();
321
+ }
322
+ }
323
+ } else {
324
+ this._onClosed();
325
+ }
326
+
327
+ const input = this.inputElement;
328
+ if (input) {
329
+ input.setAttribute('aria-expanded', !!opened);
330
+
331
+ if (opened) {
332
+ input.setAttribute('aria-controls', this._scroller.id);
333
+ } else {
334
+ input.removeAttribute('aria-controls');
335
+ }
336
+ }
337
+ }
338
+
339
+ /** @private */
340
+ _onOverlayTouchAction() {
341
+ // On touch devices, blur the input on touch start inside the overlay, in order to hide
342
+ // the virtual keyboard. But don't close the overlay on this blur.
343
+ this._closeOnBlurIsPrevented = true;
344
+ this.inputElement.blur();
345
+ this._closeOnBlurIsPrevented = false;
346
+ }
347
+
348
+ /** @protected */
349
+ _isClearButton(event) {
350
+ return event.composedPath()[0] === this.clearElement;
351
+ }
352
+
353
+ /** @private */
354
+ __onClearButtonMouseDown(event) {
355
+ event.preventDefault(); // Prevent native focusout event
356
+ this.inputElement.focus();
357
+ }
358
+
359
+ /**
360
+ * @param {Event} event
361
+ * @protected
362
+ */
363
+ _onClearButtonClick(event) {
364
+ event.preventDefault();
365
+ this._onClearAction();
366
+ }
367
+
368
+ /**
369
+ * @param {Event} event
370
+ * @private
371
+ */
372
+ _onToggleButtonClick(event) {
373
+ // Prevent parent components such as `vaadin-grid`
374
+ // from handling the click event after it bubbles.
375
+ event.preventDefault();
376
+
377
+ if (this.opened) {
378
+ this.close();
379
+ } else {
380
+ this.open();
381
+ }
382
+ }
383
+
384
+ /**
385
+ * @param {Event} event
386
+ * @protected
387
+ */
388
+ _onHostClick(event) {
389
+ if (!this.autoOpenDisabled) {
390
+ event.preventDefault();
391
+ this.open();
392
+ }
393
+ }
394
+
395
+ /** @private */
396
+ _onClick(event) {
397
+ if (this._isClearButton(event)) {
398
+ this._onClearButtonClick(event);
399
+ } else if (event.composedPath().includes(this._toggleElement)) {
400
+ this._onToggleButtonClick(event);
401
+ } else {
402
+ this._onHostClick(event);
403
+ }
404
+ }
405
+
406
+ /** @private */
407
+ _onTouchend(event) {
408
+ if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
409
+ return;
410
+ }
411
+
412
+ event.preventDefault();
413
+ this._onClearAction();
414
+ }
415
+
416
+ /**
417
+ * Override an event listener from `KeyboardMixin`.
418
+ *
419
+ * @param {KeyboardEvent} e
420
+ * @protected
421
+ * @override
422
+ */
423
+ _onKeyDown(e) {
424
+ super._onKeyDown(e);
425
+
426
+ if (e.key === 'ArrowDown') {
427
+ this._onArrowDown();
428
+
429
+ // Prevent caret from moving
430
+ e.preventDefault();
431
+ } else if (e.key === 'ArrowUp') {
432
+ this._onArrowUp();
433
+
434
+ // Prevent caret from moving
435
+ e.preventDefault();
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Override to provide logic for item label path.
441
+ * @protected
442
+ */
443
+ _getItemLabel(item) {
444
+ return item ? item.toString() : '';
445
+ }
446
+
447
+ /** @private */
448
+ _onArrowDown() {
449
+ if (this.opened) {
450
+ const items = this._dropdownItems;
451
+ if (items) {
452
+ this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
453
+ this._prefillFocusedItemLabel();
454
+ }
455
+ } else {
456
+ this.open();
457
+ }
458
+ }
459
+
460
+ /** @private */
461
+ _onArrowUp() {
462
+ if (this.opened) {
463
+ if (this._focusedIndex > -1) {
464
+ this._focusedIndex = Math.max(0, this._focusedIndex - 1);
465
+ } else {
466
+ const items = this._dropdownItems;
467
+ if (items) {
468
+ this._focusedIndex = items.length - 1;
469
+ }
470
+ }
471
+
472
+ this._prefillFocusedItemLabel();
473
+ } else {
474
+ this.open();
475
+ }
476
+ }
477
+
478
+ /** @private */
479
+ _prefillFocusedItemLabel() {
480
+ if (this._focusedIndex > -1) {
481
+ const focusedItem = this._dropdownItems[this._focusedIndex];
482
+ this._inputElementValue = this._getItemLabel(focusedItem);
483
+ this._markAllSelectionRange();
484
+ }
485
+ }
486
+
487
+ /** @private */
488
+ _setSelectionRange(start, end) {
489
+ // Setting selection range focuses and/or moves the caret in some browsers,
490
+ // and there's no need to modify the selection range if the input isn't focused anyway.
491
+ // This affects Safari. When the overlay is open, and then hitting tab, browser should focus
492
+ // the next focusable element instead of the combo-box itself.
493
+ if (this._isInputFocused() && this.inputElement.setSelectionRange) {
494
+ this.inputElement.setSelectionRange(start, end);
495
+ }
496
+ }
497
+
498
+ /** @private */
499
+ _markAllSelectionRange() {
500
+ if (this._inputElementValue !== undefined) {
501
+ this._setSelectionRange(0, this._inputElementValue.length);
502
+ }
503
+ }
504
+
505
+ /** @private */
506
+ _clearSelectionRange() {
507
+ if (this._inputElementValue !== undefined) {
508
+ const pos = this._inputElementValue ? this._inputElementValue.length : 0;
509
+ this._setSelectionRange(pos, pos);
510
+ }
511
+ }
512
+
513
+ /**
514
+ * @protected
515
+ */
516
+ _closeOrCommit() {
517
+ if (!this.opened) {
518
+ this._commitValue();
519
+ } else {
520
+ this.close();
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Override an event listener from `KeyboardMixin`.
526
+ *
527
+ * @param {KeyboardEvent} e
528
+ * @protected
529
+ * @override
530
+ */
531
+ _onEnter(e) {
532
+ // Do not commit value when custom values are disallowed and input value is not a valid option
533
+ // also stop propagation of the event, otherwise the user could submit a form while the input
534
+ // still contains an invalid value
535
+ if (!this._hasValidInputValue()) {
536
+ // Do not submit the surrounding form.
537
+ e.preventDefault();
538
+ // Do not trigger global listeners
539
+ e.stopPropagation();
540
+ return;
541
+ }
542
+
543
+ // Stop propagation of the enter event only if the dropdown is opened, this
544
+ // "consumes" the enter event for the action of closing the dropdown
545
+ if (this.opened) {
546
+ // Do not submit the surrounding form.
547
+ e.preventDefault();
548
+ // Do not trigger global listeners
549
+ e.stopPropagation();
550
+ }
551
+
552
+ this._closeOrCommit();
553
+ }
554
+
555
+ /**
556
+ * Override this method to detect whether valid value is provided.
557
+ * @protected
558
+ */
559
+ _hasValidInputValue() {
560
+ return true;
561
+ }
562
+
563
+ /**
564
+ * Override an event listener from `KeyboardMixin`.
565
+ * Do not call `super` in order to override clear
566
+ * button logic defined in `InputControlMixin`.
567
+ *
568
+ * @param {!KeyboardEvent} e
569
+ * @protected
570
+ * @override
571
+ */
572
+ _onEscape(e) {
573
+ if (
574
+ this.autoOpenDisabled &&
575
+ (this.opened || (this.value !== this._inputElementValue && this._inputElementValue.length > 0))
576
+ ) {
577
+ // Auto-open is disabled
578
+ // The overlay is open or
579
+ // The input value has changed but the change hasn't been committed, so cancel it.
580
+ e.stopPropagation();
581
+ this._focusedIndex = -1;
582
+ this._onEscapeCancel();
583
+ } else if (this.opened) {
584
+ // Auto-open is enabled
585
+ // The overlay is open
586
+ e.stopPropagation();
587
+
588
+ if (this._focusedIndex > -1) {
589
+ // An item is focused, revert the input to the filtered value
590
+ this._focusedIndex = -1;
591
+ this._revertInputValue();
592
+ } else {
593
+ // No item is focused, cancel the change and close the overlay
594
+ this._onEscapeCancel();
595
+ }
596
+ } else if (this.clearButtonVisible && !!this.value && !this.readonly) {
597
+ e.stopPropagation();
598
+ // The clear button is visible and the overlay is closed, so clear the value.
599
+ this._onClearAction();
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Override to handle canceling and closing overlay on Escape.
605
+ * @protected
606
+ */
607
+ _onEscapeCancel() {
608
+ // To be implemented
609
+ }
610
+
611
+ /** @private */
612
+ _toggleElementChanged(toggleElement) {
613
+ if (toggleElement) {
614
+ // Don't blur the input on toggle mousedown
615
+ toggleElement.addEventListener('mousedown', (e) => e.preventDefault());
616
+ // Unfocus previously focused element if focus is not inside combo box (on touch devices)
617
+ toggleElement.addEventListener('click', () => {
618
+ if (isTouch && !this._isInputFocused()) {
619
+ document.activeElement.blur();
620
+ }
621
+ });
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Override to implement logic for clearing value.
627
+ * @protected
628
+ */
629
+ _onClearAction() {
630
+ // To be implemented
631
+ }
632
+
633
+ /**
634
+ * Override to implement logic for overlay opening.
635
+ * @protected
636
+ */
637
+ _onOpened() {
638
+ // To be implemented
639
+ }
640
+
641
+ /**
642
+ * Override to implement logic for changing opened to false.
643
+ * @protected
644
+ */
645
+ _onClosed() {
646
+ // To be implemented
647
+ }
648
+
649
+ /**
650
+ * Override to implement logic for overlay closing.
651
+ * @protected
652
+ */
653
+ _onOverlayClosed() {
654
+ // To be implemented
655
+ }
656
+
657
+ /**
658
+ * Override to implement logic for committing value.
659
+ * @protected
660
+ */
661
+ _commitValue() {
662
+ // To be implemented
663
+ }
664
+
665
+ /**
666
+ * Override to implement logic for value reverting.
667
+ * @protected
668
+ */
669
+ _revertInputValue() {
670
+ this._inputElementValue = this.value;
671
+ this._clearSelectionRange();
672
+ }
673
+
674
+ /**
675
+ * Override an event listener from `InputMixin`.
676
+ * @param {!Event} event
677
+ * @protected
678
+ * @override
679
+ */
680
+ _onInput(event) {
681
+ if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
682
+ this.opened = true;
683
+ }
684
+ }
685
+
686
+ /** @private */
687
+ _getItemElements() {
688
+ return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
689
+ }
690
+
691
+ /** @protected */
692
+ _scrollIntoView(index) {
693
+ if (!this._scroller) {
694
+ return;
695
+ }
696
+ this._scroller.scrollIntoView(index);
697
+ }
698
+
699
+ /** @private */
700
+ _overlaySelectedItemChanged(e) {
701
+ // Stop this private event from leaking outside.
702
+ e.stopPropagation();
703
+
704
+ if (e.detail.item instanceof ComboBoxPlaceholder) {
705
+ // Placeholder items should not be selectable.
706
+ return;
707
+ }
708
+
709
+ if (this.opened) {
710
+ this._focusedIndex = this._dropdownItems.indexOf(e.detail.item);
711
+ this.close();
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Override method inherited from `FocusMixin`
717
+ * to close the overlay on blur and commit the value.
718
+ *
719
+ * @param {boolean} focused
720
+ * @protected
721
+ * @override
722
+ */
723
+ _setFocused(focused) {
724
+ super._setFocused(focused);
725
+
726
+ if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
727
+ this._handleFocusOut();
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Override this method to provide custom logic for focusout.
733
+ * @protected
734
+ */
735
+ _handleFocusOut() {
736
+ if (isKeyboardActive()) {
737
+ // Close on Tab key causing blur. With mouse, close on outside click instead.
738
+ this._closeOrCommit();
739
+ return;
740
+ }
741
+
742
+ if (!this.opened) {
743
+ this._commitValue();
744
+ } else if (!this._overlayOpened) {
745
+ // Combo-box is opened, but overlay is not visible -> custom value was entered.
746
+ // Make sure we close here as there won't be an "outside click" in this case.
747
+ this.close();
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Override method inherited from `FocusMixin` to not remove focused
753
+ * state when focus moves to the overlay.
754
+ *
755
+ * @param {FocusEvent} event
756
+ * @return {boolean}
757
+ * @protected
758
+ * @override
759
+ */
760
+ _shouldRemoveFocus(event) {
761
+ // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
762
+ // Do not focus the input in this case, because it would break announcement for the item.
763
+ if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
764
+ return false;
765
+ }
766
+
767
+ // Do not blur when focus moves to the overlay
768
+ // Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
769
+ if (event.relatedTarget === this._overlayElement) {
770
+ event.composedPath()[0].focus();
771
+ return false;
772
+ }
773
+
774
+ return true;
775
+ }
776
+ };