@everymatrix/general-input 1.22.0 → 1.22.2

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, g as defineCustomElement, h as html, j as ThemableMixin, o as DirMixin, P as PolymerElement, n as microTask, R as idlePeriod, U as animationFrame, W as flush, p as Debouncer, X as enqueueDebouncer, t as timeOut, q as generateUniqueId, J as ControllerMixin, V as ValidateMixin, m as FocusMixin, K as KeyboardMixin, I as InputMixin, a as DisabledMixin, b as isElementFocused, e as InputController, f 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
422
- * @private
423
- */
424
- class ComboBoxOverlay extends PositionMixin(Overlay) {
425
- static get is() {
426
- return 'vaadin-combo-box-overlay';
427
- }
428
-
429
- 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;
521
+ * @customElement
522
+ * @extends HTMLElement
523
+ * @mixes ComboBoxOverlayMixin
524
+ * @mixes DirMixin
525
+ * @mixes OverlayMixin
526
+ * @mixes ThemableMixin
527
+ * @private
528
+ */
529
+ class ComboBoxOverlay extends ComboBoxOverlayMixin(OverlayMixin(DirMixin(ThemableMixin(PolymerElement)))) {
530
+ static get is() {
531
+ return 'vaadin-combo-box-overlay';
460
532
  }
461
533
 
462
- _outsideClickListener(event) {
463
- const eventPath = event.composedPath();
464
- if (!eventPath.includes(this.positionTarget) && !eventPath.includes(this)) {
465
- this.close();
466
- }
534
+ static get template() {
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,35 @@ 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._virtualCount = this.items.length;
1570
+ }
1479
1571
 
1480
- const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
1481
- if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
1482
- this._scrollTop += fviOffsetBefore - fviOffsetAfter;
1483
- }
1572
+ // When reducing size while invisible, iron-list does not update items, so
1573
+ // their hidden state is not updated and their __lastUpdatedIndex is not
1574
+ // reset. In that case force an update here.
1575
+ if (!this._isVisible) {
1576
+ this._assignModels();
1484
1577
  }
1485
1578
 
1486
1579
  if (!this.elementsContainer.children.length) {
1487
1580
  requestAnimationFrame(() => this._resizeHandler());
1488
1581
  }
1489
1582
 
1490
- this.__preventElementUpdates = false;
1491
- // Schedule and flush a resize handler
1583
+ // Schedule and flush a resize handler. This will cause a
1584
+ // re-render for the elements.
1492
1585
  this._resizeHandler();
1493
1586
  flush();
1494
1587
  }
@@ -1553,16 +1646,20 @@ class IronListAdapter {
1553
1646
 
1554
1647
  /** @private */
1555
1648
  _assignModels(itemSet) {
1649
+ const updatedElements = [];
1556
1650
  this._iterateItems((pidx, vidx) => {
1557
1651
  const el = this._physicalItems[pidx];
1558
1652
  el.hidden = vidx >= this.size;
1559
1653
  if (!el.hidden) {
1560
1654
  el.__virtualIndex = vidx + (this._vidxOffset || 0);
1561
1655
  this.__updateElement(el, el.__virtualIndex);
1656
+ updatedElements.push(el);
1562
1657
  } else {
1563
1658
  delete el.__lastUpdatedIndex;
1564
1659
  }
1565
1660
  }, itemSet);
1661
+
1662
+ this.__afterElementsUpdated(updatedElements);
1566
1663
  }
1567
1664
 
1568
1665
  /** @private */
@@ -1691,7 +1788,9 @@ class IronListAdapter {
1691
1788
  deltaY *= this._scrollPageHeight;
1692
1789
  }
1693
1790
 
1694
- this._deltaYAcc = this._deltaYAcc || 0;
1791
+ if (!this._deltaYAcc) {
1792
+ this._deltaYAcc = 0;
1793
+ }
1695
1794
 
1696
1795
  if (this._wheelAnimationFrame) {
1697
1796
  // Accumulate wheel delta while a frame is being processed
@@ -1764,6 +1863,29 @@ class IronListAdapter {
1764
1863
  );
1765
1864
  }
1766
1865
 
1866
+ /**
1867
+ * Increases the pool size.
1868
+ * @override
1869
+ */
1870
+ _increasePoolIfNeeded(count) {
1871
+ if (this._physicalCount > 2 && count) {
1872
+ // The iron-list logic has already created some physical items and
1873
+ // has decided to create more. Since each item creation round is
1874
+ // expensive, let's try to create the remaining items in one go.
1875
+
1876
+ // Calculate the total item count that would be needed to fill the viewport
1877
+ // plus the buffer assuming rest of the items to be of the average size
1878
+ // of the items already created.
1879
+ const totalItemCount = Math.ceil(this._optPhysicalSize / this._physicalAverage);
1880
+ const missingItemCount = totalItemCount - this._physicalCount;
1881
+ // Create the remaining items in one go. Use a maximum of 100 items
1882
+ // as a safety measure.
1883
+ super._increasePoolIfNeeded(Math.max(count, Math.min(100, missingItemCount)));
1884
+ } else {
1885
+ super._increasePoolIfNeeded(count);
1886
+ }
1887
+ }
1888
+
1767
1889
  /**
1768
1890
  * @returns {Number|undefined} - The browser's default font-size in pixels
1769
1891
  * @private
@@ -1893,6 +2015,24 @@ class Virtualizer {
1893
2015
  this.__adapter = new IronListAdapter(config);
1894
2016
  }
1895
2017
 
2018
+ /**
2019
+ * Gets the index of the first visible item in the viewport.
2020
+ *
2021
+ * @return {number}
2022
+ */
2023
+ get firstVisibleIndex() {
2024
+ return this.__adapter.adjustedFirstVisibleIndex;
2025
+ }
2026
+
2027
+ /**
2028
+ * Gets the index of the last visible item in the viewport.
2029
+ *
2030
+ * @return {number}
2031
+ */
2032
+ get lastVisibleIndex() {
2033
+ return this.__adapter.adjustedLastVisibleIndex;
2034
+ }
2035
+
1896
2036
  /**
1897
2037
  * The size of the virtualizer
1898
2038
  * @return {number | undefined} The size of the virtualizer
@@ -1940,29 +2080,11 @@ class Virtualizer {
1940
2080
  flush() {
1941
2081
  this.__adapter.flush();
1942
2082
  }
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
2083
  }
1962
2084
 
1963
2085
  /**
1964
2086
  * @license
1965
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2087
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1966
2088
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1967
2089
  */
1968
2090
 
@@ -1979,394 +2101,454 @@ const ComboBoxPlaceholder = class ComboBoxPlaceholder {
1979
2101
 
1980
2102
  /**
1981
2103
  * @license
1982
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2104
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1983
2105
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1984
2106
  */
1985
2107
 
1986
2108
  /**
1987
- * Element for internal use only.
1988
- *
1989
- * @extends HTMLElement
1990
- * @private
2109
+ * @polymerMixin
1991
2110
  */
1992
- class ComboBoxScroller extends PolymerElement {
1993
- static get is() {
1994
- return 'vaadin-combo-box-scroller';
1995
- }
1996
-
1997
- static get template() {
1998
- return html`
1999
- <style>
2000
- :host {
2001
- display: block;
2002
- min-height: 1px;
2003
- overflow: auto;
2004
-
2005
- /* Fixes item background from getting on top of scrollbars on Safari */
2006
- transform: translate3d(0, 0, 0);
2007
-
2008
- /* Enable momentum scrolling on iOS */
2009
- -webkit-overflow-scrolling: touch;
2010
-
2011
- /* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
2012
- box-shadow: 0 0 0 white;
2013
- }
2014
-
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
- }
2111
+ const ComboBoxScrollerMixin = (superClass) =>
2112
+ class ComboBoxScrollerMixin extends superClass {
2113
+ static get properties() {
2114
+ return {
2115
+ /**
2116
+ * A full set of items to filter the visible options from.
2117
+ * Set to an empty array when combo-box is not opened.
2118
+ */
2119
+ items: {
2120
+ type: Array,
2121
+ observer: '__itemsChanged',
2122
+ },
2027
2123
 
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
- },
2124
+ /**
2125
+ * Index of an item that has focus outline and is scrolled into view.
2126
+ * The actual focus still remains in the input field.
2127
+ */
2128
+ focusedIndex: {
2129
+ type: Number,
2130
+ observer: '__focusedIndexChanged',
2131
+ },
2038
2132
 
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
- },
2133
+ /**
2134
+ * Set to true while combo-box fetches new page from the data provider.
2135
+ */
2136
+ loading: {
2137
+ type: Boolean,
2138
+ observer: '__loadingChanged',
2139
+ },
2047
2140
 
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
- },
2141
+ /**
2142
+ * Whether the combo-box is currently opened or not. If set to false,
2143
+ * calling `scrollIntoView` does not have any effect.
2144
+ */
2145
+ opened: {
2146
+ type: Boolean,
2147
+ observer: '__openedChanged',
2148
+ },
2055
2149
 
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
- },
2150
+ /**
2151
+ * The selected item from the `items` array.
2152
+ */
2153
+ selectedItem: {
2154
+ type: Object,
2155
+ observer: '__selectedItemChanged',
2156
+ },
2064
2157
 
2065
- /**
2066
- * The selected item from the `items` array.
2067
- */
2068
- selectedItem: {
2069
- type: Object,
2070
- observer: '__selectedItemChanged',
2071
- },
2158
+ /**
2159
+ * Path for the id of the item, used to detect whether the item is selected.
2160
+ */
2161
+ itemIdPath: {
2162
+ type: String,
2163
+ },
2072
2164
 
2073
- /**
2074
- * Path for the id of the item, used to detect whether the item is selected.
2075
- */
2076
- itemIdPath: {
2077
- type: String,
2078
- },
2165
+ /**
2166
+ * Reference to the owner (combo-box owner), used by the item elements.
2167
+ */
2168
+ owner: {
2169
+ type: Object,
2170
+ },
2079
2171
 
2080
- /**
2081
- * Reference to the combo-box, used by the item elements.
2082
- */
2083
- comboBox: {
2084
- type: Object,
2085
- },
2172
+ /**
2173
+ * Function used to set a label for every combo-box item.
2174
+ */
2175
+ getItemLabel: {
2176
+ type: Object,
2177
+ },
2086
2178
 
2087
- /**
2088
- * Function used to set a label for every combo-box item.
2089
- */
2090
- getItemLabel: {
2091
- type: Object,
2092
- },
2179
+ /**
2180
+ * Function used to render the content of every combo-box item.
2181
+ */
2182
+ renderer: {
2183
+ type: Object,
2184
+ observer: '__rendererChanged',
2185
+ },
2093
2186
 
2094
- /**
2095
- * Function used to render the content of every combo-box item.
2096
- */
2097
- renderer: {
2098
- type: Object,
2099
- observer: '__rendererChanged',
2100
- },
2187
+ /**
2188
+ * Used to propagate the `theme` attribute from the host element.
2189
+ */
2190
+ theme: {
2191
+ type: String,
2192
+ },
2193
+ };
2194
+ }
2101
2195
 
2102
- /**
2103
- * Used to propagate the `theme` attribute from the host element.
2104
- */
2105
- theme: {
2106
- type: String,
2107
- },
2108
- };
2109
- }
2196
+ constructor() {
2197
+ super();
2198
+ this.__boundOnItemClick = this.__onItemClick.bind(this);
2199
+ }
2110
2200
 
2111
- constructor() {
2112
- super();
2113
- this.__boundOnItemClick = this.__onItemClick.bind(this);
2114
- }
2201
+ /** @private */
2202
+ get _viewportTotalPaddingBottom() {
2203
+ if (this._cachedViewportTotalPaddingBottom === undefined) {
2204
+ const itemsStyle = window.getComputedStyle(this.$.selector);
2205
+ this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
2206
+ .map((v) => {
2207
+ return parseInt(v, 10);
2208
+ })
2209
+ .reduce((sum, v) => {
2210
+ return sum + v;
2211
+ });
2212
+ }
2115
2213
 
2116
- __openedChanged(opened) {
2117
- if (opened) {
2118
- this.requestContentUpdate();
2214
+ return this._cachedViewportTotalPaddingBottom;
2119
2215
  }
2120
- }
2121
-
2122
- /** @protected */
2123
- ready() {
2124
- super.ready();
2125
2216
 
2126
- // Ensure every instance has unique ID
2127
- this.id = `${this.localName}-${generateUniqueId()}`;
2217
+ /** @protected */
2218
+ ready() {
2219
+ super.ready();
2128
2220
 
2129
- // Allow extensions to customize tag name for the items
2130
- this.__hostTagName = this.constructor.is.replace('-scroller', '');
2221
+ this.setAttribute('role', 'listbox');
2131
2222
 
2132
- this.setAttribute('role', 'listbox');
2223
+ // Ensure every instance has unique ID
2224
+ this.id = `${this.localName}-${generateUniqueId()}`;
2133
2225
 
2134
- this.addEventListener('click', (e) => e.stopPropagation());
2226
+ // Allow extensions to customize tag name for the items
2227
+ this.__hostTagName = this.constructor.is.replace('-scroller', '');
2135
2228
 
2136
- this.__patchWheelOverScrolling();
2229
+ this.addEventListener('click', (e) => e.stopPropagation());
2137
2230
 
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
- }
2231
+ this.__patchWheelOverScrolling();
2146
2232
 
2147
- requestContentUpdate() {
2148
- if (this.__virtualizer) {
2149
- this.__virtualizer.update();
2233
+ this.__virtualizer = new Virtualizer({
2234
+ createElements: this.__createElements.bind(this),
2235
+ updateElement: this._updateElement.bind(this),
2236
+ elementsContainer: this,
2237
+ scrollTarget: this,
2238
+ scrollContainer: this.$.selector,
2239
+ });
2150
2240
  }
2151
- }
2152
2241
 
2153
- scrollIntoView(index) {
2154
- if (!(this.opened && index >= 0)) {
2155
- return;
2242
+ /**
2243
+ * Requests an update for the virtualizer to re-render items.
2244
+ */
2245
+ requestContentUpdate() {
2246
+ if (this.__virtualizer) {
2247
+ this.__virtualizer.update();
2248
+ }
2156
2249
  }
2157
2250
 
2158
- const visibleItemsCount = this._visibleItemsCount();
2251
+ /**
2252
+ * Scrolls an item at given index into view and adjusts `scrollTop`
2253
+ * so that the element gets fully visible on Arrow Down key press.
2254
+ * @param {number} index
2255
+ */
2256
+ scrollIntoView(index) {
2257
+ if (!(this.opened && index >= 0)) {
2258
+ return;
2259
+ }
2260
+
2261
+ const visibleItemsCount = this._visibleItemsCount();
2159
2262
 
2160
- let targetIndex = index;
2263
+ let targetIndex = index;
2161
2264
 
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;
2172
- }
2173
- this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
2265
+ if (index > this.__virtualizer.lastVisibleIndex - 1) {
2266
+ // Index is below the bottom, scrolling down. Make the item appear at the bottom.
2267
+ // First scroll to target (will be at the top of the scroller) to make sure it's rendered.
2268
+ this.__virtualizer.scrollToIndex(index);
2269
+ // Then calculate the index for the following scroll (to get the target to bottom of the scroller).
2270
+ targetIndex = index - visibleItemsCount + 1;
2271
+ } else if (index > this.__virtualizer.firstVisibleIndex) {
2272
+ // The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
2273
+ // the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
2274
+ targetIndex = this.__virtualizer.firstVisibleIndex;
2275
+ }
2276
+ this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
2174
2277
 
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;
2278
+ // Sometimes the item is partly below the bottom edge, detect and adjust.
2279
+ const lastPhysicalItem = [...this.children].find(
2280
+ (el) => !el.hidden && el.index === this.__virtualizer.lastVisibleIndex,
2281
+ );
2282
+ if (!lastPhysicalItem || index !== lastPhysicalItem.index) {
2283
+ return;
2284
+ }
2285
+ const lastPhysicalItemRect = lastPhysicalItem.getBoundingClientRect();
2286
+ const scrollerRect = this.getBoundingClientRect();
2287
+ const scrollTopAdjust = lastPhysicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
2288
+ if (scrollTopAdjust > 0) {
2289
+ this.scrollTop += scrollTopAdjust;
2290
+ }
2181
2291
  }
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;
2292
+
2293
+ /**
2294
+ * @param {string | object} item
2295
+ * @param {string | object} selectedItem
2296
+ * @param {string} itemIdPath
2297
+ * @protected
2298
+ */
2299
+ _isItemSelected(item, selectedItem, itemIdPath) {
2300
+ if (item instanceof ComboBoxPlaceholder) {
2301
+ return false;
2302
+ } else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
2303
+ return get(itemIdPath, item) === get(itemIdPath, selectedItem);
2304
+ }
2305
+ return item === selectedItem;
2187
2306
  }
2188
- }
2189
2307
 
2190
- /** @private */
2191
- __getAriaRole(itemIndex) {
2192
- return itemIndex !== undefined ? 'option' : false;
2193
- }
2308
+ /** @private */
2309
+ __itemsChanged(items) {
2310
+ if (this.__virtualizer && items) {
2311
+ this.__virtualizer.size = items.length;
2312
+ this.__virtualizer.flush();
2313
+ this.requestContentUpdate();
2314
+ }
2315
+ }
2194
2316
 
2195
- /** @private */
2196
- __isItemFocused(focusedIndex, itemIndex) {
2197
- return !this.loading && focusedIndex === itemIndex;
2198
- }
2317
+ /** @private */
2318
+ __loadingChanged() {
2319
+ this.requestContentUpdate();
2320
+ }
2199
2321
 
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);
2322
+ /** @private */
2323
+ __openedChanged(opened) {
2324
+ if (opened) {
2325
+ this.requestContentUpdate();
2326
+ }
2206
2327
  }
2207
- return item === selectedItem;
2208
- }
2209
2328
 
2210
- /** @private */
2211
- __itemsChanged(items) {
2212
- if (this.__virtualizer && items) {
2213
- this.__virtualizer.size = items.length;
2214
- this.__virtualizer.flush();
2329
+ /** @private */
2330
+ __selectedItemChanged() {
2215
2331
  this.requestContentUpdate();
2216
2332
  }
2217
- }
2218
-
2219
- /** @private */
2220
- __loadingChanged() {
2221
- this.requestContentUpdate();
2222
- }
2223
2333
 
2224
- /** @private */
2225
- __selectedItemChanged() {
2226
- this.requestContentUpdate();
2227
- }
2334
+ /** @private */
2335
+ __focusedIndexChanged(index, oldIndex) {
2336
+ if (index !== oldIndex) {
2337
+ this.requestContentUpdate();
2338
+ }
2228
2339
 
2229
- /** @private */
2230
- __focusedIndexChanged(index, oldIndex) {
2231
- if (index !== oldIndex) {
2232
- this.requestContentUpdate();
2340
+ // Do not jump back to the previously focused item while loading
2341
+ // when requesting next page from the data provider on scroll.
2342
+ if (index >= 0 && !this.loading) {
2343
+ this.scrollIntoView(index);
2344
+ }
2233
2345
  }
2234
2346
 
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);
2347
+ /** @private */
2348
+ __rendererChanged(renderer, oldRenderer) {
2349
+ if (renderer || oldRenderer) {
2350
+ this.requestContentUpdate();
2351
+ }
2239
2352
  }
2240
- }
2241
2353
 
2242
- /** @private */
2243
- __rendererChanged(renderer, oldRenderer) {
2244
- if (renderer || oldRenderer) {
2245
- this.requestContentUpdate();
2354
+ /** @private */
2355
+ __createElements(count) {
2356
+ return [...Array(count)].map(() => {
2357
+ const item = document.createElement(`${this.__hostTagName}-item`);
2358
+ item.addEventListener('click', this.__boundOnItemClick);
2359
+ // Negative tabindex prevents the item content from being focused.
2360
+ item.tabIndex = '-1';
2361
+ item.style.width = '100%';
2362
+ return item;
2363
+ });
2246
2364
  }
2247
- }
2248
2365
 
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
- }
2366
+ /**
2367
+ * @param {HTMLElement} el
2368
+ * @param {number} index
2369
+ * @protected
2370
+ */
2371
+ _updateElement(el, index) {
2372
+ const item = this.items[index];
2373
+ const focusedIndex = this.focusedIndex;
2374
+ const selected = this._isItemSelected(item, this.selectedItem, this.itemIdPath);
2375
+
2376
+ el.setProperties({
2377
+ item,
2378
+ index,
2379
+ label: this.getItemLabel(item),
2380
+ selected,
2381
+ renderer: this.renderer,
2382
+ focused: !this.loading && focusedIndex === index,
2383
+ });
2260
2384
 
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
- });
2385
+ el.id = `${this.__hostTagName}-item-${index}`;
2386
+ el.setAttribute('role', index !== undefined ? 'option' : false);
2387
+ el.setAttribute('aria-selected', selected.toString());
2388
+ el.setAttribute('aria-posinset', index + 1);
2389
+ el.setAttribute('aria-setsize', this.items.length);
2275
2390
 
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);
2391
+ if (this.theme) {
2392
+ el.setAttribute('theme', this.theme);
2393
+ } else {
2394
+ el.removeAttribute('theme');
2395
+ }
2281
2396
 
2282
- if (this.theme) {
2283
- el.setAttribute('theme', this.theme);
2284
- } else {
2285
- el.removeAttribute('theme');
2397
+ if (item instanceof ComboBoxPlaceholder) {
2398
+ this.__requestItemByIndex(index);
2399
+ }
2286
2400
  }
2287
2401
 
2288
- if (item instanceof ComboBoxPlaceholder) {
2289
- this.__requestItemByIndex(index);
2402
+ /** @private */
2403
+ __onItemClick(e) {
2404
+ this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
2290
2405
  }
2291
- }
2292
2406
 
2293
- /** @private */
2294
- __onItemClick(e) {
2295
- this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
2296
- }
2407
+ /**
2408
+ * We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
2409
+ * Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
2410
+ * scrolling the parent similarly to touch scrolling.
2411
+ * @private
2412
+ */
2413
+ __patchWheelOverScrolling() {
2414
+ this.$.selector.addEventListener('wheel', (e) => {
2415
+ const scrolledToTop = this.scrollTop === 0;
2416
+ const scrolledToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= 1;
2417
+ if (scrolledToTop && e.deltaY < 0) {
2418
+ e.preventDefault();
2419
+ } else if (scrolledToBottom && e.deltaY > 0) {
2420
+ e.preventDefault();
2421
+ }
2422
+ });
2423
+ }
2297
2424
 
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
- });
2313
- }
2425
+ /**
2426
+ * Dispatches an `index-requested` event for the given index to notify
2427
+ * the data provider that it should start loading the page containing the requested index.
2428
+ *
2429
+ * The event is dispatched asynchronously to prevent an immediate page request and therefore
2430
+ * a possible infinite recursion in case the data provider implements page request cancelation logic
2431
+ * by invoking data provider page callbacks with an empty array.
2432
+ * The infinite recursion may occur otherwise since invoking a data provider page callback with an empty array
2433
+ * triggers a synchronous scroller update and, if the callback corresponds to the currently visible page,
2434
+ * the scroller will synchronously request the page again which may lead to looping in the end.
2435
+ * That was the case for the Flow counterpart:
2436
+ * https://github.com/vaadin/flow-components/issues/3553#issuecomment-1239344828
2437
+ * @private
2438
+ */
2439
+ __requestItemByIndex(index) {
2440
+ requestAnimationFrame(() => {
2441
+ this.dispatchEvent(
2442
+ new CustomEvent('index-requested', {
2443
+ detail: {
2444
+ index,
2445
+ currentScrollerPos: this._oldScrollerPosition,
2446
+ },
2447
+ }),
2448
+ );
2449
+ });
2450
+ }
2314
2451
 
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
- });
2452
+ /** @private */
2453
+ _visibleItemsCount() {
2454
+ // Ensure items are positioned
2455
+ this.__virtualizer.scrollToIndex(this.__virtualizer.firstVisibleIndex);
2456
+ const hasItems = this.__virtualizer.size > 0;
2457
+ return hasItems ? this.__virtualizer.lastVisibleIndex - this.__virtualizer.firstVisibleIndex + 1 : 0;
2325
2458
  }
2459
+ };
2326
2460
 
2327
- return this._cachedViewportTotalPaddingBottom;
2328
- }
2461
+ /**
2462
+ * @license
2463
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2464
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2465
+ */
2329
2466
 
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
- });
2467
+ /**
2468
+ * An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
2469
+ *
2470
+ * @customElement
2471
+ * @extends HTMLElement
2472
+ * @mixes ComboBoxScrollerMixin
2473
+ * @private
2474
+ */
2475
+ class ComboBoxScroller extends ComboBoxScrollerMixin(PolymerElement) {
2476
+ static get is() {
2477
+ return 'vaadin-combo-box-scroller';
2354
2478
  }
2355
2479
 
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;
2480
+ static get template() {
2481
+ return html`
2482
+ <style>
2483
+ :host {
2484
+ display: block;
2485
+ min-height: 1px;
2486
+ overflow: auto;
2487
+
2488
+ /* Fixes item background from getting on top of scrollbars on Safari */
2489
+ transform: translate3d(0, 0, 0);
2490
+
2491
+ /* Enable momentum scrolling on iOS */
2492
+ -webkit-overflow-scrolling: touch;
2493
+
2494
+ /* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
2495
+ box-shadow: 0 0 0 white;
2496
+ }
2497
+
2498
+ #selector {
2499
+ border-width: var(--_vaadin-combo-box-items-container-border-width);
2500
+ border-style: var(--_vaadin-combo-box-items-container-border-style);
2501
+ border-color: var(--_vaadin-combo-box-items-container-border-color, transparent);
2502
+ position: relative;
2503
+ }
2504
+ </style>
2505
+ <div id="selector">
2506
+ <slot></slot>
2507
+ </div>
2508
+ `;
2362
2509
  }
2363
2510
  }
2364
2511
 
2365
- customElements.define(ComboBoxScroller.is, ComboBoxScroller);
2512
+ defineCustomElement(ComboBoxScroller);
2513
+
2514
+ /**
2515
+ * @license
2516
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2517
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2518
+ */
2519
+
2520
+ /**
2521
+ * A mixin to provide `pattern` property.
2522
+ *
2523
+ * @polymerMixin
2524
+ * @mixes InputConstraintsMixin
2525
+ */
2526
+ const PatternMixin = (superclass) =>
2527
+ class PatternMixinClass extends InputConstraintsMixin(superclass) {
2528
+ static get properties() {
2529
+ return {
2530
+ /**
2531
+ * A regular expression that the value is checked against.
2532
+ * The pattern must match the entire value, not just some subset.
2533
+ */
2534
+ pattern: {
2535
+ type: String,
2536
+ },
2537
+ };
2538
+ }
2539
+
2540
+ static get delegateAttrs() {
2541
+ return [...super.delegateAttrs, 'pattern'];
2542
+ }
2543
+
2544
+ static get constraints() {
2545
+ return [...super.constraints, 'pattern'];
2546
+ }
2547
+ };
2366
2548
 
2367
2549
  /**
2368
2550
  * @license
2369
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2551
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2370
2552
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2371
2553
  */
2372
2554
 
@@ -2676,20 +2858,18 @@ const ComboBoxDataProviderMixin = (superClass) =>
2676
2858
  _flushPendingRequests(size) {
2677
2859
  if (this._pendingRequests) {
2678
2860
  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);
2861
+ Object.entries(this._pendingRequests).forEach(([page, callback]) => {
2862
+ if (parseInt(page) >= lastPage) {
2863
+ callback([], size);
2684
2864
  }
2685
- }
2865
+ });
2686
2866
  }
2687
2867
  }
2688
2868
  };
2689
2869
 
2690
2870
  /**
2691
2871
  * @license
2692
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
2872
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2693
2873
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2694
2874
  */
2695
2875
 
@@ -2714,7 +2894,7 @@ function processTemplates(component) {
2714
2894
 
2715
2895
  /**
2716
2896
  * @license
2717
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
2897
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2718
2898
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2719
2899
  */
2720
2900
 
@@ -2748,10 +2928,19 @@ function findItemIndex(items, callback) {
2748
2928
 
2749
2929
  /**
2750
2930
  * @polymerMixin
2931
+ * @mixes ControllerMixin
2932
+ * @mixes ValidateMixin
2933
+ * @mixes DisabledMixin
2934
+ * @mixes InputMixin
2935
+ * @mixes KeyboardMixin
2936
+ * @mixes FocusMixin
2937
+ * @mixes OverlayClassMixin
2751
2938
  * @param {function(new:HTMLElement)} subclass
2752
2939
  */
2753
2940
  const ComboBoxMixin = (subclass) =>
2754
- class VaadinComboBoxMixinElement extends ControllerMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))) {
2941
+ class ComboBoxMixinClass extends OverlayClassMixin(
2942
+ ControllerMixin(ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))))),
2943
+ ) {
2755
2944
  static get properties() {
2756
2945
  return {
2757
2946
  /**
@@ -2950,7 +3139,6 @@ const ComboBoxMixin = (subclass) =>
2950
3139
 
2951
3140
  constructor() {
2952
3141
  super();
2953
- this._boundOnFocusout = this._onFocusout.bind(this);
2954
3142
  this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
2955
3143
  this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
2956
3144
  this._boundOnClick = this._onClick.bind(this);
@@ -2967,24 +3155,6 @@ const ComboBoxMixin = (subclass) =>
2967
3155
  return 'vaadin-combo-box';
2968
3156
  }
2969
3157
 
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
3158
  /**
2989
3159
  * Get a reference to the native `<input>` element.
2990
3160
  * Override to provide a custom input.
@@ -3035,8 +3205,6 @@ const ComboBoxMixin = (subclass) =>
3035
3205
  this._initOverlay();
3036
3206
  this._initScroller();
3037
3207
 
3038
- this.addEventListener('focusout', this._boundOnFocusout);
3039
-
3040
3208
  this._lastCommittedValue = this.value;
3041
3209
 
3042
3210
  this.addEventListener('click', this._boundOnClick);
@@ -3044,7 +3212,7 @@ const ComboBoxMixin = (subclass) =>
3044
3212
 
3045
3213
  const bringToFrontListener = () => {
3046
3214
  requestAnimationFrame(() => {
3047
- this.$.overlay.bringToFront();
3215
+ this._overlayElement.bringToFront();
3048
3216
  });
3049
3217
  };
3050
3218
 
@@ -3135,6 +3303,8 @@ const ComboBoxMixin = (subclass) =>
3135
3303
  overlay.addEventListener('opened-changed', (e) => {
3136
3304
  this._overlayOpened = e.detail.value;
3137
3305
  });
3306
+
3307
+ this._overlayElement = overlay;
3138
3308
  }
3139
3309
 
3140
3310
  /**
@@ -3146,7 +3316,7 @@ const ComboBoxMixin = (subclass) =>
3146
3316
  _initScroller(host) {
3147
3317
  const scrollerTag = `${this._tagNamePrefix}-scroller`;
3148
3318
 
3149
- const overlay = this.$.overlay;
3319
+ const overlay = this._overlayElement;
3150
3320
 
3151
3321
  overlay.renderer = (root) => {
3152
3322
  if (!root.firstChild) {
@@ -3159,7 +3329,7 @@ const ComboBoxMixin = (subclass) =>
3159
3329
 
3160
3330
  const scroller = overlay.querySelector(scrollerTag);
3161
3331
 
3162
- scroller.comboBox = host || this;
3332
+ scroller.owner = host || this;
3163
3333
  scroller.getItemLabel = this._getItemLabel.bind(this);
3164
3334
  scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
3165
3335
 
@@ -3254,7 +3424,7 @@ const ComboBoxMixin = (subclass) =>
3254
3424
  }
3255
3425
  }
3256
3426
 
3257
- this.$.overlay.restoreFocusOnClose = true;
3427
+ this._overlayElement.restoreFocusOnClose = true;
3258
3428
  } else {
3259
3429
  this._onClosed();
3260
3430
  if (this._openedWithFocusRing && this._isInputFocused()) {
@@ -3288,13 +3458,19 @@ const ComboBoxMixin = (subclass) =>
3288
3458
  return event.composedPath()[0] === this.clearElement;
3289
3459
  }
3290
3460
 
3461
+ /** @private */
3462
+ __onClearButtonMouseDown(event) {
3463
+ event.preventDefault(); // Prevent native focusout event
3464
+ this.inputElement.focus();
3465
+ }
3466
+
3291
3467
  /**
3292
3468
  * @param {Event} event
3293
3469
  * @protected
3294
3470
  */
3295
- _handleClearButtonClick(event) {
3471
+ _onClearButtonClick(event) {
3296
3472
  event.preventDefault();
3297
- this._clear();
3473
+ this._onClearAction();
3298
3474
 
3299
3475
  // De-select dropdown item
3300
3476
  if (this.opened) {
@@ -3330,15 +3506,13 @@ const ComboBoxMixin = (subclass) =>
3330
3506
  }
3331
3507
 
3332
3508
  /** @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);
3509
+ _onClick(event) {
3510
+ if (this._isClearButton(event)) {
3511
+ this._onClearButtonClick(event);
3512
+ } else if (event.composedPath().includes(this._toggleElement)) {
3513
+ this._onToggleButtonClick(event);
3340
3514
  } else {
3341
- this._onHostClick(e);
3515
+ this._onHostClick(event);
3342
3516
  }
3343
3517
  }
3344
3518
 
@@ -3353,7 +3527,7 @@ const ComboBoxMixin = (subclass) =>
3353
3527
  super._onKeyDown(e);
3354
3528
 
3355
3529
  if (e.key === 'Tab') {
3356
- this.$.overlay.restoreFocusOnClose = false;
3530
+ this._overlayElement.restoreFocusOnClose = false;
3357
3531
  } else if (e.key === 'ArrowDown') {
3358
3532
  this._onArrowDown();
3359
3533
 
@@ -3369,7 +3543,7 @@ const ComboBoxMixin = (subclass) =>
3369
3543
 
3370
3544
  /** @private */
3371
3545
  _getItemLabel(item) {
3372
- let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
3546
+ let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
3373
3547
  if (label === undefined || label === null) {
3374
3548
  label = item ? item.toString() : '';
3375
3549
  }
@@ -3378,7 +3552,7 @@ const ComboBoxMixin = (subclass) =>
3378
3552
 
3379
3553
  /** @private */
3380
3554
  _getItemValue(item) {
3381
- let value = item && this.itemValuePath ? this.get(this.itemValuePath, item) : undefined;
3555
+ let value = item && this.itemValuePath ? get(this.itemValuePath, item) : undefined;
3382
3556
  if (value === undefined) {
3383
3557
  value = item ? item.toString() : '';
3384
3558
  }
@@ -3516,7 +3690,7 @@ const ComboBoxMixin = (subclass) =>
3516
3690
  } else if (this.clearButtonVisible && !this.opened && !!this.value) {
3517
3691
  e.stopPropagation();
3518
3692
  // The clear button is visible and the overlay is closed, so clear the value.
3519
- this._clear();
3693
+ this._onClearAction();
3520
3694
  }
3521
3695
  } else if (this.opened) {
3522
3696
  // Auto-open is enabled
@@ -3534,7 +3708,7 @@ const ComboBoxMixin = (subclass) =>
3534
3708
  } else if (this.clearButtonVisible && !!this.value) {
3535
3709
  e.stopPropagation();
3536
3710
  // The clear button is visible and the overlay is closed, so clear the value.
3537
- this._clear();
3711
+ this._onClearAction();
3538
3712
  }
3539
3713
  }
3540
3714
 
@@ -3556,7 +3730,7 @@ const ComboBoxMixin = (subclass) =>
3556
3730
  * Clears the current value.
3557
3731
  * @protected
3558
3732
  */
3559
- _clear() {
3733
+ _onClearAction() {
3560
3734
  this.selectedItem = null;
3561
3735
 
3562
3736
  if (this.allowCustomValue) {
@@ -3607,7 +3781,7 @@ const ComboBoxMixin = (subclass) =>
3607
3781
  }
3608
3782
  } else {
3609
3783
  // Try to find an item which label matches the input value.
3610
- const items = [...(this.filteredItems || []), this.selectedItem];
3784
+ const items = [this.selectedItem, ...(this.filteredItems || [])];
3611
3785
  const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
3612
3786
 
3613
3787
  if (
@@ -3648,14 +3822,6 @@ const ComboBoxMixin = (subclass) =>
3648
3822
  this.filter = '';
3649
3823
  }
3650
3824
 
3651
- /**
3652
- * @return {string}
3653
- * @protected
3654
- */
3655
- get _propertyForValue() {
3656
- return 'value';
3657
- }
3658
-
3659
3825
  /**
3660
3826
  * Override an event listener from `InputMixin`.
3661
3827
  * @param {!Event} event
@@ -3802,6 +3968,12 @@ const ComboBoxMixin = (subclass) =>
3802
3968
 
3803
3969
  /** @private */
3804
3970
  _detectAndDispatchChange() {
3971
+ // Do not validate when focusout is caused by document
3972
+ // losing focus, which happens on browser tab switch.
3973
+ if (document.hasFocus()) {
3974
+ this.validate();
3975
+ }
3976
+
3805
3977
  if (this.value !== this._lastCommittedValue) {
3806
3978
  this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
3807
3979
  this._lastCommittedValue = this.value;
@@ -3944,26 +4116,18 @@ const ComboBoxMixin = (subclass) =>
3944
4116
  }
3945
4117
  }
3946
4118
 
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
- }
4119
+ /**
4120
+ * Override method inherited from `FocusMixin`
4121
+ * to close the overlay on blur and commit the value.
4122
+ *
4123
+ * @param {boolean} focused
4124
+ * @protected
4125
+ * @override
4126
+ */
4127
+ _setFocused(focused) {
4128
+ super._setFocused(focused);
3960
4129
 
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) {
4130
+ if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
3967
4131
  // User's logic in `custom-value-set` event listener might cause input to blur,
3968
4132
  // which will result in attempting to commit the same custom value once again.
3969
4133
  if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
@@ -3975,6 +4139,32 @@ const ComboBoxMixin = (subclass) =>
3975
4139
  }
3976
4140
  }
3977
4141
 
4142
+ /**
4143
+ * Override method inherited from `FocusMixin` to not remove focused
4144
+ * state when focus moves to the overlay.
4145
+ *
4146
+ * @param {FocusEvent} event
4147
+ * @return {boolean}
4148
+ * @protected
4149
+ * @override
4150
+ */
4151
+ _shouldRemoveFocus(event) {
4152
+ // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
4153
+ // Do not focus the input in this case, because it would break announcement for the item.
4154
+ if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
4155
+ return false;
4156
+ }
4157
+
4158
+ // Do not blur when focus moves to the overlay
4159
+ // Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
4160
+ if (event.relatedTarget === this._overlayElement) {
4161
+ event.composedPath()[0].focus();
4162
+ return false;
4163
+ }
4164
+
4165
+ return true;
4166
+ }
4167
+
3978
4168
  /** @private */
3979
4169
  _onTouchend(event) {
3980
4170
  if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
@@ -3982,7 +4172,7 @@ const ComboBoxMixin = (subclass) =>
3982
4172
  }
3983
4173
 
3984
4174
  event.preventDefault();
3985
- this._clear();
4175
+ this._onClearAction();
3986
4176
  }
3987
4177
 
3988
4178
  /**
@@ -4028,7 +4218,7 @@ const ComboBoxMixin = (subclass) =>
4028
4218
 
4029
4219
  /**
4030
4220
  * @license
4031
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
4221
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
4032
4222
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4033
4223
  */
4034
4224
 
@@ -4148,7 +4338,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4148
4338
  * Note: the `theme` attribute value set on `<vaadin-combo-box>` is
4149
4339
  * propagated to the internal components listed above.
4150
4340
  *
4151
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
4341
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
4152
4342
  *
4153
4343
  * @fires {Event} change - Fired when the user commits a value change.
4154
4344
  * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
@@ -4159,6 +4349,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4159
4349
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
4160
4350
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
4161
4351
  *
4352
+ * @customElement
4162
4353
  * @extends HTMLElement
4163
4354
  * @mixes ElementMixin
4164
4355
  * @mixes ThemableMixin
@@ -4261,6 +4452,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
4261
4452
  this._tooltipController = new TooltipController(this);
4262
4453
  this.addController(this._tooltipController);
4263
4454
  this._tooltipController.setPosition('top');
4455
+ this._tooltipController.setAriaTarget(this.inputElement);
4264
4456
  this._tooltipController.setShouldShow((target) => !target.opened);
4265
4457
 
4266
4458
  this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
@@ -4268,48 +4460,17 @@ class ComboBox extends ComboBoxDataProviderMixin(
4268
4460
  }
4269
4461
 
4270
4462
  /**
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.
4463
+ * Override the method from `InputControlMixin`
4464
+ * to stop event propagation to prevent `ComboBoxMixin`
4465
+ * from handling this click event also on its own.
4466
+ *
4305
4467
  * @param {Event} event
4306
4468
  * @protected
4307
4469
  * @override
4308
4470
  */
4309
4471
  _onClearButtonClick(event) {
4310
4472
  event.stopPropagation();
4311
-
4312
- this._handleClearButtonClick(event);
4473
+ super._onClearButtonClick(event);
4313
4474
  }
4314
4475
 
4315
4476
  /**
@@ -4326,4 +4487,4 @@ class ComboBox extends ComboBoxDataProviderMixin(
4326
4487
  }
4327
4488
  }
4328
4489
 
4329
- customElements.define(ComboBox.is, ComboBox);
4490
+ defineCustomElement(ComboBox);