@vaadin/combo-box 23.1.2 → 23.2.0-dev.48e5e3967
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 -38
- package/src/vaadin-combo-box-light.d.ts +3 -1
- package/src/vaadin-combo-box-light.js +14 -13
- package/src/vaadin-combo-box-mixin.d.ts +5 -7
- package/src/vaadin-combo-box-mixin.js +293 -128
- package/src/vaadin-combo-box-overlay.js +1 -3
- package/src/vaadin-combo-box-scroller.js +15 -3
- package/src/vaadin-combo-box.d.ts +1 -0
- package/src/vaadin-combo-box.js +13 -13
- package/src/vaadin-combo-box-dropdown.js +0 -287
|
@@ -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,15 +224,21 @@ 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
|
-
'_filterChanged(filter, itemValuePath, itemLabelPath)',
|
|
206
|
-
'_filteredItemsChanged(filteredItems)',
|
|
207
239
|
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
|
|
240
|
+
'_openedOrItemsChanged(opened, filteredItems, loading)',
|
|
241
|
+
'_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
|
|
208
242
|
];
|
|
209
243
|
}
|
|
210
244
|
|
|
@@ -213,13 +247,20 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
213
247
|
this._boundOnFocusout = this._onFocusout.bind(this);
|
|
214
248
|
this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
|
|
215
249
|
this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
|
|
216
|
-
this._boundClose = this.close.bind(this);
|
|
217
|
-
this._boundOnOpened = this._onOpened.bind(this);
|
|
218
250
|
this._boundOnClick = this._onClick.bind(this);
|
|
219
251
|
this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
|
|
220
252
|
this._boundOnTouchend = this._onTouchend.bind(this);
|
|
221
253
|
}
|
|
222
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Tag name prefix used by scroller and items.
|
|
257
|
+
* @protected
|
|
258
|
+
* @return {string}
|
|
259
|
+
*/
|
|
260
|
+
get _tagNamePrefix() {
|
|
261
|
+
return 'vaadin-combo-box';
|
|
262
|
+
}
|
|
263
|
+
|
|
223
264
|
/**
|
|
224
265
|
* @return {string | undefined}
|
|
225
266
|
* @protected
|
|
@@ -273,23 +314,19 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
273
314
|
ready() {
|
|
274
315
|
super.ready();
|
|
275
316
|
|
|
317
|
+
this._initOverlay();
|
|
318
|
+
this._initScroller();
|
|
319
|
+
|
|
276
320
|
this.addEventListener('focusout', this._boundOnFocusout);
|
|
277
321
|
|
|
278
322
|
this._lastCommittedValue = this.value;
|
|
279
323
|
|
|
280
|
-
this.$.dropdown.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
281
|
-
|
|
282
|
-
this.addEventListener('vaadin-combo-box-dropdown-closed', this._boundClose);
|
|
283
|
-
this.addEventListener('vaadin-combo-box-dropdown-opened', this._boundOnOpened);
|
|
284
324
|
this.addEventListener('click', this._boundOnClick);
|
|
285
|
-
|
|
286
|
-
this.$.dropdown.addEventListener('vaadin-overlay-touch-action', this._boundOnOverlayTouchAction);
|
|
287
|
-
|
|
288
325
|
this.addEventListener('touchend', this._boundOnTouchend);
|
|
289
326
|
|
|
290
327
|
const bringToFrontListener = () => {
|
|
291
328
|
requestAnimationFrame(() => {
|
|
292
|
-
this.$.
|
|
329
|
+
this.$.overlay.bringToFront();
|
|
293
330
|
});
|
|
294
331
|
};
|
|
295
332
|
|
|
@@ -301,6 +338,14 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
301
338
|
this.addController(new VirtualKeyboardController(this));
|
|
302
339
|
}
|
|
303
340
|
|
|
341
|
+
/** @protected */
|
|
342
|
+
disconnectedCallback() {
|
|
343
|
+
super.disconnectedCallback();
|
|
344
|
+
|
|
345
|
+
// Close the overlay on detach
|
|
346
|
+
this.close();
|
|
347
|
+
}
|
|
348
|
+
|
|
304
349
|
/**
|
|
305
350
|
* Requests an update for the content of items.
|
|
306
351
|
* While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
|
|
@@ -308,11 +353,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
308
353
|
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
309
354
|
*/
|
|
310
355
|
requestContentUpdate() {
|
|
311
|
-
if (!this
|
|
356
|
+
if (!this._scroller) {
|
|
312
357
|
return;
|
|
313
358
|
}
|
|
314
359
|
|
|
315
|
-
this
|
|
360
|
+
this._scroller.requestContentUpdate();
|
|
316
361
|
|
|
317
362
|
this._getItemElements().forEach((item) => {
|
|
318
363
|
item.requestContentUpdate();
|
|
@@ -336,6 +381,128 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
336
381
|
this.opened = false;
|
|
337
382
|
}
|
|
338
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Override Polymer lifecycle callback to handle `filter` property change after
|
|
386
|
+
* the observer for `opened` property is triggered. This is needed when opening
|
|
387
|
+
* combo-box on user input to ensure the focused index is set correctly.
|
|
388
|
+
*
|
|
389
|
+
* @param {!Object} currentProps Current accessor values
|
|
390
|
+
* @param {?Object} changedProps Properties changed since the last call
|
|
391
|
+
* @param {?Object} oldProps Previous values for each changed property
|
|
392
|
+
* @protected
|
|
393
|
+
* @override
|
|
394
|
+
*/
|
|
395
|
+
_propertiesChanged(currentProps, changedProps, oldProps) {
|
|
396
|
+
super._propertiesChanged(currentProps, changedProps, oldProps);
|
|
397
|
+
|
|
398
|
+
if (changedProps.filter !== undefined) {
|
|
399
|
+
this._filterChanged(changedProps.filter);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** @private */
|
|
404
|
+
_initOverlay() {
|
|
405
|
+
const overlay = this.$.overlay;
|
|
406
|
+
|
|
407
|
+
// Store instance for detecting "dir" attribute on opening
|
|
408
|
+
overlay._comboBox = this;
|
|
409
|
+
|
|
410
|
+
overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
|
|
411
|
+
overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
|
|
412
|
+
|
|
413
|
+
// Prevent blurring the input when clicking inside the overlay
|
|
414
|
+
overlay.addEventListener('mousedown', (e) => e.preventDefault());
|
|
415
|
+
|
|
416
|
+
// Preventing the default modal behavior of the overlay on input click
|
|
417
|
+
overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Manual two-way binding for the overlay "opened" property
|
|
422
|
+
overlay.addEventListener('opened-changed', (e) => {
|
|
423
|
+
this._overlayOpened = e.detail.value;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Create and initialize the scroller element.
|
|
429
|
+
* Override to provide custom host reference.
|
|
430
|
+
*
|
|
431
|
+
* @protected
|
|
432
|
+
*/
|
|
433
|
+
_initScroller(host) {
|
|
434
|
+
const scrollerTag = `${this._tagNamePrefix}-scroller`;
|
|
435
|
+
|
|
436
|
+
const overlay = this.$.overlay;
|
|
437
|
+
|
|
438
|
+
overlay.renderer = (root) => {
|
|
439
|
+
if (!root.firstChild) {
|
|
440
|
+
root.appendChild(document.createElement(scrollerTag));
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Ensure the scroller is rendered
|
|
445
|
+
if (!this.opened) {
|
|
446
|
+
overlay.requestContentUpdate();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const scroller = overlay.querySelector(scrollerTag);
|
|
450
|
+
|
|
451
|
+
scroller.comboBox = host || this;
|
|
452
|
+
scroller.getItemLabel = this._getItemLabel.bind(this);
|
|
453
|
+
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
454
|
+
|
|
455
|
+
// Trigger the observer to set properties
|
|
456
|
+
this._scroller = scroller;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** @private */
|
|
460
|
+
// eslint-disable-next-line max-params
|
|
461
|
+
_updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
|
|
462
|
+
if (scroller) {
|
|
463
|
+
if (opened) {
|
|
464
|
+
scroller.style.maxHeight =
|
|
465
|
+
getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
scroller.setProperties({
|
|
469
|
+
items: opened ? items : [],
|
|
470
|
+
opened,
|
|
471
|
+
loading,
|
|
472
|
+
selectedItem,
|
|
473
|
+
itemIdPath,
|
|
474
|
+
focusedIndex,
|
|
475
|
+
renderer,
|
|
476
|
+
theme,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** @protected */
|
|
482
|
+
_isOverlayHidden(items, loading) {
|
|
483
|
+
return !loading && !(items && items.length);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** @private */
|
|
487
|
+
_openedOrItemsChanged(opened, items, loading) {
|
|
488
|
+
// Close the overlay if there are no items to display.
|
|
489
|
+
// See https://github.com/vaadin/vaadin-combo-box/pull/964
|
|
490
|
+
this._overlayOpened = !!(opened && (loading || (items && items.length)));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** @private */
|
|
494
|
+
_overlayOpenedChanged(opened, wasOpened) {
|
|
495
|
+
if (opened) {
|
|
496
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
|
|
497
|
+
|
|
498
|
+
this._onOpened();
|
|
499
|
+
} else if (wasOpened && this.filteredItems && this.filteredItems.length) {
|
|
500
|
+
this.close();
|
|
501
|
+
|
|
502
|
+
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
339
506
|
/** @private */
|
|
340
507
|
_focusedIndexChanged(index, oldIndex) {
|
|
341
508
|
if (oldIndex === undefined) {
|
|
@@ -374,7 +541,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
374
541
|
this.focus();
|
|
375
542
|
}
|
|
376
543
|
|
|
377
|
-
this.
|
|
544
|
+
this.$.overlay.restoreFocusOnClose = true;
|
|
378
545
|
} else {
|
|
379
546
|
this._onClosed();
|
|
380
547
|
if (this._openedWithFocusRing && this.hasAttribute('focused')) {
|
|
@@ -387,7 +554,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
387
554
|
input.setAttribute('aria-expanded', !!opened);
|
|
388
555
|
|
|
389
556
|
if (opened) {
|
|
390
|
-
input.setAttribute('aria-controls', this
|
|
557
|
+
input.setAttribute('aria-controls', this._scroller.id);
|
|
391
558
|
} else {
|
|
392
559
|
input.removeAttribute('aria-controls');
|
|
393
560
|
}
|
|
@@ -477,7 +644,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
477
644
|
super._onKeyDown(e);
|
|
478
645
|
|
|
479
646
|
if (e.key === 'Tab') {
|
|
480
|
-
this.
|
|
647
|
+
this.$.overlay.restoreFocusOnClose = false;
|
|
481
648
|
} else if (e.key === 'ArrowDown') {
|
|
482
649
|
this._closeOnBlurIsPrevented = true;
|
|
483
650
|
this._onArrowDown();
|
|
@@ -497,7 +664,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
497
664
|
|
|
498
665
|
/** @private */
|
|
499
666
|
_getItemLabel(item) {
|
|
500
|
-
|
|
667
|
+
let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
|
|
668
|
+
if (label === undefined || label === null) {
|
|
669
|
+
label = item ? item.toString() : '';
|
|
670
|
+
}
|
|
671
|
+
return label;
|
|
501
672
|
}
|
|
502
673
|
|
|
503
674
|
/** @private */
|
|
@@ -512,7 +683,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
512
683
|
/** @private */
|
|
513
684
|
_onArrowDown() {
|
|
514
685
|
if (this.opened) {
|
|
515
|
-
const items = this.
|
|
686
|
+
const items = this.filteredItems;
|
|
516
687
|
if (items) {
|
|
517
688
|
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
|
|
518
689
|
this._prefillFocusedItemLabel();
|
|
@@ -528,7 +699,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
528
699
|
if (this._focusedIndex > -1) {
|
|
529
700
|
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
|
|
530
701
|
} else {
|
|
531
|
-
const items = this.
|
|
702
|
+
const items = this.filteredItems;
|
|
532
703
|
if (items) {
|
|
533
704
|
this._focusedIndex = items.length - 1;
|
|
534
705
|
}
|
|
@@ -543,7 +714,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
543
714
|
/** @private */
|
|
544
715
|
_prefillFocusedItemLabel() {
|
|
545
716
|
if (this._focusedIndex > -1) {
|
|
546
|
-
|
|
717
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
718
|
+
this._inputElementValue = this._getItemLabel(focusedItem);
|
|
547
719
|
this._markAllSelectionRange();
|
|
548
720
|
}
|
|
549
721
|
}
|
|
@@ -700,7 +872,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
700
872
|
_onOpened() {
|
|
701
873
|
// Defer scroll position adjustment to improve performance.
|
|
702
874
|
requestAnimationFrame(() => {
|
|
703
|
-
this
|
|
875
|
+
this._scrollIntoView(this._focusedIndex);
|
|
704
876
|
|
|
705
877
|
// Set attribute after the items are rendered when overlay is opened for the first time.
|
|
706
878
|
this._updateActiveDescendant(this._focusedIndex);
|
|
@@ -719,9 +891,8 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
719
891
|
|
|
720
892
|
/** @private */
|
|
721
893
|
_commitValue() {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const focusedItem = items[this._focusedIndex];
|
|
894
|
+
if (this._focusedIndex > -1) {
|
|
895
|
+
const focusedItem = this.filteredItems[this._focusedIndex];
|
|
725
896
|
if (this.selectedItem !== focusedItem) {
|
|
726
897
|
this.selectedItem = focusedItem;
|
|
727
898
|
}
|
|
@@ -734,18 +905,14 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
734
905
|
this.value = '';
|
|
735
906
|
}
|
|
736
907
|
} else {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
// the filteredItems array (if available) and the selectedItem (if available).
|
|
741
|
-
const itemMatchingByLabel = [...(this.filteredItems || []), this.selectedItem].find((item) => {
|
|
742
|
-
return toLowerCase(this._getItemLabel(item)) === toLowerCase(this._inputElementValue);
|
|
743
|
-
});
|
|
908
|
+
// Try to find an item which label matches the input value.
|
|
909
|
+
const items = [...(this.filteredItems || []), this.selectedItem];
|
|
910
|
+
const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
|
|
744
911
|
|
|
745
912
|
if (
|
|
746
913
|
this.allowCustomValue &&
|
|
747
914
|
// To prevent a repetitive input value being saved after pressing ESC and Tab.
|
|
748
|
-
!
|
|
915
|
+
!itemMatchingInputValue
|
|
749
916
|
) {
|
|
750
917
|
const customValue = this._inputElementValue;
|
|
751
918
|
|
|
@@ -762,12 +929,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
762
929
|
});
|
|
763
930
|
this.dispatchEvent(e);
|
|
764
931
|
if (!e.defaultPrevented) {
|
|
765
|
-
this._selectItemForValue(customValue);
|
|
766
932
|
this.value = customValue;
|
|
767
933
|
}
|
|
768
|
-
} else if (!this.allowCustomValue && !this.opened &&
|
|
934
|
+
} else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
|
|
769
935
|
// An item matching by label was found, select it.
|
|
770
|
-
this.value = this._getItemValue(
|
|
936
|
+
this.value = this._getItemValue(itemMatchingInputValue);
|
|
771
937
|
} else {
|
|
772
938
|
// Revert the input value
|
|
773
939
|
this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
|
|
@@ -796,19 +962,27 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
796
962
|
* @override
|
|
797
963
|
*/
|
|
798
964
|
_onInput(event) {
|
|
799
|
-
|
|
800
|
-
this.open();
|
|
801
|
-
}
|
|
965
|
+
const filter = this._inputElementValue;
|
|
802
966
|
|
|
803
|
-
|
|
804
|
-
|
|
967
|
+
// When opening dropdown on user input, both `opened` and `filter` properties are set.
|
|
968
|
+
// Perform a batched property update instead of relying on sync property observers.
|
|
969
|
+
// This is necessary to avoid an extra data-provider request for loading first page.
|
|
970
|
+
const props = {};
|
|
971
|
+
|
|
972
|
+
if (this.filter === filter) {
|
|
805
973
|
// Filter and input value might get out of sync, while keyboard navigating for example.
|
|
806
974
|
// Afterwards, input value might be changed to the same value as used in filtering.
|
|
807
975
|
// In situation like these, we need to make sure all the filter changes handlers are run.
|
|
808
|
-
this._filterChanged(this.filter
|
|
976
|
+
this._filterChanged(this.filter);
|
|
809
977
|
} else {
|
|
810
|
-
|
|
978
|
+
props.filter = filter;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
|
|
982
|
+
props.opened = true;
|
|
811
983
|
}
|
|
984
|
+
|
|
985
|
+
this.setProperties(props);
|
|
812
986
|
}
|
|
813
987
|
|
|
814
988
|
/**
|
|
@@ -831,13 +1005,11 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
831
1005
|
}
|
|
832
1006
|
|
|
833
1007
|
/** @private */
|
|
834
|
-
_filterChanged(filter
|
|
835
|
-
if (filter === undefined) {
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
|
|
1008
|
+
_filterChanged(filter) {
|
|
839
1009
|
// Scroll to the top of the list whenever the filter changes.
|
|
840
|
-
this
|
|
1010
|
+
this._scrollIntoView(0);
|
|
1011
|
+
|
|
1012
|
+
this._focusedIndex = -1;
|
|
841
1013
|
|
|
842
1014
|
if (this.items) {
|
|
843
1015
|
this.filteredItems = this._filterItems(this.items, filter);
|
|
@@ -849,13 +1021,6 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
849
1021
|
}
|
|
850
1022
|
}
|
|
851
1023
|
|
|
852
|
-
/** @private */
|
|
853
|
-
_loadingChanged(loading) {
|
|
854
|
-
if (loading) {
|
|
855
|
-
this._focusedIndex = -1;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
1024
|
/** @protected */
|
|
860
1025
|
_revertInputValue() {
|
|
861
1026
|
if (this.filter !== '') {
|
|
@@ -901,9 +1066,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
901
1066
|
this._inputElementValue = this._getItemLabel(selectedItem);
|
|
902
1067
|
}
|
|
903
1068
|
|
|
904
|
-
this
|
|
905
|
-
const items = this._getOverlayItems();
|
|
906
|
-
if (this.filteredItems && items) {
|
|
1069
|
+
if (this.filteredItems) {
|
|
907
1070
|
this._focusedIndex = this.filteredItems.indexOf(selectedItem);
|
|
908
1071
|
}
|
|
909
1072
|
}
|
|
@@ -920,7 +1083,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
920
1083
|
return;
|
|
921
1084
|
}
|
|
922
1085
|
|
|
923
|
-
if (
|
|
1086
|
+
if (isValidValue(value)) {
|
|
924
1087
|
if (this._getItemValue(this.selectedItem) !== value) {
|
|
925
1088
|
this._selectItemForValue(value);
|
|
926
1089
|
}
|
|
@@ -956,43 +1119,50 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
956
1119
|
|
|
957
1120
|
if (items) {
|
|
958
1121
|
this.filteredItems = items.slice(0);
|
|
959
|
-
} else if (
|
|
1122
|
+
} else if (oldItems) {
|
|
960
1123
|
// Only clear filteredItems if the component had items previously but got cleared
|
|
961
1124
|
this.filteredItems = null;
|
|
962
1125
|
}
|
|
1126
|
+
}
|
|
963
1127
|
|
|
964
|
-
|
|
965
|
-
|
|
1128
|
+
/** @private */
|
|
1129
|
+
_filteredItemsChanged(filteredItems, oldFilteredItems) {
|
|
1130
|
+
// Store the currently focused item if any. The focused index preserves
|
|
1131
|
+
// in the case when more filtered items are loading but it is reset
|
|
1132
|
+
// when the user types in a filter query.
|
|
1133
|
+
const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
|
|
1134
|
+
|
|
1135
|
+
// Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
|
|
1136
|
+
// (as a result of external filtering or when they have been loaded by the data provider).
|
|
1137
|
+
// When `value` is specified but `selectedItem` is not, it means that there was no item
|
|
1138
|
+
// matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
|
|
1139
|
+
const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
|
|
1140
|
+
if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
|
|
1141
|
+
this.selectedItem = filteredItems[valueIndex];
|
|
1142
|
+
}
|
|
966
1143
|
|
|
967
|
-
|
|
968
|
-
if
|
|
969
|
-
|
|
1144
|
+
// Try to first set focus on the item that had been focused before `filteredItems` were updated
|
|
1145
|
+
// if it is still present in the `filteredItems` array. Otherwise, set the focused index
|
|
1146
|
+
// depending on the selected item or the filter query.
|
|
1147
|
+
const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
|
|
1148
|
+
if (focusedItemIndex > -1) {
|
|
1149
|
+
this._focusedIndex = focusedItemIndex;
|
|
1150
|
+
} else {
|
|
1151
|
+
this.__setInitialFocusedIndex();
|
|
970
1152
|
}
|
|
971
|
-
this.__previousItems = items;
|
|
972
1153
|
}
|
|
973
1154
|
|
|
974
1155
|
/** @private */
|
|
975
|
-
|
|
976
|
-
this._setOverlayItems(filteredItems);
|
|
977
|
-
|
|
978
|
-
// When the external filtering is used and `value` was provided before `filteredItems`,
|
|
979
|
-
// initialize the selected item with the current value here. This will also cause
|
|
980
|
-
// the input element value to sync. In other cases, the selected item is already initialized
|
|
981
|
-
// in other observers such as `valueChanged`, `_itemsChanged`.
|
|
982
|
-
const valueIndex = this._indexOfValue(this.value, filteredItems);
|
|
983
|
-
if (this.selectedItem === null && valueIndex >= 0) {
|
|
984
|
-
this._selectItemForValue(this.value);
|
|
985
|
-
}
|
|
986
|
-
|
|
1156
|
+
__setInitialFocusedIndex() {
|
|
987
1157
|
const inputValue = this._inputElementValue;
|
|
988
1158
|
if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
|
|
989
1159
|
// When the input element value is the same as the current value or not defined,
|
|
990
1160
|
// set the focused index to the item that matches the value.
|
|
991
|
-
this._focusedIndex = this
|
|
1161
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this._getItemLabel(this.selectedItem));
|
|
992
1162
|
} else {
|
|
993
1163
|
// When the user filled in something that is different from the current value = filtering is enabled,
|
|
994
1164
|
// set the focused index to the item that matches the filter query.
|
|
995
|
-
this._focusedIndex = this
|
|
1165
|
+
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
|
|
996
1166
|
}
|
|
997
1167
|
}
|
|
998
1168
|
|
|
@@ -1013,7 +1183,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1013
1183
|
|
|
1014
1184
|
/** @private */
|
|
1015
1185
|
_selectItemForValue(value) {
|
|
1016
|
-
const valueIndex = this.
|
|
1186
|
+
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
1017
1187
|
const previouslySelectedItem = this.selectedItem;
|
|
1018
1188
|
|
|
1019
1189
|
if (valueIndex >= 0) {
|
|
@@ -1029,42 +1199,48 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1029
1199
|
}
|
|
1030
1200
|
}
|
|
1031
1201
|
|
|
1032
|
-
/** @protected */
|
|
1033
|
-
_getItemElements() {
|
|
1034
|
-
return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-combo-box-item'));
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
1202
|
/** @private */
|
|
1038
|
-
|
|
1039
|
-
return this
|
|
1203
|
+
_getItemElements() {
|
|
1204
|
+
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
|
|
1040
1205
|
}
|
|
1041
1206
|
|
|
1042
1207
|
/** @private */
|
|
1043
|
-
|
|
1044
|
-
this
|
|
1208
|
+
_scrollIntoView(index) {
|
|
1209
|
+
if (!this._scroller) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
this._scroller.scrollIntoView(index);
|
|
1045
1213
|
}
|
|
1046
1214
|
|
|
1047
|
-
/**
|
|
1048
|
-
|
|
1049
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* Returns the first item that matches the provided value.
|
|
1217
|
+
*
|
|
1218
|
+
* @private
|
|
1219
|
+
*/
|
|
1220
|
+
__getItemIndexByValue(items, value) {
|
|
1221
|
+
if (!items || !isValidValue(value)) {
|
|
1050
1222
|
return -1;
|
|
1051
1223
|
}
|
|
1052
1224
|
|
|
1053
|
-
return items
|
|
1054
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
1055
|
-
return false;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1225
|
+
return findItemIndex(items, (item) => {
|
|
1058
1226
|
return this._getItemValue(item) === value;
|
|
1059
1227
|
});
|
|
1060
1228
|
}
|
|
1061
1229
|
|
|
1062
1230
|
/**
|
|
1063
|
-
*
|
|
1231
|
+
* Returns the first item that matches the provided label.
|
|
1232
|
+
* Labels are matched against each other case insensitively.
|
|
1233
|
+
*
|
|
1064
1234
|
* @private
|
|
1065
1235
|
*/
|
|
1066
|
-
|
|
1067
|
-
|
|
1236
|
+
__getItemIndexByLabel(items, label) {
|
|
1237
|
+
if (!items || !label) {
|
|
1238
|
+
return -1;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return findItemIndex(items, (item) => {
|
|
1242
|
+
return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
|
|
1243
|
+
});
|
|
1068
1244
|
}
|
|
1069
1245
|
|
|
1070
1246
|
/** @private */
|
|
@@ -1095,7 +1271,7 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1095
1271
|
/** @private */
|
|
1096
1272
|
_onFocusout(event) {
|
|
1097
1273
|
// Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
|
|
1098
|
-
if (event.relatedTarget === this.$.
|
|
1274
|
+
if (event.relatedTarget === this.$.overlay) {
|
|
1099
1275
|
event.composedPath()[0].focus();
|
|
1100
1276
|
return;
|
|
1101
1277
|
}
|
|
@@ -1121,29 +1297,6 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1121
1297
|
this._clear();
|
|
1122
1298
|
}
|
|
1123
1299
|
|
|
1124
|
-
/**
|
|
1125
|
-
* Returns true if `value` is valid, and sets the `invalid` flag appropriately.
|
|
1126
|
-
*
|
|
1127
|
-
* @return {boolean} True if the value is valid and sets the `invalid` flag appropriately
|
|
1128
|
-
*/
|
|
1129
|
-
validate() {
|
|
1130
|
-
return !(this.invalid = !this.checkValidity());
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* Returns true if the current input value satisfies all constraints (if any).
|
|
1135
|
-
* You can override this method for custom validations.
|
|
1136
|
-
*
|
|
1137
|
-
* @return {boolean}
|
|
1138
|
-
*/
|
|
1139
|
-
checkValidity() {
|
|
1140
|
-
if (super.checkValidity) {
|
|
1141
|
-
return super.checkValidity();
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
return !this.required || !!this.value;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
1300
|
/**
|
|
1148
1301
|
* Fired when the value changes.
|
|
1149
1302
|
*
|
|
@@ -1171,4 +1324,16 @@ export const ComboBoxMixin = (subclass) =>
|
|
|
1171
1324
|
* To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
|
|
1172
1325
|
* @event change
|
|
1173
1326
|
*/
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Fired after the `vaadin-combo-box-overlay` opens.
|
|
1330
|
+
*
|
|
1331
|
+
* @event vaadin-combo-box-dropdown-opened
|
|
1332
|
+
*/
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Fired after the `vaadin-combo-box-overlay` closes.
|
|
1336
|
+
*
|
|
1337
|
+
* @event vaadin-combo-box-dropdown-closed
|
|
1338
|
+
*/
|
|
1174
1339
|
};
|