@vaadin/combo-box 23.2.0-dev.8a7678b70 → 23.3.0-alpha1

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.
@@ -6,6 +6,7 @@
6
6
  import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
7
7
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
8
8
  import { DisabledMixin } from '@vaadin/component-base/src/disabled-mixin.js';
9
+ import { isElementFocused } from '@vaadin/component-base/src/focus-utils.js';
9
10
  import { KeyboardMixin } from '@vaadin/component-base/src/keyboard-mixin.js';
10
11
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
11
12
  import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
@@ -236,7 +237,6 @@ export const ComboBoxMixin = (subclass) =>
236
237
 
237
238
  static get observers() {
238
239
  return [
239
- '_filterChanged(filter, itemValuePath, itemLabelPath)',
240
240
  '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
241
241
  '_openedOrItemsChanged(opened, filteredItems, loading)',
242
242
  '_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
@@ -280,14 +280,26 @@ export const ComboBoxMixin = (subclass) =>
280
280
  }
281
281
  }
282
282
 
283
+ /**
284
+ * Get a reference to the native `<input>` element.
285
+ * Override to provide a custom input.
286
+ * @protected
287
+ * @return {HTMLInputElement | undefined}
288
+ */
289
+ get _nativeInput() {
290
+ return this.inputElement;
291
+ }
292
+
283
293
  /**
284
294
  * Override method inherited from `InputMixin`
285
295
  * to customize the input element.
286
296
  * @protected
287
297
  * @override
288
298
  */
289
- _inputElementChanged(input) {
290
- super._inputElementChanged(input);
299
+ _inputElementChanged(inputElement) {
300
+ super._inputElementChanged(inputElement);
301
+
302
+ const input = this._nativeInput;
291
303
 
292
304
  if (input) {
293
305
  input.autocomplete = 'off';
@@ -382,6 +394,25 @@ export const ComboBoxMixin = (subclass) =>
382
394
  this.opened = false;
383
395
  }
384
396
 
397
+ /**
398
+ * Override Polymer lifecycle callback to handle `filter` property change after
399
+ * the observer for `opened` property is triggered. This is needed when opening
400
+ * combo-box on user input to ensure the focused index is set correctly.
401
+ *
402
+ * @param {!Object} currentProps Current accessor values
403
+ * @param {?Object} changedProps Properties changed since the last call
404
+ * @param {?Object} oldProps Previous values for each changed property
405
+ * @protected
406
+ * @override
407
+ */
408
+ _propertiesChanged(currentProps, changedProps, oldProps) {
409
+ super._propertiesChanged(currentProps, changedProps, oldProps);
410
+
411
+ if (changedProps.filter !== undefined) {
412
+ this._filterChanged(changedProps.filter);
413
+ }
414
+ }
415
+
385
416
  /** @private */
386
417
  _initOverlay() {
387
418
  const overlay = this.$.overlay;
@@ -395,11 +426,6 @@ export const ComboBoxMixin = (subclass) =>
395
426
  // Prevent blurring the input when clicking inside the overlay
396
427
  overlay.addEventListener('mousedown', (e) => e.preventDefault());
397
428
 
398
- // Preventing the default modal behavior of the overlay on input click
399
- overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
400
- e.preventDefault();
401
- });
402
-
403
429
  // Manual two-way binding for the overlay "opened" property
404
430
  overlay.addEventListener('opened-changed', (e) => {
405
431
  this._overlayOpened = e.detail.value;
@@ -424,9 +450,7 @@ export const ComboBoxMixin = (subclass) =>
424
450
  };
425
451
 
426
452
  // Ensure the scroller is rendered
427
- if (!this.opened) {
428
- overlay.requestContentUpdate();
429
- }
453
+ overlay.requestContentUpdate();
430
454
 
431
455
  const scroller = overlay.querySelector(scrollerTag);
432
456
 
@@ -460,11 +484,6 @@ export const ComboBoxMixin = (subclass) =>
460
484
  }
461
485
  }
462
486
 
463
- /** @protected */
464
- _isOverlayHidden(items, loading) {
465
- return !loading && !(items && items.length);
466
- }
467
-
468
487
  /** @private */
469
488
  _openedOrItemsChanged(opened, items, loading) {
470
489
  // Close the overlay if there are no items to display.
@@ -493,9 +512,14 @@ export const ComboBoxMixin = (subclass) =>
493
512
  this._updateActiveDescendant(index);
494
513
  }
495
514
 
515
+ /** @protected */
516
+ _isInputFocused() {
517
+ return this.inputElement && isElementFocused(this.inputElement);
518
+ }
519
+
496
520
  /** @private */
497
521
  _updateActiveDescendant(index) {
498
- const input = this.inputElement;
522
+ const input = this._nativeInput;
499
523
  if (!input) {
500
524
  return;
501
525
  }
@@ -519,19 +543,19 @@ export const ComboBoxMixin = (subclass) =>
519
543
  this._openedWithFocusRing = this.hasAttribute('focus-ring');
520
544
  // For touch devices, we don't want to popup virtual keyboard
521
545
  // unless input element is explicitly focused by the user.
522
- if (!this.hasAttribute('focused') && !isTouch) {
546
+ if (!this._isInputFocused() && !isTouch) {
523
547
  this.focus();
524
548
  }
525
549
 
526
550
  this.$.overlay.restoreFocusOnClose = true;
527
551
  } else {
528
552
  this._onClosed();
529
- if (this._openedWithFocusRing && this.hasAttribute('focused')) {
553
+ if (this._openedWithFocusRing && this._isInputFocused()) {
530
554
  this.setAttribute('focus-ring', '');
531
555
  }
532
556
  }
533
557
 
534
- const input = this.inputElement;
558
+ const input = this._nativeInput;
535
559
  if (input) {
536
560
  input.setAttribute('aria-expanded', !!opened);
537
561
 
@@ -600,8 +624,6 @@ export const ComboBoxMixin = (subclass) =>
600
624
 
601
625
  /** @private */
602
626
  _onClick(e) {
603
- this._closeOnBlurIsPrevented = true;
604
-
605
627
  const path = e.composedPath();
606
628
 
607
629
  if (this._isClearButton(e)) {
@@ -611,8 +633,6 @@ export const ComboBoxMixin = (subclass) =>
611
633
  } else {
612
634
  this._onHostClick(e);
613
635
  }
614
-
615
- this._closeOnBlurIsPrevented = false;
616
636
  }
617
637
 
618
638
  /**
@@ -628,16 +648,12 @@ export const ComboBoxMixin = (subclass) =>
628
648
  if (e.key === 'Tab') {
629
649
  this.$.overlay.restoreFocusOnClose = false;
630
650
  } else if (e.key === 'ArrowDown') {
631
- this._closeOnBlurIsPrevented = true;
632
651
  this._onArrowDown();
633
- this._closeOnBlurIsPrevented = false;
634
652
 
635
653
  // Prevent caret from moving
636
654
  e.preventDefault();
637
655
  } else if (e.key === 'ArrowUp') {
638
- this._closeOnBlurIsPrevented = true;
639
656
  this._onArrowUp();
640
- this._closeOnBlurIsPrevented = false;
641
657
 
642
658
  // Prevent caret from moving
643
659
  e.preventDefault();
@@ -708,8 +724,7 @@ export const ComboBoxMixin = (subclass) =>
708
724
  // and there's no need to modify the selection range if the input isn't focused anyway.
709
725
  // This affects Safari. When the overlay is open, and then hitting tab, browser should focus
710
726
  // the next focusable element instead of the combo-box itself.
711
- // Checking the focused property here is enough instead of checking the activeElement.
712
- if (this.hasAttribute('focused')) {
727
+ if (this._isInputFocused() && this.inputElement.setSelectionRange) {
713
728
  this.inputElement.setSelectionRange(start, end);
714
729
  }
715
730
  }
@@ -819,7 +834,7 @@ export const ComboBoxMixin = (subclass) =>
819
834
  toggleElement.addEventListener('mousedown', (e) => e.preventDefault());
820
835
  // Unfocus previously focused element if focus is not inside combo box (on touch devices)
821
836
  toggleElement.addEventListener('click', () => {
822
- if (isTouch && !this.hasAttribute('focused')) {
837
+ if (isTouch && !this._isInputFocused()) {
823
838
  document.activeElement.blur();
824
839
  }
825
840
  });
@@ -926,9 +941,7 @@ export const ComboBoxMixin = (subclass) =>
926
941
 
927
942
  this._clearSelectionRange();
928
943
 
929
- if (!this.dataProvider) {
930
- this.filter = '';
931
- }
944
+ this.filter = '';
932
945
  }
933
946
 
934
947
  /**
@@ -946,19 +959,27 @@ export const ComboBoxMixin = (subclass) =>
946
959
  * @override
947
960
  */
948
961
  _onInput(event) {
949
- if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
950
- this.open();
951
- }
962
+ const filter = this._inputElementValue;
963
+
964
+ // When opening dropdown on user input, both `opened` and `filter` properties are set.
965
+ // Perform a batched property update instead of relying on sync property observers.
966
+ // This is necessary to avoid an extra data-provider request for loading first page.
967
+ const props = {};
952
968
 
953
- const value = this._inputElementValue;
954
- if (this.filter === value) {
969
+ if (this.filter === filter) {
955
970
  // Filter and input value might get out of sync, while keyboard navigating for example.
956
971
  // Afterwards, input value might be changed to the same value as used in filtering.
957
972
  // In situation like these, we need to make sure all the filter changes handlers are run.
958
- this._filterChanged(this.filter, this.itemValuePath, this.itemLabelPath);
973
+ this._filterChanged(this.filter);
959
974
  } else {
960
- this.filter = value;
975
+ props.filter = filter;
961
976
  }
977
+
978
+ if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
979
+ props.opened = true;
980
+ }
981
+
982
+ this.setProperties(props);
962
983
  }
963
984
 
964
985
  /**
@@ -981,11 +1002,7 @@ export const ComboBoxMixin = (subclass) =>
981
1002
  }
982
1003
 
983
1004
  /** @private */
984
- _filterChanged(filter, _itemValuePath, _itemLabelPath) {
985
- if (filter === undefined) {
986
- return;
987
- }
988
-
1005
+ _filterChanged(filter) {
989
1006
  // Scroll to the top of the list whenever the filter changes.
990
1007
  this._scrollIntoView(0);
991
1008
 
@@ -1028,7 +1045,7 @@ export const ComboBoxMixin = (subclass) =>
1028
1045
  this.value = '';
1029
1046
  }
1030
1047
 
1031
- this._toggleHasValue(this.value !== '');
1048
+ this._toggleHasValue(this._hasValue);
1032
1049
  this._inputElementValue = this.value;
1033
1050
  }
1034
1051
  } else {
@@ -1064,21 +1081,21 @@ export const ComboBoxMixin = (subclass) =>
1064
1081
  }
1065
1082
 
1066
1083
  if (isValidValue(value)) {
1067
- let item;
1068
1084
  if (this._getItemValue(this.selectedItem) !== value) {
1069
1085
  this._selectItemForValue(value);
1070
- } else {
1071
- item = this.selectedItem;
1072
1086
  }
1073
1087
 
1074
- if (!item && this.allowCustomValue) {
1088
+ if (!this.selectedItem && this.allowCustomValue) {
1075
1089
  this._inputElementValue = value;
1076
1090
  }
1077
1091
 
1078
- this._toggleHasValue(this.value !== '');
1092
+ this._toggleHasValue(this._hasValue);
1079
1093
  } else {
1080
1094
  this.selectedItem = null;
1081
1095
  }
1096
+
1097
+ this.filter = '';
1098
+
1082
1099
  // In the next _detectAndDispatchChange() call, the change detection should pass
1083
1100
  this._lastCommittedValue = undefined;
1084
1101
  }
@@ -1236,9 +1253,6 @@ export const ComboBoxMixin = (subclass) =>
1236
1253
  if (this.opened) {
1237
1254
  this._focusedIndex = this.filteredItems.indexOf(e.detail.item);
1238
1255
  this.close();
1239
- } else if (this.selectedItem !== e.detail.item) {
1240
- this.selectedItem = e.detail.item;
1241
- this._detectAndDispatchChange();
1242
1256
  }
1243
1257
  }
1244
1258
 
@@ -1250,6 +1264,12 @@ export const ComboBoxMixin = (subclass) =>
1250
1264
 
1251
1265
  /** @private */
1252
1266
  _onFocusout(event) {
1267
+ // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
1268
+ // Do not focus the input in this case, because it would break announcement for the item.
1269
+ if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
1270
+ return;
1271
+ }
1272
+
1253
1273
  // Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
1254
1274
  if (event.relatedTarget === this.$.overlay) {
1255
1275
  event.composedPath()[0].focus();
@@ -1277,35 +1297,12 @@ export const ComboBoxMixin = (subclass) =>
1277
1297
  this._clear();
1278
1298
  }
1279
1299
 
1280
- /**
1281
- * Returns true if `value` is valid, and sets the `invalid` flag appropriately.
1282
- *
1283
- * @return {boolean} True if the value is valid and sets the `invalid` flag appropriately
1284
- */
1285
- validate() {
1286
- return !(this.invalid = !this.checkValidity());
1287
- }
1288
-
1289
- /**
1290
- * Returns true if the current input value satisfies all constraints (if any).
1291
- * You can override this method for custom validations.
1292
- *
1293
- * @return {boolean}
1294
- */
1295
- checkValidity() {
1296
- if (super.checkValidity) {
1297
- return super.checkValidity();
1298
- }
1299
-
1300
- return !this.required || !!this.value;
1301
- }
1302
-
1303
1300
  /**
1304
1301
  * Fired when the value changes.
1305
1302
  *
1306
1303
  * @event value-changed
1307
1304
  * @param {Object} detail
1308
- * @param {String} detail.value the combobox value
1305
+ * @param {String} detail.value the combobox value
1309
1306
  */
1310
1307
 
1311
1308
  /**
@@ -1313,7 +1310,7 @@ export const ComboBoxMixin = (subclass) =>
1313
1310
  *
1314
1311
  * @event selected-item-changed
1315
1312
  * @param {Object} detail
1316
- * @param {Object|String} detail.value the selected item. Type is the same as the type of `items`.
1313
+ * @param {Object|String} detail.value the selected item. Type is the same as the type of `items`.
1317
1314
  */
1318
1315
 
1319
1316
  /**
@@ -169,9 +169,7 @@ export class ComboBoxScroller extends PolymerElement {
169
169
  }
170
170
 
171
171
  requestContentUpdate() {
172
- if (this.__virtualizer) {
173
- this.__virtualizer.update();
174
- }
172
+ this.__virtualizer.update();
175
173
  }
176
174
 
177
175
  scrollIntoView(index) {
@@ -249,24 +247,16 @@ export class ComboBoxScroller extends PolymerElement {
249
247
 
250
248
  /** @private */
251
249
  __loadingChanged() {
252
- if (this.__virtualizer) {
253
- setTimeout(() => this.requestContentUpdate());
254
- }
250
+ setTimeout(() => this.requestContentUpdate());
255
251
  }
256
252
 
257
253
  /** @private */
258
254
  __selectedItemChanged() {
259
- if (this.__virtualizer) {
260
- this.requestContentUpdate();
261
- }
255
+ this.requestContentUpdate();
262
256
  }
263
257
 
264
258
  /** @private */
265
259
  __focusedIndexChanged(index, oldIndex) {
266
- if (!this.__virtualizer) {
267
- return;
268
- }
269
-
270
260
  if (index !== oldIndex) {
271
261
  this.requestContentUpdate();
272
262
  }
@@ -304,7 +294,7 @@ export class ComboBoxScroller extends PolymerElement {
304
294
 
305
295
  el.setProperties({
306
296
  item,
307
- index: this.__requestItemByIndex(item, index),
297
+ index,
308
298
  label: this.getItemLabel(item),
309
299
  selected: this.__isItemSelected(item, this.selectedItem, this.itemIdPath),
310
300
  renderer: this.renderer,
@@ -321,6 +311,10 @@ export class ComboBoxScroller extends PolymerElement {
321
311
  } else {
322
312
  el.removeAttribute('theme');
323
313
  }
314
+
315
+ if (item instanceof ComboBoxPlaceholder) {
316
+ this.__requestItemByIndex(index);
317
+ }
324
318
  }
325
319
 
326
320
  /** @private */
@@ -361,19 +355,18 @@ export class ComboBoxScroller extends PolymerElement {
361
355
  }
362
356
 
363
357
  /**
364
- * If dataProvider is used, dispatch a request for the item’s index if
365
- * the item is a placeholder object.
366
- *
367
- * @return {number}
358
+ * Dispatches an `index-requested` event for the given index to notify
359
+ * the data provider that it should start loading the page containing the requested index.
368
360
  */
369
- __requestItemByIndex(item, index) {
370
- if (item instanceof ComboBoxPlaceholder && index !== undefined) {
371
- this.dispatchEvent(
372
- new CustomEvent('index-requested', { detail: { index, currentScrollerPos: this._oldScrollerPosition } }),
373
- );
374
- }
375
-
376
- return index;
361
+ __requestItemByIndex(index) {
362
+ this.dispatchEvent(
363
+ new CustomEvent('index-requested', {
364
+ detail: {
365
+ index,
366
+ currentScrollerPos: this._oldScrollerPosition,
367
+ },
368
+ }),
369
+ );
377
370
  }
378
371
 
379
372
  /** @private */
@@ -3,24 +3,25 @@
3
3
  * Copyright (c) 2015 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
7
- import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
8
- import { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
9
- import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
10
- import { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
11
- import { DelegateFocusMixinClass } from '@vaadin/field-base/src/delegate-focus-mixin.js';
12
- import { DelegateStateMixinClass } from '@vaadin/field-base/src/delegate-state-mixin.js';
13
- import { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
14
- import { InputConstraintsMixinClass } from '@vaadin/field-base/src/input-constraints-mixin.js';
15
- import { InputControlMixinClass } from '@vaadin/field-base/src/input-control-mixin.js';
16
- import { InputMixinClass } from '@vaadin/field-base/src/input-mixin.js';
17
- import { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
18
- import { PatternMixinClass } from '@vaadin/field-base/src/pattern-mixin.js';
19
- import { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
20
- import { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
21
- import { ComboBoxDataProviderMixinClass } from './vaadin-combo-box-data-provider-mixin.js';
22
- import { ComboBoxMixinClass } from './vaadin-combo-box-mixin.js';
23
- import { ComboBoxDefaultItem } from './vaadin-combo-box-mixin.js';
6
+ import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
7
+ import type { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
8
+ import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
9
+ import type { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
10
+ import type { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
11
+ import type { DelegateFocusMixinClass } from '@vaadin/field-base/src/delegate-focus-mixin.js';
12
+ import type { DelegateStateMixinClass } from '@vaadin/field-base/src/delegate-state-mixin.js';
13
+ import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
14
+ import type { InputConstraintsMixinClass } from '@vaadin/field-base/src/input-constraints-mixin.js';
15
+ import type { InputControlMixinClass } from '@vaadin/field-base/src/input-control-mixin.js';
16
+ import type { InputMixinClass } from '@vaadin/field-base/src/input-mixin.js';
17
+ import type { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
18
+ import type { PatternMixinClass } from '@vaadin/field-base/src/pattern-mixin.js';
19
+ import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
20
+ import type { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
21
+ import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
22
+ import type { ComboBoxDataProviderMixinClass } from './vaadin-combo-box-data-provider-mixin.js';
23
+ import type { ComboBoxMixinClass } from './vaadin-combo-box-mixin.js';
24
+ import type { ComboBoxDefaultItem } from './vaadin-combo-box-mixin.js';
24
25
  export {
25
26
  ComboBoxDataProvider,
26
27
  ComboBoxDataProviderCallback,
@@ -65,6 +66,11 @@ export type ComboBoxFilterChangedEvent = CustomEvent<{ value: string }>;
65
66
  */
66
67
  export type ComboBoxSelectedItemChangedEvent<TItem> = CustomEvent<{ value: TItem | null | undefined }>;
67
68
 
69
+ /**
70
+ * Fired whenever the field is validated.
71
+ */
72
+ export type ComboBoxValidatedEvent = CustomEvent<{ valid: boolean }>;
73
+
68
74
  export interface ComboBoxEventMap<TItem> extends HTMLElementEventMap {
69
75
  change: ComboBoxChangeEvent<TItem>;
70
76
 
@@ -79,6 +85,8 @@ export interface ComboBoxEventMap<TItem> extends HTMLElementEventMap {
79
85
  'value-changed': ComboBoxValueChangedEvent;
80
86
 
81
87
  'selected-item-changed': ComboBoxSelectedItemChangedEvent<TItem>;
88
+
89
+ validated: ComboBoxValidatedEvent;
82
90
  }
83
91
 
84
92
  /**
@@ -165,6 +173,7 @@ export interface ComboBoxEventMap<TItem> extends HTMLElementEventMap {
165
173
  * Custom property | Description | Default
166
174
  * ----------------------------------------|----------------------------|---------
167
175
  * `--vaadin-field-default-width` | Default width of the field | `12em`
176
+ * `--vaadin-combo-box-overlay-width` | Width of the overlay | `auto`
168
177
  * `--vaadin-combo-box-overlay-max-height` | Max height of the overlay | `65vh`
169
178
  *
170
179
  * `<vaadin-combo-box>` provides the same set of shadow DOM parts and state attributes as `<vaadin-text-field>`.
@@ -198,7 +207,7 @@ export interface ComboBoxEventMap<TItem> extends HTMLElementEventMap {
198
207
  * Note: the `theme` attribute value set on `<vaadin-combo-box>` is
199
208
  * propagated to the internal components listed above.
200
209
  *
201
- * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
210
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
202
211
  *
203
212
  * @fires {Event} change - Fired when the user commits a value change.
204
213
  * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
@@ -207,18 +216,19 @@ export interface ComboBoxEventMap<TItem> extends HTMLElementEventMap {
207
216
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
208
217
  * @fires {CustomEvent} selected-item-changed - Fired when the `selectedItem` property changes.
209
218
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
219
+ * @fires {CustomEvent} validated - Fired whenever the field is validated.
210
220
  */
211
221
  declare class ComboBox<TItem = ComboBoxDefaultItem> extends HTMLElement {
212
222
  addEventListener<K extends keyof ComboBoxEventMap<TItem>>(
213
223
  type: K,
214
224
  listener: (this: ComboBox<TItem>, ev: ComboBoxEventMap<TItem>[K]) => void,
215
- options?: boolean | AddEventListenerOptions,
225
+ options?: AddEventListenerOptions | boolean,
216
226
  ): void;
217
227
 
218
228
  removeEventListener<K extends keyof ComboBoxEventMap<TItem>>(
219
229
  type: K,
220
230
  listener: (this: ComboBox<TItem>, ev: ComboBoxEventMap<TItem>[K]) => void,
221
- options?: boolean | EventListenerOptions,
231
+ options?: EventListenerOptions | boolean,
222
232
  ): void;
223
233
  }
224
234
 
@@ -238,6 +248,7 @@ interface ComboBox<TItem = ComboBoxDefaultItem>
238
248
  DelegateStateMixinClass,
239
249
  DelegateFocusMixinClass,
240
250
  ThemableMixinClass,
251
+ ThemePropertyMixinClass,
241
252
  ElementMixinClass,
242
253
  ControllerMixinClass {}
243
254
 
@@ -9,6 +9,7 @@ import './vaadin-combo-box-overlay.js';
9
9
  import './vaadin-combo-box-scroller.js';
10
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
12
+ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
12
13
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
13
14
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
14
15
  import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
@@ -104,6 +105,7 @@ registerStyles('vaadin-combo-box', inputFieldShared, { moduleId: 'vaadin-combo-b
104
105
  * Custom property | Description | Default
105
106
  * ----------------------------------------|----------------------------|---------
106
107
  * `--vaadin-field-default-width` | Default width of the field | `12em`
108
+ * `--vaadin-combo-box-overlay-width` | Width of the overlay | `auto`
107
109
  * `--vaadin-combo-box-overlay-max-height` | Max height of the overlay | `65vh`
108
110
  *
109
111
  * `<vaadin-combo-box>` provides the same set of shadow DOM parts and state attributes as `<vaadin-text-field>`.
@@ -137,7 +139,7 @@ registerStyles('vaadin-combo-box', inputFieldShared, { moduleId: 'vaadin-combo-b
137
139
  * Note: the `theme` attribute value set on `<vaadin-combo-box>` is
138
140
  * propagated to the internal components listed above.
139
141
  *
140
- * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
142
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
141
143
  *
142
144
  * @fires {Event} change - Fired when the user commits a value change.
143
145
  * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
@@ -146,6 +148,7 @@ registerStyles('vaadin-combo-box', inputFieldShared, { moduleId: 'vaadin-combo-b
146
148
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
147
149
  * @fires {CustomEvent} selected-item-changed - Fired when the `selectedItem` property changes.
148
150
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
151
+ * @fires {CustomEvent} validated - Fired whenever the field is validated.
149
152
  *
150
153
  * @extends HTMLElement
151
154
  * @mixes ElementMixin
@@ -200,7 +203,6 @@ class ComboBox extends ComboBoxDataProviderMixin(
200
203
 
201
204
  <vaadin-combo-box-overlay
202
205
  id="overlay"
203
- hidden$="[[_isOverlayHidden(filteredItems, loading)]]"
204
206
  opened="[[_overlayOpened]]"
205
207
  loading$="[[loading]]"
206
208
  theme$="[[_theme]]"
@@ -208,6 +210,8 @@ class ComboBox extends ComboBoxDataProviderMixin(
208
210
  no-vertical-overlap
209
211
  restore-focus-node="[[inputElement]]"
210
212
  ></vaadin-combo-box-overlay>
213
+
214
+ <slot name="tooltip"></slot>
211
215
  `;
212
216
  }
213
217
 
@@ -244,6 +248,11 @@ class ComboBox extends ComboBoxDataProviderMixin(
244
248
  }),
245
249
  );
246
250
  this.addController(new LabelledInputController(this.inputElement, this._labelController));
251
+
252
+ this._tooltipController = new TooltipController(this);
253
+ this.addController(this._tooltipController);
254
+ this._tooltipController.setShouldShow((target) => !target.opened);
255
+
247
256
  this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
248
257
  this._toggleElement = this.$.toggleButton;
249
258
  }
@@ -2,6 +2,7 @@ import '@vaadin/vaadin-lumo-styles/color.js';
2
2
  import '@vaadin/vaadin-lumo-styles/spacing.js';
3
3
  import '@vaadin/vaadin-lumo-styles/style.js';
4
4
  import '@vaadin/vaadin-overlay/theme/lumo/vaadin-overlay.js';
5
+ import { loader } from '@vaadin/vaadin-lumo-styles/mixins/loader.js';
5
6
  import { menuOverlayCore } from '@vaadin/vaadin-lumo-styles/mixins/menu-overlay.js';
6
7
  import { overlay } from '@vaadin/vaadin-lumo-styles/mixins/overlay.js';
7
8
  import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
@@ -36,10 +37,7 @@ const comboBoxOverlay = css`
36
37
  margin-bottom: var(--lumo-space-xs);
37
38
  }
38
39
 
39
- :host([loading]) [part~='loader'] {
40
- box-sizing: border-box;
41
- width: var(--lumo-icon-size-s);
42
- height: var(--lumo-icon-size-s);
40
+ [part~='loader'] {
43
41
  position: absolute;
44
42
  z-index: 1;
45
43
  left: var(--lumo-space-s);
@@ -48,38 +46,11 @@ const comboBoxOverlay = css`
48
46
  margin-left: auto;
49
47
  margin-inline-start: auto;
50
48
  margin-inline-end: 0;
51
- border: 2px solid transparent;
52
- border-color: var(--lumo-primary-color-50pct) var(--lumo-primary-color-50pct) var(--lumo-primary-color)
53
- var(--lumo-primary-color);
54
- border-radius: calc(0.5 * var(--lumo-icon-size-s));
55
- opacity: 0;
56
- animation: 1s linear infinite lumo-combo-box-loader-rotate, 0.3s 0.1s lumo-combo-box-loader-fade-in both;
57
- pointer-events: none;
58
- }
59
-
60
- @keyframes lumo-combo-box-loader-fade-in {
61
- 0% {
62
- opacity: 0;
63
- }
64
-
65
- 100% {
66
- opacity: 1;
67
- }
68
- }
69
-
70
- @keyframes lumo-combo-box-loader-rotate {
71
- 0% {
72
- transform: rotate(0deg);
73
- }
74
-
75
- 100% {
76
- transform: rotate(360deg);
77
- }
78
49
  }
79
50
 
80
51
  /* RTL specific styles */
81
52
 
82
- :host([loading][dir='rtl']) [part~='loader'] {
53
+ :host([dir='rtl']) [part~='loader'] {
83
54
  left: auto;
84
55
  margin-left: 0;
85
56
  margin-right: auto;
@@ -88,6 +59,6 @@ const comboBoxOverlay = css`
88
59
  }
89
60
  `;
90
61
 
91
- registerStyles('vaadin-combo-box-overlay', [overlay, menuOverlayCore, comboBoxOverlay], {
62
+ registerStyles('vaadin-combo-box-overlay', [overlay, menuOverlayCore, comboBoxOverlay, loader], {
92
63
  moduleId: 'lumo-combo-box-overlay',
93
64
  });