@vaadin/combo-box 24.8.4 → 25.0.0-alpha10
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/README.md +0 -23
- package/package.json +17 -19
- package/src/styles/vaadin-combo-box-base-styles.d.ts +8 -0
- package/src/styles/vaadin-combo-box-base-styles.js +17 -0
- package/src/styles/vaadin-combo-box-core-styles.d.ts +8 -0
- package/src/styles/vaadin-combo-box-core-styles.js +12 -0
- package/src/styles/vaadin-combo-box-overlay-base-styles.js +46 -0
- package/src/styles/vaadin-combo-box-overlay-core-styles.js +18 -0
- package/src/styles/vaadin-combo-box-scroller-base-styles.js +29 -0
- package/src/styles/vaadin-combo-box-scroller-core-styles.js +27 -0
- package/src/vaadin-combo-box-base-mixin.d.ts +56 -0
- package/src/vaadin-combo-box-base-mixin.js +776 -0
- package/src/vaadin-combo-box-data-provider-mixin.js +17 -32
- package/src/vaadin-combo-box-item-mixin.js +6 -1
- package/src/vaadin-combo-box-item.js +17 -16
- package/src/vaadin-combo-box-items-mixin.d.ts +53 -0
- package/src/vaadin-combo-box-items-mixin.js +275 -0
- package/src/vaadin-combo-box-mixin.d.ts +3 -72
- package/src/vaadin-combo-box-mixin.js +84 -922
- package/src/vaadin-combo-box-overlay-mixin.js +1 -22
- package/src/vaadin-combo-box-overlay.js +15 -22
- package/src/vaadin-combo-box-scroller.js +10 -26
- package/src/vaadin-combo-box.d.ts +12 -14
- package/src/vaadin-combo-box.js +81 -53
- package/web-types.json +51 -536
- package/web-types.lit.json +17 -262
- package/src/vaadin-combo-box-light-mixin.d.ts +0 -26
- package/src/vaadin-combo-box-light-mixin.js +0 -131
- package/src/vaadin-combo-box-light.d.ts +0 -161
- package/src/vaadin-combo-box-light.js +0 -94
- package/src/vaadin-lit-combo-box-item.js +0 -68
- package/src/vaadin-lit-combo-box-light.js +0 -57
- package/src/vaadin-lit-combo-box-overlay.js +0 -60
- package/src/vaadin-lit-combo-box-scroller.js +0 -59
- package/src/vaadin-lit-combo-box.js +0 -169
- package/theme/lumo/vaadin-combo-box-light.d.ts +0 -3
- package/theme/lumo/vaadin-combo-box-light.js +0 -3
- package/theme/lumo/vaadin-lit-combo-box-light.d.ts +0 -3
- package/theme/lumo/vaadin-lit-combo-box-light.js +0 -3
- package/theme/lumo/vaadin-lit-combo-box.d.ts +0 -4
- package/theme/lumo/vaadin-lit-combo-box.js +0 -4
- package/theme/material/vaadin-combo-box-item-styles.d.ts +0 -5
- package/theme/material/vaadin-combo-box-item-styles.js +0 -20
- package/theme/material/vaadin-combo-box-light.d.ts +0 -3
- package/theme/material/vaadin-combo-box-light.js +0 -3
- package/theme/material/vaadin-combo-box-overlay-styles.d.ts +0 -4
- package/theme/material/vaadin-combo-box-overlay-styles.js +0 -51
- package/theme/material/vaadin-combo-box-styles.d.ts +0 -3
- package/theme/material/vaadin-combo-box-styles.js +0 -21
- package/theme/material/vaadin-combo-box.d.ts +0 -4
- package/theme/material/vaadin-combo-box.js +0 -4
- package/theme/material/vaadin-lit-combo-box-light.d.ts +0 -3
- package/theme/material/vaadin-lit-combo-box-light.js +0 -3
- package/theme/material/vaadin-lit-combo-box.d.ts +0 -4
- package/theme/material/vaadin-lit-combo-box.js +0 -4
- package/vaadin-combo-box-light.d.ts +0 -1
- package/vaadin-combo-box-light.js +0 -2
- package/vaadin-lit-combo-box-light.d.ts +0 -1
- package/vaadin-lit-combo-box-light.js +0 -2
- package/vaadin-lit-combo-box.d.ts +0 -1
- package/vaadin-lit-combo-box.js +0 -2
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2015 - 2025 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
|
|
7
|
+
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
|
|
8
|
+
import { isElementFocused, isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
9
|
+
import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
|
|
10
|
+
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
|
|
11
|
+
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
|
|
12
|
+
import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
|
|
13
|
+
import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
|
|
14
|
+
import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @polymerMixin
|
|
18
|
+
* @mixes DisabledMixin
|
|
19
|
+
* @mixes FocusMixin
|
|
20
|
+
* @mixes InputMixin
|
|
21
|
+
* @mixes KeyboardMixin
|
|
22
|
+
* @mixes OverlayClassMixin
|
|
23
|
+
* @param {function(new:HTMLElement)} superClass
|
|
24
|
+
*/
|
|
25
|
+
export const ComboBoxBaseMixin = (superClass) =>
|
|
26
|
+
class ComboBoxMixinBaseClass extends OverlayClassMixin(
|
|
27
|
+
KeyboardMixin(InputMixin(DisabledMixin(FocusMixin(superClass)))),
|
|
28
|
+
) {
|
|
29
|
+
static get properties() {
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* True if the dropdown is open, false otherwise.
|
|
33
|
+
* @type {boolean}
|
|
34
|
+
*/
|
|
35
|
+
opened: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
notify: true,
|
|
38
|
+
value: false,
|
|
39
|
+
reflectToAttribute: true,
|
|
40
|
+
sync: true,
|
|
41
|
+
observer: '_openedChanged',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set true to prevent the overlay from opening automatically.
|
|
46
|
+
* @attr {boolean} auto-open-disabled
|
|
47
|
+
*/
|
|
48
|
+
autoOpenDisabled: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
sync: true,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* When present, it specifies that the field is read-only.
|
|
55
|
+
* @type {boolean}
|
|
56
|
+
*/
|
|
57
|
+
readonly: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
value: false,
|
|
60
|
+
reflectToAttribute: true,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @type {number}
|
|
65
|
+
* @protected
|
|
66
|
+
*/
|
|
67
|
+
_focusedIndex: {
|
|
68
|
+
type: Number,
|
|
69
|
+
observer: '_focusedIndexChanged',
|
|
70
|
+
value: -1,
|
|
71
|
+
sync: true,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @type {!HTMLElement | undefined}
|
|
76
|
+
* @protected
|
|
77
|
+
*/
|
|
78
|
+
_toggleElement: {
|
|
79
|
+
type: Object,
|
|
80
|
+
observer: '_toggleElementChanged',
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set of items to be rendered in the dropdown.
|
|
85
|
+
* @protected
|
|
86
|
+
*/
|
|
87
|
+
_dropdownItems: {
|
|
88
|
+
type: Array,
|
|
89
|
+
sync: true,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether the overlay should be opened.
|
|
94
|
+
* @protected
|
|
95
|
+
*/
|
|
96
|
+
_overlayOpened: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
sync: true,
|
|
99
|
+
observer: '_overlayOpenedChanged',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
constructor() {
|
|
105
|
+
super();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reference to the `vaadin-combo-box-scroller` element instance.
|
|
109
|
+
* Do not define in `properties` to avoid triggering updates.
|
|
110
|
+
* @type {HTMLElement}
|
|
111
|
+
* @protected
|
|
112
|
+
*/
|
|
113
|
+
this._scroller;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Used to detect if focusout should be ignored due to touch.
|
|
117
|
+
* Do not define in `properties` to avoid triggering updates.
|
|
118
|
+
* @type {boolean}
|
|
119
|
+
* @protected
|
|
120
|
+
*/
|
|
121
|
+
this._closeOnBlurIsPrevented;
|
|
122
|
+
|
|
123
|
+
this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
|
|
124
|
+
this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
|
|
125
|
+
this._boundOnClick = this._onClick.bind(this);
|
|
126
|
+
this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
|
|
127
|
+
this._boundOnTouchend = this._onTouchend.bind(this);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Tag name prefix used by scroller and items.
|
|
132
|
+
* @protected
|
|
133
|
+
* @return {string}
|
|
134
|
+
*/
|
|
135
|
+
get _tagNamePrefix() {
|
|
136
|
+
return 'vaadin-combo-box';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Override method inherited from `InputMixin`
|
|
141
|
+
* to customize the input element.
|
|
142
|
+
* @protected
|
|
143
|
+
* @override
|
|
144
|
+
*/
|
|
145
|
+
_inputElementChanged(input) {
|
|
146
|
+
super._inputElementChanged(input);
|
|
147
|
+
|
|
148
|
+
if (input) {
|
|
149
|
+
input.autocomplete = 'off';
|
|
150
|
+
input.autocapitalize = 'off';
|
|
151
|
+
|
|
152
|
+
input.setAttribute('role', 'combobox');
|
|
153
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
154
|
+
input.setAttribute('aria-expanded', !!this.opened);
|
|
155
|
+
|
|
156
|
+
// Disable the macOS Safari spell check auto corrections.
|
|
157
|
+
input.setAttribute('spellcheck', 'false');
|
|
158
|
+
|
|
159
|
+
// Disable iOS autocorrect suggestions.
|
|
160
|
+
input.setAttribute('autocorrect', 'off');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @protected */
|
|
165
|
+
firstUpdated() {
|
|
166
|
+
super.firstUpdated();
|
|
167
|
+
|
|
168
|
+
// Init scroller in `firstUpdated()` to ensure the `_scroller` reference
|
|
169
|
+
// is available by the time property observer runs. Also, do not store it
|
|
170
|
+
// in a reactive property to avoid triggering another unnecessary update.
|
|
171
|
+
this._initScroller();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @protected */
|
|
175
|
+
ready() {
|
|
176
|
+
super.ready();
|
|
177
|
+
|
|
178
|
+
this._initOverlay();
|
|
179
|
+
|
|
180
|
+
this.addEventListener('click', this._boundOnClick);
|
|
181
|
+
this.addEventListener('touchend', this._boundOnTouchend);
|
|
182
|
+
|
|
183
|
+
if (this.clearElement) {
|
|
184
|
+
this.clearElement.addEventListener('mousedown', this._boundOnClearButtonMouseDown);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.addController(new VirtualKeyboardController(this));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @protected */
|
|
191
|
+
disconnectedCallback() {
|
|
192
|
+
super.disconnectedCallback();
|
|
193
|
+
|
|
194
|
+
// Close the overlay on detach
|
|
195
|
+
this.close();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Opens the dropdown list.
|
|
200
|
+
*/
|
|
201
|
+
open() {
|
|
202
|
+
// Prevent _open() being called when input is disabled or read-only
|
|
203
|
+
if (!this.disabled && !this.readonly) {
|
|
204
|
+
this.opened = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Closes the dropdown list.
|
|
210
|
+
*/
|
|
211
|
+
close() {
|
|
212
|
+
this.opened = false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @private */
|
|
216
|
+
_initOverlay() {
|
|
217
|
+
const overlay = this.$.overlay;
|
|
218
|
+
|
|
219
|
+
overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
|
|
220
|
+
overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
|
|
221
|
+
|
|
222
|
+
// Prevent blurring the input when clicking inside the overlay
|
|
223
|
+
overlay.addEventListener('mousedown', (e) => e.preventDefault());
|
|
224
|
+
|
|
225
|
+
// Manual two-way binding for the overlay "opened" property
|
|
226
|
+
overlay.addEventListener('opened-changed', (e) => {
|
|
227
|
+
this._overlayOpened = e.detail.value;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this._overlayElement = overlay;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create and initialize the scroller element.
|
|
235
|
+
*
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
_initScroller() {
|
|
239
|
+
const scroller = document.createElement(`${this._tagNamePrefix}-scroller`);
|
|
240
|
+
|
|
241
|
+
scroller.owner = this;
|
|
242
|
+
scroller.getItemLabel = this._getItemLabel.bind(this);
|
|
243
|
+
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
244
|
+
|
|
245
|
+
this._renderScroller(scroller);
|
|
246
|
+
|
|
247
|
+
this._scroller = scroller;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render the scroller element to the overlay.
|
|
252
|
+
*
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
_renderScroller(scroller) {
|
|
256
|
+
scroller.setAttribute('slot', 'overlay');
|
|
257
|
+
// Prevent focusing scroller on input Tab
|
|
258
|
+
scroller.setAttribute('tabindex', '-1');
|
|
259
|
+
this.appendChild(scroller);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @type {boolean}
|
|
264
|
+
* @protected
|
|
265
|
+
*/
|
|
266
|
+
get _hasDropdownItems() {
|
|
267
|
+
return !!(this._dropdownItems && this._dropdownItems.length);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @private */
|
|
271
|
+
_overlayOpenedChanged(opened, wasOpened) {
|
|
272
|
+
if (opened) {
|
|
273
|
+
this._onOpened();
|
|
274
|
+
} else if (wasOpened && this._hasDropdownItems) {
|
|
275
|
+
this.close();
|
|
276
|
+
this._onOverlayClosed();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @private */
|
|
281
|
+
_focusedIndexChanged(index, oldIndex) {
|
|
282
|
+
if (oldIndex === undefined) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this._updateActiveDescendant(index);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** @protected */
|
|
289
|
+
_isInputFocused() {
|
|
290
|
+
return this.inputElement && isElementFocused(this.inputElement);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** @private */
|
|
294
|
+
_updateActiveDescendant(index) {
|
|
295
|
+
const input = this.inputElement;
|
|
296
|
+
if (!input) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const item = this._getItemElements().find((el) => el.index === index);
|
|
301
|
+
if (item) {
|
|
302
|
+
input.setAttribute('aria-activedescendant', item.id);
|
|
303
|
+
} else {
|
|
304
|
+
input.removeAttribute('aria-activedescendant');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** @private */
|
|
309
|
+
_openedChanged(opened, wasOpened) {
|
|
310
|
+
// Prevent _close() being called when opened is set to its default value (false).
|
|
311
|
+
if (wasOpened === undefined) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (opened) {
|
|
316
|
+
// For touch devices, we don't want to popup virtual keyboard
|
|
317
|
+
// unless input element is explicitly focused by the user.
|
|
318
|
+
if (!this._isInputFocused() && !isTouch) {
|
|
319
|
+
if (this.inputElement) {
|
|
320
|
+
this.inputElement.focus();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
this._onClosed();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const input = this.inputElement;
|
|
328
|
+
if (input) {
|
|
329
|
+
input.setAttribute('aria-expanded', !!opened);
|
|
330
|
+
|
|
331
|
+
if (opened) {
|
|
332
|
+
input.setAttribute('aria-controls', this._scroller.id);
|
|
333
|
+
} else {
|
|
334
|
+
input.removeAttribute('aria-controls');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @private */
|
|
340
|
+
_onOverlayTouchAction() {
|
|
341
|
+
// On touch devices, blur the input on touch start inside the overlay, in order to hide
|
|
342
|
+
// the virtual keyboard. But don't close the overlay on this blur.
|
|
343
|
+
this._closeOnBlurIsPrevented = true;
|
|
344
|
+
this.inputElement.blur();
|
|
345
|
+
this._closeOnBlurIsPrevented = false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @protected */
|
|
349
|
+
_isClearButton(event) {
|
|
350
|
+
return event.composedPath()[0] === this.clearElement;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @private */
|
|
354
|
+
__onClearButtonMouseDown(event) {
|
|
355
|
+
event.preventDefault(); // Prevent native focusout event
|
|
356
|
+
this.inputElement.focus();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {Event} event
|
|
361
|
+
* @protected
|
|
362
|
+
*/
|
|
363
|
+
_onClearButtonClick(event) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
this._onClearAction();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @param {Event} event
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
_onToggleButtonClick(event) {
|
|
373
|
+
// Prevent parent components such as `vaadin-grid`
|
|
374
|
+
// from handling the click event after it bubbles.
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
|
|
377
|
+
if (this.opened) {
|
|
378
|
+
this.close();
|
|
379
|
+
} else {
|
|
380
|
+
this.open();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @param {Event} event
|
|
386
|
+
* @protected
|
|
387
|
+
*/
|
|
388
|
+
_onHostClick(event) {
|
|
389
|
+
if (!this.autoOpenDisabled) {
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
this.open();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @private */
|
|
396
|
+
_onClick(event) {
|
|
397
|
+
if (this._isClearButton(event)) {
|
|
398
|
+
this._onClearButtonClick(event);
|
|
399
|
+
} else if (event.composedPath().includes(this._toggleElement)) {
|
|
400
|
+
this._onToggleButtonClick(event);
|
|
401
|
+
} else {
|
|
402
|
+
this._onHostClick(event);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @private */
|
|
407
|
+
_onTouchend(event) {
|
|
408
|
+
if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
this._onClearAction();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Override an event listener from `KeyboardMixin`.
|
|
418
|
+
*
|
|
419
|
+
* @param {KeyboardEvent} e
|
|
420
|
+
* @protected
|
|
421
|
+
* @override
|
|
422
|
+
*/
|
|
423
|
+
_onKeyDown(e) {
|
|
424
|
+
super._onKeyDown(e);
|
|
425
|
+
|
|
426
|
+
if (e.key === 'ArrowDown') {
|
|
427
|
+
this._onArrowDown();
|
|
428
|
+
|
|
429
|
+
// Prevent caret from moving
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
} else if (e.key === 'ArrowUp') {
|
|
432
|
+
this._onArrowUp();
|
|
433
|
+
|
|
434
|
+
// Prevent caret from moving
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Override to provide logic for item label path.
|
|
441
|
+
* @protected
|
|
442
|
+
*/
|
|
443
|
+
_getItemLabel(item) {
|
|
444
|
+
return item ? item.toString() : '';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** @private */
|
|
448
|
+
_onArrowDown() {
|
|
449
|
+
if (this.opened) {
|
|
450
|
+
const items = this._dropdownItems;
|
|
451
|
+
if (items) {
|
|
452
|
+
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
|
|
453
|
+
this._prefillFocusedItemLabel();
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
this.open();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** @private */
|
|
461
|
+
_onArrowUp() {
|
|
462
|
+
if (this.opened) {
|
|
463
|
+
if (this._focusedIndex > -1) {
|
|
464
|
+
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
|
|
465
|
+
} else {
|
|
466
|
+
const items = this._dropdownItems;
|
|
467
|
+
if (items) {
|
|
468
|
+
this._focusedIndex = items.length - 1;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this._prefillFocusedItemLabel();
|
|
473
|
+
} else {
|
|
474
|
+
this.open();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @private */
|
|
479
|
+
_prefillFocusedItemLabel() {
|
|
480
|
+
if (this._focusedIndex > -1) {
|
|
481
|
+
const focusedItem = this._dropdownItems[this._focusedIndex];
|
|
482
|
+
this._inputElementValue = this._getItemLabel(focusedItem);
|
|
483
|
+
this._markAllSelectionRange();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** @private */
|
|
488
|
+
_setSelectionRange(start, end) {
|
|
489
|
+
// Setting selection range focuses and/or moves the caret in some browsers,
|
|
490
|
+
// and there's no need to modify the selection range if the input isn't focused anyway.
|
|
491
|
+
// This affects Safari. When the overlay is open, and then hitting tab, browser should focus
|
|
492
|
+
// the next focusable element instead of the combo-box itself.
|
|
493
|
+
if (this._isInputFocused() && this.inputElement.setSelectionRange) {
|
|
494
|
+
this.inputElement.setSelectionRange(start, end);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** @private */
|
|
499
|
+
_markAllSelectionRange() {
|
|
500
|
+
if (this._inputElementValue !== undefined) {
|
|
501
|
+
this._setSelectionRange(0, this._inputElementValue.length);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** @private */
|
|
506
|
+
_clearSelectionRange() {
|
|
507
|
+
if (this._inputElementValue !== undefined) {
|
|
508
|
+
const pos = this._inputElementValue ? this._inputElementValue.length : 0;
|
|
509
|
+
this._setSelectionRange(pos, pos);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* @protected
|
|
515
|
+
*/
|
|
516
|
+
_closeOrCommit() {
|
|
517
|
+
if (!this.opened) {
|
|
518
|
+
this._commitValue();
|
|
519
|
+
} else {
|
|
520
|
+
this.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Override an event listener from `KeyboardMixin`.
|
|
526
|
+
*
|
|
527
|
+
* @param {KeyboardEvent} e
|
|
528
|
+
* @protected
|
|
529
|
+
* @override
|
|
530
|
+
*/
|
|
531
|
+
_onEnter(e) {
|
|
532
|
+
// Do not commit value when custom values are disallowed and input value is not a valid option
|
|
533
|
+
// also stop propagation of the event, otherwise the user could submit a form while the input
|
|
534
|
+
// still contains an invalid value
|
|
535
|
+
if (!this._hasValidInputValue()) {
|
|
536
|
+
// Do not submit the surrounding form.
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
// Do not trigger global listeners
|
|
539
|
+
e.stopPropagation();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Stop propagation of the enter event only if the dropdown is opened, this
|
|
544
|
+
// "consumes" the enter event for the action of closing the dropdown
|
|
545
|
+
if (this.opened) {
|
|
546
|
+
// Do not submit the surrounding form.
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
// Do not trigger global listeners
|
|
549
|
+
e.stopPropagation();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this._closeOrCommit();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Override this method to detect whether valid value is provided.
|
|
557
|
+
* @protected
|
|
558
|
+
*/
|
|
559
|
+
_hasValidInputValue() {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Override an event listener from `KeyboardMixin`.
|
|
565
|
+
* Do not call `super` in order to override clear
|
|
566
|
+
* button logic defined in `InputControlMixin`.
|
|
567
|
+
*
|
|
568
|
+
* @param {!KeyboardEvent} e
|
|
569
|
+
* @protected
|
|
570
|
+
* @override
|
|
571
|
+
*/
|
|
572
|
+
_onEscape(e) {
|
|
573
|
+
if (
|
|
574
|
+
this.autoOpenDisabled &&
|
|
575
|
+
(this.opened || (this.value !== this._inputElementValue && this._inputElementValue.length > 0))
|
|
576
|
+
) {
|
|
577
|
+
// Auto-open is disabled
|
|
578
|
+
// The overlay is open or
|
|
579
|
+
// The input value has changed but the change hasn't been committed, so cancel it.
|
|
580
|
+
e.stopPropagation();
|
|
581
|
+
this._focusedIndex = -1;
|
|
582
|
+
this._onEscapeCancel();
|
|
583
|
+
} else if (this.opened) {
|
|
584
|
+
// Auto-open is enabled
|
|
585
|
+
// The overlay is open
|
|
586
|
+
e.stopPropagation();
|
|
587
|
+
|
|
588
|
+
if (this._focusedIndex > -1) {
|
|
589
|
+
// An item is focused, revert the input to the filtered value
|
|
590
|
+
this._focusedIndex = -1;
|
|
591
|
+
this._revertInputValue();
|
|
592
|
+
} else {
|
|
593
|
+
// No item is focused, cancel the change and close the overlay
|
|
594
|
+
this._onEscapeCancel();
|
|
595
|
+
}
|
|
596
|
+
} else if (this.clearButtonVisible && !!this.value && !this.readonly) {
|
|
597
|
+
e.stopPropagation();
|
|
598
|
+
// The clear button is visible and the overlay is closed, so clear the value.
|
|
599
|
+
this._onClearAction();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Override to handle canceling and closing overlay on Escape.
|
|
605
|
+
* @protected
|
|
606
|
+
*/
|
|
607
|
+
_onEscapeCancel() {
|
|
608
|
+
// To be implemented
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** @private */
|
|
612
|
+
_toggleElementChanged(toggleElement) {
|
|
613
|
+
if (toggleElement) {
|
|
614
|
+
// Don't blur the input on toggle mousedown
|
|
615
|
+
toggleElement.addEventListener('mousedown', (e) => e.preventDefault());
|
|
616
|
+
// Unfocus previously focused element if focus is not inside combo box (on touch devices)
|
|
617
|
+
toggleElement.addEventListener('click', () => {
|
|
618
|
+
if (isTouch && !this._isInputFocused()) {
|
|
619
|
+
document.activeElement.blur();
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Override to implement logic for clearing value.
|
|
627
|
+
* @protected
|
|
628
|
+
*/
|
|
629
|
+
_onClearAction() {
|
|
630
|
+
// To be implemented
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Override to implement logic for overlay opening.
|
|
635
|
+
* @protected
|
|
636
|
+
*/
|
|
637
|
+
_onOpened() {
|
|
638
|
+
// To be implemented
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Override to implement logic for changing opened to false.
|
|
643
|
+
* @protected
|
|
644
|
+
*/
|
|
645
|
+
_onClosed() {
|
|
646
|
+
// To be implemented
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Override to implement logic for overlay closing.
|
|
651
|
+
* @protected
|
|
652
|
+
*/
|
|
653
|
+
_onOverlayClosed() {
|
|
654
|
+
// To be implemented
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Override to implement logic for committing value.
|
|
659
|
+
* @protected
|
|
660
|
+
*/
|
|
661
|
+
_commitValue() {
|
|
662
|
+
// To be implemented
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Override to implement logic for value reverting.
|
|
667
|
+
* @protected
|
|
668
|
+
*/
|
|
669
|
+
_revertInputValue() {
|
|
670
|
+
this._inputElementValue = this.value;
|
|
671
|
+
this._clearSelectionRange();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Override an event listener from `InputMixin`.
|
|
676
|
+
* @param {!Event} event
|
|
677
|
+
* @protected
|
|
678
|
+
* @override
|
|
679
|
+
*/
|
|
680
|
+
_onInput(event) {
|
|
681
|
+
if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
|
|
682
|
+
this.opened = true;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** @private */
|
|
687
|
+
_getItemElements() {
|
|
688
|
+
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** @protected */
|
|
692
|
+
_scrollIntoView(index) {
|
|
693
|
+
if (!this._scroller) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this._scroller.scrollIntoView(index);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** @private */
|
|
700
|
+
_overlaySelectedItemChanged(e) {
|
|
701
|
+
// Stop this private event from leaking outside.
|
|
702
|
+
e.stopPropagation();
|
|
703
|
+
|
|
704
|
+
if (e.detail.item instanceof ComboBoxPlaceholder) {
|
|
705
|
+
// Placeholder items should not be selectable.
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (this.opened) {
|
|
710
|
+
this._focusedIndex = this._dropdownItems.indexOf(e.detail.item);
|
|
711
|
+
this.close();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Override method inherited from `FocusMixin`
|
|
717
|
+
* to close the overlay on blur and commit the value.
|
|
718
|
+
*
|
|
719
|
+
* @param {boolean} focused
|
|
720
|
+
* @protected
|
|
721
|
+
* @override
|
|
722
|
+
*/
|
|
723
|
+
_setFocused(focused) {
|
|
724
|
+
super._setFocused(focused);
|
|
725
|
+
|
|
726
|
+
if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
|
|
727
|
+
this._handleFocusOut();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Override this method to provide custom logic for focusout.
|
|
733
|
+
* @protected
|
|
734
|
+
*/
|
|
735
|
+
_handleFocusOut() {
|
|
736
|
+
if (isKeyboardActive()) {
|
|
737
|
+
// Close on Tab key causing blur. With mouse, close on outside click instead.
|
|
738
|
+
this._closeOrCommit();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (!this.opened) {
|
|
743
|
+
this._commitValue();
|
|
744
|
+
} else if (!this._overlayOpened) {
|
|
745
|
+
// Combo-box is opened, but overlay is not visible -> custom value was entered.
|
|
746
|
+
// Make sure we close here as there won't be an "outside click" in this case.
|
|
747
|
+
this.close();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Override method inherited from `FocusMixin` to not remove focused
|
|
753
|
+
* state when focus moves to the overlay.
|
|
754
|
+
*
|
|
755
|
+
* @param {FocusEvent} event
|
|
756
|
+
* @return {boolean}
|
|
757
|
+
* @protected
|
|
758
|
+
* @override
|
|
759
|
+
*/
|
|
760
|
+
_shouldRemoveFocus(event) {
|
|
761
|
+
// VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
|
|
762
|
+
// Do not focus the input in this case, because it would break announcement for the item.
|
|
763
|
+
if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Do not blur when focus moves to the overlay
|
|
768
|
+
// Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
|
|
769
|
+
if (event.relatedTarget === this._overlayElement) {
|
|
770
|
+
event.composedPath()[0].focus();
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
};
|