@vaadin/combo-box 23.1.0 → 23.2.0-dev.53560527d
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 +270 -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-dev.53560527d",
|
|
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-dev.53560527d",
|
|
40
|
+
"@vaadin/field-base": "23.2.0-dev.53560527d",
|
|
41
|
+
"@vaadin/input-container": "23.2.0-dev.53560527d",
|
|
42
|
+
"@vaadin/item": "23.2.0-dev.53560527d",
|
|
43
|
+
"@vaadin/lit-renderer": "23.2.0-dev.53560527d",
|
|
44
|
+
"@vaadin/vaadin-lumo-styles": "23.2.0-dev.53560527d",
|
|
45
|
+
"@vaadin/vaadin-material-styles": "23.2.0-dev.53560527d",
|
|
46
|
+
"@vaadin/vaadin-overlay": "23.2.0-dev.53560527d",
|
|
47
|
+
"@vaadin/vaadin-themable-mixin": "23.2.0-dev.53560527d"
|
|
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-dev.53560527d",
|
|
52
52
|
"@vaadin/testing-helpers": "^0.3.2",
|
|
53
|
-
"@vaadin/text-field": "
|
|
53
|
+
"@vaadin/text-field": "23.2.0-dev.53560527d",
|
|
54
54
|
"lit": "^2.0.0",
|
|
55
55
|
"sinon": "^13.0.2"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "6c5c18369b09e22e76365d8a8a5e4bbb220f969b"
|
|
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,104 @@ 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
|
+
scroller.setProperties({
|
|
446
|
+
items: opened ? items : [],
|
|
447
|
+
opened,
|
|
448
|
+
loading,
|
|
449
|
+
selectedItem,
|
|
450
|
+
itemIdPath,
|
|
451
|
+
focusedIndex,
|
|
452
|
+
renderer,
|
|
453
|
+
theme,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @protected */
|
|
459
|
+
_isOverlayHidden(items, loading) {
|
|
460
|
+
return !loading && !(items && items.length);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** @private */
|
|
464
|
+
_openedOrItemsChanged(opened, items, loading) {
|
|
465
|
+
// Close the overlay if there are no items to display.
|
|
466
|
+
// See https://github.com/vaadin/vaadin-combo-box/pull/964
|
|
467
|
+
this._overlayOpened = !!(opened && (loading || (items && items.length)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** @private */
|
|
471
|
+
_overlayOpenedChanged(opened, wasOpened) {
|
|
472
|
+
if (opened) {
|
|
473
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
|
|
474
|
+
|
|
475
|
+
this._onOpened();
|
|
476
|
+
} else if (wasOpened && this.filteredItems && this.filteredItems.length) {
|
|
477
|
+
this.close();
|
|
478
|
+
|
|
479
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
340
483
|
/** @private */
|
|
341
484
|
_focusedIndexChanged(index, oldIndex) {
|
|
342
485
|
if (oldIndex === undefined) {
|
|
@@ -375,7 +518,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
375
518
|
this.focus();
|
|
376
519
|
}
|
|
377
520
|
|
|
378
|
-
this.
|
|
521
|
+
this.$.overlay.restoreFocusOnClose = true;
|
|
379
522
|
} else {
|
|
380
523
|
this._onClosed();
|
|
381
524
|
if (this._openedWithFocusRing && this.hasAttribute('focused')) {
|
|
@@ -388,7 +531,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
388
531
|
input.setAttribute('aria-expanded', !!opened);
|
|
389
532
|
|
|
390
533
|
if (opened) {
|
|
391
|
-
input.setAttribute('aria-controls', this
|
|
534
|
+
input.setAttribute('aria-controls', this._scroller.id);
|
|
392
535
|
} else {
|
|
393
536
|
input.removeAttribute('aria-controls');
|
|
394
537
|
}
|
|
@@ -478,7 +621,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
478
621
|
super._onKeyDown(e);
|
|
479
622
|
|
|
480
623
|
if (e.key === 'Tab') {
|
|
481
|
-
this.
|
|
624
|
+
this.$.overlay.restoreFocusOnClose = false;
|
|
482
625
|
} else if (e.key === 'ArrowDown') {
|
|
483
626
|
this._closeOnBlurIsPrevented = true;
|
|
484
627
|
this._onArrowDown();
|
|
@@ -498,7 +641,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
498
641
|
|
|
499
642
|
/** @private */
|
|
500
643
|
_getItemLabel(item) {
|
|
501
|
-
|
|
644
|
+
let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
|
|
645
|
+
if (label === undefined || label === null) {
|
|
646
|
+
label = item ? item.toString() : '';
|
|
647
|
+
}
|
|
648
|
+
return label;
|
|
502
649
|
}
|
|
503
650
|
|
|
504
651
|
/** @private */
|
|
@@ -513,7 +660,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
513
660
|
/** @private */
|
|
514
661
|
_onArrowDown() {
|
|
515
662
|
if (this.opened) {
|
|
516
|
-
const items = this.
|
|
663
|
+
const items = this.filteredItems;
|
|
517
664
|
if (items) {
|
|
518
665
|
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
|
|
519
666
|
this._prefillFocusedItemLabel();
|
|
@@ -529,7 +676,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
529
676
|
if (this._focusedIndex > -1) {
|
|
530
677
|
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
|
|
531
678
|
} else {
|
|
532
|
-
const items = this.
|
|
679
|
+
const items = this.filteredItems;
|
|
533
680
|
if (items) {
|
|
534
681
|
this._focusedIndex = items.length - 1;
|
|
535
682
|
}
|
|
@@ -544,7 +691,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
544
691
|
/** @private */
|
|
545
692
|
_prefillFocusedItemLabel() {
|
|
546
693
|
if (this._focusedIndex > -1) {
|
|
547
|
-
|
|
694
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
695
|
+
this._inputElementValue = this._getItemLabel(focusedItem);
|
|
548
696
|
this._markAllSelectionRange();
|
|
549
697
|
}
|
|
550
698
|
}
|
|
@@ -701,7 +849,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
701
849
|
_onOpened() {
|
|
702
850
|
// Defer scroll position adjustment to improve performance.
|
|
703
851
|
requestAnimationFrame(() => {
|
|
704
|
-
this
|
|
852
|
+
// When opened is set as attribute, this logic needs to be delayed until scroller is created.
|
|
853
|
+
this._scroller.style.maxHeight =
|
|
854
|
+
getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
|
|
855
|
+
|
|
856
|
+
this._scrollIntoView(this._focusedIndex);
|
|
705
857
|
|
|
706
858
|
// Set attribute after the items are rendered when overlay is opened for the first time.
|
|
707
859
|
this._updateActiveDescendant(this._focusedIndex);
|
|
@@ -720,9 +872,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
720
872
|
|
|
721
873
|
/** @private */
|
|
722
874
|
_commitValue() {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const focusedItem = items[this._focusedIndex];
|
|
875
|
+
if (this._focusedIndex > -1) {
|
|
876
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
726
877
|
if (this.selectedItem !== focusedItem) {
|
|
727
878
|
this.selectedItem = focusedItem;
|
|
728
879
|
}
|
|
@@ -735,18 +886,14 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
735
886
|
this.value = '';
|
|
736
887
|
}
|
|
737
888
|
} 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
|
-
});
|
|
889
|
+
// Try to find an item which label matches the input value.
|
|
890
|
+
const items = [...(this.filteredItems || []), this.selectedItem];
|
|
891
|
+
const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
|
|
745
892
|
|
|
746
893
|
if (
|
|
747
894
|
this.allowCustomValue &&
|
|
748
895
|
// To prevent a repetitive input value being saved after pressing ESC and Tab.
|
|
749
|
-
!
|
|
896
|
+
!itemMatchingInputValue
|
|
750
897
|
) {
|
|
751
898
|
const customValue = this._inputElementValue;
|
|
752
899
|
|
|
@@ -763,12 +910,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
763
910
|
});
|
|
764
911
|
this.dispatchEvent(e);
|
|
765
912
|
if (!e.defaultPrevented) {
|
|
766
|
-
this._selectItemForValue(customValue);
|
|
767
913
|
this.value = customValue;
|
|
768
914
|
}
|
|
769
|
-
} else if (!this.allowCustomValue && !this.opened &&
|
|
915
|
+
} else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
|
|
770
916
|
// An item matching by label was found, select it.
|
|
771
|
-
this.value = this._getItemValue(
|
|
917
|
+
this.value = this._getItemValue(itemMatchingInputValue);
|
|
772
918
|
} else {
|
|
773
919
|
// Revert the input value
|
|
774
920
|
this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
|
|
@@ -834,13 +980,15 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
834
980
|
}
|
|
835
981
|
|
|
836
982
|
/** @private */
|
|
837
|
-
_filterChanged(filter,
|
|
983
|
+
_filterChanged(filter, _itemValuePath, _itemLabelPath) {
|
|
838
984
|
if (filter === undefined) {
|
|
839
985
|
return;
|
|
840
986
|
}
|
|
841
987
|
|
|
842
988
|
// Scroll to the top of the list whenever the filter changes.
|
|
843
|
-
this
|
|
989
|
+
this._scrollIntoView(0);
|
|
990
|
+
|
|
991
|
+
this._focusedIndex = -1;
|
|
844
992
|
|
|
845
993
|
if (this.items) {
|
|
846
994
|
this.filteredItems = this._filterItems(this.items, filter);
|
|
@@ -848,14 +996,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
848
996
|
// With certain use cases (e. g., external filtering), `items` are
|
|
849
997
|
// undefined. Filtering is unnecessary per se, but the filteredItems
|
|
850
998
|
// 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;
|
|
999
|
+
this._filteredItemsChanged(this.filteredItems);
|
|
859
1000
|
}
|
|
860
1001
|
}
|
|
861
1002
|
|
|
@@ -904,9 +1045,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
904
1045
|
this._inputElementValue = this._getItemLabel(selectedItem);
|
|
905
1046
|
}
|
|
906
1047
|
|
|
907
|
-
this
|
|
908
|
-
const items = this._getOverlayItems();
|
|
909
|
-
if (this.filteredItems && items) {
|
|
1048
|
+
if (this.filteredItems) {
|
|
910
1049
|
this._focusedIndex = this.filteredItems.indexOf(selectedItem);
|
|
911
1050
|
}
|
|
912
1051
|
}
|
|
@@ -923,7 +1062,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
923
1062
|
return;
|
|
924
1063
|
}
|
|
925
1064
|
|
|
926
|
-
if (
|
|
1065
|
+
if (isValidValue(value)) {
|
|
927
1066
|
let item;
|
|
928
1067
|
if (this._getItemValue(this.selectedItem) !== value) {
|
|
929
1068
|
this._selectItemForValue(value);
|
|
@@ -956,53 +1095,53 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
956
1095
|
this._ensureItemsOrDataProvider(() => {
|
|
957
1096
|
this.items = oldItems;
|
|
958
1097
|
});
|
|
1098
|
+
|
|
1099
|
+
if (items) {
|
|
1100
|
+
this.filteredItems = items.slice(0);
|
|
1101
|
+
} else if (oldItems) {
|
|
1102
|
+
// Only clear filteredItems if the component had items previously but got cleared
|
|
1103
|
+
this.filteredItems = null;
|
|
1104
|
+
}
|
|
959
1105
|
}
|
|
960
1106
|
|
|
961
1107
|
/** @private */
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1108
|
+
_filteredItemsChanged(filteredItems, oldFilteredItems) {
|
|
1109
|
+
// Store the currently focused item if any. The focused index preserves
|
|
1110
|
+
// in the case when more filtered items are loading but it is reset
|
|
1111
|
+
// when the user types in a filter query.
|
|
1112
|
+
const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
|
|
1113
|
+
|
|
1114
|
+
// Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
|
|
1115
|
+
// (as a result of external filtering or when they have been loaded by the data provider).
|
|
1116
|
+
// When `value` is specified but `selectedItem` is not, it means that there was no item
|
|
1117
|
+
// matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
|
|
1118
|
+
const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
|
|
1119
|
+
if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
|
|
1120
|
+
this.selectedItem = filteredItems[valueIndex];
|
|
1121
|
+
}
|
|
973
1122
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1123
|
+
// Try to first set focus on the item that had been focused before `filteredItems` were updated
|
|
1124
|
+
// if it is still present in the `filteredItems` array. Otherwise, set the focused index
|
|
1125
|
+
// depending on the selected item or the filter query.
|
|
1126
|
+
const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
|
|
1127
|
+
if (focusedItemIndex > -1) {
|
|
1128
|
+
this._focusedIndex = focusedItemIndex;
|
|
1129
|
+
} else {
|
|
1130
|
+
this.__setInitialFocusedIndex();
|
|
978
1131
|
}
|
|
979
|
-
this.__previousItems = e.value;
|
|
980
1132
|
}
|
|
981
1133
|
|
|
982
1134
|
/** @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
|
-
}
|
|
1135
|
+
__setInitialFocusedIndex() {
|
|
1136
|
+
const inputValue = this._inputElementValue;
|
|
1137
|
+
if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
|
|
1138
|
+
// When the input element value is the same as the current value or not defined,
|
|
1139
|
+
// set the focused index to the item that matches the value.
|
|
1140
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this._getItemLabel(this.selectedItem));
|
|
1141
|
+
} else {
|
|
1142
|
+
// When the user filled in something that is different from the current value = filtering is enabled,
|
|
1143
|
+
// set the focused index to the item that matches the filter query.
|
|
1144
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
|
|
1006
1145
|
}
|
|
1007
1146
|
}
|
|
1008
1147
|
|
|
@@ -1023,7 +1162,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1023
1162
|
|
|
1024
1163
|
/** @private */
|
|
1025
1164
|
_selectItemForValue(value) {
|
|
1026
|
-
const valueIndex = this.
|
|
1165
|
+
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
1027
1166
|
const previouslySelectedItem = this.selectedItem;
|
|
1028
1167
|
|
|
1029
1168
|
if (valueIndex >= 0) {
|
|
@@ -1039,42 +1178,48 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1039
1178
|
}
|
|
1040
1179
|
}
|
|
1041
1180
|
|
|
1042
|
-
/** @protected */
|
|
1043
|
-
_getItemElements() {
|
|
1044
|
-
return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-combo-box-item'));
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1181
|
/** @private */
|
|
1048
|
-
|
|
1049
|
-
return this
|
|
1182
|
+
_getItemElements() {
|
|
1183
|
+
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
|
|
1050
1184
|
}
|
|
1051
1185
|
|
|
1052
1186
|
/** @private */
|
|
1053
|
-
|
|
1054
|
-
this
|
|
1187
|
+
_scrollIntoView(index) {
|
|
1188
|
+
if (!this._scroller) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
this._scroller.scrollIntoView(index);
|
|
1055
1192
|
}
|
|
1056
1193
|
|
|
1057
|
-
/**
|
|
1058
|
-
|
|
1059
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Returns the first item that matches the provided value.
|
|
1196
|
+
*
|
|
1197
|
+
* @private
|
|
1198
|
+
*/
|
|
1199
|
+
__getItemIndexByValue(items, value) {
|
|
1200
|
+
if (!items || !isValidValue(value)) {
|
|
1060
1201
|
return -1;
|
|
1061
1202
|
}
|
|
1062
1203
|
|
|
1063
|
-
return items
|
|
1064
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
1065
|
-
return false;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1204
|
+
return findItemIndex(items, (item) => {
|
|
1068
1205
|
return this._getItemValue(item) === value;
|
|
1069
1206
|
});
|
|
1070
1207
|
}
|
|
1071
1208
|
|
|
1072
1209
|
/**
|
|
1073
|
-
*
|
|
1210
|
+
* Returns the first item that matches the provided label.
|
|
1211
|
+
* Labels are matched against each other case insensitively.
|
|
1212
|
+
*
|
|
1074
1213
|
* @private
|
|
1075
1214
|
*/
|
|
1076
|
-
|
|
1077
|
-
|
|
1215
|
+
__getItemIndexByLabel(items, label) {
|
|
1216
|
+
if (!items || !label) {
|
|
1217
|
+
return -1;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return findItemIndex(items, (item) => {
|
|
1221
|
+
return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
|
|
1222
|
+
});
|
|
1078
1223
|
}
|
|
1079
1224
|
|
|
1080
1225
|
/** @private */
|
|
@@ -1105,7 +1250,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1105
1250
|
/** @private */
|
|
1106
1251
|
_onFocusout(event) {
|
|
1107
1252
|
// Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
|
|
1108
|
-
if (event.relatedTarget === this.$.
|
|
1253
|
+
if (event.relatedTarget === this.$.overlay) {
|
|
1109
1254
|
event.composedPath()[0].focus();
|
|
1110
1255
|
return;
|
|
1111
1256
|
}
|
|
@@ -1181,4 +1326,16 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1181
1326
|
* To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
|
|
1182
1327
|
* @event change
|
|
1183
1328
|
*/
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Fired after the `vaadin-combo-box-overlay` opens.
|
|
1332
|
+
*
|
|
1333
|
+
* @event vaadin-combo-box-dropdown-opened
|
|
1334
|
+
*/
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Fired after the `vaadin-combo-box-overlay` closes.
|
|
1338
|
+
*
|
|
1339
|
+
* @event vaadin-combo-box-dropdown-closed
|
|
1340
|
+
*/
|
|
1184
1341
|
};
|
|
@@ -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);
|