@vaadin/combo-box 23.1.0 → 23.2.0-alpha2
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.
- package/package.json +15 -15
- package/src/vaadin-combo-box-data-provider-mixin.js +38 -41
- package/src/vaadin-combo-box-light.js +11 -12
- package/src/vaadin-combo-box-mixin.d.ts +5 -2
- package/src/vaadin-combo-box-mixin.js +271 -113
- package/src/vaadin-combo-box-overlay.js +1 -3
- package/src/vaadin-combo-box-scroller.js +15 -3
- package/src/vaadin-combo-box.js +12 -13
- package/src/vaadin-combo-box-dropdown.js +0 -287
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/combo-box",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.2.0-alpha2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"main": "vaadin-combo-box.js",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"files": [
|
|
22
|
+
"lit.d.ts",
|
|
23
|
+
"lit.js",
|
|
22
24
|
"src",
|
|
23
25
|
"theme",
|
|
24
|
-
"lit.js",
|
|
25
|
-
"lit.d.ts",
|
|
26
26
|
"vaadin-*.d.ts",
|
|
27
27
|
"vaadin-*.js"
|
|
28
28
|
],
|
|
@@ -36,23 +36,23 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
38
38
|
"@polymer/polymer": "^3.0.0",
|
|
39
|
-
"@vaadin/component-base": "
|
|
40
|
-
"@vaadin/field-base": "
|
|
41
|
-
"@vaadin/input-container": "
|
|
42
|
-
"@vaadin/item": "
|
|
43
|
-
"@vaadin/lit-renderer": "
|
|
44
|
-
"@vaadin/vaadin-lumo-styles": "
|
|
45
|
-
"@vaadin/vaadin-material-styles": "
|
|
46
|
-
"@vaadin/vaadin-overlay": "
|
|
47
|
-
"@vaadin/vaadin-themable-mixin": "
|
|
39
|
+
"@vaadin/component-base": "23.2.0-alpha2",
|
|
40
|
+
"@vaadin/field-base": "23.2.0-alpha2",
|
|
41
|
+
"@vaadin/input-container": "23.2.0-alpha2",
|
|
42
|
+
"@vaadin/item": "23.2.0-alpha2",
|
|
43
|
+
"@vaadin/lit-renderer": "23.2.0-alpha2",
|
|
44
|
+
"@vaadin/vaadin-lumo-styles": "23.2.0-alpha2",
|
|
45
|
+
"@vaadin/vaadin-material-styles": "23.2.0-alpha2",
|
|
46
|
+
"@vaadin/vaadin-overlay": "23.2.0-alpha2",
|
|
47
|
+
"@vaadin/vaadin-themable-mixin": "23.2.0-alpha2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@esm-bundle/chai": "^4.3.4",
|
|
51
|
-
"@vaadin/polymer-legacy-adapter": "
|
|
51
|
+
"@vaadin/polymer-legacy-adapter": "23.2.0-alpha2",
|
|
52
52
|
"@vaadin/testing-helpers": "^0.3.2",
|
|
53
|
-
"@vaadin/text-field": "
|
|
53
|
+
"@vaadin/text-field": "23.2.0-alpha2",
|
|
54
54
|
"lit": "^2.0.0",
|
|
55
55
|
"sinon": "^13.0.2"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "c9b8113d0fa9a602f8b9cb915c1826355af2e8df"
|
|
58
58
|
}
|
|
@@ -78,7 +78,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
|
|
|
78
78
|
ready() {
|
|
79
79
|
super.ready();
|
|
80
80
|
this.clearCache();
|
|
81
|
-
this
|
|
81
|
+
this._scroller.addEventListener('index-requested', (e) => {
|
|
82
82
|
const index = e.detail.index;
|
|
83
83
|
const currentScrollerPos = e.detail.currentScrollerPos;
|
|
84
84
|
const allowedIndexRange = Math.floor(this.pageSize * 1.5);
|
|
@@ -177,47 +177,44 @@ export const ComboBoxDataProviderMixin = (superClass) =>
|
|
|
177
177
|
/** @private */
|
|
178
178
|
_loadPage(page) {
|
|
179
179
|
// Make sure same page isn't requested multiple times.
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const callback = (items, size) => {
|
|
190
|
-
if (this._pendingRequests[page] === callback) {
|
|
191
|
-
if (!this.filteredItems) {
|
|
192
|
-
const filteredItems = [];
|
|
193
|
-
filteredItems.splice(params.page * params.pageSize, items.length, ...items);
|
|
194
|
-
this.filteredItems = filteredItems;
|
|
195
|
-
} else {
|
|
196
|
-
this.splice('filteredItems', params.page * params.pageSize, items.length, ...items);
|
|
197
|
-
}
|
|
198
|
-
// Update selectedItem from filteredItems if value is set
|
|
199
|
-
if (this._isValidValue(this.value) && this._getItemValue(this.selectedItem) !== this.value) {
|
|
200
|
-
this._selectItemForValue(this.value);
|
|
201
|
-
}
|
|
202
|
-
if (!this.opened && !this.hasAttribute('focused')) {
|
|
203
|
-
this._commitValue();
|
|
204
|
-
}
|
|
205
|
-
this.size = size;
|
|
206
|
-
|
|
207
|
-
delete this._pendingRequests[page];
|
|
208
|
-
|
|
209
|
-
if (Object.keys(this._pendingRequests).length === 0) {
|
|
210
|
-
this.loading = false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
};
|
|
180
|
+
if (this._pendingRequests[page] || !this.dataProvider) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const params = {
|
|
185
|
+
page,
|
|
186
|
+
pageSize: this.pageSize,
|
|
187
|
+
filter: this.filter,
|
|
188
|
+
};
|
|
214
189
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.dataProvider(params, callback);
|
|
190
|
+
const callback = (items, size) => {
|
|
191
|
+
if (this._pendingRequests[page] !== callback) {
|
|
192
|
+
return;
|
|
219
193
|
}
|
|
220
|
-
|
|
194
|
+
|
|
195
|
+
const filteredItems = this.filteredItems ? [...this.filteredItems] : [];
|
|
196
|
+
filteredItems.splice(params.page * params.pageSize, items.length, ...items);
|
|
197
|
+
this.filteredItems = filteredItems;
|
|
198
|
+
|
|
199
|
+
if (!this.opened && !this.hasAttribute('focused')) {
|
|
200
|
+
this._commitValue();
|
|
201
|
+
}
|
|
202
|
+
this.size = size;
|
|
203
|
+
|
|
204
|
+
delete this._pendingRequests[page];
|
|
205
|
+
|
|
206
|
+
if (Object.keys(this._pendingRequests).length === 0) {
|
|
207
|
+
this.loading = false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
this._pendingRequests[page] = callback;
|
|
212
|
+
// Set the `loading` flag only after marking the request as pending
|
|
213
|
+
// to prevent the same page from getting requested multiple times
|
|
214
|
+
// as a result of `__loadingChanged` in the scroller which requests
|
|
215
|
+
// a virtualizer update which in turn may trigger a data provider page request.
|
|
216
|
+
this.loading = true;
|
|
217
|
+
this.dataProvider(params, callback);
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
/** @private */
|
|
@@ -287,7 +284,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
|
|
|
287
284
|
/** @private */
|
|
288
285
|
_warnDataProviderValue(dataProvider, value) {
|
|
289
286
|
if (dataProvider && value !== '' && (this.selectedItem === undefined || this.selectedItem === null)) {
|
|
290
|
-
const valueIndex = this.
|
|
287
|
+
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
291
288
|
if (valueIndex < 0 || !this._getItemLabel(this.filteredItems[valueIndex])) {
|
|
292
289
|
console.warn(
|
|
293
290
|
'Warning: unable to determine the label for the provided `value`. ' +
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Copyright (c) 2015 - 2022 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import './vaadin-combo-box-
|
|
6
|
+
import './vaadin-combo-box-item.js';
|
|
7
|
+
import './vaadin-combo-box-overlay.js';
|
|
8
|
+
import './vaadin-combo-box-scroller.js';
|
|
7
9
|
import { dashToCamelCase } from '@polymer/polymer/lib/utils/case-map.js';
|
|
8
10
|
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
9
11
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
@@ -75,19 +77,16 @@ class ComboBoxLight extends ComboBoxDataProviderMixin(ComboBoxMixin(ThemableMixi
|
|
|
75
77
|
|
|
76
78
|
<slot></slot>
|
|
77
79
|
|
|
78
|
-
<vaadin-combo-box-
|
|
79
|
-
id="
|
|
80
|
-
|
|
80
|
+
<vaadin-combo-box-overlay
|
|
81
|
+
id="overlay"
|
|
82
|
+
hidden$="[[_isOverlayHidden(filteredItems, loading)]]"
|
|
83
|
+
opened="[[_overlayOpened]]"
|
|
84
|
+
loading$="[[loading]]"
|
|
85
|
+
theme$="[[_theme]]"
|
|
81
86
|
position-target="[[inputElement]]"
|
|
82
|
-
|
|
87
|
+
no-vertical-overlap
|
|
83
88
|
restore-focus-node="[[inputElement]]"
|
|
84
|
-
|
|
85
|
-
_focused-index="[[_focusedIndex]]"
|
|
86
|
-
_item-id-path="[[itemIdPath]]"
|
|
87
|
-
_item-label-path="[[itemLabelPath]]"
|
|
88
|
-
loading="[[loading]]"
|
|
89
|
-
theme="[[_theme]]"
|
|
90
|
-
></vaadin-combo-box-dropdown>
|
|
89
|
+
></vaadin-combo-box-overlay>
|
|
91
90
|
`;
|
|
92
91
|
}
|
|
93
92
|
|
|
@@ -147,6 +147,11 @@ export declare class ComboBoxMixinClass<TItem> {
|
|
|
147
147
|
|
|
148
148
|
protected _inputElementValue: string | undefined;
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Tag name prefix used by scroller and items.
|
|
152
|
+
*/
|
|
153
|
+
protected readonly _tagNamePrefix: string;
|
|
154
|
+
|
|
150
155
|
/**
|
|
151
156
|
* Requests an update for the content of items.
|
|
152
157
|
* While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
|
|
@@ -177,6 +182,4 @@ export declare class ComboBoxMixinClass<TItem> {
|
|
|
177
182
|
checkValidity(): boolean;
|
|
178
183
|
|
|
179
184
|
protected _revertInputValue(): void;
|
|
180
|
-
|
|
181
|
-
protected _getItemElements(): HTMLElement[];
|
|
182
185
|
}
|
|
@@ -12,6 +12,34 @@ import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
|
|
|
12
12
|
import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
|
|
13
13
|
import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Checks if the value is supported as an item value in this control.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} value
|
|
19
|
+
* @return {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isValidValue(value) {
|
|
22
|
+
return value !== undefined && value !== null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the index of the first item that satisfies the provided testing function
|
|
27
|
+
* ignoring placeholder items.
|
|
28
|
+
*
|
|
29
|
+
* @param {Array<ComboBoxItem | string>} items
|
|
30
|
+
* @param {Function} callback
|
|
31
|
+
* @return {number}
|
|
32
|
+
*/
|
|
33
|
+
function findItemIndex(items, callback) {
|
|
34
|
+
return items.findIndex((item) => {
|
|
35
|
+
if (item instanceof ComboBoxPlaceholder) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return callback(item);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
/**
|
|
16
44
|
* @polymerMixin
|
|
17
45
|
* @param {function(new:HTMLElement)} subclass
|
|
@@ -95,6 +123,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
95
123
|
*/
|
|
96
124
|
filteredItems: {
|
|
97
125
|
type: Array,
|
|
126
|
+
observer: '_filteredItemsChanged',
|
|
98
127
|
},
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -111,7 +140,6 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
111
140
|
type: Boolean,
|
|
112
141
|
value: false,
|
|
113
142
|
reflectToAttribute: true,
|
|
114
|
-
observer: '_loadingChanged',
|
|
115
143
|
},
|
|
116
144
|
|
|
117
145
|
/**
|
|
@@ -196,16 +224,22 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
196
224
|
_closeOnBlurIsPrevented: Boolean,
|
|
197
225
|
|
|
198
226
|
/** @private */
|
|
199
|
-
|
|
227
|
+
_scroller: Object,
|
|
228
|
+
|
|
229
|
+
/** @private */
|
|
230
|
+
_overlayOpened: {
|
|
231
|
+
type: Boolean,
|
|
232
|
+
observer: '_overlayOpenedChanged',
|
|
233
|
+
},
|
|
200
234
|
};
|
|
201
235
|
}
|
|
202
236
|
|
|
203
237
|
static get observers() {
|
|
204
238
|
return [
|
|
205
239
|
'_filterChanged(filter, itemValuePath, itemLabelPath)',
|
|
206
|
-
'_itemsOrPathsChanged(items.*, itemValuePath, itemLabelPath)',
|
|
207
|
-
'_filteredItemsChanged(filteredItems.*, itemValuePath, itemLabelPath)',
|
|
208
240
|
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
|
|
241
|
+
'_openedOrItemsChanged(opened, filteredItems, loading)',
|
|
242
|
+
'_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
|
|
209
243
|
];
|
|
210
244
|
}
|
|
211
245
|
|
|
@@ -214,13 +248,20 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
214
248
|
this._boundOnFocusout = this._onFocusout.bind(this);
|
|
215
249
|
this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
|
|
216
250
|
this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
|
|
217
|
-
this._boundClose = this.close.bind(this);
|
|
218
|
-
this._boundOnOpened = this._onOpened.bind(this);
|
|
219
251
|
this._boundOnClick = this._onClick.bind(this);
|
|
220
252
|
this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
|
|
221
253
|
this._boundOnTouchend = this._onTouchend.bind(this);
|
|
222
254
|
}
|
|
223
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Tag name prefix used by scroller and items.
|
|
258
|
+
* @protected
|
|
259
|
+
* @return {string}
|
|
260
|
+
*/
|
|
261
|
+
get _tagNamePrefix() {
|
|
262
|
+
return 'vaadin-combo-box';
|
|
263
|
+
}
|
|
264
|
+
|
|
224
265
|
/**
|
|
225
266
|
* @return {string | undefined}
|
|
226
267
|
* @protected
|
|
@@ -274,23 +315,19 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
274
315
|
ready() {
|
|
275
316
|
super.ready();
|
|
276
317
|
|
|
318
|
+
this._initOverlay();
|
|
319
|
+
this._initScroller();
|
|
320
|
+
|
|
277
321
|
this.addEventListener('focusout', this._boundOnFocusout);
|
|
278
322
|
|
|
279
323
|
this._lastCommittedValue = this.value;
|
|
280
324
|
|
|
281
|
-
this.$.dropdown.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
282
|
-
|
|
283
|
-
this.addEventListener('vaadin-combo-box-dropdown-closed', this._boundClose);
|
|
284
|
-
this.addEventListener('vaadin-combo-box-dropdown-opened', this._boundOnOpened);
|
|
285
325
|
this.addEventListener('click', this._boundOnClick);
|
|
286
|
-
|
|
287
|
-
this.$.dropdown.addEventListener('vaadin-overlay-touch-action', this._boundOnOverlayTouchAction);
|
|
288
|
-
|
|
289
326
|
this.addEventListener('touchend', this._boundOnTouchend);
|
|
290
327
|
|
|
291
328
|
const bringToFrontListener = () => {
|
|
292
329
|
requestAnimationFrame(() => {
|
|
293
|
-
this.$.
|
|
330
|
+
this.$.overlay.bringToFront();
|
|
294
331
|
});
|
|
295
332
|
};
|
|
296
333
|
|
|
@@ -302,6 +339,14 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
302
339
|
this.addController(new VirtualKeyboardController(this));
|
|
303
340
|
}
|
|
304
341
|
|
|
342
|
+
/** @protected */
|
|
343
|
+
disconnectedCallback() {
|
|
344
|
+
super.disconnectedCallback();
|
|
345
|
+
|
|
346
|
+
// Close the overlay on detach
|
|
347
|
+
this.close();
|
|
348
|
+
}
|
|
349
|
+
|
|
305
350
|
/**
|
|
306
351
|
* Requests an update for the content of items.
|
|
307
352
|
* While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
|
|
@@ -309,11 +354,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
309
354
|
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
310
355
|
*/
|
|
311
356
|
requestContentUpdate() {
|
|
312
|
-
if (!this
|
|
357
|
+
if (!this._scroller) {
|
|
313
358
|
return;
|
|
314
359
|
}
|
|
315
360
|
|
|
316
|
-
this
|
|
361
|
+
this._scroller.requestContentUpdate();
|
|
317
362
|
|
|
318
363
|
this._getItemElements().forEach((item) => {
|
|
319
364
|
item.requestContentUpdate();
|
|
@@ -337,6 +382,109 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
337
382
|
this.opened = false;
|
|
338
383
|
}
|
|
339
384
|
|
|
385
|
+
/** @private */
|
|
386
|
+
_initOverlay() {
|
|
387
|
+
const overlay = this.$.overlay;
|
|
388
|
+
|
|
389
|
+
// Store instance for detecting "dir" attribute on opening
|
|
390
|
+
overlay._comboBox = this;
|
|
391
|
+
|
|
392
|
+
overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
|
|
393
|
+
overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
|
|
394
|
+
|
|
395
|
+
// Prevent blurring the input when clicking inside the overlay
|
|
396
|
+
overlay.addEventListener('mousedown', (e) => e.preventDefault());
|
|
397
|
+
|
|
398
|
+
// Preventing the default modal behavior of the overlay on input click
|
|
399
|
+
overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Manual two-way binding for the overlay "opened" property
|
|
404
|
+
overlay.addEventListener('opened-changed', (e) => {
|
|
405
|
+
this._overlayOpened = e.detail.value;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create and initialize the scroller element.
|
|
411
|
+
* Override to provide custom host reference.
|
|
412
|
+
*
|
|
413
|
+
* @protected
|
|
414
|
+
*/
|
|
415
|
+
_initScroller(host) {
|
|
416
|
+
const scrollerTag = `${this._tagNamePrefix}-scroller`;
|
|
417
|
+
|
|
418
|
+
const overlay = this.$.overlay;
|
|
419
|
+
|
|
420
|
+
overlay.renderer = (root) => {
|
|
421
|
+
if (!root.firstChild) {
|
|
422
|
+
root.appendChild(document.createElement(scrollerTag));
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Ensure the scroller is rendered
|
|
427
|
+
if (!this.opened) {
|
|
428
|
+
overlay.requestContentUpdate();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const scroller = overlay.querySelector(scrollerTag);
|
|
432
|
+
|
|
433
|
+
scroller.comboBox = host || this;
|
|
434
|
+
scroller.getItemLabel = this._getItemLabel.bind(this);
|
|
435
|
+
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
436
|
+
|
|
437
|
+
// Trigger the observer to set properties
|
|
438
|
+
this._scroller = scroller;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** @private */
|
|
442
|
+
// eslint-disable-next-line max-params
|
|
443
|
+
_updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
|
|
444
|
+
if (scroller) {
|
|
445
|
+
if (opened) {
|
|
446
|
+
scroller.style.maxHeight =
|
|
447
|
+
getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
scroller.setProperties({
|
|
451
|
+
items: opened ? items : [],
|
|
452
|
+
opened,
|
|
453
|
+
loading,
|
|
454
|
+
selectedItem,
|
|
455
|
+
itemIdPath,
|
|
456
|
+
focusedIndex,
|
|
457
|
+
renderer,
|
|
458
|
+
theme,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** @protected */
|
|
464
|
+
_isOverlayHidden(items, loading) {
|
|
465
|
+
return !loading && !(items && items.length);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** @private */
|
|
469
|
+
_openedOrItemsChanged(opened, items, loading) {
|
|
470
|
+
// Close the overlay if there are no items to display.
|
|
471
|
+
// See https://github.com/vaadin/vaadin-combo-box/pull/964
|
|
472
|
+
this._overlayOpened = !!(opened && (loading || (items && items.length)));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** @private */
|
|
476
|
+
_overlayOpenedChanged(opened, wasOpened) {
|
|
477
|
+
if (opened) {
|
|
478
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
|
|
479
|
+
|
|
480
|
+
this._onOpened();
|
|
481
|
+
} else if (wasOpened && this.filteredItems && this.filteredItems.length) {
|
|
482
|
+
this.close();
|
|
483
|
+
|
|
484
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
340
488
|
/** @private */
|
|
341
489
|
_focusedIndexChanged(index, oldIndex) {
|
|
342
490
|
if (oldIndex === undefined) {
|
|
@@ -375,7 +523,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
375
523
|
this.focus();
|
|
376
524
|
}
|
|
377
525
|
|
|
378
|
-
this.
|
|
526
|
+
this.$.overlay.restoreFocusOnClose = true;
|
|
379
527
|
} else {
|
|
380
528
|
this._onClosed();
|
|
381
529
|
if (this._openedWithFocusRing && this.hasAttribute('focused')) {
|
|
@@ -388,7 +536,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
388
536
|
input.setAttribute('aria-expanded', !!opened);
|
|
389
537
|
|
|
390
538
|
if (opened) {
|
|
391
|
-
input.setAttribute('aria-controls', this
|
|
539
|
+
input.setAttribute('aria-controls', this._scroller.id);
|
|
392
540
|
} else {
|
|
393
541
|
input.removeAttribute('aria-controls');
|
|
394
542
|
}
|
|
@@ -478,7 +626,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
478
626
|
super._onKeyDown(e);
|
|
479
627
|
|
|
480
628
|
if (e.key === 'Tab') {
|
|
481
|
-
this.
|
|
629
|
+
this.$.overlay.restoreFocusOnClose = false;
|
|
482
630
|
} else if (e.key === 'ArrowDown') {
|
|
483
631
|
this._closeOnBlurIsPrevented = true;
|
|
484
632
|
this._onArrowDown();
|
|
@@ -498,7 +646,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
498
646
|
|
|
499
647
|
/** @private */
|
|
500
648
|
_getItemLabel(item) {
|
|
501
|
-
|
|
649
|
+
let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
|
|
650
|
+
if (label === undefined || label === null) {
|
|
651
|
+
label = item ? item.toString() : '';
|
|
652
|
+
}
|
|
653
|
+
return label;
|
|
502
654
|
}
|
|
503
655
|
|
|
504
656
|
/** @private */
|
|
@@ -513,7 +665,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
513
665
|
/** @private */
|
|
514
666
|
_onArrowDown() {
|
|
515
667
|
if (this.opened) {
|
|
516
|
-
const items = this.
|
|
668
|
+
const items = this.filteredItems;
|
|
517
669
|
if (items) {
|
|
518
670
|
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
|
|
519
671
|
this._prefillFocusedItemLabel();
|
|
@@ -529,7 +681,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
529
681
|
if (this._focusedIndex > -1) {
|
|
530
682
|
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
|
|
531
683
|
} else {
|
|
532
|
-
const items = this.
|
|
684
|
+
const items = this.filteredItems;
|
|
533
685
|
if (items) {
|
|
534
686
|
this._focusedIndex = items.length - 1;
|
|
535
687
|
}
|
|
@@ -544,7 +696,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
544
696
|
/** @private */
|
|
545
697
|
_prefillFocusedItemLabel() {
|
|
546
698
|
if (this._focusedIndex > -1) {
|
|
547
|
-
|
|
699
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
700
|
+
this._inputElementValue = this._getItemLabel(focusedItem);
|
|
548
701
|
this._markAllSelectionRange();
|
|
549
702
|
}
|
|
550
703
|
}
|
|
@@ -701,7 +854,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
701
854
|
_onOpened() {
|
|
702
855
|
// Defer scroll position adjustment to improve performance.
|
|
703
856
|
requestAnimationFrame(() => {
|
|
704
|
-
this
|
|
857
|
+
this._scrollIntoView(this._focusedIndex);
|
|
705
858
|
|
|
706
859
|
// Set attribute after the items are rendered when overlay is opened for the first time.
|
|
707
860
|
this._updateActiveDescendant(this._focusedIndex);
|
|
@@ -720,9 +873,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
720
873
|
|
|
721
874
|
/** @private */
|
|
722
875
|
_commitValue() {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const focusedItem = items[this._focusedIndex];
|
|
876
|
+
if (this._focusedIndex > -1) {
|
|
877
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
726
878
|
if (this.selectedItem !== focusedItem) {
|
|
727
879
|
this.selectedItem = focusedItem;
|
|
728
880
|
}
|
|
@@ -735,18 +887,14 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
735
887
|
this.value = '';
|
|
736
888
|
}
|
|
737
889
|
} else {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
// the filteredItems array (if available) and the selectedItem (if available).
|
|
742
|
-
const itemMatchingByLabel = [...(this.filteredItems || []), this.selectedItem].find((item) => {
|
|
743
|
-
return toLowerCase(this._getItemLabel(item)) === toLowerCase(this._inputElementValue);
|
|
744
|
-
});
|
|
890
|
+
// Try to find an item which label matches the input value.
|
|
891
|
+
const items = [...(this.filteredItems || []), this.selectedItem];
|
|
892
|
+
const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
|
|
745
893
|
|
|
746
894
|
if (
|
|
747
895
|
this.allowCustomValue &&
|
|
748
896
|
// To prevent a repetitive input value being saved after pressing ESC and Tab.
|
|
749
|
-
!
|
|
897
|
+
!itemMatchingInputValue
|
|
750
898
|
) {
|
|
751
899
|
const customValue = this._inputElementValue;
|
|
752
900
|
|
|
@@ -763,12 +911,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
763
911
|
});
|
|
764
912
|
this.dispatchEvent(e);
|
|
765
913
|
if (!e.defaultPrevented) {
|
|
766
|
-
this._selectItemForValue(customValue);
|
|
767
914
|
this.value = customValue;
|
|
768
915
|
}
|
|
769
|
-
} else if (!this.allowCustomValue && !this.opened &&
|
|
916
|
+
} else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
|
|
770
917
|
// An item matching by label was found, select it.
|
|
771
|
-
this.value = this._getItemValue(
|
|
918
|
+
this.value = this._getItemValue(itemMatchingInputValue);
|
|
772
919
|
} else {
|
|
773
920
|
// Revert the input value
|
|
774
921
|
this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
|
|
@@ -834,13 +981,15 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
834
981
|
}
|
|
835
982
|
|
|
836
983
|
/** @private */
|
|
837
|
-
_filterChanged(filter,
|
|
984
|
+
_filterChanged(filter, _itemValuePath, _itemLabelPath) {
|
|
838
985
|
if (filter === undefined) {
|
|
839
986
|
return;
|
|
840
987
|
}
|
|
841
988
|
|
|
842
989
|
// Scroll to the top of the list whenever the filter changes.
|
|
843
|
-
this
|
|
990
|
+
this._scrollIntoView(0);
|
|
991
|
+
|
|
992
|
+
this._focusedIndex = -1;
|
|
844
993
|
|
|
845
994
|
if (this.items) {
|
|
846
995
|
this.filteredItems = this._filterItems(this.items, filter);
|
|
@@ -848,14 +997,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
848
997
|
// With certain use cases (e. g., external filtering), `items` are
|
|
849
998
|
// undefined. Filtering is unnecessary per se, but the filteredItems
|
|
850
999
|
// observer should still be invoked to update focused item.
|
|
851
|
-
this._filteredItemsChanged(
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/** @private */
|
|
856
|
-
_loadingChanged(loading) {
|
|
857
|
-
if (loading) {
|
|
858
|
-
this._focusedIndex = -1;
|
|
1000
|
+
this._filteredItemsChanged(this.filteredItems);
|
|
859
1001
|
}
|
|
860
1002
|
}
|
|
861
1003
|
|
|
@@ -904,9 +1046,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
904
1046
|
this._inputElementValue = this._getItemLabel(selectedItem);
|
|
905
1047
|
}
|
|
906
1048
|
|
|
907
|
-
this
|
|
908
|
-
const items = this._getOverlayItems();
|
|
909
|
-
if (this.filteredItems && items) {
|
|
1049
|
+
if (this.filteredItems) {
|
|
910
1050
|
this._focusedIndex = this.filteredItems.indexOf(selectedItem);
|
|
911
1051
|
}
|
|
912
1052
|
}
|
|
@@ -923,7 +1063,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
923
1063
|
return;
|
|
924
1064
|
}
|
|
925
1065
|
|
|
926
|
-
if (
|
|
1066
|
+
if (isValidValue(value)) {
|
|
927
1067
|
let item;
|
|
928
1068
|
if (this._getItemValue(this.selectedItem) !== value) {
|
|
929
1069
|
this._selectItemForValue(value);
|
|
@@ -956,53 +1096,53 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
956
1096
|
this._ensureItemsOrDataProvider(() => {
|
|
957
1097
|
this.items = oldItems;
|
|
958
1098
|
});
|
|
1099
|
+
|
|
1100
|
+
if (items) {
|
|
1101
|
+
this.filteredItems = items.slice(0);
|
|
1102
|
+
} else if (oldItems) {
|
|
1103
|
+
// Only clear filteredItems if the component had items previously but got cleared
|
|
1104
|
+
this.filteredItems = null;
|
|
1105
|
+
}
|
|
959
1106
|
}
|
|
960
1107
|
|
|
961
1108
|
/** @private */
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1109
|
+
_filteredItemsChanged(filteredItems, oldFilteredItems) {
|
|
1110
|
+
// Store the currently focused item if any. The focused index preserves
|
|
1111
|
+
// in the case when more filtered items are loading but it is reset
|
|
1112
|
+
// when the user types in a filter query.
|
|
1113
|
+
const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
|
|
1114
|
+
|
|
1115
|
+
// Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
|
|
1116
|
+
// (as a result of external filtering or when they have been loaded by the data provider).
|
|
1117
|
+
// When `value` is specified but `selectedItem` is not, it means that there was no item
|
|
1118
|
+
// matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
|
|
1119
|
+
const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
|
|
1120
|
+
if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
|
|
1121
|
+
this.selectedItem = filteredItems[valueIndex];
|
|
1122
|
+
}
|
|
973
1123
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1124
|
+
// Try to first set focus on the item that had been focused before `filteredItems` were updated
|
|
1125
|
+
// if it is still present in the `filteredItems` array. Otherwise, set the focused index
|
|
1126
|
+
// depending on the selected item or the filter query.
|
|
1127
|
+
const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
|
|
1128
|
+
if (focusedItemIndex > -1) {
|
|
1129
|
+
this._focusedIndex = focusedItemIndex;
|
|
1130
|
+
} else {
|
|
1131
|
+
this.__setInitialFocusedIndex();
|
|
978
1132
|
}
|
|
979
|
-
this.__previousItems = e.value;
|
|
980
1133
|
}
|
|
981
1134
|
|
|
982
1135
|
/** @private */
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
// in
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
this._selectItemForValue(this.value);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
const inputValue = this._inputElementValue;
|
|
997
|
-
if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
|
|
998
|
-
// When the input element value is the same as the current value or not defined,
|
|
999
|
-
// set the focused index to the item that matches the value.
|
|
1000
|
-
this._focusedIndex = this.$.dropdown.indexOfLabel(this._getItemLabel(this.selectedItem));
|
|
1001
|
-
} else {
|
|
1002
|
-
// When the user filled in something that is different from the current value = filtering is enabled,
|
|
1003
|
-
// set the focused index to the item that matches the filter query.
|
|
1004
|
-
this._focusedIndex = this.$.dropdown.indexOfLabel(this.filter);
|
|
1005
|
-
}
|
|
1136
|
+
__setInitialFocusedIndex() {
|
|
1137
|
+
const inputValue = this._inputElementValue;
|
|
1138
|
+
if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
|
|
1139
|
+
// When the input element value is the same as the current value or not defined,
|
|
1140
|
+
// set the focused index to the item that matches the value.
|
|
1141
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this._getItemLabel(this.selectedItem));
|
|
1142
|
+
} else {
|
|
1143
|
+
// When the user filled in something that is different from the current value = filtering is enabled,
|
|
1144
|
+
// set the focused index to the item that matches the filter query.
|
|
1145
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
|
|
1006
1146
|
}
|
|
1007
1147
|
}
|
|
1008
1148
|
|
|
@@ -1023,7 +1163,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1023
1163
|
|
|
1024
1164
|
/** @private */
|
|
1025
1165
|
_selectItemForValue(value) {
|
|
1026
|
-
const valueIndex = this.
|
|
1166
|
+
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
1027
1167
|
const previouslySelectedItem = this.selectedItem;
|
|
1028
1168
|
|
|
1029
1169
|
if (valueIndex >= 0) {
|
|
@@ -1039,42 +1179,48 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1039
1179
|
}
|
|
1040
1180
|
}
|
|
1041
1181
|
|
|
1042
|
-
/** @protected */
|
|
1043
|
-
_getItemElements() {
|
|
1044
|
-
return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-combo-box-item'));
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1182
|
/** @private */
|
|
1048
|
-
|
|
1049
|
-
return this
|
|
1183
|
+
_getItemElements() {
|
|
1184
|
+
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
|
|
1050
1185
|
}
|
|
1051
1186
|
|
|
1052
1187
|
/** @private */
|
|
1053
|
-
|
|
1054
|
-
this
|
|
1188
|
+
_scrollIntoView(index) {
|
|
1189
|
+
if (!this._scroller) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
this._scroller.scrollIntoView(index);
|
|
1055
1193
|
}
|
|
1056
1194
|
|
|
1057
|
-
/**
|
|
1058
|
-
|
|
1059
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Returns the first item that matches the provided value.
|
|
1197
|
+
*
|
|
1198
|
+
* @private
|
|
1199
|
+
*/
|
|
1200
|
+
__getItemIndexByValue(items, value) {
|
|
1201
|
+
if (!items || !isValidValue(value)) {
|
|
1060
1202
|
return -1;
|
|
1061
1203
|
}
|
|
1062
1204
|
|
|
1063
|
-
return items
|
|
1064
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
1065
|
-
return false;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1205
|
+
return findItemIndex(items, (item) => {
|
|
1068
1206
|
return this._getItemValue(item) === value;
|
|
1069
1207
|
});
|
|
1070
1208
|
}
|
|
1071
1209
|
|
|
1072
1210
|
/**
|
|
1073
|
-
*
|
|
1211
|
+
* Returns the first item that matches the provided label.
|
|
1212
|
+
* Labels are matched against each other case insensitively.
|
|
1213
|
+
*
|
|
1074
1214
|
* @private
|
|
1075
1215
|
*/
|
|
1076
|
-
|
|
1077
|
-
|
|
1216
|
+
__getItemIndexByLabel(items, label) {
|
|
1217
|
+
if (!items || !label) {
|
|
1218
|
+
return -1;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return findItemIndex(items, (item) => {
|
|
1222
|
+
return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
|
|
1223
|
+
});
|
|
1078
1224
|
}
|
|
1079
1225
|
|
|
1080
1226
|
/** @private */
|
|
@@ -1105,7 +1251,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1105
1251
|
/** @private */
|
|
1106
1252
|
_onFocusout(event) {
|
|
1107
1253
|
// Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
|
|
1108
|
-
if (event.relatedTarget === this.$.
|
|
1254
|
+
if (event.relatedTarget === this.$.overlay) {
|
|
1109
1255
|
event.composedPath()[0].focus();
|
|
1110
1256
|
return;
|
|
1111
1257
|
}
|
|
@@ -1181,4 +1327,16 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1181
1327
|
* To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
|
|
1182
1328
|
* @event change
|
|
1183
1329
|
*/
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Fired after the `vaadin-combo-box-overlay` opens.
|
|
1333
|
+
*
|
|
1334
|
+
* @event vaadin-combo-box-dropdown-opened
|
|
1335
|
+
*/
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Fired after the `vaadin-combo-box-overlay` closes.
|
|
1339
|
+
*
|
|
1340
|
+
* @event vaadin-combo-box-dropdown-closed
|
|
1341
|
+
*/
|
|
1184
1342
|
};
|
|
@@ -52,9 +52,7 @@ export class ComboBoxOverlay extends PositionMixin(OverlayElement) {
|
|
|
52
52
|
connectedCallback() {
|
|
53
53
|
super.connectedCallback();
|
|
54
54
|
|
|
55
|
-
const
|
|
56
|
-
const comboBox = dropdown && dropdown.getRootNode().host;
|
|
57
|
-
this._comboBox = comboBox;
|
|
55
|
+
const comboBox = this._comboBox;
|
|
58
56
|
|
|
59
57
|
const hostDir = comboBox && comboBox.getAttribute('dir');
|
|
60
58
|
if (hostDir) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
7
|
+
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
|
|
7
8
|
import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
|
|
8
9
|
import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
|
|
9
10
|
|
|
@@ -90,6 +91,7 @@ export class ComboBoxScroller extends PolymerElement {
|
|
|
90
91
|
*/
|
|
91
92
|
selectedItem: {
|
|
92
93
|
type: Object,
|
|
94
|
+
observer: '__selectedItemChanged',
|
|
93
95
|
},
|
|
94
96
|
|
|
95
97
|
/**
|
|
@@ -145,6 +147,9 @@ export class ComboBoxScroller extends PolymerElement {
|
|
|
145
147
|
ready() {
|
|
146
148
|
super.ready();
|
|
147
149
|
|
|
150
|
+
// Ensure every instance has unique ID
|
|
151
|
+
this.id = `${this.localName}-${generateUniqueId()}`;
|
|
152
|
+
|
|
148
153
|
// Allow extensions to customize tag name for the items
|
|
149
154
|
this.__hostTagName = this.constructor.is.replace('-scroller', '');
|
|
150
155
|
|
|
@@ -218,7 +223,7 @@ export class ComboBoxScroller extends PolymerElement {
|
|
|
218
223
|
|
|
219
224
|
/** @private */
|
|
220
225
|
__isItemFocused(focusedIndex, itemIndex) {
|
|
221
|
-
return focusedIndex === itemIndex;
|
|
226
|
+
return !this.loading && focusedIndex === itemIndex;
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
/** @private */
|
|
@@ -243,12 +248,19 @@ export class ComboBoxScroller extends PolymerElement {
|
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
/** @private */
|
|
246
|
-
__loadingChanged(
|
|
247
|
-
if (this.__virtualizer
|
|
251
|
+
__loadingChanged() {
|
|
252
|
+
if (this.__virtualizer) {
|
|
248
253
|
setTimeout(() => this.requestContentUpdate());
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
|
|
257
|
+
/** @private */
|
|
258
|
+
__selectedItemChanged() {
|
|
259
|
+
if (this.__virtualizer) {
|
|
260
|
+
this.requestContentUpdate();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/** @private */
|
|
253
265
|
__focusedIndexChanged(index, oldIndex) {
|
|
254
266
|
if (!this.__virtualizer) {
|
package/src/vaadin-combo-box.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import '@vaadin/input-container/src/vaadin-input-container.js';
|
|
7
|
-
import './vaadin-combo-box-
|
|
7
|
+
import './vaadin-combo-box-item.js';
|
|
8
|
+
import './vaadin-combo-box-overlay.js';
|
|
9
|
+
import './vaadin-combo-box-scroller.js';
|
|
8
10
|
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
9
11
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
10
12
|
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
|
|
@@ -196,19 +198,16 @@ class ComboBox extends ComboBoxDataProviderMixin(
|
|
|
196
198
|
</div>
|
|
197
199
|
</div>
|
|
198
200
|
|
|
199
|
-
<vaadin-combo-box-
|
|
200
|
-
id="
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
<vaadin-combo-box-overlay
|
|
202
|
+
id="overlay"
|
|
203
|
+
hidden$="[[_isOverlayHidden(filteredItems, loading)]]"
|
|
204
|
+
opened="[[_overlayOpened]]"
|
|
205
|
+
loading$="[[loading]]"
|
|
206
|
+
theme$="[[_theme]]"
|
|
203
207
|
position-target="[[_positionTarget]]"
|
|
204
|
-
|
|
208
|
+
no-vertical-overlap
|
|
205
209
|
restore-focus-node="[[inputElement]]"
|
|
206
|
-
|
|
207
|
-
_item-id-path="[[itemIdPath]]"
|
|
208
|
-
_item-label-path="[[itemLabelPath]]"
|
|
209
|
-
loading="[[loading]]"
|
|
210
|
-
theme="[[_theme]]"
|
|
211
|
-
></vaadin-combo-box-dropdown>
|
|
210
|
+
></vaadin-combo-box-overlay>
|
|
212
211
|
`;
|
|
213
212
|
}
|
|
214
213
|
|
|
@@ -273,7 +272,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
|
|
|
273
272
|
*/
|
|
274
273
|
_shouldRemoveFocus(event) {
|
|
275
274
|
// Do not blur when focus moves to the overlay
|
|
276
|
-
if (event.relatedTarget === this.$.
|
|
275
|
+
if (event.relatedTarget === this.$.overlay) {
|
|
277
276
|
event.composedPath()[0].focus();
|
|
278
277
|
return false;
|
|
279
278
|
}
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2015 - 2022 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
import './vaadin-combo-box-item.js';
|
|
7
|
-
import './vaadin-combo-box-overlay.js';
|
|
8
|
-
import './vaadin-combo-box-scroller.js';
|
|
9
|
-
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Element for internal use only.
|
|
13
|
-
*
|
|
14
|
-
* @extends HTMLElement
|
|
15
|
-
* @private
|
|
16
|
-
*/
|
|
17
|
-
export class ComboBoxDropdown extends PolymerElement {
|
|
18
|
-
static get is() {
|
|
19
|
-
return 'vaadin-combo-box-dropdown';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static get template() {
|
|
23
|
-
return html`
|
|
24
|
-
<vaadin-combo-box-overlay
|
|
25
|
-
id="overlay"
|
|
26
|
-
hidden$="[[_isOverlayHidden(_items.*, loading)]]"
|
|
27
|
-
loading$="[[loading]]"
|
|
28
|
-
opened="{{_overlayOpened}}"
|
|
29
|
-
theme$="[[theme]]"
|
|
30
|
-
position-target="[[positionTarget]]"
|
|
31
|
-
no-vertical-overlap
|
|
32
|
-
restore-focus-on-close="[[restoreFocusOnClose]]"
|
|
33
|
-
restore-focus-node="[[restoreFocusNode]]"
|
|
34
|
-
></vaadin-combo-box-overlay>
|
|
35
|
-
`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
static get properties() {
|
|
39
|
-
return {
|
|
40
|
-
/**
|
|
41
|
-
* True if the combo-box has been activate by the user.
|
|
42
|
-
* The actual opened state depends on whether the dropdown has items.
|
|
43
|
-
*/
|
|
44
|
-
opened: Boolean,
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The element to position/align the dropdown by.
|
|
48
|
-
*/
|
|
49
|
-
positionTarget: {
|
|
50
|
-
type: Object,
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Custom function for rendering the content of the `<vaadin-combo-box-item>` propagated from the combo box element.
|
|
55
|
-
*/
|
|
56
|
-
renderer: Function,
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* `true` when new items are being loaded.
|
|
60
|
-
*/
|
|
61
|
-
loading: {
|
|
62
|
-
type: Boolean,
|
|
63
|
-
value: false,
|
|
64
|
-
reflectToAttribute: true,
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Used to propagate the `theme` attribute from the host element.
|
|
69
|
-
*/
|
|
70
|
-
theme: String,
|
|
71
|
-
|
|
72
|
-
_selectedItem: {
|
|
73
|
-
type: Object,
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
_items: {
|
|
77
|
-
type: Array,
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
_focusedIndex: {
|
|
81
|
-
type: Number,
|
|
82
|
-
value: -1,
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
focusedItem: {
|
|
86
|
-
type: String,
|
|
87
|
-
computed: '_getFocusedItem(_focusedIndex)',
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
_itemLabelPath: {
|
|
91
|
-
type: String,
|
|
92
|
-
value: 'label',
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
_itemValuePath: {
|
|
96
|
-
type: String,
|
|
97
|
-
value: 'value',
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
_scroller: Object,
|
|
101
|
-
|
|
102
|
-
_itemIdPath: String,
|
|
103
|
-
|
|
104
|
-
_overlayOpened: {
|
|
105
|
-
type: Boolean,
|
|
106
|
-
observer: '_openedChanged',
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
static get observers() {
|
|
112
|
-
return [
|
|
113
|
-
'_openedOrItemsChanged(opened, _items, loading)',
|
|
114
|
-
'__updateScroller(_scroller, _items, opened, loading, _selectedItem, _itemIdPath, _focusedIndex, renderer, theme)',
|
|
115
|
-
];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
constructor() {
|
|
119
|
-
super();
|
|
120
|
-
|
|
121
|
-
// Ensure every instance has unique ID
|
|
122
|
-
const uniqueId = (ComboBoxDropdown._uniqueId = 1 + ComboBoxDropdown._uniqueId || 0);
|
|
123
|
-
this.scrollerId = `${this.localName}-scroller-${uniqueId}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
ready() {
|
|
127
|
-
super.ready();
|
|
128
|
-
|
|
129
|
-
// Allow extensions to customize tag name for the items
|
|
130
|
-
this.__hostTagName = this.constructor.is.replace('-dropdown', '');
|
|
131
|
-
|
|
132
|
-
const overlay = this.$.overlay;
|
|
133
|
-
const scrollerTag = `${this.__hostTagName}-scroller`;
|
|
134
|
-
|
|
135
|
-
overlay.renderer = (root) => {
|
|
136
|
-
if (!root.firstChild) {
|
|
137
|
-
const scroller = document.createElement(scrollerTag);
|
|
138
|
-
root.appendChild(scroller);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Ensure the scroller is rendered
|
|
143
|
-
overlay.requestContentUpdate();
|
|
144
|
-
|
|
145
|
-
this._scroller = overlay.content.querySelector(scrollerTag);
|
|
146
|
-
this._scroller.id = this.scrollerId;
|
|
147
|
-
|
|
148
|
-
this._scroller.getItemLabel = this.getItemLabel.bind(this);
|
|
149
|
-
this._scroller.comboBox = this.getRootNode().host;
|
|
150
|
-
|
|
151
|
-
this._scroller.addEventListener('selection-changed', (e) => this._forwardScrollerEvent(e));
|
|
152
|
-
this._scroller.addEventListener('index-requested', (e) => this._forwardScrollerEvent(e));
|
|
153
|
-
|
|
154
|
-
overlay.addEventListener('touchend', (e) => this._fireTouchAction(e));
|
|
155
|
-
overlay.addEventListener('touchmove', (e) => this._fireTouchAction(e));
|
|
156
|
-
|
|
157
|
-
// Prevent blurring the input when clicking inside the overlay.
|
|
158
|
-
overlay.addEventListener('mousedown', (e) => e.preventDefault());
|
|
159
|
-
|
|
160
|
-
// Preventing the default modal behaviour of the overlay on input clicking
|
|
161
|
-
overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
|
|
162
|
-
e.preventDefault();
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
disconnectedCallback() {
|
|
167
|
-
super.disconnectedCallback();
|
|
168
|
-
|
|
169
|
-
// Making sure the overlay is closed and removed from DOM after detaching the dropdown.
|
|
170
|
-
this._overlayOpened = false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
_fireTouchAction(sourceEvent) {
|
|
174
|
-
this.dispatchEvent(
|
|
175
|
-
new CustomEvent('vaadin-overlay-touch-action', {
|
|
176
|
-
detail: { sourceEvent },
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
_forwardScrollerEvent(event) {
|
|
182
|
-
this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
_openedChanged(opened, wasOpened) {
|
|
186
|
-
if (opened) {
|
|
187
|
-
this._scroller.style.maxHeight =
|
|
188
|
-
getComputedStyle(this).getPropertyValue(`--${this.__hostTagName}-overlay-max-height`) || '65vh';
|
|
189
|
-
|
|
190
|
-
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
|
|
191
|
-
} else if (wasOpened && !this.__emptyItems) {
|
|
192
|
-
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
_openedOrItemsChanged(opened, items, loading) {
|
|
197
|
-
// See https://github.com/vaadin/vaadin-combo-box/pull/964
|
|
198
|
-
const hasItems = items && items.length;
|
|
199
|
-
if (!hasItems) {
|
|
200
|
-
this.__emptyItems = true;
|
|
201
|
-
}
|
|
202
|
-
this._overlayOpened = !!(opened && (loading || hasItems));
|
|
203
|
-
this.__emptyItems = false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
_getFocusedItem(focusedIndex) {
|
|
207
|
-
if (focusedIndex >= 0) {
|
|
208
|
-
return this._items[focusedIndex];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Gets the index of the item with the provided label.
|
|
214
|
-
* @return {number}
|
|
215
|
-
*/
|
|
216
|
-
indexOfLabel(label) {
|
|
217
|
-
if (this._items && label) {
|
|
218
|
-
for (let i = 0; i < this._items.length; i++) {
|
|
219
|
-
if (this.getItemLabel(this._items[i]).toString().toLowerCase() === label.toString().toLowerCase()) {
|
|
220
|
-
return i;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return -1;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Gets the label string for the item based on the `_itemLabelPath`.
|
|
230
|
-
* @return {string}
|
|
231
|
-
*/
|
|
232
|
-
getItemLabel(item, itemLabelPath) {
|
|
233
|
-
itemLabelPath = itemLabelPath || this._itemLabelPath;
|
|
234
|
-
let label = item && itemLabelPath ? this.get(itemLabelPath, item) : undefined;
|
|
235
|
-
if (label === undefined || label === null) {
|
|
236
|
-
label = item ? item.toString() : '';
|
|
237
|
-
}
|
|
238
|
-
return label;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
_scrollIntoView(index) {
|
|
242
|
-
if (!this._scroller) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
this._scroller.scrollIntoView(index);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
adjustScrollPosition() {
|
|
249
|
-
if (this.opened && this._items) {
|
|
250
|
-
this._scrollIntoView(this._focusedIndex);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// eslint-disable-next-line max-params
|
|
255
|
-
__updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
|
|
256
|
-
if (scroller) {
|
|
257
|
-
scroller.setProperties({
|
|
258
|
-
items: opened ? items : [],
|
|
259
|
-
opened,
|
|
260
|
-
loading,
|
|
261
|
-
selectedItem,
|
|
262
|
-
itemIdPath,
|
|
263
|
-
focusedIndex,
|
|
264
|
-
renderer,
|
|
265
|
-
theme,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
_isOverlayHidden() {
|
|
271
|
-
return !this.loading && !(this._items && this._items.length);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Fired after the `vaadin-combo-box-dropdown` opens.
|
|
276
|
-
*
|
|
277
|
-
* @event vaadin-combo-box-dropdown-opened
|
|
278
|
-
*/
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Fired after the `vaadin-combo-box-dropdown` closes.
|
|
282
|
-
*
|
|
283
|
-
* @event vaadin-combo-box-dropdown-closed
|
|
284
|
-
*/
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
customElements.define(ComboBoxDropdown.is, ComboBoxDropdown);
|