@everymatrix/general-input 1.22.0 → 1.22.1

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,111 +1,73 @@
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 { o, i, h as html, P as PolymerElement, u as usageStatistics, d as dedupingMixin, V as ValidateMixin, j as FocusMixin, K as KeyboardMixin, I as InputMixin, a as DisabledMixin, b as isElementFocused, e as InputController, f as LabelledInputController } from './field-mixin.js';
2
+ import { b as overlay, c as menuOverlayCore, P as PositionMixin, O as OverlayMixin, o as overlayStyles, V as VirtualKeyboardController } from './virtual-keyboard-controller.js';
3
+ import { i as inputFieldShared, I as InputConstraintsMixin, a as InputControlMixin, b as inputFieldShared$1 } from './input-field-shared-styles.js';
5
4
 
6
5
  /**
7
6
  * @license
8
- * Copyright (c) 2022 Vaadin Ltd.
7
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
9
8
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
10
9
  */
11
10
 
12
- const loader = i`
13
- [part~='loader'] {
14
- box-sizing: border-box;
15
- width: var(--lumo-icon-size-s);
16
- height: var(--lumo-icon-size-s);
17
- border: 2px solid transparent;
18
- border-color: var(--lumo-primary-color-10pct) var(--lumo-primary-color-10pct) var(--lumo-primary-color)
19
- var(--lumo-primary-color);
20
- border-radius: calc(0.5 * var(--lumo-icon-size-s));
21
- opacity: 0;
22
- pointer-events: none;
23
- }
24
-
25
- :host(:not([loading])) [part~='loader'] {
26
- display: none;
27
- }
28
-
29
- :host([loading]) [part~='loader'] {
30
- animation: 1s linear infinite lumo-loader-rotate, 0.3s 0.1s lumo-loader-fade-in both;
31
- }
32
-
33
- @keyframes lumo-loader-fade-in {
34
- 0% {
35
- opacity: 0;
36
- }
11
+ /**
12
+ * Check if the custom element type has themes applied.
13
+ * @param {Function} elementClass
14
+ * @returns {boolean}
15
+ */
16
+ function classHasThemes$1(elementClass) {
17
+ return elementClass && Object.prototype.hasOwnProperty.call(elementClass, '__themes');
18
+ }
37
19
 
38
- 100% {
39
- opacity: 1;
40
- }
41
- }
20
+ /**
21
+ * Check if the custom element type has themes applied.
22
+ * @param {string} tagName
23
+ * @returns {boolean}
24
+ */
25
+ function hasThemes$1(tagName) {
26
+ return classHasThemes$1(customElements.get(tagName));
27
+ }
42
28
 
43
- @keyframes lumo-loader-rotate {
44
- 0% {
45
- transform: rotate(0deg);
29
+ /**
30
+ * Flattens the styles into a single array of styles.
31
+ * @param {CSSResultGroup} styles
32
+ * @param {CSSResult[]} result
33
+ * @returns {CSSResult[]}
34
+ */
35
+ function flattenStyles$1(styles = []) {
36
+ return [styles].flat(Infinity).filter((style) => {
37
+ if (style instanceof o) {
38
+ return true;
46
39
  }
40
+ console.warn('An item in styles is not of type CSSResult. Use `unsafeCSS` or `css`.');
41
+ return false;
42
+ });
43
+ }
47
44
 
48
- 100% {
49
- transform: rotate(360deg);
45
+ /**
46
+ * Registers CSS styles for a component type. Make sure to register the styles before
47
+ * the first instance of a component of the type is attached to DOM.
48
+ *
49
+ * @param {string} themeFor The local/tag name of the component type to register the styles for
50
+ * @param {CSSResultGroup} styles The CSS style rules to be registered for the component type
51
+ * matching themeFor and included in the local scope of each component instance
52
+ * @param {{moduleId?: string, include?: string | string[]}} options Additional options
53
+ * @return {void}
54
+ */
55
+ function registerStyles$1(themeFor, styles, options = {}) {
56
+ if (themeFor) {
57
+ if (hasThemes$1(themeFor)) {
58
+ console.warn(`The custom element definition for "${themeFor}"
59
+ was finalized before a style module was registered.
60
+ Make sure to add component specific style modules before
61
+ importing the corresponding custom element.`);
50
62
  }
51
63
  }
52
- `;
53
-
54
- const comboBoxOverlay = i`
55
- [part='content'] {
56
- padding: 0;
57
- }
58
-
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
- /* When items are empty, the spinner needs some room */
68
- :host(:not([closing])) [part~='content'] {
69
- min-height: calc(2 * var(--lumo-space-s) + var(--lumo-icon-size-s));
70
- }
71
-
72
- [part~='overlay'] {
73
- position: relative;
74
- }
75
-
76
- :host([top-aligned]) [part~='overlay'] {
77
- margin-top: var(--lumo-space-xs);
78
- }
79
-
80
- :host([bottom-aligned]) [part~='overlay'] {
81
- margin-bottom: var(--lumo-space-xs);
82
- }
83
-
84
- [part~='loader'] {
85
- position: absolute;
86
- z-index: 1;
87
- left: var(--lumo-space-s);
88
- right: var(--lumo-space-s);
89
- top: var(--lumo-space-s);
90
- margin-left: auto;
91
- margin-inline-start: auto;
92
- margin-inline-end: 0;
93
- }
94
64
 
95
- /* RTL specific styles */
65
+ styles = flattenStyles$1(styles);
96
66
 
97
- :host([dir='rtl']) [part~='loader'] {
98
- left: auto;
99
- margin-left: 0;
100
- margin-right: auto;
101
- margin-inline-start: 0;
102
- margin-inline-end: auto;
67
+ if (window.Vaadin && window.Vaadin.styleModules) {
68
+ window.Vaadin.styleModules.registerStyles(themeFor, styles, options);
103
69
  }
104
- `;
105
-
106
- registerStyles('vaadin-combo-box-overlay', [overlay, menuOverlayCore, comboBoxOverlay, loader], {
107
- moduleId: 'lumo-combo-box-overlay',
108
- });
70
+ }
109
71
 
110
72
  const item = i`
111
73
  :host {
@@ -182,309 +144,1377 @@ const item = i`
182
144
  }
183
145
 
184
146
  /* Slotted icons */
185
- :host ::slotted(vaadin-icon),
186
- :host ::slotted(iron-icon) {
147
+ :host ::slotted(vaadin-icon) {
187
148
  width: var(--lumo-icon-size-m);
188
149
  height: var(--lumo-icon-size-m);
189
150
  }
190
151
  `;
191
152
 
192
- registerStyles('vaadin-item', item, { moduleId: 'lumo-item' });
193
-
194
- const comboBoxItem = i`
195
- :host {
196
- transition: background-color 100ms;
197
- overflow: hidden;
198
- --_lumo-item-selected-icon-display: block;
199
- }
153
+ registerStyles$1('vaadin-item', item, { moduleId: 'lumo-item' });
200
154
 
201
- @media (any-hover: hover) {
202
- :host([focused]:not([disabled])) {
203
- box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
155
+ /**
156
+ * @license
157
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
158
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
159
+ */
160
+ /**
161
+ * @polymerMixin
162
+ */
163
+ const ThemePropertyMixin = (superClass) =>
164
+ class VaadinThemePropertyMixin extends superClass {
165
+ static get properties() {
166
+ return {
167
+ /**
168
+ * Helper property with theme attribute value facilitating propagation
169
+ * in shadow DOM.
170
+ *
171
+ * Enables the component implementation to propagate the `theme`
172
+ * attribute value to the sub-components in Shadow DOM by binding
173
+ * the sub-component's "theme" attribute to the `theme` property of
174
+ * the host.
175
+ *
176
+ * **NOTE:** Extending the mixin only provides the property for binding,
177
+ * and does not make the propagation alone.
178
+ *
179
+ * See [Styling Components: Sub-components](https://vaadin.com/docs/latest/styling/styling-components/#sub-components).
180
+ * page for more information.
181
+ *
182
+ * @protected
183
+ */
184
+ _theme: {
185
+ type: String,
186
+ readOnly: true,
187
+ },
188
+ };
204
189
  }
205
- }
206
- `;
207
190
 
208
- registerStyles('vaadin-combo-box-item', [item, comboBoxItem], {
209
- moduleId: 'lumo-combo-box-item',
210
- });
211
-
212
- const comboBox = i`
213
- :host {
214
- outline: none;
215
- }
191
+ static get observedAttributes() {
192
+ return [...super.observedAttributes, 'theme'];
193
+ }
216
194
 
217
- [part='toggle-button']::before {
218
- content: var(--lumo-icons-dropdown);
219
- }
220
- `;
195
+ /** @protected */
196
+ attributeChangedCallback(name, oldValue, newValue) {
197
+ super.attributeChangedCallback(name, oldValue, newValue);
221
198
 
222
- registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lumo-combo-box' });
199
+ if (name === 'theme') {
200
+ this._set_theme(newValue);
201
+ }
202
+ }
203
+ };
223
204
 
224
205
  /**
225
206
  * @license
226
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
207
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
227
208
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
228
209
  */
229
210
 
230
211
  /**
231
- * An item element used by the `<vaadin-combo-box>` dropdown.
232
- *
233
- * ### Styling
234
- *
235
- * The following shadow DOM parts are available for styling:
236
- *
237
- * Part name | Description
238
- * ------------|--------------
239
- * `checkmark` | The graphical checkmark shown for a selected item
240
- * `content` | The element that wraps the item content
241
- *
242
- * The following state attributes are exposed for styling:
243
- *
244
- * Attribute | Description
245
- * -------------|-------------
246
- * `selected` | Set when the item is selected
247
- * `focused` | Set when the item is focused
248
- *
249
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
212
+ * @typedef {Object} Theme
213
+ * @property {string} themeFor
214
+ * @property {CSSResult[]} styles
215
+ * @property {string | string[]} [include]
216
+ * @property {string} [moduleId]
250
217
  *
251
- * @mixes ThemableMixin
252
- * @mixes DirMixin
253
- * @private
218
+ * @typedef {CSSResult[] | CSSResult} CSSResultGroup
254
219
  */
255
- class ComboBoxItem extends ThemableMixin(DirMixin(PolymerElement)) {
256
- static get template() {
257
- return html`
258
- <style>
259
- :host {
260
- display: block;
261
- }
262
-
263
- :host([hidden]) {
264
- display: none;
265
- }
266
- </style>
267
- <span part="checkmark" aria-hidden="true"></span>
268
- <div part="content">
269
- <slot></slot>
270
- </div>
271
- `;
272
- }
273
-
274
- static get is() {
275
- return 'vaadin-combo-box-item';
276
- }
277
-
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
220
 
326
- static get observers() {
327
- return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
328
- }
221
+ /**
222
+ * @type {Theme[]}
223
+ */
224
+ const themeRegistry = [];
329
225
 
330
- connectedCallback() {
331
- super.connectedCallback();
226
+ /**
227
+ * Check if the custom element type has themes applied.
228
+ * @param {Function} elementClass
229
+ * @returns {boolean}
230
+ */
231
+ function classHasThemes(elementClass) {
232
+ return elementClass && Object.prototype.hasOwnProperty.call(elementClass, '__themes');
233
+ }
332
234
 
333
- this._comboBox = this.parentNode.comboBox;
235
+ /**
236
+ * Check if the custom element type has themes applied.
237
+ * @param {string} tagName
238
+ * @returns {boolean}
239
+ */
240
+ function hasThemes(tagName) {
241
+ return classHasThemes(customElements.get(tagName));
242
+ }
334
243
 
335
- const hostDir = this._comboBox.getAttribute('dir');
336
- if (hostDir) {
337
- this.setAttribute('dir', hostDir);
244
+ /**
245
+ * Flattens the styles into a single array of styles.
246
+ * @param {CSSResultGroup} styles
247
+ * @param {CSSResult[]} result
248
+ * @returns {CSSResult[]}
249
+ */
250
+ function flattenStyles(styles = []) {
251
+ return [styles].flat(Infinity).filter((style) => {
252
+ if (style instanceof o) {
253
+ return true;
338
254
  }
339
- }
255
+ console.warn('An item in styles is not of type CSSResult. Use `unsafeCSS` or `css`.');
256
+ return false;
257
+ });
258
+ }
340
259
 
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;
260
+ /**
261
+ * Registers CSS styles for a component type. Make sure to register the styles before
262
+ * the first instance of a component of the type is attached to DOM.
263
+ *
264
+ * @param {string} themeFor The local/tag name of the component type to register the styles for
265
+ * @param {CSSResultGroup} styles The CSS style rules to be registered for the component type
266
+ * matching themeFor and included in the local scope of each component instance
267
+ * @param {{moduleId?: string, include?: string | string[]}} options Additional options
268
+ * @return {void}
269
+ */
270
+ function registerStyles(themeFor, styles, options = {}) {
271
+ if (themeFor) {
272
+ if (hasThemes(themeFor)) {
273
+ console.warn(`The custom element definition for "${themeFor}"
274
+ was finalized before a style module was registered.
275
+ Make sure to add component specific style modules before
276
+ importing the corresponding custom element.`);
350
277
  }
351
-
352
- const model = {
353
- index: this.index,
354
- item: this.item,
355
- focused: this.focused,
356
- selected: this.selected,
357
- };
358
-
359
- this.renderer(this, this._comboBox, model);
360
278
  }
361
279
 
362
- /** @private */
363
- __rendererOrItemChanged(renderer, index, item) {
364
- if (item === undefined || index === undefined) {
365
- return;
366
- }
367
-
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$;
374
- }
280
+ styles = flattenStyles(styles);
375
281
 
376
- if (renderer) {
377
- this._oldRenderer = renderer;
378
- this.requestContentUpdate();
379
- }
282
+ if (window.Vaadin && window.Vaadin.styleModules) {
283
+ window.Vaadin.styleModules.registerStyles(themeFor, styles, options);
284
+ } else {
285
+ themeRegistry.push({
286
+ themeFor,
287
+ styles,
288
+ include: options.include,
289
+ moduleId: options.moduleId,
290
+ });
380
291
  }
292
+ }
381
293
 
382
- /** @private */
383
- __updateLabel(label, renderer) {
384
- if (renderer) {
385
- return;
386
- }
387
-
388
- this.textContent = label;
294
+ /**
295
+ * Returns all registered themes. By default the themeRegistry is returned as is.
296
+ * In case the style-modules adapter is imported, the themes are obtained from there instead
297
+ * @returns {Theme[]}
298
+ */
299
+ function getAllThemes() {
300
+ if (window.Vaadin && window.Vaadin.styleModules) {
301
+ return window.Vaadin.styleModules.getAllThemes();
389
302
  }
303
+ return themeRegistry;
390
304
  }
391
305
 
392
- customElements.define(ComboBoxItem.is, ComboBoxItem);
393
-
306
+ /**
307
+ * Returns true if the themeFor string matches the tag name
308
+ * @param {string} themeFor
309
+ * @param {string} tagName
310
+ * @returns {boolean}
311
+ */
312
+ function matchesThemeFor(themeFor, tagName) {
313
+ return (themeFor || '').split(' ').some((themeForToken) => {
314
+ return new RegExp(`^${themeForToken.split('*').join('.*')}$`, 'u').test(tagName);
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Maps the moduleName to an include priority number which is used for
320
+ * determining the order in which styles are applied.
321
+ * @param {string} moduleName
322
+ * @returns {number}
323
+ */
324
+ function getIncludePriority(moduleName = '') {
325
+ let includePriority = 0;
326
+ if (moduleName.startsWith('lumo-') || moduleName.startsWith('material-')) {
327
+ includePriority = 1;
328
+ } else if (moduleName.startsWith('vaadin-')) {
329
+ includePriority = 2;
330
+ }
331
+ return includePriority;
332
+ }
333
+
334
+ /**
335
+ * Gets an array of CSSResults matching the include property of the theme.
336
+ * @param {Theme} theme
337
+ * @returns {CSSResult[]}
338
+ */
339
+ function getIncludedStyles(theme) {
340
+ const includedStyles = [];
341
+ if (theme.include) {
342
+ [].concat(theme.include).forEach((includeModuleId) => {
343
+ const includedTheme = getAllThemes().find((s) => s.moduleId === includeModuleId);
344
+ if (includedTheme) {
345
+ includedStyles.push(...getIncludedStyles(includedTheme), ...includedTheme.styles);
346
+ } else {
347
+ console.warn(`Included moduleId ${includeModuleId} not found in style registry`);
348
+ }
349
+ }, theme.styles);
350
+ }
351
+ return includedStyles;
352
+ }
353
+
354
+ /**
355
+ * Includes the styles to the template.
356
+ * @param {CSSResult[]} styles
357
+ * @param {HTMLTemplateElement} template
358
+ */
359
+ function addStylesToTemplate(styles, template) {
360
+ const styleEl = document.createElement('style');
361
+ styleEl.innerHTML = styles.map((style) => style.cssText).join('\n');
362
+ template.content.appendChild(styleEl);
363
+ }
364
+
365
+ /**
366
+ * Returns an array of themes that should be used for styling a component matching
367
+ * the tag name. The array is sorted by the include order.
368
+ * @param {string} tagName
369
+ * @returns {Theme[]}
370
+ */
371
+ function getThemes(tagName) {
372
+ const defaultModuleName = `${tagName}-default-theme`;
373
+
374
+ const themes = getAllThemes()
375
+ // Filter by matching themeFor properties
376
+ .filter((theme) => theme.moduleId !== defaultModuleName && matchesThemeFor(theme.themeFor, tagName))
377
+ .map((theme) => ({
378
+ ...theme,
379
+ // Prepend styles from included themes
380
+ styles: [...getIncludedStyles(theme), ...theme.styles],
381
+ // Map moduleId to includePriority
382
+ includePriority: getIncludePriority(theme.moduleId),
383
+ }))
384
+ // Sort by includePriority
385
+ .sort((themeA, themeB) => themeB.includePriority - themeA.includePriority);
386
+
387
+ if (themes.length > 0) {
388
+ return themes;
389
+ }
390
+ // No theme modules found, return the default module if it exists
391
+ return getAllThemes().filter((theme) => theme.moduleId === defaultModuleName);
392
+ }
393
+
394
+ /**
395
+ * @polymerMixin
396
+ * @mixes ThemePropertyMixin
397
+ */
398
+ const ThemableMixin = (superClass) =>
399
+ class VaadinThemableMixin extends ThemePropertyMixin(superClass) {
400
+ /**
401
+ * Covers PolymerElement based component styling
402
+ * @protected
403
+ */
404
+ static finalize() {
405
+ super.finalize();
406
+
407
+ // Make sure not to run the logic intended for PolymerElement when LitElement is used.
408
+ if (this.elementStyles) {
409
+ return;
410
+ }
411
+
412
+ const template = this.prototype._template;
413
+ if (!template || classHasThemes(this)) {
414
+ return;
415
+ }
416
+
417
+ addStylesToTemplate(this.getStylesForThis(), template);
418
+ }
419
+
420
+ /**
421
+ * Covers LitElement based component styling
422
+ *
423
+ * @protected
424
+ */
425
+ static finalizeStyles(styles) {
426
+ // The "styles" object originates from the "static get styles()" function of
427
+ // a LitElement based component. The theme styles are added after it
428
+ // so that they can override the component styles.
429
+ const themeStyles = this.getStylesForThis();
430
+ return styles ? [...super.finalizeStyles(styles), ...themeStyles] : themeStyles;
431
+ }
432
+
433
+ /**
434
+ * Get styles for the component type
435
+ *
436
+ * @private
437
+ */
438
+ static getStylesForThis() {
439
+ const parent = Object.getPrototypeOf(this.prototype);
440
+ const inheritedThemes = (parent ? parent.constructor.__themes : []) || [];
441
+ this.__themes = [...inheritedThemes, ...getThemes(this.is)];
442
+ const themeStyles = this.__themes.flatMap((theme) => theme.styles);
443
+ // Remove duplicates
444
+ return themeStyles.filter((style, index) => index === themeStyles.lastIndexOf(style));
445
+ }
446
+ };
447
+
448
+ const comboBoxItem = i`
449
+ :host {
450
+ transition: background-color 100ms;
451
+ overflow: hidden;
452
+ --_lumo-item-selected-icon-display: block;
453
+ }
454
+
455
+ @media (any-hover: hover) {
456
+ :host([focused]:not([disabled])) {
457
+ box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
458
+ }
459
+ }
460
+ `;
461
+
462
+ registerStyles('vaadin-combo-box-item', [item, comboBoxItem], {
463
+ moduleId: 'lumo-combo-box-item',
464
+ });
465
+
466
+ /**
467
+ * @license
468
+ * Copyright (c) 2022 - 2023 Vaadin Ltd.
469
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
470
+ */
471
+
472
+ const loader = i`
473
+ [part~='loader'] {
474
+ box-sizing: border-box;
475
+ width: var(--lumo-icon-size-s);
476
+ height: var(--lumo-icon-size-s);
477
+ border: 2px solid transparent;
478
+ border-color: var(--lumo-primary-color-10pct) var(--lumo-primary-color-10pct) var(--lumo-primary-color)
479
+ var(--lumo-primary-color);
480
+ border-radius: calc(0.5 * var(--lumo-icon-size-s));
481
+ opacity: 0;
482
+ pointer-events: none;
483
+ }
484
+
485
+ :host(:not([loading])) [part~='loader'] {
486
+ display: none;
487
+ }
488
+
489
+ :host([loading]) [part~='loader'] {
490
+ animation: 1s linear infinite lumo-loader-rotate, 0.3s 0.1s lumo-loader-fade-in both;
491
+ }
492
+
493
+ @keyframes lumo-loader-fade-in {
494
+ 0% {
495
+ opacity: 0;
496
+ }
497
+
498
+ 100% {
499
+ opacity: 1;
500
+ }
501
+ }
502
+
503
+ @keyframes lumo-loader-rotate {
504
+ 0% {
505
+ transform: rotate(0deg);
506
+ }
507
+
508
+ 100% {
509
+ transform: rotate(360deg);
510
+ }
511
+ }
512
+ `;
513
+
514
+ const comboBoxOverlay = i`
515
+ [part='content'] {
516
+ padding: 0;
517
+ }
518
+
519
+ /* When items are empty, the spinner needs some room */
520
+ :host(:not([closing])) [part~='content'] {
521
+ min-height: calc(2 * var(--lumo-space-s) + var(--lumo-icon-size-s));
522
+ }
523
+
524
+ [part~='overlay'] {
525
+ position: relative;
526
+ }
527
+
528
+ :host([top-aligned]) [part~='overlay'] {
529
+ margin-top: var(--lumo-space-xs);
530
+ }
531
+
532
+ :host([bottom-aligned]) [part~='overlay'] {
533
+ margin-bottom: var(--lumo-space-xs);
534
+ }
535
+ `;
536
+
537
+ const comboBoxLoader = i`
538
+ [part~='loader'] {
539
+ position: absolute;
540
+ z-index: 1;
541
+ left: var(--lumo-space-s);
542
+ right: var(--lumo-space-s);
543
+ top: var(--lumo-space-s);
544
+ margin-left: auto;
545
+ margin-inline-start: auto;
546
+ margin-inline-end: 0;
547
+ }
548
+
549
+ :host([dir='rtl']) [part~='loader'] {
550
+ left: auto;
551
+ margin-left: 0;
552
+ margin-right: auto;
553
+ margin-inline-start: 0;
554
+ margin-inline-end: auto;
555
+ }
556
+ `;
557
+
558
+ registerStyles(
559
+ 'vaadin-combo-box-overlay',
560
+ [
561
+ overlay,
562
+ menuOverlayCore,
563
+ comboBoxOverlay,
564
+ loader,
565
+ comboBoxLoader,
566
+ i`
567
+ :host {
568
+ --_vaadin-combo-box-items-container-border-width: var(--lumo-space-xs);
569
+ --_vaadin-combo-box-items-container-border-style: solid;
570
+ }
571
+ `,
572
+ ],
573
+ { moduleId: 'lumo-combo-box-overlay' },
574
+ );
575
+
576
+ const comboBox = i`
577
+ :host {
578
+ outline: none;
579
+ }
580
+
581
+ [part='toggle-button']::before {
582
+ content: var(--lumo-icons-dropdown);
583
+ }
584
+ `;
585
+
586
+ registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lumo-combo-box' });
587
+
588
+ /**
589
+ * @license
590
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
591
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
592
+ */
593
+ function defineCustomElement(CustomElement) {
594
+ const defined = customElements.get(CustomElement.is);
595
+ if (!defined) {
596
+ customElements.define(CustomElement.is, CustomElement);
597
+ } else {
598
+ const definedVersion = defined.version;
599
+ if (definedVersion && CustomElement.version && definedVersion === CustomElement.version) {
600
+ // Just loading the same thing again
601
+ console.warn(`The component ${CustomElement.is} has been loaded twice`);
602
+ } else {
603
+ console.error(
604
+ `Tried to define ${CustomElement.is} version ${CustomElement.version} when version ${defined.version} is already in use. Something will probably break.`,
605
+ );
606
+ }
607
+ }
608
+ }
609
+
610
+ /**
611
+ * @license
612
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
613
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
614
+ */
615
+
616
+ /**
617
+ * Array of Vaadin custom element classes that have been subscribed to the dir changes.
618
+ */
619
+ const directionSubscribers = [];
620
+
621
+ function alignDirs(element, documentDir, elementDir = element.getAttribute('dir')) {
622
+ if (documentDir) {
623
+ element.setAttribute('dir', documentDir);
624
+ } else if (elementDir != null) {
625
+ element.removeAttribute('dir');
626
+ }
627
+ }
628
+
629
+ function getDocumentDir() {
630
+ return document.documentElement.getAttribute('dir');
631
+ }
632
+
633
+ function directionUpdater() {
634
+ const documentDir = getDocumentDir();
635
+ directionSubscribers.forEach((element) => {
636
+ alignDirs(element, documentDir);
637
+ });
638
+ }
639
+
640
+ const directionObserver = new MutationObserver(directionUpdater);
641
+ directionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
642
+
643
+ /**
644
+ * A mixin to handle `dir` attribute based on the one set on the `<html>` element.
645
+ *
646
+ * @polymerMixin
647
+ */
648
+ const DirMixin = (superClass) =>
649
+ class VaadinDirMixin extends superClass {
650
+ static get properties() {
651
+ return {
652
+ /**
653
+ * @protected
654
+ */
655
+ dir: {
656
+ type: String,
657
+ value: '',
658
+ reflectToAttribute: true,
659
+ converter: {
660
+ fromAttribute: (attr) => {
661
+ return !attr ? '' : attr;
662
+ },
663
+ toAttribute: (prop) => {
664
+ return prop === '' ? null : prop;
665
+ },
666
+ },
667
+ },
668
+ };
669
+ }
670
+
671
+ /**
672
+ * @return {boolean}
673
+ * @protected
674
+ */
675
+ get __isRTL() {
676
+ return this.getAttribute('dir') === 'rtl';
677
+ }
678
+
679
+ /** @protected */
680
+ connectedCallback() {
681
+ super.connectedCallback();
682
+
683
+ if (!this.hasAttribute('dir') || this.__restoreSubscription) {
684
+ this.__subscribe();
685
+ alignDirs(this, getDocumentDir(), null);
686
+ }
687
+ }
688
+
689
+ /** @protected */
690
+ attributeChangedCallback(name, oldValue, newValue) {
691
+ super.attributeChangedCallback(name, oldValue, newValue);
692
+ if (name !== 'dir') {
693
+ return;
694
+ }
695
+
696
+ const documentDir = getDocumentDir();
697
+
698
+ // New value equals to the document direction and the element is not subscribed to the changes
699
+ const newValueEqlDocDir = newValue === documentDir && directionSubscribers.indexOf(this) === -1;
700
+ // Value was emptied and the element is not subscribed to the changes
701
+ const newValueEmptied = !newValue && oldValue && directionSubscribers.indexOf(this) === -1;
702
+ // New value is different and the old equals to document direction and the element is not subscribed to the changes
703
+ const newDiffValue = newValue !== documentDir && oldValue === documentDir;
704
+
705
+ if (newValueEqlDocDir || newValueEmptied) {
706
+ this.__subscribe();
707
+ alignDirs(this, documentDir, newValue);
708
+ } else if (newDiffValue) {
709
+ this.__unsubscribe();
710
+ }
711
+ }
712
+
713
+ /** @protected */
714
+ disconnectedCallback() {
715
+ super.disconnectedCallback();
716
+ this.__restoreSubscription = directionSubscribers.includes(this);
717
+ this.__unsubscribe();
718
+ }
719
+
720
+ /** @protected */
721
+ _valueToNodeAttribute(node, value, attribute) {
722
+ // Override default Polymer attribute reflection to match native behavior of HTMLElement.dir property
723
+ // If the property contains an empty string then it should not create an empty attribute
724
+ if (attribute === 'dir' && value === '' && !node.hasAttribute('dir')) {
725
+ return;
726
+ }
727
+ super._valueToNodeAttribute(node, value, attribute);
728
+ }
729
+
730
+ /** @protected */
731
+ _attributeToProperty(attribute, value, type) {
732
+ // Override default Polymer attribute reflection to match native behavior of HTMLElement.dir property
733
+ // If the attribute is removed, then the dir property should contain an empty string instead of null
734
+ if (attribute === 'dir' && !value) {
735
+ this.dir = '';
736
+ } else {
737
+ super._attributeToProperty(attribute, value, type);
738
+ }
739
+ }
740
+
741
+ /** @private */
742
+ __subscribe() {
743
+ if (!directionSubscribers.includes(this)) {
744
+ directionSubscribers.push(this);
745
+ }
746
+ }
747
+
748
+ /** @private */
749
+ __unsubscribe() {
750
+ if (directionSubscribers.includes(this)) {
751
+ directionSubscribers.splice(directionSubscribers.indexOf(this), 1);
752
+ }
753
+ }
754
+ };
755
+
756
+ /**
757
+ * @license
758
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
759
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
760
+ */
761
+
762
+ /**
763
+ * @polymerMixin
764
+ */
765
+ const ComboBoxItemMixin = (superClass) =>
766
+ class ComboBoxItemMixinClass extends superClass {
767
+ static get properties() {
768
+ return {
769
+ /**
770
+ * The index of the item.
771
+ */
772
+ index: {
773
+ type: Number,
774
+ },
775
+
776
+ /**
777
+ * The item to render.
778
+ */
779
+ item: {
780
+ type: Object,
781
+ },
782
+
783
+ /**
784
+ * The text to render in the item.
785
+ */
786
+ label: {
787
+ type: String,
788
+ },
789
+
790
+ /**
791
+ * True when item is selected.
792
+ */
793
+ selected: {
794
+ type: Boolean,
795
+ value: false,
796
+ reflectToAttribute: true,
797
+ },
798
+
799
+ /**
800
+ * True when item is focused.
801
+ */
802
+ focused: {
803
+ type: Boolean,
804
+ value: false,
805
+ reflectToAttribute: true,
806
+ },
807
+
808
+ /**
809
+ * Custom function for rendering the item content.
810
+ */
811
+ renderer: {
812
+ type: Function,
813
+ },
814
+ };
815
+ }
816
+
817
+ static get observers() {
818
+ return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
819
+ }
820
+
821
+ static get observedAttributes() {
822
+ return [...super.observedAttributes, 'hidden'];
823
+ }
824
+
825
+ attributeChangedCallback(name, oldValue, newValue) {
826
+ if (name === 'hidden' && newValue !== null) {
827
+ // The element is being hidden (by virtualizer). Mark one of the __rendererOrItemChanged
828
+ // dependencies as undefined to make sure it's called when the element is shown again
829
+ // and assigned properties with possibly identical values as before hiding.
830
+ this.index = undefined;
831
+ } else {
832
+ super.attributeChangedCallback(name, oldValue, newValue);
833
+ }
834
+ }
835
+
836
+ /** @protected */
837
+ connectedCallback() {
838
+ super.connectedCallback();
839
+
840
+ this._owner = this.parentNode.owner;
841
+
842
+ const hostDir = this._owner.getAttribute('dir');
843
+ if (hostDir) {
844
+ this.setAttribute('dir', hostDir);
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Requests an update for the content of the item.
850
+ * While performing the update, it invokes the renderer passed in the `renderer` property.
851
+ *
852
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
853
+ */
854
+ requestContentUpdate() {
855
+ if (!this.renderer) {
856
+ return;
857
+ }
858
+
859
+ const model = {
860
+ index: this.index,
861
+ item: this.item,
862
+ focused: this.focused,
863
+ selected: this.selected,
864
+ };
865
+
866
+ this.renderer(this, this._owner, model);
867
+ }
868
+
869
+ /** @private */
870
+ __rendererOrItemChanged(renderer, index, item) {
871
+ if (item === undefined || index === undefined) {
872
+ return;
873
+ }
874
+
875
+ if (this._oldRenderer !== renderer) {
876
+ this.innerHTML = '';
877
+ // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
878
+ // When clearing the rendered content, this part needs to be manually disposed of.
879
+ // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
880
+ delete this._$litPart$;
881
+ }
882
+
883
+ if (renderer) {
884
+ this._oldRenderer = renderer;
885
+ this.requestContentUpdate();
886
+ }
887
+ }
888
+
889
+ /** @private */
890
+ __updateLabel(label, renderer) {
891
+ if (renderer) {
892
+ return;
893
+ }
894
+
895
+ this.textContent = label;
896
+ }
897
+ };
898
+
899
+ /**
900
+ * @license
901
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
902
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
903
+ */
904
+
905
+ /**
906
+ * An item element used by the `<vaadin-combo-box>` dropdown.
907
+ *
908
+ * ### Styling
909
+ *
910
+ * The following shadow DOM parts are available for styling:
911
+ *
912
+ * Part name | Description
913
+ * ------------|--------------
914
+ * `checkmark` | The graphical checkmark shown for a selected item
915
+ * `content` | The element that wraps the item content
916
+ *
917
+ * The following state attributes are exposed for styling:
918
+ *
919
+ * Attribute | Description
920
+ * -------------|-------------
921
+ * `selected` | Set when the item is selected
922
+ * `focused` | Set when the item is focused
923
+ *
924
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
925
+ *
926
+ * @customElement
927
+ * @mixes ComboBoxItemMixin
928
+ * @mixes ThemableMixin
929
+ * @mixes DirMixin
930
+ * @private
931
+ */
932
+ class ComboBoxItem extends ComboBoxItemMixin(ThemableMixin(DirMixin(PolymerElement))) {
933
+ static get template() {
934
+ return html`
935
+ <style>
936
+ :host {
937
+ display: block;
938
+ }
939
+
940
+ :host([hidden]) {
941
+ display: none;
942
+ }
943
+ </style>
944
+ <span part="checkmark" aria-hidden="true"></span>
945
+ <div part="content">
946
+ <slot></slot>
947
+ </div>
948
+ `;
949
+ }
950
+
951
+ static get is() {
952
+ return 'vaadin-combo-box-item';
953
+ }
954
+ }
955
+
956
+ defineCustomElement(ComboBoxItem);
957
+
394
958
  /**
395
959
  * @license
396
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
960
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
397
961
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
398
962
  */
399
963
 
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));
964
+ /**
965
+ * @polymerMixin
966
+ * @mixes PositionMixin
967
+ */
968
+ const ComboBoxOverlayMixin = (superClass) =>
969
+ class ComboBoxOverlayMixin extends PositionMixin(superClass) {
970
+ static get observers() {
971
+ return ['_setOverlayWidth(positionTarget, opened)'];
405
972
  }
406
973
 
407
- [part='content'] {
408
- display: flex;
409
- flex-direction: column;
410
- height: 100%;
974
+ constructor() {
975
+ super();
976
+
977
+ this.requiredVerticalSpace = 200;
411
978
  }
412
- `,
413
- { moduleId: 'vaadin-combo-box-overlay-styles' },
414
- );
415
979
 
416
- let memoizedTemplate;
980
+ /** @protected */
981
+ connectedCallback() {
982
+ super.connectedCallback();
983
+
984
+ const comboBox = this._comboBox;
985
+
986
+ const hostDir = comboBox && comboBox.getAttribute('dir');
987
+ if (hostDir) {
988
+ this.setAttribute('dir', hostDir);
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Override method inherited from `Overlay`
994
+ * to not close on position target click.
995
+ *
996
+ * @param {Event} event
997
+ * @return {boolean}
998
+ * @protected
999
+ */
1000
+ _shouldCloseOnOutsideClick(event) {
1001
+ const eventPath = event.composedPath();
1002
+ return !eventPath.includes(this.positionTarget) && !eventPath.includes(this);
1003
+ }
1004
+
1005
+ /** @private */
1006
+ _setOverlayWidth(positionTarget, opened) {
1007
+ if (positionTarget && opened) {
1008
+ const propPrefix = this.localName;
1009
+ this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
1010
+
1011
+ const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
1012
+
1013
+ if (customWidth === '') {
1014
+ this.style.removeProperty(`--${propPrefix}-width`);
1015
+ } else {
1016
+ this.style.setProperty(`--${propPrefix}-width`, customWidth);
1017
+ }
1018
+
1019
+ this._updatePosition();
1020
+ }
1021
+ }
1022
+ };
1023
+
1024
+ /**
1025
+ * @license
1026
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1027
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1028
+ */
1029
+
1030
+ const comboBoxOverlayStyles = i`
1031
+ #overlay {
1032
+ width: var(--vaadin-combo-box-overlay-width, var(--_vaadin-combo-box-overlay-default-width, auto));
1033
+ }
1034
+
1035
+ [part='content'] {
1036
+ display: flex;
1037
+ flex-direction: column;
1038
+ height: 100%;
1039
+ }
1040
+ `;
1041
+
1042
+ registerStyles('vaadin-combo-box-overlay', [overlayStyles, comboBoxOverlayStyles], {
1043
+ moduleId: 'vaadin-combo-box-overlay-styles',
1044
+ });
417
1045
 
418
1046
  /**
419
1047
  * An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
420
1048
  *
421
- * @extends Overlay
1049
+ * @customElement
1050
+ * @extends HTMLElement
1051
+ * @mixes ComboBoxOverlayMixin
1052
+ * @mixes DirMixin
1053
+ * @mixes OverlayMixin
1054
+ * @mixes ThemableMixin
422
1055
  * @private
423
1056
  */
424
- class ComboBoxOverlay extends PositionMixin(Overlay) {
1057
+ class ComboBoxOverlay extends ComboBoxOverlayMixin(OverlayMixin(DirMixin(ThemableMixin(PolymerElement)))) {
425
1058
  static get is() {
426
1059
  return 'vaadin-combo-box-overlay';
427
1060
  }
428
1061
 
429
1062
  static get template() {
430
- if (!memoizedTemplate) {
431
- memoizedTemplate = super.template.cloneNode(true);
432
- memoizedTemplate.content.querySelector('[part~="overlay"]').removeAttribute('tabindex');
433
- }
1063
+ return html`
1064
+ <div id="backdrop" part="backdrop" hidden></div>
1065
+ <div part="overlay" id="overlay">
1066
+ <div part="loader"></div>
1067
+ <div part="content" id="content"><slot></slot></div>
1068
+ </div>
1069
+ `;
1070
+ }
1071
+ }
1072
+
1073
+ defineCustomElement(ComboBoxOverlay);
1074
+
1075
+ /**
1076
+ * @license
1077
+ * Copyright (c) 2023 Vaadin Ltd.
1078
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1079
+ */
1080
+
1081
+ /**
1082
+ * Convenience method for reading a value from a path.
1083
+ *
1084
+ * @param {string} path
1085
+ * @param {object} object
1086
+ */
1087
+ function get(path, object) {
1088
+ return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
1089
+ }
1090
+
1091
+ /**
1092
+ * @license
1093
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1094
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1095
+ */
1096
+
1097
+ let uniqueId = 0;
1098
+
1099
+ /**
1100
+ * Returns a unique integer id.
1101
+ *
1102
+ * @return {number}
1103
+ */
1104
+ function generateUniqueId() {
1105
+ // eslint-disable-next-line no-plusplus
1106
+ return uniqueId++;
1107
+ }
1108
+
1109
+ /**
1110
+ * @license
1111
+ * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1112
+ * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
1113
+ * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
1114
+ * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
1115
+ * Code distributed by Google as part of the polymer project is also
1116
+ * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
1117
+ */
1118
+
1119
+ /**
1120
+ * @fileoverview
1121
+ *
1122
+ * This module provides a number of strategies for enqueuing asynchronous
1123
+ * tasks. Each sub-module provides a standard `run(fn)` interface that returns a
1124
+ * handle, and a `cancel(handle)` interface for canceling async tasks before
1125
+ * they run.
1126
+ *
1127
+ * @summary Module that provides a number of strategies for enqueuing
1128
+ * asynchronous tasks.
1129
+ */
434
1130
 
435
- return memoizedTemplate;
1131
+ let microtaskCurrHandle = 0;
1132
+ let microtaskLastHandle = 0;
1133
+ const microtaskCallbacks = [];
1134
+ let microtaskScheduled = false;
1135
+
1136
+ function microtaskFlush() {
1137
+ microtaskScheduled = false;
1138
+ const len = microtaskCallbacks.length;
1139
+ for (let i = 0; i < len; i++) {
1140
+ const cb = microtaskCallbacks[i];
1141
+ if (cb) {
1142
+ try {
1143
+ cb();
1144
+ } catch (e) {
1145
+ setTimeout(() => {
1146
+ throw e;
1147
+ });
1148
+ }
1149
+ }
436
1150
  }
1151
+ microtaskCallbacks.splice(0, len);
1152
+ microtaskLastHandle += len;
1153
+ }
1154
+
1155
+ /**
1156
+ * Async interface wrapper around `setTimeout`.
1157
+ *
1158
+ * @namespace
1159
+ * @summary Async interface wrapper around `setTimeout`.
1160
+ */
1161
+ const timeOut = {
1162
+ /**
1163
+ * Returns a sub-module with the async interface providing the provided
1164
+ * delay.
1165
+ *
1166
+ * @memberof timeOut
1167
+ * @param {number=} delay Time to wait before calling callbacks in ms
1168
+ * @return {!AsyncInterface} An async timeout interface
1169
+ */
1170
+ after(delay) {
1171
+ return {
1172
+ run(fn) {
1173
+ return window.setTimeout(fn, delay);
1174
+ },
1175
+ cancel(handle) {
1176
+ window.clearTimeout(handle);
1177
+ },
1178
+ };
1179
+ },
1180
+ /**
1181
+ * Enqueues a function called in the next task.
1182
+ *
1183
+ * @memberof timeOut
1184
+ * @param {!Function} fn Callback to run
1185
+ * @param {number=} delay Delay in milliseconds
1186
+ * @return {number} Handle used for canceling task
1187
+ */
1188
+ run(fn, delay) {
1189
+ return window.setTimeout(fn, delay);
1190
+ },
1191
+ /**
1192
+ * Cancels a previously enqueued `timeOut` callback.
1193
+ *
1194
+ * @memberof timeOut
1195
+ * @param {number} handle Handle returned from `run` of callback to cancel
1196
+ * @return {void}
1197
+ */
1198
+ cancel(handle) {
1199
+ window.clearTimeout(handle);
1200
+ },
1201
+ };
1202
+
1203
+ /**
1204
+ * Async interface wrapper around `requestAnimationFrame`.
1205
+ *
1206
+ * @namespace
1207
+ * @summary Async interface wrapper around `requestAnimationFrame`.
1208
+ */
1209
+ const animationFrame = {
1210
+ /**
1211
+ * Enqueues a function called at `requestAnimationFrame` timing.
1212
+ *
1213
+ * @memberof animationFrame
1214
+ * @param {function(number):void} fn Callback to run
1215
+ * @return {number} Handle used for canceling task
1216
+ */
1217
+ run(fn) {
1218
+ return window.requestAnimationFrame(fn);
1219
+ },
1220
+ /**
1221
+ * Cancels a previously enqueued `animationFrame` callback.
1222
+ *
1223
+ * @memberof animationFrame
1224
+ * @param {number} handle Handle returned from `run` of callback to cancel
1225
+ * @return {void}
1226
+ */
1227
+ cancel(handle) {
1228
+ window.cancelAnimationFrame(handle);
1229
+ },
1230
+ };
1231
+
1232
+ /**
1233
+ * Async interface wrapper around `requestIdleCallback`. Falls back to
1234
+ * `setTimeout` on browsers that do not support `requestIdleCallback`.
1235
+ *
1236
+ * @namespace
1237
+ * @summary Async interface wrapper around `requestIdleCallback`.
1238
+ */
1239
+ const idlePeriod = {
1240
+ /**
1241
+ * Enqueues a function called at `requestIdleCallback` timing.
1242
+ *
1243
+ * @memberof idlePeriod
1244
+ * @param {function(!IdleDeadline):void} fn Callback to run
1245
+ * @return {number} Handle used for canceling task
1246
+ */
1247
+ run(fn) {
1248
+ return window.requestIdleCallback ? window.requestIdleCallback(fn) : window.setTimeout(fn, 16);
1249
+ },
1250
+ /**
1251
+ * Cancels a previously enqueued `idlePeriod` callback.
1252
+ *
1253
+ * @memberof idlePeriod
1254
+ * @param {number} handle Handle returned from `run` of callback to cancel
1255
+ * @return {void}
1256
+ */
1257
+ cancel(handle) {
1258
+ if (window.cancelIdleCallback) {
1259
+ window.cancelIdleCallback(handle);
1260
+ } else {
1261
+ window.clearTimeout(handle);
1262
+ }
1263
+ },
1264
+ };
1265
+
1266
+ /**
1267
+ * Async interface for enqueuing callbacks that run at microtask timing.
1268
+ *
1269
+ * @namespace
1270
+ * @summary Async interface for enqueuing callbacks that run at microtask
1271
+ * timing.
1272
+ */
1273
+ const microTask = {
1274
+ /**
1275
+ * Enqueues a function called at microtask timing.
1276
+ *
1277
+ * @memberof microTask
1278
+ * @param {!Function=} callback Callback to run
1279
+ * @return {number} Handle used for canceling task
1280
+ */
1281
+ run(callback) {
1282
+ if (!microtaskScheduled) {
1283
+ microtaskScheduled = true;
1284
+ queueMicrotask(() => microtaskFlush());
1285
+ }
1286
+ microtaskCallbacks.push(callback);
1287
+ const result = microtaskCurrHandle;
1288
+ microtaskCurrHandle += 1;
1289
+ return result;
1290
+ },
1291
+
1292
+ /**
1293
+ * Cancels a previously enqueued `microTask` callback.
1294
+ *
1295
+ * @memberof microTask
1296
+ * @param {number} handle Handle returned from `run` of callback to cancel
1297
+ * @return {void}
1298
+ */
1299
+ cancel(handle) {
1300
+ const idx = handle - microtaskLastHandle;
1301
+ if (idx >= 0) {
1302
+ if (!microtaskCallbacks[idx]) {
1303
+ throw new Error(`invalid async handle: ${handle}`);
1304
+ }
1305
+ microtaskCallbacks[idx] = null;
1306
+ }
1307
+ },
1308
+ };
1309
+
1310
+ /**
1311
+ * @license
1312
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1313
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1314
+ */
1315
+
1316
+ const testUserAgent = (regexp) => regexp.test(navigator.userAgent);
1317
+
1318
+ const testPlatform = (regexp) => regexp.test(navigator.platform);
1319
+
1320
+ const testVendor = (regexp) => regexp.test(navigator.vendor);
1321
+
1322
+ testUserAgent(/Android/u);
1323
+
1324
+ testUserAgent(/Chrome/u) && testVendor(/Google Inc/u);
1325
+
1326
+ testUserAgent(/Firefox/u);
437
1327
 
438
- static get observers() {
439
- return ['_setOverlayWidth(positionTarget, opened)'];
1328
+ // IPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
1329
+ testPlatform(/^iPad/u) || (testPlatform(/^Mac/u) && navigator.maxTouchPoints > 1);
1330
+
1331
+ testPlatform(/^iPhone/u);
1332
+
1333
+ const isSafari = testUserAgent(/^((?!chrome|android).)*safari/iu);
1334
+
1335
+ const isTouch = (() => {
1336
+ try {
1337
+ document.createEvent('TouchEvent');
1338
+ return true;
1339
+ } catch (e) {
1340
+ return false;
440
1341
  }
1342
+ })();
441
1343
 
442
- connectedCallback() {
443
- super.connectedCallback();
1344
+ /**
1345
+ @license
1346
+ Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1347
+ This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
1348
+ The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
1349
+ The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
1350
+ Code distributed by Google as part of the polymer project is also
1351
+ subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
1352
+ */
444
1353
 
445
- const comboBox = this._comboBox;
1354
+ const debouncerQueue = new Set();
446
1355
 
447
- const hostDir = comboBox && comboBox.getAttribute('dir');
448
- if (hostDir) {
449
- this.setAttribute('dir', hostDir);
1356
+ /**
1357
+ * @summary Collapse multiple callbacks into one invocation after a timer.
1358
+ */
1359
+ class Debouncer {
1360
+ /**
1361
+ * Creates a debouncer if no debouncer is passed as a parameter
1362
+ * or it cancels an active debouncer otherwise. The following
1363
+ * example shows how a debouncer can be called multiple times within a
1364
+ * microtask and "debounced" such that the provided callback function is
1365
+ * called once. Add this method to a custom element:
1366
+ *
1367
+ * ```js
1368
+ * import {microTask} from '@vaadin/component-base/src/async.js';
1369
+ * import {Debouncer} from '@vaadin/component-base/src/debounce.js';
1370
+ * // ...
1371
+ *
1372
+ * _debounceWork() {
1373
+ * this._debounceJob = Debouncer.debounce(this._debounceJob,
1374
+ * microTask, () => this._doWork());
1375
+ * }
1376
+ * ```
1377
+ *
1378
+ * If the `_debounceWork` method is called multiple times within the same
1379
+ * microtask, the `_doWork` function will be called only once at the next
1380
+ * microtask checkpoint.
1381
+ *
1382
+ * Note: In testing it is often convenient to avoid asynchrony. To accomplish
1383
+ * this with a debouncer, you can use `enqueueDebouncer` and
1384
+ * `flush`. For example, extend the above example by adding
1385
+ * `enqueueDebouncer(this._debounceJob)` at the end of the
1386
+ * `_debounceWork` method. Then in a test, call `flush` to ensure
1387
+ * the debouncer has completed.
1388
+ *
1389
+ * @param {Debouncer?} debouncer Debouncer object.
1390
+ * @param {!AsyncInterface} asyncModule Object with Async interface
1391
+ * @param {function()} callback Callback to run.
1392
+ * @return {!Debouncer} Returns a debouncer object.
1393
+ */
1394
+ static debounce(debouncer, asyncModule, callback) {
1395
+ if (debouncer instanceof Debouncer) {
1396
+ // Cancel the async callback, but leave in debouncerQueue if it was
1397
+ // enqueued, to maintain 1.x flush order
1398
+ debouncer._cancelAsync();
1399
+ } else {
1400
+ debouncer = new Debouncer();
450
1401
  }
1402
+ debouncer.setConfig(asyncModule, callback);
1403
+ return debouncer;
451
1404
  }
452
1405
 
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;
1406
+ constructor() {
1407
+ this._asyncModule = null;
1408
+ this._callback = null;
1409
+ this._timer = null;
460
1410
  }
461
1411
 
462
- _outsideClickListener(event) {
463
- const eventPath = event.composedPath();
464
- if (!eventPath.includes(this.positionTarget) && !eventPath.includes(this)) {
465
- this.close();
1412
+ /**
1413
+ * Sets the scheduler; that is, a module with the Async interface,
1414
+ * a callback and optional arguments to be passed to the run function
1415
+ * from the async module.
1416
+ *
1417
+ * @param {!AsyncInterface} asyncModule Object with Async interface.
1418
+ * @param {function()} callback Callback to run.
1419
+ * @return {void}
1420
+ */
1421
+ setConfig(asyncModule, callback) {
1422
+ this._asyncModule = asyncModule;
1423
+ this._callback = callback;
1424
+ this._timer = this._asyncModule.run(() => {
1425
+ this._timer = null;
1426
+ debouncerQueue.delete(this);
1427
+ this._callback();
1428
+ });
1429
+ }
1430
+
1431
+ /**
1432
+ * Cancels an active debouncer and returns a reference to itself.
1433
+ *
1434
+ * @return {void}
1435
+ */
1436
+ cancel() {
1437
+ if (this.isActive()) {
1438
+ this._cancelAsync();
1439
+ // Canceling a debouncer removes its spot from the flush queue,
1440
+ // so if a debouncer is manually canceled and re-debounced, it
1441
+ // will reset its flush order (this is a very minor difference from 1.x)
1442
+ // Re-debouncing via the `debounce` API retains the 1.x FIFO flush order
1443
+ debouncerQueue.delete(this);
1444
+ }
1445
+ }
1446
+
1447
+ /**
1448
+ * Cancels a debouncer's async callback.
1449
+ *
1450
+ * @return {void}
1451
+ */
1452
+ _cancelAsync() {
1453
+ if (this.isActive()) {
1454
+ this._asyncModule.cancel(/** @type {number} */ (this._timer));
1455
+ this._timer = null;
466
1456
  }
467
1457
  }
468
1458
 
469
- _setOverlayWidth(positionTarget, opened) {
470
- if (positionTarget && opened) {
471
- const propPrefix = this.localName;
472
- this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
1459
+ /**
1460
+ * Flushes an active debouncer and returns a reference to itself.
1461
+ *
1462
+ * @return {void}
1463
+ */
1464
+ flush() {
1465
+ if (this.isActive()) {
1466
+ this.cancel();
1467
+ this._callback();
1468
+ }
1469
+ }
473
1470
 
474
- const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
1471
+ /**
1472
+ * Returns true if the debouncer is active.
1473
+ *
1474
+ * @return {boolean} True if active.
1475
+ */
1476
+ isActive() {
1477
+ return this._timer != null;
1478
+ }
1479
+ }
475
1480
 
476
- if (customWidth === '') {
477
- this.style.removeProperty(`--${propPrefix}-width`);
478
- } else {
479
- this.style.setProperty(`--${propPrefix}-width`, customWidth);
480
- }
1481
+ /**
1482
+ * Adds a `Debouncer` to a list of globally flushable tasks.
1483
+ *
1484
+ * @param {!Debouncer} debouncer Debouncer to enqueue
1485
+ * @return {void}
1486
+ */
1487
+ function enqueueDebouncer(debouncer) {
1488
+ debouncerQueue.add(debouncer);
1489
+ }
481
1490
 
482
- this._updatePosition();
1491
+ /**
1492
+ * Flushes any enqueued debouncers
1493
+ *
1494
+ * @return {boolean} Returns whether any debouncers were flushed
1495
+ */
1496
+ function flushDebouncers() {
1497
+ const didFlush = Boolean(debouncerQueue.size);
1498
+ // If new debouncers are added while flushing, Set.forEach will ensure
1499
+ // newly added ones are also flushed
1500
+ debouncerQueue.forEach((debouncer) => {
1501
+ try {
1502
+ debouncer.flush();
1503
+ } catch (e) {
1504
+ setTimeout(() => {
1505
+ throw e;
1506
+ });
483
1507
  }
484
- }
1508
+ });
1509
+ return didFlush;
485
1510
  }
486
1511
 
487
- customElements.define(ComboBoxOverlay.is, ComboBoxOverlay);
1512
+ const flush = () => {
1513
+ let debouncers;
1514
+ do {
1515
+ debouncers = flushDebouncers();
1516
+ } while (debouncers);
1517
+ };
488
1518
 
489
1519
  /**
490
1520
  * @license
@@ -496,7 +1526,7 @@ customElements.define(ComboBoxOverlay.is, ComboBoxOverlay);
496
1526
  * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
497
1527
  */
498
1528
 
499
- const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
1529
+ const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/u);
500
1530
  const IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
501
1531
  const DEFAULT_PHYSICAL_COUNT = 3;
502
1532
 
@@ -986,9 +2016,12 @@ const ironList = {
986
2016
  this._physicalIndexForKey = {};
987
2017
  this._firstVisibleIndexVal = null;
988
2018
  this._lastVisibleIndexVal = null;
989
- this._physicalCount = this._physicalCount || 0;
990
- this._physicalItems = this._physicalItems || [];
991
- this._physicalSizes = this._physicalSizes || [];
2019
+ if (!this._physicalItems) {
2020
+ this._physicalItems = [];
2021
+ }
2022
+ if (!this._physicalSizes) {
2023
+ this._physicalSizes = [];
2024
+ }
992
2025
  this._physicalStart = 0;
993
2026
  if (this._scrollTop > this._scrollOffset) {
994
2027
  this._resetScrollPosition(0);
@@ -1097,16 +2130,21 @@ const ironList = {
1097
2130
  * @param {boolean=} forceUpdate If true, updates the height no matter what.
1098
2131
  */
1099
2132
  _updateScrollerSize(forceUpdate) {
1100
- this._estScrollHeight =
2133
+ const estScrollHeight =
1101
2134
  this._physicalBottom +
1102
2135
  Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
1103
2136
 
1104
- forceUpdate = forceUpdate || this._scrollHeight === 0;
1105
- forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
2137
+ this._estScrollHeight = estScrollHeight;
2138
+
1106
2139
  // 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;
2140
+ if (
2141
+ forceUpdate ||
2142
+ this._scrollHeight === 0 ||
2143
+ this._scrollPosition >= estScrollHeight - this._physicalSize ||
2144
+ Math.abs(estScrollHeight - this._scrollHeight) >= this._viewportHeight
2145
+ ) {
2146
+ this.$.items.style.height = `${estScrollHeight}px`;
2147
+ this._scrollHeight = estScrollHeight;
1110
2148
  }
1111
2149
  },
1112
2150
 
@@ -1202,7 +2240,9 @@ const ironList = {
1202
2240
  },
1203
2241
 
1204
2242
  _debounce(name, cb, asyncModule) {
1205
- this._debouncers = this._debouncers || {};
2243
+ if (!this._debouncers) {
2244
+ this._debouncers = {};
2245
+ }
1206
2246
  this._debouncers[name] = Debouncer.debounce(this._debouncers[name], asyncModule, cb.bind(this));
1207
2247
  enqueueDebouncer(this._debouncers[name]);
1208
2248
  },
@@ -1210,7 +2250,7 @@ const ironList = {
1210
2250
 
1211
2251
  /**
1212
2252
  * @license
1213
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
2253
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1214
2254
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1215
2255
  */
1216
2256
 
@@ -1340,11 +2380,15 @@ class IronListAdapter {
1340
2380
  }
1341
2381
 
1342
2382
  update(startIndex = 0, endIndex = this.size - 1) {
2383
+ const updatedElements = [];
1343
2384
  this.__getVisibleElements().forEach((el) => {
1344
2385
  if (el.__virtualIndex >= startIndex && el.__virtualIndex <= endIndex) {
1345
2386
  this.__updateElement(el, el.__virtualIndex, true);
2387
+ updatedElements.push(el);
1346
2388
  }
1347
2389
  });
2390
+
2391
+ this.__afterElementsUpdated(updatedElements);
1348
2392
  }
1349
2393
 
1350
2394
  /**
@@ -1407,28 +2451,40 @@ class IronListAdapter {
1407
2451
  this.updateElement(el, index);
1408
2452
  el.__lastUpdatedIndex = index;
1409
2453
  }
2454
+ }
1410
2455
 
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
- }
2456
+ /**
2457
+ * Called synchronously right after elements have been updated.
2458
+ * This is a good place to do any post-update work.
2459
+ *
2460
+ * @param {!Array<!HTMLElement>} updatedElements
2461
+ */
2462
+ __afterElementsUpdated(updatedElements) {
2463
+ updatedElements.forEach((el) => {
2464
+ const elementHeight = el.offsetHeight;
2465
+ if (elementHeight === 0) {
2466
+ // If the elements have 0 height after update (for example due to lazy rendering),
2467
+ // it results in iron-list requesting to create an unlimited count of elements.
2468
+ // Assign a temporary placeholder sizing to elements that would otherwise end up having
2469
+ // no height.
2470
+ el.style.paddingTop = `${this.__placeholderHeight}px`;
2471
+
2472
+ // Manually schedule the resize handler to make sure the placeholder padding is
2473
+ // cleared in case the resize observer never triggers.
2474
+ this.__placeholderClearDebouncer = Debouncer.debounce(this.__placeholderClearDebouncer, animationFrame, () =>
2475
+ this._resizeHandler(),
2476
+ );
2477
+ } else {
2478
+ // Add element height to the queue
2479
+ this.__elementHeightQueue.push(elementHeight);
2480
+ this.__elementHeightQueue.shift();
2481
+
2482
+ // Calculate new placeholder height based on the average of the defined values in the
2483
+ // element height queue
2484
+ const filteredHeights = this.__elementHeightQueue.filter((h) => h !== undefined);
2485
+ this.__placeholderHeight = Math.round(filteredHeights.reduce((a, b) => a + b, 0) / filteredHeights.length);
2486
+ }
2487
+ });
1432
2488
  }
1433
2489
 
1434
2490
  __getIndexScrollOffset(index) {
@@ -1453,42 +2509,35 @@ class IronListAdapter {
1453
2509
  this._debouncers._increasePoolIfNeeded.cancel();
1454
2510
  }
1455
2511
 
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
2512
  // Change the size
1468
2513
  this.__size = size;
1469
2514
 
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);
2515
+ if (!this._physicalItems) {
2516
+ // Not initialized yet
2517
+ this._itemsChanged({
2518
+ path: 'items',
2519
+ });
2520
+ this.__preventElementUpdates = true;
2521
+ flush();
2522
+ this.__preventElementUpdates = false;
2523
+ } else {
2524
+ // Already initialized, just update _virtualCount
2525
+ this._virtualCount = this.items.length;
2526
+ }
1479
2527
 
1480
- const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
1481
- if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
1482
- this._scrollTop += fviOffsetBefore - fviOffsetAfter;
1483
- }
2528
+ // When reducing size while invisible, iron-list does not update items, so
2529
+ // their hidden state is not updated and their __lastUpdatedIndex is not
2530
+ // reset. In that case force an update here.
2531
+ if (!this._isVisible) {
2532
+ this._assignModels();
1484
2533
  }
1485
2534
 
1486
2535
  if (!this.elementsContainer.children.length) {
1487
2536
  requestAnimationFrame(() => this._resizeHandler());
1488
2537
  }
1489
2538
 
1490
- this.__preventElementUpdates = false;
1491
- // Schedule and flush a resize handler
2539
+ // Schedule and flush a resize handler. This will cause a
2540
+ // re-render for the elements.
1492
2541
  this._resizeHandler();
1493
2542
  flush();
1494
2543
  }
@@ -1553,16 +2602,20 @@ class IronListAdapter {
1553
2602
 
1554
2603
  /** @private */
1555
2604
  _assignModels(itemSet) {
2605
+ const updatedElements = [];
1556
2606
  this._iterateItems((pidx, vidx) => {
1557
2607
  const el = this._physicalItems[pidx];
1558
2608
  el.hidden = vidx >= this.size;
1559
2609
  if (!el.hidden) {
1560
2610
  el.__virtualIndex = vidx + (this._vidxOffset || 0);
1561
2611
  this.__updateElement(el, el.__virtualIndex);
2612
+ updatedElements.push(el);
1562
2613
  } else {
1563
2614
  delete el.__lastUpdatedIndex;
1564
2615
  }
1565
2616
  }, itemSet);
2617
+
2618
+ this.__afterElementsUpdated(updatedElements);
1566
2619
  }
1567
2620
 
1568
2621
  /** @private */
@@ -1691,7 +2744,9 @@ class IronListAdapter {
1691
2744
  deltaY *= this._scrollPageHeight;
1692
2745
  }
1693
2746
 
1694
- this._deltaYAcc = this._deltaYAcc || 0;
2747
+ if (!this._deltaYAcc) {
2748
+ this._deltaYAcc = 0;
2749
+ }
1695
2750
 
1696
2751
  if (this._wheelAnimationFrame) {
1697
2752
  // Accumulate wheel delta while a frame is being processed
@@ -1764,6 +2819,29 @@ class IronListAdapter {
1764
2819
  );
1765
2820
  }
1766
2821
 
2822
+ /**
2823
+ * Increases the pool size.
2824
+ * @override
2825
+ */
2826
+ _increasePoolIfNeeded(count) {
2827
+ if (this._physicalCount > 2 && count) {
2828
+ // The iron-list logic has already created some physical items and
2829
+ // has decided to create more. Since each item creation round is
2830
+ // expensive, let's try to create the remaining items in one go.
2831
+
2832
+ // Calculate the total item count that would be needed to fill the viewport
2833
+ // plus the buffer assuming rest of the items to be of the average size
2834
+ // of the items already created.
2835
+ const totalItemCount = Math.ceil(this._optPhysicalSize / this._physicalAverage);
2836
+ const missingItemCount = totalItemCount - this._physicalCount;
2837
+ // Create the remaining items in one go. Use a maximum of 100 items
2838
+ // as a safety measure.
2839
+ super._increasePoolIfNeeded(Math.max(count, Math.min(100, missingItemCount)));
2840
+ } else {
2841
+ super._increasePoolIfNeeded(count);
2842
+ }
2843
+ }
2844
+
1767
2845
  /**
1768
2846
  * @returns {Number|undefined} - The browser's default font-size in pixels
1769
2847
  * @private
@@ -1893,6 +2971,24 @@ class Virtualizer {
1893
2971
  this.__adapter = new IronListAdapter(config);
1894
2972
  }
1895
2973
 
2974
+ /**
2975
+ * Gets the index of the first visible item in the viewport.
2976
+ *
2977
+ * @return {number}
2978
+ */
2979
+ get firstVisibleIndex() {
2980
+ return this.__adapter.adjustedFirstVisibleIndex;
2981
+ }
2982
+
2983
+ /**
2984
+ * Gets the index of the last visible item in the viewport.
2985
+ *
2986
+ * @return {number}
2987
+ */
2988
+ get lastVisibleIndex() {
2989
+ return this.__adapter.adjustedLastVisibleIndex;
2990
+ }
2991
+
1896
2992
  /**
1897
2993
  * The size of the virtualizer
1898
2994
  * @return {number | undefined} The size of the virtualizer
@@ -1940,56 +3036,399 @@ class Virtualizer {
1940
3036
  flush() {
1941
3037
  this.__adapter.flush();
1942
3038
  }
3039
+ }
1943
3040
 
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
- }
3041
+ /**
3042
+ * @license
3043
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
3044
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3045
+ */
1952
3046
 
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;
3047
+ /*
3048
+ * Placeholder object class representing items being loaded.
3049
+ *
3050
+ * @private
3051
+ */
3052
+ const ComboBoxPlaceholder = class ComboBoxPlaceholder {
3053
+ toString() {
3054
+ return '';
1960
3055
  }
1961
- }
3056
+ };
1962
3057
 
1963
3058
  /**
1964
3059
  * @license
1965
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
3060
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1966
3061
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1967
3062
  */
1968
3063
 
1969
- /*
1970
- * Placeholder object class representing items being loaded.
1971
- *
1972
- * @private
1973
- */
1974
- const ComboBoxPlaceholder = class ComboBoxPlaceholder {
1975
- toString() {
1976
- return '';
1977
- }
1978
- };
3064
+ /**
3065
+ * @polymerMixin
3066
+ */
3067
+ const ComboBoxScrollerMixin = (superClass) =>
3068
+ class ComboBoxScrollerMixin extends superClass {
3069
+ static get properties() {
3070
+ return {
3071
+ /**
3072
+ * A full set of items to filter the visible options from.
3073
+ * Set to an empty array when combo-box is not opened.
3074
+ */
3075
+ items: {
3076
+ type: Array,
3077
+ observer: '__itemsChanged',
3078
+ },
3079
+
3080
+ /**
3081
+ * Index of an item that has focus outline and is scrolled into view.
3082
+ * The actual focus still remains in the input field.
3083
+ */
3084
+ focusedIndex: {
3085
+ type: Number,
3086
+ observer: '__focusedIndexChanged',
3087
+ },
3088
+
3089
+ /**
3090
+ * Set to true while combo-box fetches new page from the data provider.
3091
+ */
3092
+ loading: {
3093
+ type: Boolean,
3094
+ observer: '__loadingChanged',
3095
+ },
3096
+
3097
+ /**
3098
+ * Whether the combo-box is currently opened or not. If set to false,
3099
+ * calling `scrollIntoView` does not have any effect.
3100
+ */
3101
+ opened: {
3102
+ type: Boolean,
3103
+ observer: '__openedChanged',
3104
+ },
3105
+
3106
+ /**
3107
+ * The selected item from the `items` array.
3108
+ */
3109
+ selectedItem: {
3110
+ type: Object,
3111
+ observer: '__selectedItemChanged',
3112
+ },
3113
+
3114
+ /**
3115
+ * Path for the id of the item, used to detect whether the item is selected.
3116
+ */
3117
+ itemIdPath: {
3118
+ type: String,
3119
+ },
3120
+
3121
+ /**
3122
+ * Reference to the owner (combo-box owner), used by the item elements.
3123
+ */
3124
+ owner: {
3125
+ type: Object,
3126
+ },
3127
+
3128
+ /**
3129
+ * Function used to set a label for every combo-box item.
3130
+ */
3131
+ getItemLabel: {
3132
+ type: Object,
3133
+ },
3134
+
3135
+ /**
3136
+ * Function used to render the content of every combo-box item.
3137
+ */
3138
+ renderer: {
3139
+ type: Object,
3140
+ observer: '__rendererChanged',
3141
+ },
3142
+
3143
+ /**
3144
+ * Used to propagate the `theme` attribute from the host element.
3145
+ */
3146
+ theme: {
3147
+ type: String,
3148
+ },
3149
+ };
3150
+ }
3151
+
3152
+ constructor() {
3153
+ super();
3154
+ this.__boundOnItemClick = this.__onItemClick.bind(this);
3155
+ }
3156
+
3157
+ /** @private */
3158
+ get _viewportTotalPaddingBottom() {
3159
+ if (this._cachedViewportTotalPaddingBottom === undefined) {
3160
+ const itemsStyle = window.getComputedStyle(this.$.selector);
3161
+ this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
3162
+ .map((v) => {
3163
+ return parseInt(v, 10);
3164
+ })
3165
+ .reduce((sum, v) => {
3166
+ return sum + v;
3167
+ });
3168
+ }
3169
+
3170
+ return this._cachedViewportTotalPaddingBottom;
3171
+ }
3172
+
3173
+ /** @protected */
3174
+ ready() {
3175
+ super.ready();
3176
+
3177
+ this.setAttribute('role', 'listbox');
3178
+
3179
+ // Ensure every instance has unique ID
3180
+ this.id = `${this.localName}-${generateUniqueId()}`;
3181
+
3182
+ // Allow extensions to customize tag name for the items
3183
+ this.__hostTagName = this.constructor.is.replace('-scroller', '');
3184
+
3185
+ this.addEventListener('click', (e) => e.stopPropagation());
3186
+
3187
+ this.__patchWheelOverScrolling();
3188
+
3189
+ this.__virtualizer = new Virtualizer({
3190
+ createElements: this.__createElements.bind(this),
3191
+ updateElement: this._updateElement.bind(this),
3192
+ elementsContainer: this,
3193
+ scrollTarget: this,
3194
+ scrollContainer: this.$.selector,
3195
+ });
3196
+ }
3197
+
3198
+ /**
3199
+ * Requests an update for the virtualizer to re-render items.
3200
+ */
3201
+ requestContentUpdate() {
3202
+ if (this.__virtualizer) {
3203
+ this.__virtualizer.update();
3204
+ }
3205
+ }
3206
+
3207
+ /**
3208
+ * Scrolls an item at given index into view and adjusts `scrollTop`
3209
+ * so that the element gets fully visible on Arrow Down key press.
3210
+ * @param {number} index
3211
+ */
3212
+ scrollIntoView(index) {
3213
+ if (!(this.opened && index >= 0)) {
3214
+ return;
3215
+ }
3216
+
3217
+ const visibleItemsCount = this._visibleItemsCount();
3218
+
3219
+ let targetIndex = index;
3220
+
3221
+ if (index > this.__virtualizer.lastVisibleIndex - 1) {
3222
+ // Index is below the bottom, scrolling down. Make the item appear at the bottom.
3223
+ // First scroll to target (will be at the top of the scroller) to make sure it's rendered.
3224
+ this.__virtualizer.scrollToIndex(index);
3225
+ // Then calculate the index for the following scroll (to get the target to bottom of the scroller).
3226
+ targetIndex = index - visibleItemsCount + 1;
3227
+ } else if (index > this.__virtualizer.firstVisibleIndex) {
3228
+ // The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
3229
+ // the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
3230
+ targetIndex = this.__virtualizer.firstVisibleIndex;
3231
+ }
3232
+ this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
3233
+
3234
+ // Sometimes the item is partly below the bottom edge, detect and adjust.
3235
+ const lastPhysicalItem = [...this.children].find(
3236
+ (el) => !el.hidden && el.index === this.__virtualizer.lastVisibleIndex,
3237
+ );
3238
+ if (!lastPhysicalItem || index !== lastPhysicalItem.index) {
3239
+ return;
3240
+ }
3241
+ const lastPhysicalItemRect = lastPhysicalItem.getBoundingClientRect();
3242
+ const scrollerRect = this.getBoundingClientRect();
3243
+ const scrollTopAdjust = lastPhysicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
3244
+ if (scrollTopAdjust > 0) {
3245
+ this.scrollTop += scrollTopAdjust;
3246
+ }
3247
+ }
3248
+
3249
+ /**
3250
+ * @param {string | object} item
3251
+ * @param {string | object} selectedItem
3252
+ * @param {string} itemIdPath
3253
+ * @protected
3254
+ */
3255
+ _isItemSelected(item, selectedItem, itemIdPath) {
3256
+ if (item instanceof ComboBoxPlaceholder) {
3257
+ return false;
3258
+ } else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
3259
+ return get(itemIdPath, item) === get(itemIdPath, selectedItem);
3260
+ }
3261
+ return item === selectedItem;
3262
+ }
3263
+
3264
+ /** @private */
3265
+ __itemsChanged(items) {
3266
+ if (this.__virtualizer && items) {
3267
+ this.__virtualizer.size = items.length;
3268
+ this.__virtualizer.flush();
3269
+ this.requestContentUpdate();
3270
+ }
3271
+ }
3272
+
3273
+ /** @private */
3274
+ __loadingChanged() {
3275
+ this.requestContentUpdate();
3276
+ }
3277
+
3278
+ /** @private */
3279
+ __openedChanged(opened) {
3280
+ if (opened) {
3281
+ this.requestContentUpdate();
3282
+ }
3283
+ }
3284
+
3285
+ /** @private */
3286
+ __selectedItemChanged() {
3287
+ this.requestContentUpdate();
3288
+ }
3289
+
3290
+ /** @private */
3291
+ __focusedIndexChanged(index, oldIndex) {
3292
+ if (index !== oldIndex) {
3293
+ this.requestContentUpdate();
3294
+ }
3295
+
3296
+ // Do not jump back to the previously focused item while loading
3297
+ // when requesting next page from the data provider on scroll.
3298
+ if (index >= 0 && !this.loading) {
3299
+ this.scrollIntoView(index);
3300
+ }
3301
+ }
3302
+
3303
+ /** @private */
3304
+ __rendererChanged(renderer, oldRenderer) {
3305
+ if (renderer || oldRenderer) {
3306
+ this.requestContentUpdate();
3307
+ }
3308
+ }
3309
+
3310
+ /** @private */
3311
+ __createElements(count) {
3312
+ return [...Array(count)].map(() => {
3313
+ const item = document.createElement(`${this.__hostTagName}-item`);
3314
+ item.addEventListener('click', this.__boundOnItemClick);
3315
+ // Negative tabindex prevents the item content from being focused.
3316
+ item.tabIndex = '-1';
3317
+ item.style.width = '100%';
3318
+ return item;
3319
+ });
3320
+ }
3321
+
3322
+ /**
3323
+ * @param {HTMLElement} el
3324
+ * @param {number} index
3325
+ * @protected
3326
+ */
3327
+ _updateElement(el, index) {
3328
+ const item = this.items[index];
3329
+ const focusedIndex = this.focusedIndex;
3330
+ const selected = this._isItemSelected(item, this.selectedItem, this.itemIdPath);
3331
+
3332
+ el.setProperties({
3333
+ item,
3334
+ index,
3335
+ label: this.getItemLabel(item),
3336
+ selected,
3337
+ renderer: this.renderer,
3338
+ focused: !this.loading && focusedIndex === index,
3339
+ });
3340
+
3341
+ el.id = `${this.__hostTagName}-item-${index}`;
3342
+ el.setAttribute('role', index !== undefined ? 'option' : false);
3343
+ el.setAttribute('aria-selected', selected.toString());
3344
+ el.setAttribute('aria-posinset', index + 1);
3345
+ el.setAttribute('aria-setsize', this.items.length);
3346
+
3347
+ if (this.theme) {
3348
+ el.setAttribute('theme', this.theme);
3349
+ } else {
3350
+ el.removeAttribute('theme');
3351
+ }
3352
+
3353
+ if (item instanceof ComboBoxPlaceholder) {
3354
+ this.__requestItemByIndex(index);
3355
+ }
3356
+ }
3357
+
3358
+ /** @private */
3359
+ __onItemClick(e) {
3360
+ this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
3361
+ }
3362
+
3363
+ /**
3364
+ * We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
3365
+ * Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
3366
+ * scrolling the parent similarly to touch scrolling.
3367
+ * @private
3368
+ */
3369
+ __patchWheelOverScrolling() {
3370
+ this.$.selector.addEventListener('wheel', (e) => {
3371
+ const scrolledToTop = this.scrollTop === 0;
3372
+ const scrolledToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= 1;
3373
+ if (scrolledToTop && e.deltaY < 0) {
3374
+ e.preventDefault();
3375
+ } else if (scrolledToBottom && e.deltaY > 0) {
3376
+ e.preventDefault();
3377
+ }
3378
+ });
3379
+ }
3380
+
3381
+ /**
3382
+ * Dispatches an `index-requested` event for the given index to notify
3383
+ * the data provider that it should start loading the page containing the requested index.
3384
+ *
3385
+ * The event is dispatched asynchronously to prevent an immediate page request and therefore
3386
+ * a possible infinite recursion in case the data provider implements page request cancelation logic
3387
+ * by invoking data provider page callbacks with an empty array.
3388
+ * The infinite recursion may occur otherwise since invoking a data provider page callback with an empty array
3389
+ * triggers a synchronous scroller update and, if the callback corresponds to the currently visible page,
3390
+ * the scroller will synchronously request the page again which may lead to looping in the end.
3391
+ * That was the case for the Flow counterpart:
3392
+ * https://github.com/vaadin/flow-components/issues/3553#issuecomment-1239344828
3393
+ * @private
3394
+ */
3395
+ __requestItemByIndex(index) {
3396
+ requestAnimationFrame(() => {
3397
+ this.dispatchEvent(
3398
+ new CustomEvent('index-requested', {
3399
+ detail: {
3400
+ index,
3401
+ currentScrollerPos: this._oldScrollerPosition,
3402
+ },
3403
+ }),
3404
+ );
3405
+ });
3406
+ }
3407
+
3408
+ /** @private */
3409
+ _visibleItemsCount() {
3410
+ // Ensure items are positioned
3411
+ this.__virtualizer.scrollToIndex(this.__virtualizer.firstVisibleIndex);
3412
+ const hasItems = this.__virtualizer.size > 0;
3413
+ return hasItems ? this.__virtualizer.lastVisibleIndex - this.__virtualizer.firstVisibleIndex + 1 : 0;
3414
+ }
3415
+ };
1979
3416
 
1980
3417
  /**
1981
3418
  * @license
1982
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
3419
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
1983
3420
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1984
3421
  */
1985
3422
 
1986
3423
  /**
1987
- * Element for internal use only.
3424
+ * An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
1988
3425
  *
3426
+ * @customElement
1989
3427
  * @extends HTMLElement
3428
+ * @mixes ComboBoxScrollerMixin
1990
3429
  * @private
1991
3430
  */
1992
- class ComboBoxScroller extends PolymerElement {
3431
+ class ComboBoxScroller extends ComboBoxScrollerMixin(PolymerElement) {
1993
3432
  static get is() {
1994
3433
  return 'vaadin-combo-box-scroller';
1995
3434
  }
@@ -2015,7 +3454,7 @@ class ComboBoxScroller extends PolymerElement {
2015
3454
  #selector {
2016
3455
  border-width: var(--_vaadin-combo-box-items-container-border-width);
2017
3456
  border-style: var(--_vaadin-combo-box-items-container-border-style);
2018
- border-color: var(--_vaadin-combo-box-items-container-border-color);
3457
+ border-color: var(--_vaadin-combo-box-items-container-border-color, transparent);
2019
3458
  position: relative;
2020
3459
  }
2021
3460
  </style>
@@ -2024,349 +3463,642 @@ class ComboBoxScroller extends PolymerElement {
2024
3463
  </div>
2025
3464
  `;
2026
3465
  }
3466
+ }
2027
3467
 
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
- },
3468
+ defineCustomElement(ComboBoxScroller);
2038
3469
 
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
- },
3470
+ /**
3471
+ * @license
3472
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
3473
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3474
+ */
2047
3475
 
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
- },
3476
+ if (!window.Vaadin) {
3477
+ window.Vaadin = {};
3478
+ }
2055
3479
 
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
- },
3480
+ /**
3481
+ * Array of Vaadin custom element classes that have been finalized.
3482
+ */
3483
+ if (!window.Vaadin.registrations) {
3484
+ window.Vaadin.registrations = [];
3485
+ }
2064
3486
 
2065
- /**
2066
- * The selected item from the `items` array.
2067
- */
2068
- selectedItem: {
2069
- type: Object,
2070
- observer: '__selectedItemChanged',
2071
- },
3487
+ if (!window.Vaadin.developmentModeCallback) {
3488
+ window.Vaadin.developmentModeCallback = {};
3489
+ }
2072
3490
 
2073
- /**
2074
- * Path for the id of the item, used to detect whether the item is selected.
2075
- */
2076
- itemIdPath: {
2077
- type: String,
2078
- },
3491
+ window.Vaadin.developmentModeCallback['vaadin-usage-statistics'] = function () {
3492
+ usageStatistics();
3493
+ };
2079
3494
 
2080
- /**
2081
- * Reference to the combo-box, used by the item elements.
2082
- */
2083
- comboBox: {
2084
- type: Object,
2085
- },
3495
+ let statsJob;
2086
3496
 
2087
- /**
2088
- * Function used to set a label for every combo-box item.
2089
- */
2090
- getItemLabel: {
2091
- type: Object,
2092
- },
3497
+ const registered = new Set();
2093
3498
 
2094
- /**
2095
- * Function used to render the content of every combo-box item.
2096
- */
2097
- renderer: {
2098
- type: Object,
2099
- observer: '__rendererChanged',
2100
- },
3499
+ /**
3500
+ * @polymerMixin
3501
+ * @mixes DirMixin
3502
+ */
3503
+ const ElementMixin = (superClass) =>
3504
+ class VaadinElementMixin extends DirMixin(superClass) {
3505
+ static get version() {
3506
+ return '24.2.3';
3507
+ }
2101
3508
 
2102
- /**
2103
- * Used to propagate the `theme` attribute from the host element.
2104
- */
2105
- theme: {
2106
- type: String,
2107
- },
3509
+ /** @protected */
3510
+ static finalize() {
3511
+ super.finalize();
3512
+
3513
+ const { is } = this;
3514
+
3515
+ // Registers a class prototype for telemetry purposes.
3516
+ if (is && !registered.has(is)) {
3517
+ window.Vaadin.registrations.push(this);
3518
+ registered.add(is);
3519
+
3520
+ if (window.Vaadin.developmentModeCallback) {
3521
+ statsJob = Debouncer.debounce(statsJob, idlePeriod, () => {
3522
+ window.Vaadin.developmentModeCallback['vaadin-usage-statistics']();
3523
+ });
3524
+ enqueueDebouncer(statsJob);
3525
+ }
3526
+ }
3527
+ }
3528
+
3529
+ constructor() {
3530
+ super();
3531
+
3532
+ if (document.doctype === null) {
3533
+ console.warn(
3534
+ 'Vaadin components require the "standards mode" declaration. Please add <!DOCTYPE html> to the HTML document.',
3535
+ );
3536
+ }
3537
+ }
3538
+ };
3539
+
3540
+ /**
3541
+ * @license
3542
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
3543
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3544
+ */
3545
+
3546
+ /**
3547
+ * Returns true if the given node is an empty text node, false otherwise.
3548
+ *
3549
+ * @param {Node} node
3550
+ * @return {boolean}
3551
+ */
3552
+ function isEmptyTextNode(node) {
3553
+ return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '';
3554
+ }
3555
+
3556
+ /**
3557
+ * @license
3558
+ * Copyright (c) 2023 Vaadin Ltd.
3559
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3560
+ */
3561
+
3562
+ /**
3563
+ * A helper for observing slot changes.
3564
+ */
3565
+ class SlotObserver {
3566
+ constructor(slot, callback) {
3567
+ /** @type HTMLSlotElement */
3568
+ this.slot = slot;
3569
+
3570
+ /** @type Function */
3571
+ this.callback = callback;
3572
+
3573
+ /** @type {Node[]} */
3574
+ this._storedNodes = [];
3575
+
3576
+ this._connected = false;
3577
+ this._scheduled = false;
3578
+
3579
+ this._boundSchedule = () => {
3580
+ this._schedule();
2108
3581
  };
3582
+
3583
+ this.connect();
3584
+ this._schedule();
2109
3585
  }
2110
3586
 
2111
- constructor() {
2112
- super();
2113
- this.__boundOnItemClick = this.__onItemClick.bind(this);
3587
+ /**
3588
+ * Activates an observer. This method is automatically called when
3589
+ * a `SlotObserver` is created. It should only be called to re-activate
3590
+ * an observer that has been deactivated via the `disconnect` method.
3591
+ */
3592
+ connect() {
3593
+ this.slot.addEventListener('slotchange', this._boundSchedule);
3594
+ this._connected = true;
2114
3595
  }
2115
3596
 
2116
- __openedChanged(opened) {
2117
- if (opened) {
2118
- this.requestContentUpdate();
3597
+ /**
3598
+ * Deactivates the observer. After calling this method the observer callback
3599
+ * will not be called when changes to slotted nodes occur. The `connect` method
3600
+ * may be subsequently called to reactivate the observer.
3601
+ */
3602
+ disconnect() {
3603
+ this.slot.removeEventListener('slotchange', this._boundSchedule);
3604
+ this._connected = false;
3605
+ }
3606
+
3607
+ /** @private */
3608
+ _schedule() {
3609
+ if (!this._scheduled) {
3610
+ this._scheduled = true;
3611
+
3612
+ queueMicrotask(() => {
3613
+ this.flush();
3614
+ });
2119
3615
  }
2120
3616
  }
2121
3617
 
2122
- /** @protected */
2123
- ready() {
2124
- super.ready();
3618
+ /**
3619
+ * Run the observer callback synchronously.
3620
+ */
3621
+ flush() {
3622
+ if (!this._connected) {
3623
+ return;
3624
+ }
2125
3625
 
2126
- // Ensure every instance has unique ID
2127
- this.id = `${this.localName}-${generateUniqueId()}`;
3626
+ this._scheduled = false;
2128
3627
 
2129
- // Allow extensions to customize tag name for the items
2130
- this.__hostTagName = this.constructor.is.replace('-scroller', '');
3628
+ this._processNodes();
3629
+ }
2131
3630
 
2132
- this.setAttribute('role', 'listbox');
3631
+ /** @private */
3632
+ _processNodes() {
3633
+ const currentNodes = this.slot.assignedNodes({ flatten: true });
2133
3634
 
2134
- this.addEventListener('click', (e) => e.stopPropagation());
3635
+ let addedNodes = [];
3636
+ const removedNodes = [];
3637
+ const movedNodes = [];
2135
3638
 
2136
- this.__patchWheelOverScrolling();
3639
+ if (currentNodes.length) {
3640
+ addedNodes = currentNodes.filter((node) => !this._storedNodes.includes(node));
3641
+ }
2137
3642
 
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
- });
3643
+ if (this._storedNodes.length) {
3644
+ this._storedNodes.forEach((node, index) => {
3645
+ const idx = currentNodes.indexOf(node);
3646
+ if (idx === -1) {
3647
+ removedNodes.push(node);
3648
+ } else if (idx !== index) {
3649
+ movedNodes.push(node);
3650
+ }
3651
+ });
3652
+ }
3653
+
3654
+ if (addedNodes.length || removedNodes.length || movedNodes.length) {
3655
+ this.callback({ addedNodes, movedNodes, removedNodes });
3656
+ }
3657
+
3658
+ this._storedNodes = currentNodes;
3659
+ }
3660
+ }
3661
+
3662
+ /**
3663
+ * @license
3664
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
3665
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3666
+ */
3667
+
3668
+ /**
3669
+ * A controller for providing content to slot element and observing changes.
3670
+ */
3671
+ class SlotController extends EventTarget {
3672
+ /**
3673
+ * Ensure that every instance has unique ID.
3674
+ *
3675
+ * @param {HTMLElement} host
3676
+ * @param {string} slotName
3677
+ * @return {string}
3678
+ * @protected
3679
+ */
3680
+ static generateId(host, slotName) {
3681
+ const prefix = slotName || 'default';
3682
+ return `${prefix}-${host.localName}-${generateUniqueId()}`;
2145
3683
  }
2146
3684
 
2147
- requestContentUpdate() {
2148
- if (this.__virtualizer) {
2149
- this.__virtualizer.update();
3685
+ constructor(host, slotName, tagName, config = {}) {
3686
+ super();
3687
+
3688
+ const { initializer, multiple, observe, useUniqueId } = config;
3689
+
3690
+ this.host = host;
3691
+ this.slotName = slotName;
3692
+ this.tagName = tagName;
3693
+ this.observe = typeof observe === 'boolean' ? observe : true;
3694
+ this.multiple = typeof multiple === 'boolean' ? multiple : false;
3695
+ this.slotInitializer = initializer;
3696
+
3697
+ if (multiple) {
3698
+ this.nodes = [];
3699
+ }
3700
+
3701
+ // Only generate the default ID if requested by the controller.
3702
+ if (useUniqueId) {
3703
+ this.defaultId = this.constructor.generateId(host, slotName);
2150
3704
  }
2151
3705
  }
2152
3706
 
2153
- scrollIntoView(index) {
2154
- if (!(this.opened && index >= 0)) {
2155
- return;
3707
+ hostConnected() {
3708
+ if (!this.initialized) {
3709
+ if (this.multiple) {
3710
+ this.initMultiple();
3711
+ } else {
3712
+ this.initSingle();
3713
+ }
3714
+
3715
+ if (this.observe) {
3716
+ this.observeSlot();
3717
+ }
3718
+
3719
+ this.initialized = true;
2156
3720
  }
3721
+ }
2157
3722
 
2158
- const visibleItemsCount = this._visibleItemsCount();
3723
+ /** @protected */
3724
+ initSingle() {
3725
+ let node = this.getSlotChild();
2159
3726
 
2160
- let targetIndex = index;
3727
+ if (!node) {
3728
+ node = this.attachDefaultNode();
3729
+ this.initNode(node);
3730
+ } else {
3731
+ this.node = node;
3732
+ this.initAddedNode(node);
3733
+ }
3734
+ }
2161
3735
 
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;
3736
+ /** @protected */
3737
+ initMultiple() {
3738
+ const children = this.getSlotChildren();
3739
+
3740
+ if (children.length === 0) {
3741
+ const defaultNode = this.attachDefaultNode();
3742
+ if (defaultNode) {
3743
+ this.nodes = [defaultNode];
3744
+ this.initNode(defaultNode);
3745
+ }
3746
+ } else {
3747
+ this.nodes = children;
3748
+ children.forEach((node) => {
3749
+ this.initAddedNode(node);
3750
+ });
2172
3751
  }
2173
- this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
3752
+ }
2174
3753
 
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;
3754
+ /**
3755
+ * Create and attach default node using the provided tag name, if any.
3756
+ * @return {Node | undefined}
3757
+ * @protected
3758
+ */
3759
+ attachDefaultNode() {
3760
+ const { host, slotName, tagName } = this;
3761
+
3762
+ // Check if the node was created previously and if so, reuse it.
3763
+ let node = this.defaultNode;
3764
+
3765
+ // Tag name is optional, sometimes we don't init default content.
3766
+ if (!node && tagName) {
3767
+ node = document.createElement(tagName);
3768
+ if (node instanceof Element) {
3769
+ if (slotName !== '') {
3770
+ node.setAttribute('slot', slotName);
3771
+ }
3772
+ this.node = node;
3773
+ this.defaultNode = node;
3774
+ }
2181
3775
  }
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;
3776
+
3777
+ if (node) {
3778
+ host.appendChild(node);
2187
3779
  }
3780
+
3781
+ return node;
2188
3782
  }
2189
3783
 
2190
- /** @private */
2191
- __getAriaRole(itemIndex) {
2192
- return itemIndex !== undefined ? 'option' : false;
3784
+ /**
3785
+ * Return the list of nodes matching the slot managed by the controller.
3786
+ * @return {Node}
3787
+ */
3788
+ getSlotChildren() {
3789
+ const { slotName } = this;
3790
+ return Array.from(this.host.childNodes).filter((node) => {
3791
+ // Either an element (any slot) or a text node (only un-named slot).
3792
+ return (
3793
+ (node.nodeType === Node.ELEMENT_NODE && node.slot === slotName) ||
3794
+ (node.nodeType === Node.TEXT_NODE && node.textContent.trim() && slotName === '')
3795
+ );
3796
+ });
2193
3797
  }
2194
3798
 
2195
- /** @private */
2196
- __isItemFocused(focusedIndex, itemIndex) {
2197
- return !this.loading && focusedIndex === itemIndex;
3799
+ /**
3800
+ * Return a reference to the node managed by the controller.
3801
+ * @return {Node}
3802
+ */
3803
+ getSlotChild() {
3804
+ return this.getSlotChildren()[0];
2198
3805
  }
2199
3806
 
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);
3807
+ /**
3808
+ * Run `slotInitializer` for the node managed by the controller.
3809
+ *
3810
+ * @param {Node} node
3811
+ * @protected
3812
+ */
3813
+ initNode(node) {
3814
+ const { slotInitializer } = this;
3815
+ // Don't try to bind `this` to initializer (normally it's arrow function).
3816
+ // Instead, pass the host as a first argument to access component's state.
3817
+ if (slotInitializer) {
3818
+ slotInitializer(node, this.host);
2206
3819
  }
2207
- return item === selectedItem;
2208
3820
  }
2209
3821
 
2210
- /** @private */
2211
- __itemsChanged(items) {
2212
- if (this.__virtualizer && items) {
2213
- this.__virtualizer.size = items.length;
2214
- this.__virtualizer.flush();
2215
- this.requestContentUpdate();
3822
+ /**
3823
+ * Override to initialize the newly added custom node.
3824
+ *
3825
+ * @param {Node} _node
3826
+ * @protected
3827
+ */
3828
+ initCustomNode(_node) {}
3829
+
3830
+ /**
3831
+ * Override to teardown slotted node when it's removed.
3832
+ *
3833
+ * @param {Node} _node
3834
+ * @protected
3835
+ */
3836
+ teardownNode(_node) {}
3837
+
3838
+ /**
3839
+ * Run both `initCustomNode` and `initNode` for a custom slotted node.
3840
+ *
3841
+ * @param {Node} node
3842
+ * @protected
3843
+ */
3844
+ initAddedNode(node) {
3845
+ if (node !== this.defaultNode) {
3846
+ this.initCustomNode(node);
3847
+ this.initNode(node);
2216
3848
  }
2217
3849
  }
2218
3850
 
2219
- /** @private */
2220
- __loadingChanged() {
2221
- this.requestContentUpdate();
3851
+ /**
3852
+ * Setup the observer to manage slot content changes.
3853
+ * @protected
3854
+ */
3855
+ observeSlot() {
3856
+ const { slotName } = this;
3857
+ const selector = slotName === '' ? 'slot:not([name])' : `slot[name=${slotName}]`;
3858
+ const slot = this.host.shadowRoot.querySelector(selector);
3859
+
3860
+ this.__slotObserver = new SlotObserver(slot, ({ addedNodes, removedNodes }) => {
3861
+ const current = this.multiple ? this.nodes : [this.node];
3862
+
3863
+ // Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
3864
+ // unlike comment nodes, they are not filtered out. So we need to manually ignore them.
3865
+ const newNodes = addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
3866
+
3867
+ if (removedNodes.length) {
3868
+ this.nodes = current.filter((node) => !removedNodes.includes(node));
3869
+
3870
+ removedNodes.forEach((node) => {
3871
+ this.teardownNode(node);
3872
+ });
3873
+ }
3874
+
3875
+ if (newNodes && newNodes.length > 0) {
3876
+ if (this.multiple) {
3877
+ // Remove default node if exists
3878
+ if (this.defaultNode) {
3879
+ this.defaultNode.remove();
3880
+ }
3881
+ this.nodes = [...current, ...newNodes].filter((node) => node !== this.defaultNode);
3882
+ newNodes.forEach((node) => {
3883
+ this.initAddedNode(node);
3884
+ });
3885
+ } else {
3886
+ // Remove previous node if exists
3887
+ if (this.node) {
3888
+ this.node.remove();
3889
+ }
3890
+ this.node = newNodes[0];
3891
+ this.initAddedNode(this.node);
3892
+ }
3893
+ }
3894
+ });
2222
3895
  }
3896
+ }
2223
3897
 
2224
- /** @private */
2225
- __selectedItemChanged() {
2226
- this.requestContentUpdate();
3898
+ /**
3899
+ * @license
3900
+ * Copyright (c) 2022 - 2023 Vaadin Ltd.
3901
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
3902
+ */
3903
+
3904
+ /**
3905
+ * A controller that manages the slotted tooltip element.
3906
+ */
3907
+ class TooltipController extends SlotController {
3908
+ constructor(host) {
3909
+ // Do not provide slot factory to create tooltip lazily.
3910
+ super(host, 'tooltip');
3911
+
3912
+ this.setTarget(host);
2227
3913
  }
2228
3914
 
2229
- /** @private */
2230
- __focusedIndexChanged(index, oldIndex) {
2231
- if (index !== oldIndex) {
2232
- this.requestContentUpdate();
3915
+ /**
3916
+ * Override to initialize the newly added custom tooltip.
3917
+ *
3918
+ * @param {Node} tooltipNode
3919
+ * @protected
3920
+ * @override
3921
+ */
3922
+ initCustomNode(tooltipNode) {
3923
+ tooltipNode.target = this.target;
3924
+
3925
+ if (this.ariaTarget !== undefined) {
3926
+ tooltipNode.ariaTarget = this.ariaTarget;
3927
+ }
3928
+
3929
+ if (this.context !== undefined) {
3930
+ tooltipNode.context = this.context;
3931
+ }
3932
+
3933
+ if (this.manual !== undefined) {
3934
+ tooltipNode.manual = this.manual;
3935
+ }
3936
+
3937
+ if (this.opened !== undefined) {
3938
+ tooltipNode.opened = this.opened;
2233
3939
  }
2234
3940
 
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);
3941
+ if (this.position !== undefined) {
3942
+ tooltipNode._position = this.position;
2239
3943
  }
2240
- }
2241
3944
 
2242
- /** @private */
2243
- __rendererChanged(renderer, oldRenderer) {
2244
- if (renderer || oldRenderer) {
2245
- this.requestContentUpdate();
3945
+ if (this.shouldShow !== undefined) {
3946
+ tooltipNode.shouldShow = this.shouldShow;
2246
3947
  }
2247
- }
2248
3948
 
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
- });
3949
+ this.__notifyChange();
2259
3950
  }
2260
3951
 
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
- });
3952
+ /**
3953
+ * Override to notify the host when the tooltip is removed.
3954
+ *
3955
+ * @param {Node} tooltipNode
3956
+ * @protected
3957
+ * @override
3958
+ */
3959
+ teardownNode() {
3960
+ this.__notifyChange();
3961
+ }
2275
3962
 
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);
3963
+ /**
3964
+ * Set an HTML element for linking with the tooltip overlay
3965
+ * via `aria-describedby` attribute used by screen readers.
3966
+ * @param {HTMLElement} ariaTarget
3967
+ */
3968
+ setAriaTarget(ariaTarget) {
3969
+ this.ariaTarget = ariaTarget;
2281
3970
 
2282
- if (this.theme) {
2283
- el.setAttribute('theme', this.theme);
2284
- } else {
2285
- el.removeAttribute('theme');
3971
+ const tooltipNode = this.node;
3972
+ if (tooltipNode) {
3973
+ tooltipNode.ariaTarget = ariaTarget;
2286
3974
  }
3975
+ }
2287
3976
 
2288
- if (item instanceof ComboBoxPlaceholder) {
2289
- this.__requestItemByIndex(index);
3977
+ /**
3978
+ * Set a context object to be used by generator.
3979
+ * @param {object} context
3980
+ */
3981
+ setContext(context) {
3982
+ this.context = context;
3983
+
3984
+ const tooltipNode = this.node;
3985
+ if (tooltipNode) {
3986
+ tooltipNode.context = context;
2290
3987
  }
2291
3988
  }
2292
3989
 
2293
- /** @private */
2294
- __onItemClick(e) {
2295
- this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
3990
+ /**
3991
+ * Toggle manual state on the slotted tooltip.
3992
+ * @param {boolean} manual
3993
+ */
3994
+ setManual(manual) {
3995
+ this.manual = manual;
3996
+
3997
+ const tooltipNode = this.node;
3998
+ if (tooltipNode) {
3999
+ tooltipNode.manual = manual;
4000
+ }
2296
4001
  }
2297
4002
 
2298
4003
  /**
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.
4004
+ * Toggle opened state on the slotted tooltip.
4005
+ * @param {boolean} opened
2302
4006
  */
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
- });
4007
+ setOpened(opened) {
4008
+ this.opened = opened;
4009
+
4010
+ const tooltipNode = this.node;
4011
+ if (tooltipNode) {
4012
+ tooltipNode.opened = opened;
4013
+ }
2313
4014
  }
2314
4015
 
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
- });
4016
+ /**
4017
+ * Set default position for the slotted tooltip.
4018
+ * This can be overridden by setting the position
4019
+ * using corresponding property or attribute.
4020
+ * @param {string} position
4021
+ */
4022
+ setPosition(position) {
4023
+ this.position = position;
4024
+
4025
+ const tooltipNode = this.node;
4026
+ if (tooltipNode) {
4027
+ tooltipNode._position = position;
2325
4028
  }
4029
+ }
4030
+
4031
+ /**
4032
+ * Set function used to detect whether to show
4033
+ * the tooltip based on a condition.
4034
+ * @param {Function} shouldShow
4035
+ */
4036
+ setShouldShow(shouldShow) {
4037
+ this.shouldShow = shouldShow;
2326
4038
 
2327
- return this._cachedViewportTotalPaddingBottom;
4039
+ const tooltipNode = this.node;
4040
+ if (tooltipNode) {
4041
+ tooltipNode.shouldShow = shouldShow;
4042
+ }
2328
4043
  }
2329
4044
 
2330
4045
  /**
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
4046
+ * Set an HTML element to attach the tooltip to.
4047
+ * @param {HTMLElement} target
2342
4048
  */
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
- });
4049
+ setTarget(target) {
4050
+ this.target = target;
4051
+
4052
+ const tooltipNode = this.node;
4053
+ if (tooltipNode) {
4054
+ tooltipNode.target = target;
4055
+ }
2354
4056
  }
2355
4057
 
2356
4058
  /** @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;
4059
+ __notifyChange() {
4060
+ this.dispatchEvent(new CustomEvent('tooltip-changed', { detail: { node: this.node } }));
2362
4061
  }
2363
4062
  }
2364
4063
 
2365
- customElements.define(ComboBoxScroller.is, ComboBoxScroller);
4064
+ /**
4065
+ * @license
4066
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
4067
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4068
+ */
4069
+
4070
+ /**
4071
+ * A mixin to provide `pattern` property.
4072
+ *
4073
+ * @polymerMixin
4074
+ * @mixes InputConstraintsMixin
4075
+ */
4076
+ const PatternMixin = (superclass) =>
4077
+ class PatternMixinClass extends InputConstraintsMixin(superclass) {
4078
+ static get properties() {
4079
+ return {
4080
+ /**
4081
+ * A regular expression that the value is checked against.
4082
+ * The pattern must match the entire value, not just some subset.
4083
+ */
4084
+ pattern: {
4085
+ type: String,
4086
+ },
4087
+ };
4088
+ }
4089
+
4090
+ static get delegateAttrs() {
4091
+ return [...super.delegateAttrs, 'pattern'];
4092
+ }
4093
+
4094
+ static get constraints() {
4095
+ return [...super.constraints, 'pattern'];
4096
+ }
4097
+ };
2366
4098
 
2367
4099
  /**
2368
4100
  * @license
2369
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
4101
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2370
4102
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2371
4103
  */
2372
4104
 
@@ -2676,20 +4408,179 @@ const ComboBoxDataProviderMixin = (superClass) =>
2676
4408
  _flushPendingRequests(size) {
2677
4409
  if (this._pendingRequests) {
2678
4410
  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);
4411
+ Object.entries(this._pendingRequests).forEach(([page, callback]) => {
4412
+ if (parseInt(page) >= lastPage) {
4413
+ callback([], size);
2684
4414
  }
4415
+ });
4416
+ }
4417
+ }
4418
+ };
4419
+
4420
+ /**
4421
+ * @license
4422
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
4423
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4424
+ */
4425
+
4426
+ /**
4427
+ * @typedef ReactiveController
4428
+ * @type {import('lit').ReactiveController}
4429
+ */
4430
+
4431
+ /**
4432
+ * A mixin for connecting controllers to the element.
4433
+ *
4434
+ * @polymerMixin
4435
+ */
4436
+ const ControllerMixin = dedupingMixin((superClass) => {
4437
+ // If the superclass extends from LitElement,
4438
+ // use its own controllers implementation.
4439
+ if (typeof superClass.prototype.addController === 'function') {
4440
+ return superClass;
4441
+ }
4442
+
4443
+ return class ControllerMixinClass extends superClass {
4444
+ constructor() {
4445
+ super();
4446
+
4447
+ /**
4448
+ * @type {Set<ReactiveController>}
4449
+ */
4450
+ this.__controllers = new Set();
4451
+ }
4452
+
4453
+ /** @protected */
4454
+ connectedCallback() {
4455
+ super.connectedCallback();
4456
+
4457
+ this.__controllers.forEach((c) => {
4458
+ if (c.hostConnected) {
4459
+ c.hostConnected();
4460
+ }
4461
+ });
4462
+ }
4463
+
4464
+ /** @protected */
4465
+ disconnectedCallback() {
4466
+ super.disconnectedCallback();
4467
+
4468
+ this.__controllers.forEach((c) => {
4469
+ if (c.hostDisconnected) {
4470
+ c.hostDisconnected();
4471
+ }
4472
+ });
4473
+ }
4474
+
4475
+ /**
4476
+ * Registers a controller to participate in the element update cycle.
4477
+ *
4478
+ * @param {ReactiveController} controller
4479
+ * @protected
4480
+ */
4481
+ addController(controller) {
4482
+ this.__controllers.add(controller);
4483
+ // Call hostConnected if a controller is added after the element is attached.
4484
+ if (this.$ !== undefined && this.isConnected && controller.hostConnected) {
4485
+ controller.hostConnected();
4486
+ }
4487
+ }
4488
+
4489
+ /**
4490
+ * Removes a controller from the element.
4491
+ *
4492
+ * @param {ReactiveController} controller
4493
+ * @protected
4494
+ */
4495
+ removeController(controller) {
4496
+ this.__controllers.delete(controller);
4497
+ }
4498
+ };
4499
+ });
4500
+
4501
+ /**
4502
+ * @license
4503
+ * Copyright (c) 2023 Vaadin Ltd.
4504
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4505
+ */
4506
+
4507
+ /**
4508
+ * A mixin that forwards CSS class names to the internal overlay element
4509
+ * by setting the `overlayClass` property or `overlay-class` attribute.
4510
+ *
4511
+ * @polymerMixin
4512
+ */
4513
+ const OverlayClassMixin = (superclass) =>
4514
+ class OverlayClassMixinClass extends superclass {
4515
+ static get properties() {
4516
+ return {
4517
+ /**
4518
+ * A space-delimited list of CSS class names to set on the overlay element.
4519
+ * This property does not affect other CSS class names set manually via JS.
4520
+ *
4521
+ * Note, if the CSS class name was set with this property, clearing it will
4522
+ * remove it from the overlay, even if the same class name was also added
4523
+ * manually, e.g. by using `classList.add()` in the `renderer` function.
4524
+ *
4525
+ * @attr {string} overlay-class
4526
+ */
4527
+ overlayClass: {
4528
+ type: String,
4529
+ },
4530
+
4531
+ /**
4532
+ * An overlay element on which CSS class names are set.
4533
+ *
4534
+ * @protected
4535
+ */
4536
+ _overlayElement: {
4537
+ type: Object,
4538
+ },
4539
+ };
4540
+ }
4541
+
4542
+ static get observers() {
4543
+ return ['__updateOverlayClassNames(overlayClass, _overlayElement)'];
4544
+ }
4545
+
4546
+ /** @private */
4547
+ __updateOverlayClassNames(overlayClass, overlayElement) {
4548
+ if (!overlayElement) {
4549
+ return;
4550
+ }
4551
+
4552
+ // Overlay is set but overlayClass is not set
4553
+ if (overlayClass === undefined) {
4554
+ return;
4555
+ }
4556
+
4557
+ const { classList } = overlayElement;
4558
+
4559
+ if (!this.__initialClasses) {
4560
+ this.__initialClasses = new Set(classList);
4561
+ }
4562
+
4563
+ if (Array.isArray(this.__previousClasses)) {
4564
+ // Remove old classes that no longer apply
4565
+ const classesToRemove = this.__previousClasses.filter((name) => !this.__initialClasses.has(name));
4566
+ if (classesToRemove.length > 0) {
4567
+ classList.remove(...classesToRemove);
2685
4568
  }
2686
4569
  }
4570
+
4571
+ // Add new classes based on the overlayClass
4572
+ const classesToAdd = typeof overlayClass === 'string' ? overlayClass.split(' ') : [];
4573
+ if (classesToAdd.length > 0) {
4574
+ classList.add(...classesToAdd);
4575
+ }
4576
+
4577
+ this.__previousClasses = classesToAdd;
2687
4578
  }
2688
4579
  };
2689
4580
 
2690
4581
  /**
2691
4582
  * @license
2692
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
4583
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2693
4584
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2694
4585
  */
2695
4586
 
@@ -2714,7 +4605,7 @@ function processTemplates(component) {
2714
4605
 
2715
4606
  /**
2716
4607
  * @license
2717
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
4608
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
2718
4609
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2719
4610
  */
2720
4611
 
@@ -2748,10 +4639,19 @@ function findItemIndex(items, callback) {
2748
4639
 
2749
4640
  /**
2750
4641
  * @polymerMixin
4642
+ * @mixes ControllerMixin
4643
+ * @mixes ValidateMixin
4644
+ * @mixes DisabledMixin
4645
+ * @mixes InputMixin
4646
+ * @mixes KeyboardMixin
4647
+ * @mixes FocusMixin
4648
+ * @mixes OverlayClassMixin
2751
4649
  * @param {function(new:HTMLElement)} subclass
2752
4650
  */
2753
4651
  const ComboBoxMixin = (subclass) =>
2754
- class VaadinComboBoxMixinElement extends ControllerMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))) {
4652
+ class ComboBoxMixinClass extends OverlayClassMixin(
4653
+ ControllerMixin(ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))))),
4654
+ ) {
2755
4655
  static get properties() {
2756
4656
  return {
2757
4657
  /**
@@ -2950,7 +4850,6 @@ const ComboBoxMixin = (subclass) =>
2950
4850
 
2951
4851
  constructor() {
2952
4852
  super();
2953
- this._boundOnFocusout = this._onFocusout.bind(this);
2954
4853
  this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
2955
4854
  this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
2956
4855
  this._boundOnClick = this._onClick.bind(this);
@@ -2967,24 +4866,6 @@ const ComboBoxMixin = (subclass) =>
2967
4866
  return 'vaadin-combo-box';
2968
4867
  }
2969
4868
 
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
4869
  /**
2989
4870
  * Get a reference to the native `<input>` element.
2990
4871
  * Override to provide a custom input.
@@ -3035,8 +4916,6 @@ const ComboBoxMixin = (subclass) =>
3035
4916
  this._initOverlay();
3036
4917
  this._initScroller();
3037
4918
 
3038
- this.addEventListener('focusout', this._boundOnFocusout);
3039
-
3040
4919
  this._lastCommittedValue = this.value;
3041
4920
 
3042
4921
  this.addEventListener('click', this._boundOnClick);
@@ -3044,7 +4923,7 @@ const ComboBoxMixin = (subclass) =>
3044
4923
 
3045
4924
  const bringToFrontListener = () => {
3046
4925
  requestAnimationFrame(() => {
3047
- this.$.overlay.bringToFront();
4926
+ this._overlayElement.bringToFront();
3048
4927
  });
3049
4928
  };
3050
4929
 
@@ -3135,6 +5014,8 @@ const ComboBoxMixin = (subclass) =>
3135
5014
  overlay.addEventListener('opened-changed', (e) => {
3136
5015
  this._overlayOpened = e.detail.value;
3137
5016
  });
5017
+
5018
+ this._overlayElement = overlay;
3138
5019
  }
3139
5020
 
3140
5021
  /**
@@ -3146,7 +5027,7 @@ const ComboBoxMixin = (subclass) =>
3146
5027
  _initScroller(host) {
3147
5028
  const scrollerTag = `${this._tagNamePrefix}-scroller`;
3148
5029
 
3149
- const overlay = this.$.overlay;
5030
+ const overlay = this._overlayElement;
3150
5031
 
3151
5032
  overlay.renderer = (root) => {
3152
5033
  if (!root.firstChild) {
@@ -3159,7 +5040,7 @@ const ComboBoxMixin = (subclass) =>
3159
5040
 
3160
5041
  const scroller = overlay.querySelector(scrollerTag);
3161
5042
 
3162
- scroller.comboBox = host || this;
5043
+ scroller.owner = host || this;
3163
5044
  scroller.getItemLabel = this._getItemLabel.bind(this);
3164
5045
  scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
3165
5046
 
@@ -3254,7 +5135,7 @@ const ComboBoxMixin = (subclass) =>
3254
5135
  }
3255
5136
  }
3256
5137
 
3257
- this.$.overlay.restoreFocusOnClose = true;
5138
+ this._overlayElement.restoreFocusOnClose = true;
3258
5139
  } else {
3259
5140
  this._onClosed();
3260
5141
  if (this._openedWithFocusRing && this._isInputFocused()) {
@@ -3288,13 +5169,19 @@ const ComboBoxMixin = (subclass) =>
3288
5169
  return event.composedPath()[0] === this.clearElement;
3289
5170
  }
3290
5171
 
5172
+ /** @private */
5173
+ __onClearButtonMouseDown(event) {
5174
+ event.preventDefault(); // Prevent native focusout event
5175
+ this.inputElement.focus();
5176
+ }
5177
+
3291
5178
  /**
3292
5179
  * @param {Event} event
3293
5180
  * @protected
3294
5181
  */
3295
- _handleClearButtonClick(event) {
5182
+ _onClearButtonClick(event) {
3296
5183
  event.preventDefault();
3297
- this._clear();
5184
+ this._onClearAction();
3298
5185
 
3299
5186
  // De-select dropdown item
3300
5187
  if (this.opened) {
@@ -3330,15 +5217,13 @@ const ComboBoxMixin = (subclass) =>
3330
5217
  }
3331
5218
 
3332
5219
  /** @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);
5220
+ _onClick(event) {
5221
+ if (this._isClearButton(event)) {
5222
+ this._onClearButtonClick(event);
5223
+ } else if (event.composedPath().includes(this._toggleElement)) {
5224
+ this._onToggleButtonClick(event);
3340
5225
  } else {
3341
- this._onHostClick(e);
5226
+ this._onHostClick(event);
3342
5227
  }
3343
5228
  }
3344
5229
 
@@ -3353,7 +5238,7 @@ const ComboBoxMixin = (subclass) =>
3353
5238
  super._onKeyDown(e);
3354
5239
 
3355
5240
  if (e.key === 'Tab') {
3356
- this.$.overlay.restoreFocusOnClose = false;
5241
+ this._overlayElement.restoreFocusOnClose = false;
3357
5242
  } else if (e.key === 'ArrowDown') {
3358
5243
  this._onArrowDown();
3359
5244
 
@@ -3369,7 +5254,7 @@ const ComboBoxMixin = (subclass) =>
3369
5254
 
3370
5255
  /** @private */
3371
5256
  _getItemLabel(item) {
3372
- let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
5257
+ let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
3373
5258
  if (label === undefined || label === null) {
3374
5259
  label = item ? item.toString() : '';
3375
5260
  }
@@ -3378,7 +5263,7 @@ const ComboBoxMixin = (subclass) =>
3378
5263
 
3379
5264
  /** @private */
3380
5265
  _getItemValue(item) {
3381
- let value = item && this.itemValuePath ? this.get(this.itemValuePath, item) : undefined;
5266
+ let value = item && this.itemValuePath ? get(this.itemValuePath, item) : undefined;
3382
5267
  if (value === undefined) {
3383
5268
  value = item ? item.toString() : '';
3384
5269
  }
@@ -3516,7 +5401,7 @@ const ComboBoxMixin = (subclass) =>
3516
5401
  } else if (this.clearButtonVisible && !this.opened && !!this.value) {
3517
5402
  e.stopPropagation();
3518
5403
  // The clear button is visible and the overlay is closed, so clear the value.
3519
- this._clear();
5404
+ this._onClearAction();
3520
5405
  }
3521
5406
  } else if (this.opened) {
3522
5407
  // Auto-open is enabled
@@ -3534,7 +5419,7 @@ const ComboBoxMixin = (subclass) =>
3534
5419
  } else if (this.clearButtonVisible && !!this.value) {
3535
5420
  e.stopPropagation();
3536
5421
  // The clear button is visible and the overlay is closed, so clear the value.
3537
- this._clear();
5422
+ this._onClearAction();
3538
5423
  }
3539
5424
  }
3540
5425
 
@@ -3556,7 +5441,7 @@ const ComboBoxMixin = (subclass) =>
3556
5441
  * Clears the current value.
3557
5442
  * @protected
3558
5443
  */
3559
- _clear() {
5444
+ _onClearAction() {
3560
5445
  this.selectedItem = null;
3561
5446
 
3562
5447
  if (this.allowCustomValue) {
@@ -3607,7 +5492,7 @@ const ComboBoxMixin = (subclass) =>
3607
5492
  }
3608
5493
  } else {
3609
5494
  // Try to find an item which label matches the input value.
3610
- const items = [...(this.filteredItems || []), this.selectedItem];
5495
+ const items = [this.selectedItem, ...(this.filteredItems || [])];
3611
5496
  const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
3612
5497
 
3613
5498
  if (
@@ -3648,14 +5533,6 @@ const ComboBoxMixin = (subclass) =>
3648
5533
  this.filter = '';
3649
5534
  }
3650
5535
 
3651
- /**
3652
- * @return {string}
3653
- * @protected
3654
- */
3655
- get _propertyForValue() {
3656
- return 'value';
3657
- }
3658
-
3659
5536
  /**
3660
5537
  * Override an event listener from `InputMixin`.
3661
5538
  * @param {!Event} event
@@ -3802,6 +5679,12 @@ const ComboBoxMixin = (subclass) =>
3802
5679
 
3803
5680
  /** @private */
3804
5681
  _detectAndDispatchChange() {
5682
+ // Do not validate when focusout is caused by document
5683
+ // losing focus, which happens on browser tab switch.
5684
+ if (document.hasFocus()) {
5685
+ this.validate();
5686
+ }
5687
+
3805
5688
  if (this.value !== this._lastCommittedValue) {
3806
5689
  this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
3807
5690
  this._lastCommittedValue = this.value;
@@ -3944,26 +5827,18 @@ const ComboBoxMixin = (subclass) =>
3944
5827
  }
3945
5828
  }
3946
5829
 
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
- }
5830
+ /**
5831
+ * Override method inherited from `FocusMixin`
5832
+ * to close the overlay on blur and commit the value.
5833
+ *
5834
+ * @param {boolean} focused
5835
+ * @protected
5836
+ * @override
5837
+ */
5838
+ _setFocused(focused) {
5839
+ super._setFocused(focused);
3960
5840
 
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) {
5841
+ if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
3967
5842
  // User's logic in `custom-value-set` event listener might cause input to blur,
3968
5843
  // which will result in attempting to commit the same custom value once again.
3969
5844
  if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
@@ -3975,6 +5850,32 @@ const ComboBoxMixin = (subclass) =>
3975
5850
  }
3976
5851
  }
3977
5852
 
5853
+ /**
5854
+ * Override method inherited from `FocusMixin` to not remove focused
5855
+ * state when focus moves to the overlay.
5856
+ *
5857
+ * @param {FocusEvent} event
5858
+ * @return {boolean}
5859
+ * @protected
5860
+ * @override
5861
+ */
5862
+ _shouldRemoveFocus(event) {
5863
+ // VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
5864
+ // Do not focus the input in this case, because it would break announcement for the item.
5865
+ if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
5866
+ return false;
5867
+ }
5868
+
5869
+ // Do not blur when focus moves to the overlay
5870
+ // Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
5871
+ if (event.relatedTarget === this._overlayElement) {
5872
+ event.composedPath()[0].focus();
5873
+ return false;
5874
+ }
5875
+
5876
+ return true;
5877
+ }
5878
+
3978
5879
  /** @private */
3979
5880
  _onTouchend(event) {
3980
5881
  if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
@@ -3982,7 +5883,7 @@ const ComboBoxMixin = (subclass) =>
3982
5883
  }
3983
5884
 
3984
5885
  event.preventDefault();
3985
- this._clear();
5886
+ this._onClearAction();
3986
5887
  }
3987
5888
 
3988
5889
  /**
@@ -4028,7 +5929,7 @@ const ComboBoxMixin = (subclass) =>
4028
5929
 
4029
5930
  /**
4030
5931
  * @license
4031
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
5932
+ * Copyright (c) 2015 - 2023 Vaadin Ltd.
4032
5933
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
4033
5934
  */
4034
5935
 
@@ -4148,7 +6049,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4148
6049
  * Note: the `theme` attribute value set on `<vaadin-combo-box>` is
4149
6050
  * propagated to the internal components listed above.
4150
6051
  *
4151
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
6052
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
4152
6053
  *
4153
6054
  * @fires {Event} change - Fired when the user commits a value change.
4154
6055
  * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
@@ -4159,6 +6060,7 @@ registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo
4159
6060
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
4160
6061
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
4161
6062
  *
6063
+ * @customElement
4162
6064
  * @extends HTMLElement
4163
6065
  * @mixes ElementMixin
4164
6066
  * @mixes ThemableMixin
@@ -4261,6 +6163,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
4261
6163
  this._tooltipController = new TooltipController(this);
4262
6164
  this.addController(this._tooltipController);
4263
6165
  this._tooltipController.setPosition('top');
6166
+ this._tooltipController.setAriaTarget(this.inputElement);
4264
6167
  this._tooltipController.setShouldShow((target) => !target.opened);
4265
6168
 
4266
6169
  this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
@@ -4268,48 +6171,17 @@ class ComboBox extends ComboBoxDataProviderMixin(
4268
6171
  }
4269
6172
 
4270
6173
  /**
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.
6174
+ * Override the method from `InputControlMixin`
6175
+ * to stop event propagation to prevent `ComboBoxMixin`
6176
+ * from handling this click event also on its own.
6177
+ *
4305
6178
  * @param {Event} event
4306
6179
  * @protected
4307
6180
  * @override
4308
6181
  */
4309
6182
  _onClearButtonClick(event) {
4310
6183
  event.stopPropagation();
4311
-
4312
- this._handleClearButtonClick(event);
6184
+ super._onClearButtonClick(event);
4313
6185
  }
4314
6186
 
4315
6187
  /**
@@ -4326,4 +6198,4 @@ class ComboBox extends ComboBoxDataProviderMixin(
4326
6198
  }
4327
6199
  }
4328
6200
 
4329
- customElements.define(ComboBox.is, ComboBox);
6201
+ defineCustomElement(ComboBox);