@everymatrix/general-input 1.27.6 → 1.27.8

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.
@@ -1,11 +1,111 @@
1
- import { i, r as registerStyles, T as ThemableMixin, A as DirMixin, P as PolymerElement, h as html, n as microTask, O as idlePeriod, Q as animationFrame, R as flush, y as Debouncer, U as enqueueDebouncer, z as timeOut, W as generateUniqueId, C as ControllerMixin, K as KeyboardMixin, I as InputMixin, a as DisabledMixin, b as isElementFocused, e as InputController, f as LabelledInputController, g as TooltipController, E as ElementMixin } from './field-mixin.js';
2
- import { o as overlay, d as menuOverlayCore, P as PositionMixin, O as Overlay, V as VirtualKeyboardController } from './virtual-keyboard-controller.js';
3
- import { i as inputFieldShared, e as isSafari, f as isTouch, c as InputControlMixin, d as inputFieldShared$1 } from './input-field-shared-styles.js';
4
- import { P as PatternMixin } from './pattern-mixin.js';
1
+ import { i, r as registerStyles, f as defineCustomElement, h as html, g as ThemableMixin, n as DirMixin, P as PolymerElement, m as microTask, R as idlePeriod, U as animationFrame, W as flush, o as Debouncer, X as enqueueDebouncer, t as timeOut, p as generateUniqueId, H as ControllerMixin, V as ValidateMixin, l as FocusMixin, K as KeyboardMixin, I as InputMixin, a as DisabledMixin, N as isElementFocused, c as InputController, e as LabelledInputController, T as TooltipController, E as ElementMixin } from './field-mixin.js';
2
+ import { c as overlay, d as menuOverlayCore, P as PositionMixin, O as OverlayMixin, o as overlayStyles, b as OverlayClassMixin, V as VirtualKeyboardController } from './virtual-keyboard-controller.js';
3
+ import { i as inputFieldShared, e as isSafari, I as InputConstraintsMixin, f as isTouch, c as InputControlMixin, d as inputFieldShared$1 } from './input-field-shared-styles.js';
4
+
5
+ const item = i`
6
+ :host {
7
+ display: flex;
8
+ align-items: center;
9
+ box-sizing: border-box;
10
+ font-family: var(--lumo-font-family);
11
+ font-size: var(--lumo-font-size-m);
12
+ line-height: var(--lumo-line-height-xs);
13
+ padding: 0.5em calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4) 0.5em
14
+ var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
15
+ min-height: var(--lumo-size-m);
16
+ outline: none;
17
+ border-radius: var(--lumo-border-radius-m);
18
+ cursor: var(--lumo-clickable-cursor);
19
+ -webkit-font-smoothing: antialiased;
20
+ -moz-osx-font-smoothing: grayscale;
21
+ -webkit-tap-highlight-color: var(--lumo-primary-color-10pct);
22
+ }
23
+
24
+ /* Checkmark */
25
+ [part='checkmark']::before {
26
+ display: var(--_lumo-item-selected-icon-display, none);
27
+ content: var(--lumo-icons-checkmark);
28
+ font-family: lumo-icons;
29
+ font-size: var(--lumo-icon-size-m);
30
+ line-height: 1;
31
+ font-weight: normal;
32
+ width: 1em;
33
+ height: 1em;
34
+ margin: calc((1 - var(--lumo-line-height-xs)) * var(--lumo-font-size-m) / 2) 0;
35
+ color: var(--lumo-primary-text-color);
36
+ flex: none;
37
+ opacity: 0;
38
+ transition: transform 0.2s cubic-bezier(0.12, 0.32, 0.54, 2), opacity 0.1s;
39
+ }
40
+
41
+ :host([selected]) [part='checkmark']::before {
42
+ opacity: 1;
43
+ }
44
+
45
+ :host([active]:not([selected])) [part='checkmark']::before {
46
+ transform: scale(0.8);
47
+ opacity: 0;
48
+ transition-duration: 0s;
49
+ }
50
+
51
+ [part='content'] {
52
+ flex: auto;
53
+ }
54
+
55
+ /* Disabled */
56
+ :host([disabled]) {
57
+ color: var(--lumo-disabled-text-color);
58
+ cursor: default;
59
+ pointer-events: none;
60
+ }
61
+
62
+ /* TODO a workaround until we have "focus-follows-mouse". After that, use the hover style for focus-ring as well */
63
+ @media (any-hover: hover) {
64
+ :host(:hover:not([disabled])) {
65
+ background-color: var(--lumo-primary-color-10pct);
66
+ }
67
+
68
+ :host([focus-ring]:not([disabled])) {
69
+ box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
70
+ }
71
+ }
72
+
73
+ /* RTL specific styles */
74
+ :host([dir='rtl']) {
75
+ padding-left: calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4);
76
+ padding-right: var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
77
+ }
78
+
79
+ /* Slotted icons */
80
+ :host ::slotted(vaadin-icon) {
81
+ width: var(--lumo-icon-size-m);
82
+ height: var(--lumo-icon-size-m);
83
+ }
84
+ `;
85
+
86
+ registerStyles('vaadin-item', item, { moduleId: 'lumo-item' });
87
+
88
+ const comboBoxItem = i`
89
+ :host {
90
+ transition: background-color 100ms;
91
+ overflow: hidden;
92
+ --_lumo-item-selected-icon-display: block;
93
+ }
94
+
95
+ @media (any-hover: hover) {
96
+ :host([focused]:not([disabled])) {
97
+ box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
98
+ }
99
+ }
100
+ `;
101
+
102
+ registerStyles('vaadin-combo-box-item', [item, comboBoxItem], {
103
+ moduleId: 'lumo-combo-box-item',
104
+ });
5
105
 
6
106
  /**
7
107
  * @license
8
- * Copyright (c) 2022 Vaadin Ltd.
108
+ * Copyright (c) 2022 - 2023 Vaadin Ltd.
9
109
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
10
110
  */
11
111
 
@@ -56,14 +156,6 @@ const comboBoxOverlay = i`
56
156
  padding: 0;
57
157
  }
58
158
 
59
- :host {
60
- --_vaadin-combo-box-items-container-border-width: var(--lumo-space-xs);
61
- --_vaadin-combo-box-items-container-border-style: solid;
62
- --_vaadin-combo-box-items-container-border-color: transparent;
63
- }
64
-
65
- /* Loading state */
66
-
67
159
  /* When items are empty, the spinner needs some room */
68
160
  :host(:not([closing])) [part~='content'] {
69
161
  min-height: calc(2 * var(--lumo-space-s) + var(--lumo-icon-size-s));
@@ -80,7 +172,9 @@ const comboBoxOverlay = i`
80
172
  :host([bottom-aligned]) [part~='overlay'] {
81
173
  margin-bottom: var(--lumo-space-xs);
82
174
  }
175
+ `;
83
176
 
177
+ const comboBoxLoader = i`
84
178
  [part~='loader'] {
85
179
  position: absolute;
86
180
  z-index: 1;
@@ -92,8 +186,6 @@ const comboBoxOverlay = i`
92
186
  margin-inline-end: 0;
93
187
  }
94
188
 
95
- /* RTL specific styles */
96
-
97
189
  :host([dir='rtl']) [part~='loader'] {
98
190
  left: auto;
99
191
  margin-left: 0;
@@ -103,127 +195,182 @@ const comboBoxOverlay = i`
103
195
  }
104
196
  `;
105
197
 
106
- registerStyles('vaadin-combo-box-overlay', [overlay, menuOverlayCore, comboBoxOverlay, loader], {
107
- moduleId: 'lumo-combo-box-overlay',
108
- });
198
+ registerStyles(
199
+ 'vaadin-combo-box-overlay',
200
+ [
201
+ overlay,
202
+ menuOverlayCore,
203
+ comboBoxOverlay,
204
+ loader,
205
+ comboBoxLoader,
206
+ i`
207
+ :host {
208
+ --_vaadin-combo-box-items-container-border-width: var(--lumo-space-xs);
209
+ --_vaadin-combo-box-items-container-border-style: solid;
210
+ }
211
+ `,
212
+ ],
213
+ { moduleId: 'lumo-combo-box-overlay' },
214
+ );
109
215
 
110
- const item = i`
216
+ const comboBox = i`
111
217
  :host {
112
- display: flex;
113
- align-items: center;
114
- box-sizing: border-box;
115
- font-family: var(--lumo-font-family);
116
- font-size: var(--lumo-font-size-m);
117
- line-height: var(--lumo-line-height-xs);
118
- padding: 0.5em calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4) 0.5em
119
- var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
120
- min-height: var(--lumo-size-m);
121
218
  outline: none;
122
- border-radius: var(--lumo-border-radius-m);
123
- cursor: var(--lumo-clickable-cursor);
124
- -webkit-font-smoothing: antialiased;
125
- -moz-osx-font-smoothing: grayscale;
126
- -webkit-tap-highlight-color: var(--lumo-primary-color-10pct);
127
219
  }
128
220
 
129
- /* Checkmark */
130
- [part='checkmark']::before {
131
- display: var(--_lumo-item-selected-icon-display, none);
132
- content: var(--lumo-icons-checkmark);
133
- font-family: lumo-icons;
134
- font-size: var(--lumo-icon-size-m);
135
- line-height: 1;
136
- font-weight: normal;
137
- width: 1em;
138
- height: 1em;
139
- margin: calc((1 - var(--lumo-line-height-xs)) * var(--lumo-font-size-m) / 2) 0;
140
- color: var(--lumo-primary-text-color);
141
- flex: none;
142
- opacity: 0;
143
- transition: transform 0.2s cubic-bezier(0.12, 0.32, 0.54, 2), opacity 0.1s;
221
+ [part='toggle-button']::before {
222
+ content: var(--lumo-icons-dropdown);
144
223
  }
224
+ `;
145
225
 
146
- :host([selected]) [part='checkmark']::before {
147
- opacity: 1;
148
- }
226
+ registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lumo-combo-box' });
149
227
 
150
- :host([active]:not([selected])) [part='checkmark']::before {
151
- transform: scale(0.8);
152
- opacity: 0;
153
- transition-duration: 0s;
154
- }
228
+ /**
229
+ * @license
230
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
231
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
232
+ */
155
233
 
156
- [part='content'] {
157
- flex: auto;
158
- }
234
+ /**
235
+ * @polymerMixin
236
+ */
237
+ const ComboBoxItemMixin = (superClass) =>
238
+ class ComboBoxItemMixinClass extends superClass {
239
+ static get properties() {
240
+ return {
241
+ /**
242
+ * The index of the item.
243
+ */
244
+ index: {
245
+ type: Number,
246
+ },
159
247
 
160
- /* Disabled */
161
- :host([disabled]) {
162
- color: var(--lumo-disabled-text-color);
163
- cursor: default;
164
- pointer-events: none;
165
- }
248
+ /**
249
+ * The item to render.
250
+ */
251
+ item: {
252
+ type: Object,
253
+ },
166
254
 
167
- /* TODO a workaround until we have "focus-follows-mouse". After that, use the hover style for focus-ring as well */
168
- @media (any-hover: hover) {
169
- :host(:hover:not([disabled])) {
170
- background-color: var(--lumo-primary-color-10pct);
255
+ /**
256
+ * The text to render in the item.
257
+ */
258
+ label: {
259
+ type: String,
260
+ },
261
+
262
+ /**
263
+ * True when item is selected.
264
+ */
265
+ selected: {
266
+ type: Boolean,
267
+ value: false,
268
+ reflectToAttribute: true,
269
+ },
270
+
271
+ /**
272
+ * True when item is focused.
273
+ */
274
+ focused: {
275
+ type: Boolean,
276
+ value: false,
277
+ reflectToAttribute: true,
278
+ },
279
+
280
+ /**
281
+ * Custom function for rendering the item content.
282
+ */
283
+ renderer: {
284
+ type: Function,
285
+ },
286
+ };
171
287
  }
172
288
 
173
- :host([focus-ring]:not([disabled])) {
174
- box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
289
+ static get observers() {
290
+ return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
175
291
  }
176
- }
177
292
 
178
- /* RTL specific styles */
179
- :host([dir='rtl']) {
180
- padding-left: calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4);
181
- padding-right: var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
182
- }
293
+ static get observedAttributes() {
294
+ return [...super.observedAttributes, 'hidden'];
295
+ }
183
296
 
184
- /* Slotted icons */
185
- :host ::slotted(vaadin-icon),
186
- :host ::slotted(iron-icon) {
187
- width: var(--lumo-icon-size-m);
188
- height: var(--lumo-icon-size-m);
189
- }
190
- `;
297
+ attributeChangedCallback(name, oldValue, newValue) {
298
+ if (name === 'hidden' && newValue !== null) {
299
+ // The element is being hidden (by virtualizer). Mark one of the __rendererOrItemChanged
300
+ // dependencies as undefined to make sure it's called when the element is shown again
301
+ // and assigned properties with possibly identical values as before hiding.
302
+ this.index = undefined;
303
+ } else {
304
+ super.attributeChangedCallback(name, oldValue, newValue);
305
+ }
306
+ }
191
307
 
192
- registerStyles('vaadin-item', item, { moduleId: 'lumo-item' });
308
+ /** @protected */
309
+ connectedCallback() {
310
+ super.connectedCallback();
193
311
 
194
- const comboBoxItem = i`
195
- :host {
196
- transition: background-color 100ms;
197
- overflow: hidden;
198
- --_lumo-item-selected-icon-display: block;
199
- }
312
+ this._owner = this.parentNode.owner;
200
313
 
201
- @media (any-hover: hover) {
202
- :host([focused]:not([disabled])) {
203
- box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
314
+ const hostDir = this._owner.getAttribute('dir');
315
+ if (hostDir) {
316
+ this.setAttribute('dir', hostDir);
317
+ }
204
318
  }
205
- }
206
- `;
207
319
 
208
- registerStyles('vaadin-combo-box-item', [item, comboBoxItem], {
209
- moduleId: 'lumo-combo-box-item',
210
- });
320
+ /**
321
+ * Requests an update for the content of the item.
322
+ * While performing the update, it invokes the renderer passed in the `renderer` property.
323
+ *
324
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
325
+ */
326
+ requestContentUpdate() {
327
+ if (!this.renderer) {
328
+ return;
329
+ }
211
330
 
212
- const comboBox = i`
213
- :host {
214
- outline: none;
215
- }
331
+ const model = {
332
+ index: this.index,
333
+ item: this.item,
334
+ focused: this.focused,
335
+ selected: this.selected,
336
+ };
216
337
 
217
- [part='toggle-button']::before {
218
- content: var(--lumo-icons-dropdown);
219
- }
220
- `;
338
+ this.renderer(this, this._owner, model);
339
+ }
221
340
 
222
- registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lumo-combo-box' });
341
+ /** @private */
342
+ __rendererOrItemChanged(renderer, index, item) {
343
+ if (item === undefined || index === undefined) {
344
+ return;
345
+ }
346
+
347
+ if (this._oldRenderer !== renderer) {
348
+ this.innerHTML = '';
349
+ // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
350
+ // When clearing the rendered content, this part needs to be manually disposed of.
351
+ // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
352
+ delete this._$litPart$;
353
+ }
354
+
355
+ if (renderer) {
356
+ this._oldRenderer = renderer;
357
+ this.requestContentUpdate();
358
+ }
359
+ }
360
+
361
+ /** @private */
362
+ __updateLabel(label, renderer) {
363
+ if (renderer) {
364
+ return;
365
+ }
366
+
367
+ this.textContent = label;
368
+ }
369
+ };
223
370
 
224
371
  /**
225
372
  * @license
226
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
373
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
227
374
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
228
375
  */
229
376
 
@@ -246,13 +393,15 @@ registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lu
246
393
  * `selected` | Set when the item is selected
247
394
  * `focused` | Set when the item is focused
248
395
  *
249
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
396
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
250
397
  *
398
+ * @customElement
399
+ * @mixes ComboBoxItemMixin
251
400
  * @mixes ThemableMixin
252
401
  * @mixes DirMixin
253
402
  * @private
254
403
  */
255
- class ComboBoxItem extends ThemableMixin(DirMixin(PolymerElement)) {
404
+ class ComboBoxItem extends ComboBoxItemMixin(ThemableMixin(DirMixin(PolymerElement))) {
256
405
  static get template() {
257
406
  return html`
258
407
  <style>
@@ -274,218 +423,143 @@ class ComboBoxItem extends ThemableMixin(DirMixin(PolymerElement)) {
274
423
  static get is() {
275
424
  return 'vaadin-combo-box-item';
276
425
  }
426
+ }
277
427
 
278
- static get properties() {
279
- return {
280
- /**
281
- * The index of the item
282
- */
283
- index: Number,
284
-
285
- /**
286
- * The item to render
287
- * @type {(String|Object)}
288
- */
289
- item: Object,
290
-
291
- /**
292
- * The text label corresponding to the item
293
- */
294
- label: String,
295
-
296
- /**
297
- * True when item is selected
298
- */
299
- selected: {
300
- type: Boolean,
301
- value: false,
302
- reflectToAttribute: true,
303
- },
304
-
305
- /**
306
- * True when item is focused
307
- */
308
- focused: {
309
- type: Boolean,
310
- value: false,
311
- reflectToAttribute: true,
312
- },
313
-
314
- /**
315
- * Custom function for rendering the content of the `<vaadin-combo-box-item>` propagated from the combo box element.
316
- */
317
- renderer: Function,
318
-
319
- /**
320
- * Saved instance of a custom renderer function.
321
- */
322
- _oldRenderer: Function,
323
- };
324
- }
325
-
326
- static get observers() {
327
- return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
328
- }
329
-
330
- connectedCallback() {
331
- super.connectedCallback();
428
+ defineCustomElement(ComboBoxItem);
332
429
 
333
- this._comboBox = this.parentNode.comboBox;
430
+ /**
431
+ * @license
432
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
433
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
434
+ */
334
435
 
335
- const hostDir = this._comboBox.getAttribute('dir');
336
- if (hostDir) {
337
- this.setAttribute('dir', hostDir);
436
+ /**
437
+ * @polymerMixin
438
+ * @mixes PositionMixin
439
+ */
440
+ const ComboBoxOverlayMixin = (superClass) =>
441
+ class ComboBoxOverlayMixin extends PositionMixin(superClass) {
442
+ static get observers() {
443
+ return ['_setOverlayWidth(positionTarget, opened)'];
338
444
  }
339
- }
340
445
 
341
- /**
342
- * Requests an update for the content of the item.
343
- * While performing the update, it invokes the renderer passed in the `renderer` property.
344
- *
345
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
346
- */
347
- requestContentUpdate() {
348
- if (!this.renderer) {
349
- return;
446
+ constructor() {
447
+ super();
448
+
449
+ this.requiredVerticalSpace = 200;
350
450
  }
351
451
 
352
- const model = {
353
- index: this.index,
354
- item: this.item,
355
- focused: this.focused,
356
- selected: this.selected,
357
- };
452
+ /** @protected */
453
+ connectedCallback() {
454
+ super.connectedCallback();
358
455
 
359
- this.renderer(this, this._comboBox, model);
360
- }
456
+ const comboBox = this._comboBox;
361
457
 
362
- /** @private */
363
- __rendererOrItemChanged(renderer, index, item) {
364
- if (item === undefined || index === undefined) {
365
- return;
458
+ const hostDir = comboBox && comboBox.getAttribute('dir');
459
+ if (hostDir) {
460
+ this.setAttribute('dir', hostDir);
461
+ }
366
462
  }
367
463
 
368
- if (this._oldRenderer !== renderer) {
369
- this.innerHTML = '';
370
- // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
371
- // When clearing the rendered content, this part needs to be manually disposed of.
372
- // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
373
- delete this._$litPart$;
464
+ /**
465
+ * Override method inherited from `Overlay`
466
+ * to not close on position target click.
467
+ *
468
+ * @param {Event} event
469
+ * @return {boolean}
470
+ * @protected
471
+ */
472
+ _shouldCloseOnOutsideClick(event) {
473
+ const eventPath = event.composedPath();
474
+ return !eventPath.includes(this.positionTarget) && !eventPath.includes(this);
374
475
  }
375
476
 
376
- if (renderer) {
377
- this._oldRenderer = renderer;
378
- this.requestContentUpdate();
379
- }
380
- }
477
+ /** @private */
478
+ _setOverlayWidth(positionTarget, opened) {
479
+ if (positionTarget && opened) {
480
+ const propPrefix = this.localName;
481
+ this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
381
482
 
382
- /** @private */
383
- __updateLabel(label, renderer) {
384
- if (renderer) {
385
- return;
386
- }
483
+ const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
387
484
 
388
- this.textContent = label;
389
- }
390
- }
485
+ if (customWidth === '') {
486
+ this.style.removeProperty(`--${propPrefix}-width`);
487
+ } else {
488
+ this.style.setProperty(`--${propPrefix}-width`, customWidth);
489
+ }
391
490
 
392
- customElements.define(ComboBoxItem.is, ComboBoxItem);
491
+ this._updatePosition();
492
+ }
493
+ }
494
+ };
393
495
 
394
496
  /**
395
497
  * @license
396
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
498
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
397
499
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
398
500
  */
399
501
 
400
- registerStyles(
401
- 'vaadin-combo-box-overlay',
402
- i`
403
- #overlay {
404
- width: var(--vaadin-combo-box-overlay-width, var(--_vaadin-combo-box-overlay-default-width, auto));
405
- }
502
+ const comboBoxOverlayStyles = i`
503
+ #overlay {
504
+ width: var(--vaadin-combo-box-overlay-width, var(--_vaadin-combo-box-overlay-default-width, auto));
505
+ }
406
506
 
407
- [part='content'] {
408
- display: flex;
409
- flex-direction: column;
410
- height: 100%;
411
- }
412
- `,
413
- { moduleId: 'vaadin-combo-box-overlay-styles' },
414
- );
507
+ [part='content'] {
508
+ display: flex;
509
+ flex-direction: column;
510
+ height: 100%;
511
+ }
512
+ `;
415
513
 
416
- let memoizedTemplate;
514
+ registerStyles('vaadin-combo-box-overlay', [overlayStyles, comboBoxOverlayStyles], {
515
+ moduleId: 'vaadin-combo-box-overlay-styles',
516
+ });
417
517
 
418
518
  /**
419
519
  * An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
420
520
  *
421
- * @extends Overlay
521
+ * @customElement
522
+ * @extends HTMLElement
523
+ * @mixes ComboBoxOverlayMixin
524
+ * @mixes DirMixin
525
+ * @mixes OverlayMixin
526
+ * @mixes ThemableMixin
422
527
  * @private
423
528
  */
424
- class ComboBoxOverlay extends PositionMixin(Overlay) {
529
+ class ComboBoxOverlay extends ComboBoxOverlayMixin(OverlayMixin(DirMixin(ThemableMixin(PolymerElement)))) {
425
530
  static get is() {
426
531
  return 'vaadin-combo-box-overlay';
427
532
  }
428
533
 
429
534
  static get template() {
430
- if (!memoizedTemplate) {
431
- memoizedTemplate = super.template.cloneNode(true);
432
- memoizedTemplate.content.querySelector('[part~="overlay"]').removeAttribute('tabindex');
433
- }
434
-
435
- return memoizedTemplate;
436
- }
437
-
438
- static get observers() {
439
- return ['_setOverlayWidth(positionTarget, opened)'];
440
- }
441
-
442
- connectedCallback() {
443
- super.connectedCallback();
444
-
445
- const comboBox = this._comboBox;
446
-
447
- const hostDir = comboBox && comboBox.getAttribute('dir');
448
- if (hostDir) {
449
- this.setAttribute('dir', hostDir);
450
- }
451
- }
452
-
453
- ready() {
454
- super.ready();
455
- const loader = document.createElement('div');
456
- loader.setAttribute('part', 'loader');
457
- const content = this.shadowRoot.querySelector('[part~="content"]');
458
- content.parentNode.insertBefore(loader, content);
459
- this.requiredVerticalSpace = 200;
460
- }
461
-
462
- _outsideClickListener(event) {
463
- const eventPath = event.composedPath();
464
- if (!eventPath.includes(this.positionTarget) && !eventPath.includes(this)) {
465
- this.close();
466
- }
535
+ return html`
536
+ <div id="backdrop" part="backdrop" hidden></div>
537
+ <div part="overlay" id="overlay">
538
+ <div part="loader"></div>
539
+ <div part="content" id="content"><slot></slot></div>
540
+ </div>
541
+ `;
467
542
  }
543
+ }
468
544
 
469
- _setOverlayWidth(positionTarget, opened) {
470
- if (positionTarget && opened) {
471
- const propPrefix = this.localName;
472
- this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
473
-
474
- const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
545
+ defineCustomElement(ComboBoxOverlay);
475
546
 
476
- if (customWidth === '') {
477
- this.style.removeProperty(`--${propPrefix}-width`);
478
- } else {
479
- this.style.setProperty(`--${propPrefix}-width`, customWidth);
480
- }
547
+ /**
548
+ * @license
549
+ * Copyright (c) 2023 Vaadin Ltd.
550
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
551
+ */
481
552
 
482
- this._updatePosition();
483
- }
484
- }
553
+ /**
554
+ * Convenience method for reading a value from a path.
555
+ *
556
+ * @param {string} path
557
+ * @param {object} object
558
+ */
559
+ function get(path, object) {
560
+ return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
485
561
  }
486
562
 
487
- customElements.define(ComboBoxOverlay.is, ComboBoxOverlay);
488
-
489
563
  /**
490
564
  * @license
491
565
  * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
@@ -496,7 +570,7 @@ customElements.define(ComboBoxOverlay.is, ComboBoxOverlay);
496
570
  * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
497
571
  */
498
572
 
499
- const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
573
+ const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/u);
500
574
  const IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
501
575
  const DEFAULT_PHYSICAL_COUNT = 3;
502
576
 
@@ -986,9 +1060,12 @@ const ironList = {
986
1060
  this._physicalIndexForKey = {};
987
1061
  this._firstVisibleIndexVal = null;
988
1062
  this._lastVisibleIndexVal = null;
989
- this._physicalCount = this._physicalCount || 0;
990
- this._physicalItems = this._physicalItems || [];
991
- this._physicalSizes = this._physicalSizes || [];
1063
+ if (!this._physicalItems) {
1064
+ this._physicalItems = [];
1065
+ }
1066
+ if (!this._physicalSizes) {
1067
+ this._physicalSizes = [];
1068
+ }
992
1069
  this._physicalStart = 0;
993
1070
  if (this._scrollTop > this._scrollOffset) {
994
1071
  this._resetScrollPosition(0);
@@ -1097,16 +1174,21 @@ const ironList = {
1097
1174
  * @param {boolean=} forceUpdate If true, updates the height no matter what.
1098
1175
  */
1099
1176
  _updateScrollerSize(forceUpdate) {
1100
- this._estScrollHeight =
1177
+ const estScrollHeight =
1101
1178
  this._physicalBottom +
1102
1179
  Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
1103
1180
 
1104
- forceUpdate = forceUpdate || this._scrollHeight === 0;
1105
- forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
1181
+ this._estScrollHeight = estScrollHeight;
1182
+
1106
1183
  // Amortize height adjustment, so it won't trigger large repaints too often.
1107
- if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._viewportHeight) {
1108
- this.$.items.style.height = `${this._estScrollHeight}px`;
1109
- this._scrollHeight = this._estScrollHeight;
1184
+ if (
1185
+ forceUpdate ||
1186
+ this._scrollHeight === 0 ||
1187
+ this._scrollPosition >= estScrollHeight - this._physicalSize ||
1188
+ Math.abs(estScrollHeight - this._scrollHeight) >= this._viewportHeight
1189
+ ) {
1190
+ this.$.items.style.height = `${estScrollHeight}px`;
1191
+ this._scrollHeight = estScrollHeight;
1110
1192
  }
1111
1193
  },
1112
1194
 
@@ -1202,7 +1284,9 @@ const ironList = {
1202
1284
  },
1203
1285
 
1204
1286
  _debounce(name, cb, asyncModule) {
1205
- this._debouncers = this._debouncers || {};
1287
+ if (!this._debouncers) {
1288
+ this._debouncers = {};
1289
+ }
1206
1290
  this._debouncers[name] = Debouncer.debounce(this._debouncers[name], asyncModule, cb.bind(this));
1207
1291
  enqueueDebouncer(this._debouncers[name]);
1208
1292
  },
@@ -1210,7 +1294,7 @@ const ironList = {
1210
1294
 
1211
1295
  /**
1212
1296
  * @license
1213
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
1297
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1214
1298
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1215
1299
  */
1216
1300
 
@@ -1340,11 +1424,15 @@ class IronListAdapter {
1340
1424
  }
1341
1425
 
1342
1426
  update(startIndex = 0, endIndex = this.size - 1) {
1427
+ const updatedElements = [];
1343
1428
  this.__getVisibleElements().forEach((el) => {
1344
1429
  if (el.__virtualIndex >= startIndex && el.__virtualIndex <= endIndex) {
1345
1430
  this.__updateElement(el, el.__virtualIndex, true);
1431
+ updatedElements.push(el);
1346
1432
  }
1347
1433
  });
1434
+
1435
+ this.__afterElementsUpdated(updatedElements);
1348
1436
  }
1349
1437
 
1350
1438
  /**
@@ -1407,28 +1495,40 @@ class IronListAdapter {
1407
1495
  this.updateElement(el, index);
1408
1496
  el.__lastUpdatedIndex = index;
1409
1497
  }
1498
+ }
1410
1499
 
1411
- const elementHeight = el.offsetHeight;
1412
- if (elementHeight === 0) {
1413
- // If the elements have 0 height after update (for example due to lazy rendering),
1414
- // it results in iron-list requesting to create an unlimited count of elements.
1415
- // Assign a temporary placeholder sizing to elements that would otherwise end up having
1416
- // no height.
1417
- el.style.paddingTop = `${this.__placeholderHeight}px`;
1418
-
1419
- // Manually schedule the resize handler to make sure the placeholder padding is
1420
- // cleared in case the resize observer never triggers.
1421
- requestAnimationFrame(() => this._resizeHandler());
1422
- } else {
1423
- // Add element height to the queue
1424
- this.__elementHeightQueue.push(elementHeight);
1425
- this.__elementHeightQueue.shift();
1426
-
1427
- // Calcualte new placeholder height based on the average of the defined values in the
1428
- // element height queue
1429
- const filteredHeights = this.__elementHeightQueue.filter((h) => h !== undefined);
1430
- this.__placeholderHeight = Math.round(filteredHeights.reduce((a, b) => a + b, 0) / filteredHeights.length);
1431
- }
1500
+ /**
1501
+ * Called synchronously right after elements have been updated.
1502
+ * This is a good place to do any post-update work.
1503
+ *
1504
+ * @param {!Array<!HTMLElement>} updatedElements
1505
+ */
1506
+ __afterElementsUpdated(updatedElements) {
1507
+ updatedElements.forEach((el) => {
1508
+ const elementHeight = el.offsetHeight;
1509
+ if (elementHeight === 0) {
1510
+ // If the elements have 0 height after update (for example due to lazy rendering),
1511
+ // it results in iron-list requesting to create an unlimited count of elements.
1512
+ // Assign a temporary placeholder sizing to elements that would otherwise end up having
1513
+ // no height.
1514
+ el.style.paddingTop = `${this.__placeholderHeight}px`;
1515
+
1516
+ // Manually schedule the resize handler to make sure the placeholder padding is
1517
+ // cleared in case the resize observer never triggers.
1518
+ this.__placeholderClearDebouncer = Debouncer.debounce(this.__placeholderClearDebouncer, animationFrame, () =>
1519
+ this._resizeHandler(),
1520
+ );
1521
+ } else {
1522
+ // Add element height to the queue
1523
+ this.__elementHeightQueue.push(elementHeight);
1524
+ this.__elementHeightQueue.shift();
1525
+
1526
+ // Calculate new placeholder height based on the average of the defined values in the
1527
+ // element height queue
1528
+ const filteredHeights = this.__elementHeightQueue.filter((h) => h !== undefined);
1529
+ this.__placeholderHeight = Math.round(filteredHeights.reduce((a, b) => a + b, 0) / filteredHeights.length);
1530
+ }
1531
+ });
1432
1532
  }
1433
1533
 
1434
1534
  __getIndexScrollOffset(index) {
@@ -1453,42 +1553,37 @@ class IronListAdapter {
1453
1553
  this._debouncers._increasePoolIfNeeded.cancel();
1454
1554
  }
1455
1555
 
1456
- // Prevent element update while the scroll position is being restored
1457
- this.__preventElementUpdates = true;
1458
-
1459
- // Record the scroll position before changing the size
1460
- let fvi; // First visible index
1461
- let fviOffsetBefore; // Scroll offset of the first visible index
1462
- if (size > 0) {
1463
- fvi = this.adjustedFirstVisibleIndex;
1464
- fviOffsetBefore = this.__getIndexScrollOffset(fvi);
1465
- }
1466
-
1467
1556
  // Change the size
1468
1557
  this.__size = size;
1469
1558
 
1470
- this._itemsChanged({
1471
- path: 'items',
1472
- });
1473
- flush();
1474
-
1475
- // Try to restore the scroll position if the new size is larger than 0
1476
- if (size > 0) {
1477
- fvi = Math.min(fvi, size - 1);
1478
- this.scrollToIndex(fvi);
1559
+ if (!this._physicalItems) {
1560
+ // Not initialized yet
1561
+ this._itemsChanged({
1562
+ path: 'items',
1563
+ });
1564
+ this.__preventElementUpdates = true;
1565
+ flush();
1566
+ this.__preventElementUpdates = false;
1567
+ } else {
1568
+ // Already initialized, just update _virtualCount
1569
+ this._updateScrollerSize();
1570
+ this._virtualCount = this.items.length;
1571
+ this._render();
1572
+ }
1479
1573
 
1480
- const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
1481
- if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
1482
- this._scrollTop += fviOffsetBefore - fviOffsetAfter;
1483
- }
1574
+ // When reducing size while invisible, iron-list does not update items, so
1575
+ // their hidden state is not updated and their __lastUpdatedIndex is not
1576
+ // reset. In that case force an update here.
1577
+ if (!this._isVisible) {
1578
+ this._assignModels();
1484
1579
  }
1485
1580
 
1486
1581
  if (!this.elementsContainer.children.length) {
1487
1582
  requestAnimationFrame(() => this._resizeHandler());
1488
1583
  }
1489
1584
 
1490
- this.__preventElementUpdates = false;
1491
- // Schedule and flush a resize handler
1585
+ // Schedule and flush a resize handler. This will cause a
1586
+ // re-render for the elements.
1492
1587
  this._resizeHandler();
1493
1588
  flush();
1494
1589
  }
@@ -1553,16 +1648,20 @@ class IronListAdapter {
1553
1648
 
1554
1649
  /** @private */
1555
1650
  _assignModels(itemSet) {
1651
+ const updatedElements = [];
1556
1652
  this._iterateItems((pidx, vidx) => {
1557
1653
  const el = this._physicalItems[pidx];
1558
1654
  el.hidden = vidx >= this.size;
1559
1655
  if (!el.hidden) {
1560
1656
  el.__virtualIndex = vidx + (this._vidxOffset || 0);
1561
1657
  this.__updateElement(el, el.__virtualIndex);
1658
+ updatedElements.push(el);
1562
1659
  } else {
1563
1660
  delete el.__lastUpdatedIndex;
1564
1661
  }
1565
1662
  }, itemSet);
1663
+
1664
+ this.__afterElementsUpdated(updatedElements);
1566
1665
  }
1567
1666
 
1568
1667
  /** @private */
@@ -1691,7 +1790,9 @@ class IronListAdapter {
1691
1790
  deltaY *= this._scrollPageHeight;
1692
1791
  }
1693
1792
 
1694
- this._deltaYAcc = this._deltaYAcc || 0;
1793
+ if (!this._deltaYAcc) {
1794
+ this._deltaYAcc = 0;
1795
+ }
1695
1796
 
1696
1797
  if (this._wheelAnimationFrame) {
1697
1798
  // Accumulate wheel delta while a frame is being processed
@@ -1764,6 +1865,29 @@ class IronListAdapter {
1764
1865
  );
1765
1866
  }
1766
1867
 
1868
+ /**
1869
+ * Increases the pool size.
1870
+ * @override
1871
+ */
1872
+ _increasePoolIfNeeded(count) {
1873
+ if (this._physicalCount > 2 && count) {
1874
+ // The iron-list logic has already created some physical items and
1875
+ // has decided to create more. Since each item creation round is
1876
+ // expensive, let's try to create the remaining items in one go.
1877
+
1878
+ // Calculate the total item count that would be needed to fill the viewport
1879
+ // plus the buffer assuming rest of the items to be of the average size
1880
+ // of the items already created.
1881
+ const totalItemCount = Math.ceil(this._optPhysicalSize / this._physicalAverage);
1882
+ const missingItemCount = totalItemCount - this._physicalCount;
1883
+ // Create the remaining items in one go. Use a maximum of 100 items
1884
+ // as a safety measure.
1885
+ super._increasePoolIfNeeded(Math.max(count, Math.min(100, missingItemCount)));
1886
+ } else {
1887
+ super._increasePoolIfNeeded(count);
1888
+ }
1889
+ }
1890
+
1767
1891
  /**
1768
1892
  * @returns {Number|undefined} - The browser's default font-size in pixels
1769
1893
  * @private
@@ -1893,6 +2017,24 @@ class Virtualizer {
1893
2017
  this.__adapter = new IronListAdapter(config);
1894
2018
  }
1895
2019
 
2020
+ /**
2021
+ * Gets the index of the first visible item in the viewport.
2022
+ *
2023
+ * @return {number}
2024
+ */
2025
+ get firstVisibleIndex() {
2026
+ return this.__adapter.adjustedFirstVisibleIndex;
2027
+ }
2028
+
2029
+ /**
2030
+ * Gets the index of the last visible item in the viewport.
2031
+ *
2032
+ * @return {number}
2033
+ */
2034
+ get lastVisibleIndex() {
2035
+ return this.__adapter.adjustedLastVisibleIndex;
2036
+ }
2037
+
1896
2038
  /**
1897
2039
  * The size of the virtualizer
1898
2040
  * @return {number | undefined} The size of the virtualizer
@@ -1940,29 +2082,11 @@ class Virtualizer {
1940
2082
  flush() {
1941
2083
  this.__adapter.flush();
1942
2084
  }
1943
-
1944
- /**
1945
- * Gets the index of the first visible item in the viewport.
1946
- *
1947
- * @return {number}
1948
- */
1949
- get firstVisibleIndex() {
1950
- return this.__adapter.adjustedFirstVisibleIndex;
1951
- }
1952
-
1953
- /**
1954
- * Gets the index of the last visible item in the viewport.
1955
- *
1956
- * @return {number}
1957
- */
1958
- get lastVisibleIndex() {
1959
- return this.__adapter.adjustedLastVisibleIndex;
1960
- }
1961
2085
  }
1962
2086
 
1963
2087
  /**
1964
2088
  * @license
1965
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2089
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1966
2090
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1967
2091
  */
1968
2092
 
@@ -1979,394 +2103,454 @@ const ComboBoxPlaceholder = class ComboBoxPlaceholder {
1979
2103
 
1980
2104
  /**
1981
2105
  * @license
1982
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2106
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1983
2107
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1984
2108
  */
1985
2109
 
1986
2110
  /**
1987
- * Element for internal use only.
1988
- *
1989
- * @extends HTMLElement
1990
- * @private
2111
+ * @polymerMixin
1991
2112
  */
1992
- class ComboBoxScroller extends PolymerElement {
1993
- static get is() {
1994
- return 'vaadin-combo-box-scroller';
1995
- }
2113
+ const ComboBoxScrollerMixin = (superClass) =>
2114
+ class ComboBoxScrollerMixin extends superClass {
2115
+ static get properties() {
2116
+ return {
2117
+ /**
2118
+ * A full set of items to filter the visible options from.
2119
+ * Set to an empty array when combo-box is not opened.
2120
+ */
2121
+ items: {
2122
+ type: Array,
2123
+ observer: '__itemsChanged',
2124
+ },
1996
2125
 
1997
- static get template() {
1998
- return html`
1999
- <style>
2000
- :host {
2001
- display: block;
2002
- min-height: 1px;
2003
- overflow: auto;
2126
+ /**
2127
+ * Index of an item that has focus outline and is scrolled into view.
2128
+ * The actual focus still remains in the input field.
2129
+ */
2130
+ focusedIndex: {
2131
+ type: Number,
2132
+ observer: '__focusedIndexChanged',
2133
+ },
2004
2134
 
2005
- /* Fixes item background from getting on top of scrollbars on Safari */
2006
- transform: translate3d(0, 0, 0);
2135
+ /**
2136
+ * Set to true while combo-box fetches new page from the data provider.
2137
+ */
2138
+ loading: {
2139
+ type: Boolean,
2140
+ observer: '__loadingChanged',
2141
+ },
2007
2142
 
2008
- /* Enable momentum scrolling on iOS */
2009
- -webkit-overflow-scrolling: touch;
2143
+ /**
2144
+ * Whether the combo-box is currently opened or not. If set to false,
2145
+ * calling `scrollIntoView` does not have any effect.
2146
+ */
2147
+ opened: {
2148
+ type: Boolean,
2149
+ observer: '__openedChanged',
2150
+ },
2010
2151
 
2011
- /* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
2012
- box-shadow: 0 0 0 white;
2013
- }
2152
+ /**
2153
+ * The selected item from the `items` array.
2154
+ */
2155
+ selectedItem: {
2156
+ type: Object,
2157
+ observer: '__selectedItemChanged',
2158
+ },
2014
2159
 
2015
- #selector {
2016
- border-width: var(--_vaadin-combo-box-items-container-border-width);
2017
- border-style: var(--_vaadin-combo-box-items-container-border-style);
2018
- border-color: var(--_vaadin-combo-box-items-container-border-color);
2019
- position: relative;
2020
- }
2021
- </style>
2022
- <div id="selector">
2023
- <slot></slot>
2024
- </div>
2025
- `;
2026
- }
2160
+ /**
2161
+ * Path for the id of the item, used to detect whether the item is selected.
2162
+ */
2163
+ itemIdPath: {
2164
+ type: String,
2165
+ },
2027
2166
 
2028
- static get properties() {
2029
- return {
2030
- /**
2031
- * A full set of items to filter the visible options from.
2032
- * Set to an empty array when combo-box is not opened.
2033
- */
2034
- items: {
2035
- type: Array,
2036
- observer: '__itemsChanged',
2037
- },
2167
+ /**
2168
+ * Reference to the owner (combo-box owner), used by the item elements.
2169
+ */
2170
+ owner: {
2171
+ type: Object,
2172
+ },
2038
2173
 
2039
- /**
2040
- * Index of an item that has focus outline and is scrolled into view.
2041
- * The actual focus still remains in the input field.
2042
- */
2043
- focusedIndex: {
2044
- type: Number,
2045
- observer: '__focusedIndexChanged',
2046
- },
2174
+ /**
2175
+ * Function used to set a label for every combo-box item.
2176
+ */
2177
+ getItemLabel: {
2178
+ type: Object,
2179
+ },
2047
2180
 
2048
- /**
2049
- * Set to true while combo-box fetches new page from the data provider.
2050
- */
2051
- loading: {
2052
- type: Boolean,
2053
- observer: '__loadingChanged',
2054
- },
2181
+ /**
2182
+ * Function used to render the content of every combo-box item.
2183
+ */
2184
+ renderer: {
2185
+ type: Object,
2186
+ observer: '__rendererChanged',
2187
+ },
2055
2188
 
2056
- /**
2057
- * Whether the combo-box is currently opened or not. If set to false,
2058
- * calling `scrollIntoView` does not have any effect.
2059
- */
2060
- opened: {
2061
- type: Boolean,
2062
- observer: '__openedChanged',
2063
- },
2189
+ /**
2190
+ * Used to propagate the `theme` attribute from the host element.
2191
+ */
2192
+ theme: {
2193
+ type: String,
2194
+ },
2195
+ };
2196
+ }
2064
2197
 
2065
- /**
2066
- * The selected item from the `items` array.
2067
- */
2068
- selectedItem: {
2069
- type: Object,
2070
- observer: '__selectedItemChanged',
2071
- },
2198
+ constructor() {
2199
+ super();
2200
+ this.__boundOnItemClick = this.__onItemClick.bind(this);
2201
+ }
2072
2202
 
2073
- /**
2074
- * Path for the id of the item, used to detect whether the item is selected.
2075
- */
2076
- itemIdPath: {
2077
- type: String,
2078
- },
2203
+ /** @private */
2204
+ get _viewportTotalPaddingBottom() {
2205
+ if (this._cachedViewportTotalPaddingBottom === undefined) {
2206
+ const itemsStyle = window.getComputedStyle(this.$.selector);
2207
+ this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
2208
+ .map((v) => {
2209
+ return parseInt(v, 10);
2210
+ })
2211
+ .reduce((sum, v) => {
2212
+ return sum + v;
2213
+ });
2214
+ }
2079
2215
 
2080
- /**
2081
- * Reference to the combo-box, used by the item elements.
2082
- */
2083
- comboBox: {
2084
- type: Object,
2085
- },
2216
+ return this._cachedViewportTotalPaddingBottom;
2217
+ }
2086
2218
 
2087
- /**
2088
- * Function used to set a label for every combo-box item.
2089
- */
2090
- getItemLabel: {
2091
- type: Object,
2092
- },
2219
+ /** @protected */
2220
+ ready() {
2221
+ super.ready();
2093
2222
 
2094
- /**
2095
- * Function used to render the content of every combo-box item.
2096
- */
2097
- renderer: {
2098
- type: Object,
2099
- observer: '__rendererChanged',
2100
- },
2223
+ this.setAttribute('role', 'listbox');
2101
2224
 
2102
- /**
2103
- * Used to propagate the `theme` attribute from the host element.
2104
- */
2105
- theme: {
2106
- type: String,
2107
- },
2108
- };
2109
- }
2225
+ // Ensure every instance has unique ID
2226
+ this.id = `${this.localName}-${generateUniqueId()}`;
2110
2227
 
2111
- constructor() {
2112
- super();
2113
- this.__boundOnItemClick = this.__onItemClick.bind(this);
2114
- }
2228
+ // Allow extensions to customize tag name for the items
2229
+ this.__hostTagName = this.constructor.is.replace('-scroller', '');
2115
2230
 
2116
- __openedChanged(opened) {
2117
- if (opened) {
2118
- this.requestContentUpdate();
2231
+ this.addEventListener('click', (e) => e.stopPropagation());
2232
+
2233
+ this.__patchWheelOverScrolling();
2234
+
2235
+ this.__virtualizer = new Virtualizer({
2236
+ createElements: this.__createElements.bind(this),
2237
+ updateElement: this._updateElement.bind(this),
2238
+ elementsContainer: this,
2239
+ scrollTarget: this,
2240
+ scrollContainer: this.$.selector,
2241
+ });
2119
2242
  }
2120
- }
2121
2243
 
2122
- /** @protected */
2123
- ready() {
2124
- super.ready();
2244
+ /**
2245
+ * Requests an update for the virtualizer to re-render items.
2246
+ */
2247
+ requestContentUpdate() {
2248
+ if (this.__virtualizer) {
2249
+ this.__virtualizer.update();
2250
+ }
2251
+ }
2125
2252
 
2126
- // Ensure every instance has unique ID
2127
- this.id = `${this.localName}-${generateUniqueId()}`;
2253
+ /**
2254
+ * Scrolls an item at given index into view and adjusts `scrollTop`
2255
+ * so that the element gets fully visible on Arrow Down key press.
2256
+ * @param {number} index
2257
+ */
2258
+ scrollIntoView(index) {
2259
+ if (!(this.opened && index >= 0)) {
2260
+ return;
2261
+ }
2128
2262
 
2129
- // Allow extensions to customize tag name for the items
2130
- this.__hostTagName = this.constructor.is.replace('-scroller', '');
2263
+ const visibleItemsCount = this._visibleItemsCount();
2131
2264
 
2132
- this.setAttribute('role', 'listbox');
2265
+ let targetIndex = index;
2133
2266
 
2134
- this.addEventListener('click', (e) => e.stopPropagation());
2267
+ if (index > this.__virtualizer.lastVisibleIndex - 1) {
2268
+ // Index is below the bottom, scrolling down. Make the item appear at the bottom.
2269
+ // First scroll to target (will be at the top of the scroller) to make sure it's rendered.
2270
+ this.__virtualizer.scrollToIndex(index);
2271
+ // Then calculate the index for the following scroll (to get the target to bottom of the scroller).
2272
+ targetIndex = index - visibleItemsCount + 1;
2273
+ } else if (index > this.__virtualizer.firstVisibleIndex) {
2274
+ // The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
2275
+ // the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
2276
+ targetIndex = this.__virtualizer.firstVisibleIndex;
2277
+ }
2278
+ this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
2279
+
2280
+ // Sometimes the item is partly below the bottom edge, detect and adjust.
2281
+ const lastPhysicalItem = [...this.children].find(
2282
+ (el) => !el.hidden && el.index === this.__virtualizer.lastVisibleIndex,
2283
+ );
2284
+ if (!lastPhysicalItem || index !== lastPhysicalItem.index) {
2285
+ return;
2286
+ }
2287
+ const lastPhysicalItemRect = lastPhysicalItem.getBoundingClientRect();
2288
+ const scrollerRect = this.getBoundingClientRect();
2289
+ const scrollTopAdjust = lastPhysicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
2290
+ if (scrollTopAdjust > 0) {
2291
+ this.scrollTop += scrollTopAdjust;
2292
+ }
2293
+ }
2135
2294
 
2136
- this.__patchWheelOverScrolling();
2295
+ /**
2296
+ * @param {string | object} item
2297
+ * @param {string | object} selectedItem
2298
+ * @param {string} itemIdPath
2299
+ * @protected
2300
+ */
2301
+ _isItemSelected(item, selectedItem, itemIdPath) {
2302
+ if (item instanceof ComboBoxPlaceholder) {
2303
+ return false;
2304
+ } else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
2305
+ return get(itemIdPath, item) === get(itemIdPath, selectedItem);
2306
+ }
2307
+ return item === selectedItem;
2308
+ }
2137
2309
 
2138
- this.__virtualizer = new Virtualizer({
2139
- createElements: this.__createElements.bind(this),
2140
- updateElement: this.__updateElement.bind(this),
2141
- elementsContainer: this,
2142
- scrollTarget: this,
2143
- scrollContainer: this.$.selector,
2144
- });
2145
- }
2310
+ /** @private */
2311
+ __itemsChanged(items) {
2312
+ if (this.__virtualizer && items) {
2313
+ this.__virtualizer.size = items.length;
2314
+ this.__virtualizer.flush();
2315
+ this.requestContentUpdate();
2316
+ }
2317
+ }
2146
2318
 
2147
- requestContentUpdate() {
2148
- if (this.__virtualizer) {
2149
- this.__virtualizer.update();
2319
+ /** @private */
2320
+ __loadingChanged() {
2321
+ this.requestContentUpdate();
2150
2322
  }
2151
- }
2152
2323
 
2153
- scrollIntoView(index) {
2154
- if (!(this.opened && index >= 0)) {
2155
- return;
2324
+ /** @private */
2325
+ __openedChanged(opened) {
2326
+ if (opened) {
2327
+ this.requestContentUpdate();
2328
+ }
2156
2329
  }
2157
2330
 
2158
- const visibleItemsCount = this._visibleItemsCount();
2331
+ /** @private */
2332
+ __selectedItemChanged() {
2333
+ this.requestContentUpdate();
2334
+ }
2159
2335
 
2160
- let targetIndex = index;
2336
+ /** @private */
2337
+ __focusedIndexChanged(index, oldIndex) {
2338
+ if (index !== oldIndex) {
2339
+ this.requestContentUpdate();
2340
+ }
2161
2341
 
2162
- if (index > this.__virtualizer.lastVisibleIndex - 1) {
2163
- // Index is below the bottom, scrolling down. Make the item appear at the bottom.
2164
- // First scroll to target (will be at the top of the scroller) to make sure it's rendered.
2165
- this.__virtualizer.scrollToIndex(index);
2166
- // Then calculate the index for the following scroll (to get the target to bottom of the scroller).
2167
- targetIndex = index - visibleItemsCount + 1;
2168
- } else if (index > this.__virtualizer.firstVisibleIndex) {
2169
- // The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
2170
- // the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
2171
- targetIndex = this.__virtualizer.firstVisibleIndex;
2342
+ // Do not jump back to the previously focused item while loading
2343
+ // when requesting next page from the data provider on scroll.
2344
+ if (index >= 0 && !this.loading) {
2345
+ this.scrollIntoView(index);
2346
+ }
2172
2347
  }
2173
- this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
2174
2348
 
2175
- // Sometimes the item is partly below the bottom edge, detect and adjust.
2176
- const lastPhysicalItem = [...this.children].find(
2177
- (el) => !el.hidden && el.index === this.__virtualizer.lastVisibleIndex,
2178
- );
2179
- if (!lastPhysicalItem || index !== lastPhysicalItem.index) {
2180
- return;
2349
+ /** @private */
2350
+ __rendererChanged(renderer, oldRenderer) {
2351
+ if (renderer || oldRenderer) {
2352
+ this.requestContentUpdate();
2353
+ }
2181
2354
  }
2182
- const lastPhysicalItemRect = lastPhysicalItem.getBoundingClientRect();
2183
- const scrollerRect = this.getBoundingClientRect();
2184
- const scrollTopAdjust = lastPhysicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
2185
- if (scrollTopAdjust > 0) {
2186
- this.scrollTop += scrollTopAdjust;
2355
+
2356
+ /** @private */
2357
+ __createElements(count) {
2358
+ return [...Array(count)].map(() => {
2359
+ const item = document.createElement(`${this.__hostTagName}-item`);
2360
+ item.addEventListener('click', this.__boundOnItemClick);
2361
+ // Negative tabindex prevents the item content from being focused.
2362
+ item.tabIndex = '-1';
2363
+ item.style.width = '100%';
2364
+ return item;
2365
+ });
2187
2366
  }
2188
- }
2189
2367
 
2190
- /** @private */
2191
- __getAriaRole(itemIndex) {
2192
- return itemIndex !== undefined ? 'option' : false;
2193
- }
2368
+ /**
2369
+ * @param {HTMLElement} el
2370
+ * @param {number} index
2371
+ * @protected
2372
+ */
2373
+ _updateElement(el, index) {
2374
+ const item = this.items[index];
2375
+ const focusedIndex = this.focusedIndex;
2376
+ const selected = this._isItemSelected(item, this.selectedItem, this.itemIdPath);
2377
+
2378
+ el.setProperties({
2379
+ item,
2380
+ index,
2381
+ label: this.getItemLabel(item),
2382
+ selected,
2383
+ renderer: this.renderer,
2384
+ focused: !this.loading && focusedIndex === index,
2385
+ });
2194
2386
 
2195
- /** @private */
2196
- __isItemFocused(focusedIndex, itemIndex) {
2197
- return !this.loading && focusedIndex === itemIndex;
2198
- }
2387
+ el.id = `${this.__hostTagName}-item-${index}`;
2388
+ el.setAttribute('role', index !== undefined ? 'option' : false);
2389
+ el.setAttribute('aria-selected', selected.toString());
2390
+ el.setAttribute('aria-posinset', index + 1);
2391
+ el.setAttribute('aria-setsize', this.items.length);
2199
2392
 
2200
- /** @protected */
2201
- _isItemSelected(item, selectedItem, itemIdPath) {
2202
- if (item instanceof ComboBoxPlaceholder) {
2203
- return false;
2204
- } else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
2205
- return this.get(itemIdPath, item) === this.get(itemIdPath, selectedItem);
2206
- }
2207
- return item === selectedItem;
2208
- }
2393
+ if (this.theme) {
2394
+ el.setAttribute('theme', this.theme);
2395
+ } else {
2396
+ el.removeAttribute('theme');
2397
+ }
2209
2398
 
2210
- /** @private */
2211
- __itemsChanged(items) {
2212
- if (this.__virtualizer && items) {
2213
- this.__virtualizer.size = items.length;
2214
- this.__virtualizer.flush();
2215
- this.requestContentUpdate();
2399
+ if (item instanceof ComboBoxPlaceholder) {
2400
+ this.__requestItemByIndex(index);
2401
+ }
2216
2402
  }
2217
- }
2218
-
2219
- /** @private */
2220
- __loadingChanged() {
2221
- this.requestContentUpdate();
2222
- }
2223
2403
 
2224
- /** @private */
2225
- __selectedItemChanged() {
2226
- this.requestContentUpdate();
2227
- }
2404
+ /** @private */
2405
+ __onItemClick(e) {
2406
+ this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
2407
+ }
2228
2408
 
2229
- /** @private */
2230
- __focusedIndexChanged(index, oldIndex) {
2231
- if (index !== oldIndex) {
2232
- this.requestContentUpdate();
2409
+ /**
2410
+ * We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
2411
+ * Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
2412
+ * scrolling the parent similarly to touch scrolling.
2413
+ * @private
2414
+ */
2415
+ __patchWheelOverScrolling() {
2416
+ this.$.selector.addEventListener('wheel', (e) => {
2417
+ const scrolledToTop = this.scrollTop === 0;
2418
+ const scrolledToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= 1;
2419
+ if (scrolledToTop && e.deltaY < 0) {
2420
+ e.preventDefault();
2421
+ } else if (scrolledToBottom && e.deltaY > 0) {
2422
+ e.preventDefault();
2423
+ }
2424
+ });
2233
2425
  }
2234
2426
 
2235
- // Do not jump back to the previously focused item while loading
2236
- // when requesting next page from the data provider on scroll.
2237
- if (index >= 0 && !this.loading) {
2238
- this.scrollIntoView(index);
2427
+ /**
2428
+ * Dispatches an `index-requested` event for the given index to notify
2429
+ * the data provider that it should start loading the page containing the requested index.
2430
+ *
2431
+ * The event is dispatched asynchronously to prevent an immediate page request and therefore
2432
+ * a possible infinite recursion in case the data provider implements page request cancelation logic
2433
+ * by invoking data provider page callbacks with an empty array.
2434
+ * The infinite recursion may occur otherwise since invoking a data provider page callback with an empty array
2435
+ * triggers a synchronous scroller update and, if the callback corresponds to the currently visible page,
2436
+ * the scroller will synchronously request the page again which may lead to looping in the end.
2437
+ * That was the case for the Flow counterpart:
2438
+ * https://github.com/vaadin/flow-components/issues/3553#issuecomment-1239344828
2439
+ * @private
2440
+ */
2441
+ __requestItemByIndex(index) {
2442
+ requestAnimationFrame(() => {
2443
+ this.dispatchEvent(
2444
+ new CustomEvent('index-requested', {
2445
+ detail: {
2446
+ index,
2447
+ currentScrollerPos: this._oldScrollerPosition,
2448
+ },
2449
+ }),
2450
+ );
2451
+ });
2239
2452
  }
2240
- }
2241
2453
 
2242
- /** @private */
2243
- __rendererChanged(renderer, oldRenderer) {
2244
- if (renderer || oldRenderer) {
2245
- this.requestContentUpdate();
2454
+ /** @private */
2455
+ _visibleItemsCount() {
2456
+ // Ensure items are positioned
2457
+ this.__virtualizer.scrollToIndex(this.__virtualizer.firstVisibleIndex);
2458
+ const hasItems = this.__virtualizer.size > 0;
2459
+ return hasItems ? this.__virtualizer.lastVisibleIndex - this.__virtualizer.firstVisibleIndex + 1 : 0;
2246
2460
  }
2247
- }
2461
+ };
2248
2462
 
2249
- /** @private */
2250
- __createElements(count) {
2251
- return [...Array(count)].map(() => {
2252
- const item = document.createElement(`${this.__hostTagName}-item`);
2253
- item.addEventListener('click', this.__boundOnItemClick);
2254
- // Negative tabindex prevents the item content from being focused.
2255
- item.tabIndex = '-1';
2256
- item.style.width = '100%';
2257
- return item;
2258
- });
2259
- }
2463
+ /**
2464
+ * @license
2465
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2466
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2467
+ */
2260
2468
 
2261
- /** @private */
2262
- __updateElement(el, index) {
2263
- const item = this.items[index];
2264
- const focusedIndex = this.focusedIndex;
2265
- const selected = this._isItemSelected(item, this.selectedItem, this.itemIdPath);
2266
-
2267
- el.setProperties({
2268
- item,
2269
- index,
2270
- label: this.getItemLabel(item),
2271
- selected,
2272
- renderer: this.renderer,
2273
- focused: this.__isItemFocused(focusedIndex, index),
2274
- });
2469
+ /**
2470
+ * An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
2471
+ *
2472
+ * @customElement
2473
+ * @extends HTMLElement
2474
+ * @mixes ComboBoxScrollerMixin
2475
+ * @private
2476
+ */
2477
+ class ComboBoxScroller extends ComboBoxScrollerMixin(PolymerElement) {
2478
+ static get is() {
2479
+ return 'vaadin-combo-box-scroller';
2480
+ }
2275
2481
 
2276
- el.id = `${this.__hostTagName}-item-${index}`;
2277
- el.setAttribute('role', this.__getAriaRole(index));
2278
- el.setAttribute('aria-selected', selected.toString());
2279
- el.setAttribute('aria-posinset', index + 1);
2280
- el.setAttribute('aria-setsize', this.items.length);
2482
+ static get template() {
2483
+ return html`
2484
+ <style>
2485
+ :host {
2486
+ display: block;
2487
+ min-height: 1px;
2488
+ overflow: auto;
2281
2489
 
2282
- if (this.theme) {
2283
- el.setAttribute('theme', this.theme);
2284
- } else {
2285
- el.removeAttribute('theme');
2286
- }
2490
+ /* Fixes item background from getting on top of scrollbars on Safari */
2491
+ transform: translate3d(0, 0, 0);
2287
2492
 
2288
- if (item instanceof ComboBoxPlaceholder) {
2289
- this.__requestItemByIndex(index);
2290
- }
2291
- }
2493
+ /* Enable momentum scrolling on iOS */
2494
+ -webkit-overflow-scrolling: touch;
2292
2495
 
2293
- /** @private */
2294
- __onItemClick(e) {
2295
- this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
2296
- }
2496
+ /* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
2497
+ box-shadow: 0 0 0 white;
2498
+ }
2297
2499
 
2298
- /**
2299
- * We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
2300
- * Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
2301
- * scrolling the parent similarly to touch scrolling.
2302
- */
2303
- __patchWheelOverScrolling() {
2304
- this.$.selector.addEventListener('wheel', (e) => {
2305
- const scrolledToTop = this.scrollTop === 0;
2306
- const scrolledToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= 1;
2307
- if (scrolledToTop && e.deltaY < 0) {
2308
- e.preventDefault();
2309
- } else if (scrolledToBottom && e.deltaY > 0) {
2310
- e.preventDefault();
2311
- }
2312
- });
2500
+ #selector {
2501
+ border-width: var(--_vaadin-combo-box-items-container-border-width);
2502
+ border-style: var(--_vaadin-combo-box-items-container-border-style);
2503
+ border-color: var(--_vaadin-combo-box-items-container-border-color, transparent);
2504
+ position: relative;
2505
+ }
2506
+ </style>
2507
+ <div id="selector">
2508
+ <slot></slot>
2509
+ </div>
2510
+ `;
2313
2511
  }
2512
+ }
2314
2513
 
2315
- get _viewportTotalPaddingBottom() {
2316
- if (this._cachedViewportTotalPaddingBottom === undefined) {
2317
- const itemsStyle = window.getComputedStyle(this.$.selector);
2318
- this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
2319
- .map((v) => {
2320
- return parseInt(v, 10);
2321
- })
2322
- .reduce((sum, v) => {
2323
- return sum + v;
2324
- });
2325
- }
2514
+ defineCustomElement(ComboBoxScroller);
2326
2515
 
2327
- return this._cachedViewportTotalPaddingBottom;
2328
- }
2516
+ /**
2517
+ * @license
2518
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2519
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2520
+ */
2329
2521
 
2330
- /**
2331
- * Dispatches an `index-requested` event for the given index to notify
2332
- * the data provider that it should start loading the page containing the requested index.
2333
- *
2334
- * The event is dispatched asynchronously to prevent an immediate page request and therefore
2335
- * a possible infinite recursion in case the data provider implements page request cancelation logic
2336
- * by invoking data provider page callbacks with an empty array.
2337
- * The infinite recursion may occur otherwise since invoking a data provider page callback with an empty array
2338
- * triggers a synchronous scroller update and, if the callback corresponds to the currently visible page,
2339
- * the scroller will synchronously request the page again which may lead to looping in the end.
2340
- * That was the case for the Flow counterpart:
2341
- * https://github.com/vaadin/flow-components/issues/3553#issuecomment-1239344828
2342
- */
2343
- __requestItemByIndex(index) {
2344
- requestAnimationFrame(() => {
2345
- this.dispatchEvent(
2346
- new CustomEvent('index-requested', {
2347
- detail: {
2348
- index,
2349
- currentScrollerPos: this._oldScrollerPosition,
2350
- },
2351
- }),
2352
- );
2353
- });
2354
- }
2522
+ /**
2523
+ * A mixin to provide `pattern` property.
2524
+ *
2525
+ * @polymerMixin
2526
+ * @mixes InputConstraintsMixin
2527
+ */
2528
+ const PatternMixin = (superclass) =>
2529
+ class PatternMixinClass extends InputConstraintsMixin(superclass) {
2530
+ static get properties() {
2531
+ return {
2532
+ /**
2533
+ * A regular expression that the value is checked against.
2534
+ * The pattern must match the entire value, not just some subset.
2535
+ */
2536
+ pattern: {
2537
+ type: String,
2538
+ },
2539
+ };
2540
+ }
2355
2541
 
2356
- /** @private */
2357
- _visibleItemsCount() {
2358
- // Ensure items are positioned
2359
- this.__virtualizer.scrollToIndex(this.__virtualizer.firstVisibleIndex);
2360
- const hasItems = this.__virtualizer.size > 0;
2361
- return hasItems ? this.__virtualizer.lastVisibleIndex - this.__virtualizer.firstVisibleIndex + 1 : 0;
2362
- }
2363
- }
2542
+ static get delegateAttrs() {
2543
+ return [...super.delegateAttrs, 'pattern'];
2544
+ }
2364
2545
 
2365
- customElements.define(ComboBoxScroller.is, ComboBoxScroller);
2546
+ static get constraints() {
2547
+ return [...super.constraints, 'pattern'];
2548
+ }
2549
+ };
2366
2550
 
2367
2551
  /**
2368
2552
  * @license
2369
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2553
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2370
2554
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2371
2555
  */
2372
2556
 
@@ -2676,20 +2860,18 @@ const ComboBoxDataProviderMixin = (superClass) =>
2676
2860
  _flushPendingRequests(size) {
2677
2861
  if (this._pendingRequests) {
2678
2862
  const lastPage = Math.ceil(size / this.pageSize);
2679
- const pendingRequestsKeys = Object.keys(this._pendingRequests);
2680
- for (let reqIdx = 0; reqIdx < pendingRequestsKeys.length; reqIdx++) {
2681
- const page = parseInt(pendingRequestsKeys[reqIdx]);
2682
- if (page >= lastPage) {
2683
- this._pendingRequests[page]([], size);
2863
+ Object.entries(this._pendingRequests).forEach(([page, callback]) => {
2864
+ if (parseInt(page) >= lastPage) {
2865
+ callback([], size);
2684
2866
  }
2685
- }
2867
+ });
2686
2868
  }
2687
2869
  }
2688
2870
  };
2689
2871
 
2690
2872
  /**
2691
2873
  * @license
2692
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
2874
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2693
2875
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2694
2876
  */
2695
2877
 
@@ -2714,7 +2896,7 @@ function processTemplates(component) {
2714
2896
 
2715
2897
  /**
2716
2898
  * @license
2717
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2899
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2718
2900
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2719
2901
  */
2720
2902
 
@@ -2748,10 +2930,19 @@ function findItemIndex(items, callback) {
2748
2930
 
2749
2931
  /**
2750
2932
  * @polymerMixin
2933
+ * @mixes ControllerMixin
2934
+ * @mixes ValidateMixin
2935
+ * @mixes DisabledMixin
2936
+ * @mixes InputMixin
2937
+ * @mixes KeyboardMixin
2938
+ * @mixes FocusMixin
2939
+ * @mixes OverlayClassMixin
2751
2940
  * @param {function(new:HTMLElement)} subclass
2752
2941
  */
2753
2942
  const ComboBoxMixin = (subclass) =>
2754
- class VaadinComboBoxMixinElement extends ControllerMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))) {
2943
+ class ComboBoxMixinClass extends OverlayClassMixin(
2944
+ ControllerMixin(ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))))),
2945
+ ) {
2755
2946
  static get properties() {
2756
2947
  return {
2757
2948
  /**
@@ -2926,6 +3117,14 @@ const ComboBoxMixin = (subclass) =>
2926
3117
  observer: '_toggleElementChanged',
2927
3118
  },
2928
3119
 
3120
+ /**
3121
+ * Set of items to be rendered in the dropdown.
3122
+ * @protected
3123
+ */
3124
+ _dropdownItems: {
3125
+ type: Array,
3126
+ },
3127
+
2929
3128
  /** @private */
2930
3129
  _closeOnBlurIsPrevented: Boolean,
2931
3130
 
@@ -2943,14 +3142,13 @@ const ComboBoxMixin = (subclass) =>
2943
3142
  static get observers() {
2944
3143
  return [
2945
3144
  '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
2946
- '_openedOrItemsChanged(opened, filteredItems, loading)',
2947
- '_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
3145
+ '_openedOrItemsChanged(opened, _dropdownItems, loading)',
3146
+ '_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
2948
3147
  ];
2949
3148
  }
2950
3149
 
2951
3150
  constructor() {
2952
3151
  super();
2953
- this._boundOnFocusout = this._onFocusout.bind(this);
2954
3152
  this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
2955
3153
  this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
2956
3154
  this._boundOnClick = this._onClick.bind(this);
@@ -2967,24 +3165,6 @@ const ComboBoxMixin = (subclass) =>
2967
3165
  return 'vaadin-combo-box';
2968
3166
  }
2969
3167
 
2970
- /**
2971
- * @return {string | undefined}
2972
- * @protected
2973
- */
2974
- get _inputElementValue() {
2975
- return this.inputElement ? this.inputElement[this._propertyForValue] : undefined;
2976
- }
2977
-
2978
- /**
2979
- * @param {string} value
2980
- * @protected
2981
- */
2982
- set _inputElementValue(value) {
2983
- if (this.inputElement) {
2984
- this.inputElement[this._propertyForValue] = value;
2985
- }
2986
- }
2987
-
2988
3168
  /**
2989
3169
  * Get a reference to the native `<input>` element.
2990
3170
  * Override to provide a custom input.
@@ -3035,8 +3215,6 @@ const ComboBoxMixin = (subclass) =>
3035
3215
  this._initOverlay();
3036
3216
  this._initScroller();
3037
3217
 
3038
- this.addEventListener('focusout', this._boundOnFocusout);
3039
-
3040
3218
  this._lastCommittedValue = this.value;
3041
3219
 
3042
3220
  this.addEventListener('click', this._boundOnClick);
@@ -3044,7 +3222,7 @@ const ComboBoxMixin = (subclass) =>
3044
3222
 
3045
3223
  const bringToFrontListener = () => {
3046
3224
  requestAnimationFrame(() => {
3047
- this.$.overlay.bringToFront();
3225
+ this._overlayElement.bringToFront();
3048
3226
  });
3049
3227
  };
3050
3228
 
@@ -3135,6 +3313,8 @@ const ComboBoxMixin = (subclass) =>
3135
3313
  overlay.addEventListener('opened-changed', (e) => {
3136
3314
  this._overlayOpened = e.detail.value;
3137
3315
  });
3316
+
3317
+ this._overlayElement = overlay;
3138
3318
  }
3139
3319
 
3140
3320
  /**
@@ -3146,7 +3326,7 @@ const ComboBoxMixin = (subclass) =>
3146
3326
  _initScroller(host) {
3147
3327
  const scrollerTag = `${this._tagNamePrefix}-scroller`;
3148
3328
 
3149
- const overlay = this.$.overlay;
3329
+ const overlay = this._overlayElement;
3150
3330
 
3151
3331
  overlay.renderer = (root) => {
3152
3332
  if (!root.firstChild) {
@@ -3159,7 +3339,7 @@ const ComboBoxMixin = (subclass) =>
3159
3339
 
3160
3340
  const scroller = overlay.querySelector(scrollerTag);
3161
3341
 
3162
- scroller.comboBox = host || this;
3342
+ scroller.owner = host || this;
3163
3343
  scroller.getItemLabel = this._getItemLabel.bind(this);
3164
3344
  scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
3165
3345
 
@@ -3202,7 +3382,7 @@ const ComboBoxMixin = (subclass) =>
3202
3382
  this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
3203
3383
 
3204
3384
  this._onOpened();
3205
- } else if (wasOpened && this.filteredItems && this.filteredItems.length) {
3385
+ } else if (wasOpened && this._dropdownItems && this._dropdownItems.length) {
3206
3386
  this.close();
3207
3387
 
3208
3388
  this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
@@ -3254,7 +3434,7 @@ const ComboBoxMixin = (subclass) =>
3254
3434
  }
3255
3435
  }
3256
3436
 
3257
- this.$.overlay.restoreFocusOnClose = true;
3437
+ this._overlayElement.restoreFocusOnClose = true;
3258
3438
  } else {
3259
3439
  this._onClosed();
3260
3440
  if (this._openedWithFocusRing && this._isInputFocused()) {
@@ -3288,13 +3468,19 @@ const ComboBoxMixin = (subclass) =>
3288
3468
  return event.composedPath()[0] === this.clearElement;
3289
3469
  }
3290
3470
 
3471
+ /** @private */
3472
+ __onClearButtonMouseDown(event) {
3473
+ event.preventDefault(); // Prevent native focusout event
3474
+ this.inputElement.focus();
3475
+ }
3476
+
3291
3477
  /**
3292
3478
  * @param {Event} event
3293
3479
  * @protected
3294
3480
  */
3295
- _handleClearButtonClick(event) {
3481
+ _onClearButtonClick(event) {
3296
3482
  event.preventDefault();
3297
- this._clear();
3483
+ this._onClearAction();
3298
3484
 
3299
3485
  // De-select dropdown item
3300
3486
  if (this.opened) {
@@ -3330,15 +3516,13 @@ const ComboBoxMixin = (subclass) =>
3330
3516
  }
3331
3517
 
3332
3518
  /** @private */
3333
- _onClick(e) {
3334
- const path = e.composedPath();
3335
-
3336
- if (this._isClearButton(e)) {
3337
- this._handleClearButtonClick(e);
3338
- } else if (path.indexOf(this._toggleElement) > -1) {
3339
- this._onToggleButtonClick(e);
3519
+ _onClick(event) {
3520
+ if (this._isClearButton(event)) {
3521
+ this._onClearButtonClick(event);
3522
+ } else if (event.composedPath().includes(this._toggleElement)) {
3523
+ this._onToggleButtonClick(event);
3340
3524
  } else {
3341
- this._onHostClick(e);
3525
+ this._onHostClick(event);
3342
3526
  }
3343
3527
  }
3344
3528
 
@@ -3353,7 +3537,7 @@ const ComboBoxMixin = (subclass) =>
3353
3537
  super._onKeyDown(e);
3354
3538
 
3355
3539
  if (e.key === 'Tab') {
3356
- this.$.overlay.restoreFocusOnClose = false;
3540
+ this._overlayElement.restoreFocusOnClose = false;
3357
3541
  } else if (e.key === 'ArrowDown') {
3358
3542
  this._onArrowDown();
3359
3543
 
@@ -3369,7 +3553,7 @@ const ComboBoxMixin = (subclass) =>
3369
3553
 
3370
3554
  /** @private */
3371
3555
  _getItemLabel(item) {
3372
- let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
3556
+ let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
3373
3557
  if (label === undefined || label === null) {
3374
3558
  label = item ? item.toString() : '';
3375
3559
  }
@@ -3378,7 +3562,7 @@ const ComboBoxMixin = (subclass) =>
3378
3562
 
3379
3563
  /** @private */
3380
3564
  _getItemValue(item) {
3381
- let value = item && this.itemValuePath ? this.get(this.itemValuePath, item) : undefined;
3565
+ let value = item && this.itemValuePath ? get(this.itemValuePath, item) : undefined;
3382
3566
  if (value === undefined) {
3383
3567
  value = item ? item.toString() : '';
3384
3568
  }
@@ -3388,7 +3572,7 @@ const ComboBoxMixin = (subclass) =>
3388
3572
  /** @private */
3389
3573
  _onArrowDown() {
3390
3574
  if (this.opened) {
3391
- const items = this.filteredItems;
3575
+ const items = this._dropdownItems;
3392
3576
  if (items) {
3393
3577
  this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
3394
3578
  this._prefillFocusedItemLabel();
@@ -3404,7 +3588,7 @@ const ComboBoxMixin = (subclass) =>
3404
3588
  if (this._focusedIndex > -1) {
3405
3589
  this._focusedIndex = Math.max(0, this._focusedIndex - 1);
3406
3590
  } else {
3407
- const items = this.filteredItems;
3591
+ const items = this._dropdownItems;
3408
3592
  if (items) {
3409
3593
  this._focusedIndex = items.length - 1;
3410
3594
  }
@@ -3419,7 +3603,7 @@ const ComboBoxMixin = (subclass) =>
3419
3603
  /** @private */
3420
3604
  _prefillFocusedItemLabel() {
3421
3605
  if (this._focusedIndex > -1) {
3422
- const focusedItem = this.filteredItems[this._focusedIndex];
3606
+ const focusedItem = this._dropdownItems[this._focusedIndex];
3423
3607
  this._inputElementValue = this._getItemLabel(focusedItem);
3424
3608
  this._markAllSelectionRange();
3425
3609
  }
@@ -3516,7 +3700,7 @@ const ComboBoxMixin = (subclass) =>
3516
3700
  } else if (this.clearButtonVisible && !this.opened && !!this.value) {
3517
3701
  e.stopPropagation();
3518
3702
  // The clear button is visible and the overlay is closed, so clear the value.
3519
- this._clear();
3703
+ this._onClearAction();
3520
3704
  }
3521
3705
  } else if (this.opened) {
3522
3706
  // Auto-open is enabled
@@ -3534,7 +3718,7 @@ const ComboBoxMixin = (subclass) =>
3534
3718
  } else if (this.clearButtonVisible && !!this.value) {
3535
3719
  e.stopPropagation();
3536
3720
  // The clear button is visible and the overlay is closed, so clear the value.
3537
- this._clear();
3721
+ this._onClearAction();
3538
3722
  }
3539
3723
  }
3540
3724
 
@@ -3556,7 +3740,7 @@ const ComboBoxMixin = (subclass) =>
3556
3740
  * Clears the current value.
3557
3741
  * @protected
3558
3742
  */
3559
- _clear() {
3743
+ _onClearAction() {
3560
3744
  this.selectedItem = null;
3561
3745
 
3562
3746
  if (this.allowCustomValue) {
@@ -3592,7 +3776,7 @@ const ComboBoxMixin = (subclass) =>
3592
3776
  /** @private */
3593
3777
  _commitValue() {
3594
3778
  if (this._focusedIndex > -1) {
3595
- const focusedItem = this.filteredItems[this._focusedIndex];
3779
+ const focusedItem = this._dropdownItems[this._focusedIndex];
3596
3780
  if (this.selectedItem !== focusedItem) {
3597
3781
  this.selectedItem = focusedItem;
3598
3782
  }
@@ -3607,7 +3791,7 @@ const ComboBoxMixin = (subclass) =>
3607
3791
  }
3608
3792
  } else {
3609
3793
  // Try to find an item which label matches the input value.
3610
- const items = [...(this.filteredItems || []), this.selectedItem];
3794
+ const items = [this.selectedItem, ...(this._dropdownItems || [])];
3611
3795
  const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
3612
3796
 
3613
3797
  if (
@@ -3648,14 +3832,6 @@ const ComboBoxMixin = (subclass) =>
3648
3832
  this.filter = '';
3649
3833
  }
3650
3834
 
3651
- /**
3652
- * @return {string}
3653
- * @protected
3654
- */
3655
- get _propertyForValue() {
3656
- return 'value';
3657
- }
3658
-
3659
3835
  /**
3660
3836
  * Override an event listener from `InputMixin`.
3661
3837
  * @param {!Event} event
@@ -3802,6 +3978,12 @@ const ComboBoxMixin = (subclass) =>
3802
3978
 
3803
3979
  /** @private */
3804
3980
  _detectAndDispatchChange() {
3981
+ // Do not validate when focusout is caused by document
3982
+ // losing focus, which happens on browser tab switch.
3983
+ if (document.hasFocus()) {
3984
+ this.validate();
3985
+ }
3986
+
3805
3987
  if (this.value !== this._lastCommittedValue) {
3806
3988
  this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
3807
3989
  this._lastCommittedValue = this.value;
@@ -3824,6 +4006,8 @@ const ComboBoxMixin = (subclass) =>
3824
4006
 
3825
4007
  /** @private */
3826
4008
  _filteredItemsChanged(filteredItems, oldFilteredItems) {
4009
+ this._setDropdownItems(filteredItems);
4010
+
3827
4011
  // Store the currently focused item if any. The focused index preserves
3828
4012
  // in the case when more filtered items are loading but it is reset
3829
4013
  // when the user types in a filter query.
@@ -3884,6 +4068,16 @@ const ComboBoxMixin = (subclass) =>
3884
4068
  }
3885
4069
  }
3886
4070
 
4071
+ /**
4072
+ * Provide items to be rendered in the dropdown.
4073
+ * Override this method to show custom items.
4074
+ *
4075
+ * @protected
4076
+ */
4077
+ _setDropdownItems(items) {
4078
+ this._dropdownItems = items;
4079
+ }
4080
+
3887
4081
  /** @private */
3888
4082
  _getItemElements() {
3889
4083
  return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
@@ -3944,26 +4138,18 @@ const ComboBoxMixin = (subclass) =>
3944
4138
  }
3945
4139
  }
3946
4140
 
3947
- /** @private */
3948
- __onClearButtonMouseDown(event) {
3949
- event.preventDefault(); // Prevent native focusout event
3950
- this.inputElement.focus();
3951
- }
3952
-
3953
- /** @private */
3954
- _onFocusout(event) {
3955
- // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
3956
- // Do not focus the input in this case, because it would break announcement for the item.
3957
- if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
3958
- return;
3959
- }
4141
+ /**
4142
+ * Override method inherited from `FocusMixin`
4143
+ * to close the overlay on blur and commit the value.
4144
+ *
4145
+ * @param {boolean} focused
4146
+ * @protected
4147
+ * @override
4148
+ */
4149
+ _setFocused(focused) {
4150
+ super._setFocused(focused);
3960
4151
 
3961
- // Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
3962
- if (event.relatedTarget === this.$.overlay) {
3963
- event.composedPath()[0].focus();
3964
- return;
3965
- }
3966
- if (!this.readonly && !this._closeOnBlurIsPrevented) {
4152
+ if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
3967
4153
  // User's logic in `custom-value-set` event listener might cause input to blur,
3968
4154
  // which will result in attempting to commit the same custom value once again.
3969
4155
  if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
@@ -3975,6 +4161,32 @@ const ComboBoxMixin = (subclass) =>
3975
4161
  }
3976
4162
  }
3977
4163
 
4164
+ /**
4165
+ * Override method inherited from `FocusMixin` to not remove focused
4166
+ * state when focus moves to the overlay.
4167
+ *
4168
+ * @param {FocusEvent} event
4169
+ * @return {boolean}
4170
+ * @protected
4171
+ * @override
4172
+ */
4173
+ _shouldRemoveFocus(event) {
4174
+ // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
4175
+ // Do not focus the input in this case, because it would break announcement for the item.
4176
+ if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
4177
+ return false;
4178
+ }
4179
+
4180
+ // Do not blur when focus moves to the overlay
4181
+ // Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
4182
+ if (event.relatedTarget === this._overlayElement) {
4183
+ event.composedPath()[0].focus();
4184
+ return false;
4185
+ }
4186
+
4187
+ return true;
4188
+ }
4189
+
3978
4190
  /** @private */
3979
4191
  _onTouchend(event) {
3980
4192
  if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
@@ -3982,7 +4194,7 @@ const ComboBoxMixin = (subclass) =>
3982
4194
  }
3983
4195
 
3984
4196
  event.preventDefault();
3985
- this._clear();
4197
+ this._onClearAction();
3986
4198
  }
3987
4199
 
3988
4200
  /**
@@ -4028,7 +4240,7 @@ const ComboBoxMixin = (subclass) =>
4028
4240
 
4029
4241
  /**
4030
4242
  * @license
4031
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
4243
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
4032
4244
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4033
4245
  */
4034
4246
 
@@ -4148,7 +4360,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4148
4360
  * Note: the `theme` attribute value set on `<vaadin-combo-box>` is
4149
4361
  * propagated to the internal components listed above.
4150
4362
  *
4151
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
4363
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
4152
4364
  *
4153
4365
  * @fires {Event} change - Fired when the user commits a value change.
4154
4366
  * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
@@ -4159,6 +4371,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4159
4371
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
4160
4372
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
4161
4373
  *
4374
+ * @customElement
4162
4375
  * @extends HTMLElement
4163
4376
  * @mixes ElementMixin
4164
4377
  * @mixes ThemableMixin
@@ -4261,6 +4474,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
4261
4474
  this._tooltipController = new TooltipController(this);
4262
4475
  this.addController(this._tooltipController);
4263
4476
  this._tooltipController.setPosition('top');
4477
+ this._tooltipController.setAriaTarget(this.inputElement);
4264
4478
  this._tooltipController.setShouldShow((target) => !target.opened);
4265
4479
 
4266
4480
  this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
@@ -4268,48 +4482,17 @@ class ComboBox extends ComboBoxDataProviderMixin(
4268
4482
  }
4269
4483
 
4270
4484
  /**
4271
- * Override method inherited from `FocusMixin` to validate on blur.
4272
- * @param {boolean} focused
4273
- * @protected
4274
- * @override
4275
- */
4276
- _setFocused(focused) {
4277
- super._setFocused(focused);
4278
-
4279
- if (!focused) {
4280
- this.validate();
4281
- }
4282
- }
4283
-
4284
- /**
4285
- * Override method inherited from `FocusMixin` to not remove focused
4286
- * state when focus moves to the overlay.
4287
- * @param {FocusEvent} event
4288
- * @return {boolean}
4289
- * @protected
4290
- * @override
4291
- */
4292
- _shouldRemoveFocus(event) {
4293
- // Do not blur when focus moves to the overlay
4294
- if (event.relatedTarget === this.$.overlay) {
4295
- event.composedPath()[0].focus();
4296
- return false;
4297
- }
4298
-
4299
- return true;
4300
- }
4301
-
4302
- /**
4303
- * Override method inherited from `InputControlMixin` to handle clear
4304
- * button click and stop event from propagating to the host element.
4485
+ * Override the method from `InputControlMixin`
4486
+ * to stop event propagation to prevent `ComboBoxMixin`
4487
+ * from handling this click event also on its own.
4488
+ *
4305
4489
  * @param {Event} event
4306
4490
  * @protected
4307
4491
  * @override
4308
4492
  */
4309
4493
  _onClearButtonClick(event) {
4310
4494
  event.stopPropagation();
4311
-
4312
- this._handleClearButtonClick(event);
4495
+ super._onClearButtonClick(event);
4313
4496
  }
4314
4497
 
4315
4498
  /**
@@ -4326,4 +4509,4 @@ class ComboBox extends ComboBoxDataProviderMixin(
4326
4509
  }
4327
4510
  }
4328
4511
 
4329
- customElements.define(ComboBox.is, ComboBox);
4512
+ defineCustomElement(ComboBox);