@vaadin/combo-box 24.4.0-alpha9 → 24.4.0-dev.4b20a0c55

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 CHANGED
@@ -5,7 +5,6 @@ A web component for choosing a value from a filterable list of options presented
5
5
  [Documentation + Live Demo ↗](https://vaadin.com/docs/latest/components/combo-box)
6
6
 
7
7
  [![npm version](https://badgen.net/npm/v/@vaadin/combo-box)](https://www.npmjs.com/package/@vaadin/combo-box)
8
- [![Discord](https://img.shields.io/discord/732335336448852018?label=discord)](https://discord.gg/PHmkCKC)
9
8
 
10
9
  ```html
11
10
  <vaadin-combo-box
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/combo-box",
3
- "version": "24.4.0-alpha9",
3
+ "version": "24.4.0-dev.4b20a0c55",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,21 +38,21 @@
38
38
  "dependencies": {
39
39
  "@open-wc/dedupe-mixin": "^1.3.0",
40
40
  "@polymer/polymer": "^3.0.0",
41
- "@vaadin/a11y-base": "24.4.0-alpha9",
42
- "@vaadin/component-base": "24.4.0-alpha9",
43
- "@vaadin/field-base": "24.4.0-alpha9",
44
- "@vaadin/input-container": "24.4.0-alpha9",
45
- "@vaadin/item": "24.4.0-alpha9",
46
- "@vaadin/lit-renderer": "24.4.0-alpha9",
47
- "@vaadin/overlay": "24.4.0-alpha9",
48
- "@vaadin/vaadin-lumo-styles": "24.4.0-alpha9",
49
- "@vaadin/vaadin-material-styles": "24.4.0-alpha9",
50
- "@vaadin/vaadin-themable-mixin": "24.4.0-alpha9"
41
+ "@vaadin/a11y-base": "24.4.0-dev.4b20a0c55",
42
+ "@vaadin/component-base": "24.4.0-dev.4b20a0c55",
43
+ "@vaadin/field-base": "24.4.0-dev.4b20a0c55",
44
+ "@vaadin/input-container": "24.4.0-dev.4b20a0c55",
45
+ "@vaadin/item": "24.4.0-dev.4b20a0c55",
46
+ "@vaadin/lit-renderer": "24.4.0-dev.4b20a0c55",
47
+ "@vaadin/overlay": "24.4.0-dev.4b20a0c55",
48
+ "@vaadin/vaadin-lumo-styles": "24.4.0-dev.4b20a0c55",
49
+ "@vaadin/vaadin-material-styles": "24.4.0-dev.4b20a0c55",
50
+ "@vaadin/vaadin-themable-mixin": "24.4.0-dev.4b20a0c55"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@esm-bundle/chai": "^4.3.4",
54
54
  "@vaadin/testing-helpers": "^0.6.0",
55
- "@vaadin/text-field": "24.4.0-alpha9",
55
+ "@vaadin/text-field": "24.4.0-dev.4b20a0c55",
56
56
  "lit": "^3.0.0",
57
57
  "sinon": "^13.0.2"
58
58
  },
@@ -60,5 +60,5 @@
60
60
  "web-types.json",
61
61
  "web-types.lit.json"
62
62
  ],
63
- "gitHead": "effb81abe3c6283a6ec620cc0cee56069af58226"
63
+ "gitHead": "b79c81e5f6fd24684b34ee0dc434e94d943ea13e"
64
64
  }
@@ -3,6 +3,8 @@
3
3
  * Copyright (c) 2015 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { DataProviderController } from '@vaadin/component-base/src/data-provider-controller/data-provider-controller.js';
7
+ import { get } from '@vaadin/component-base/src/path-utils.js';
6
8
  import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
7
9
 
8
10
  /**
@@ -51,13 +53,6 @@ export const ComboBoxDataProviderMixin = (superClass) =>
51
53
  observer: '_dataProviderChanged',
52
54
  },
53
55
 
54
- /** @private */
55
- _pendingRequests: {
56
- value: () => {
57
- return {};
58
- },
59
- },
60
-
61
56
  /** @private */
62
57
  __placeHolder: {
63
58
  value: new ComboBoxPlaceholder(),
@@ -67,6 +62,12 @@ export const ComboBoxDataProviderMixin = (superClass) =>
67
62
  __previousDataProviderFilter: {
68
63
  type: String,
69
64
  },
65
+
66
+ /** @private */
67
+ _hasData: {
68
+ type: Boolean,
69
+ value: false,
70
+ },
70
71
  };
71
72
  }
72
73
 
@@ -78,10 +79,32 @@ export const ComboBoxDataProviderMixin = (superClass) =>
78
79
  ];
79
80
  }
80
81
 
82
+ constructor() {
83
+ super();
84
+
85
+ /** @type {DataProviderController} */
86
+ this._dataProviderController = new DataProviderController(this, {
87
+ size: this.size,
88
+ pageSize: this.pageSize,
89
+ getItemId: (item) => get(this.itemIdPath, item),
90
+ placeholder: this.__placeHolder,
91
+ dataProvider: this.dataProvider ? this.dataProvider.bind(this) : null,
92
+ dataProviderParams: () => ({ filter: this.filter }),
93
+ });
94
+ }
95
+
81
96
  /** @protected */
82
97
  ready() {
83
98
  super.ready();
99
+
100
+ this._dataProviderController.addEventListener('page-requested', this.__onDataProviderPageRequested.bind(this));
101
+ this._dataProviderController.addEventListener('page-loaded', this.__onDataProviderPageLoaded.bind(this));
102
+
84
103
  this._scroller.addEventListener('index-requested', (e) => {
104
+ if (!this._shouldFetchData()) {
105
+ return;
106
+ }
107
+
85
108
  const index = e.detail.index;
86
109
  const currentScrollerPos = e.detail.currentScrollerPos;
87
110
  const allowedIndexRange = Math.floor(this.pageSize * 1.5);
@@ -95,10 +118,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
95
118
  }
96
119
 
97
120
  if (index !== undefined) {
98
- const page = this._getPageForIndex(index);
99
- if (this._shouldLoadPage(page)) {
100
- this._loadPage(page);
101
- }
121
+ this._dataProviderController.ensureFlatIndexLoaded(index);
102
122
  }
103
123
  });
104
124
  }
@@ -113,19 +133,14 @@ export const ComboBoxDataProviderMixin = (superClass) =>
113
133
  if (this.__previousDataProviderFilter !== filter) {
114
134
  this.__previousDataProviderFilter = filter;
115
135
 
116
- this._pendingRequests = {};
117
- // Immediately mark as loading if this refresh leads to re-fetching pages
118
- // This prevents some issues with the properties below triggering
119
- // observers that also rely on the loading state
120
- this.loading = this._shouldFetchData();
121
- // Reset size and internal loading state
136
+ this._keepOverlayOpened = true;
122
137
  this.size = undefined;
123
-
124
138
  this.clearCache();
139
+ this._keepOverlayOpened = false;
125
140
  }
126
141
  }
127
142
 
128
- /** @private */
143
+ /** @protected */
129
144
  _shouldFetchData() {
130
145
  if (!this.dataProvider) {
131
146
  return false;
@@ -136,8 +151,12 @@ export const ComboBoxDataProviderMixin = (superClass) =>
136
151
 
137
152
  /** @private */
138
153
  _ensureFirstPage(opened) {
139
- if (opened && this._shouldLoadPage(0)) {
140
- this._loadPage(0);
154
+ if (!this._shouldFetchData()) {
155
+ return;
156
+ }
157
+
158
+ if (opened && !this._hasData) {
159
+ this._dataProviderController.loadFirstPage();
141
160
  }
142
161
  }
143
162
 
@@ -151,104 +170,99 @@ export const ComboBoxDataProviderMixin = (superClass) =>
151
170
  }
152
171
 
153
172
  /** @private */
154
- _shouldLoadPage(page) {
155
- if (!this.filteredItems || this._forceNextRequest) {
156
- this._forceNextRequest = false;
157
- return true;
158
- }
159
-
160
- const loadedItem = this.filteredItems[page * this.pageSize];
161
- if (loadedItem !== undefined) {
162
- return loadedItem instanceof ComboBoxPlaceholder;
163
- }
164
- return this.size === undefined;
173
+ __onDataProviderPageRequested() {
174
+ this.loading = true;
165
175
  }
166
176
 
167
177
  /** @private */
168
- _loadPage(page) {
169
- // Make sure same page isn't requested multiple times.
170
- if (this._pendingRequests[page] || !this.dataProvider) {
171
- return;
172
- }
178
+ __onDataProviderPageLoaded() {
179
+ this._hasData = true;
173
180
 
174
- const params = {
175
- page,
176
- pageSize: this.pageSize,
177
- filter: this.filter,
178
- };
179
-
180
- const callback = (items, size) => {
181
- if (this._pendingRequests[page] !== callback) {
182
- return;
183
- }
181
+ this.requestContentUpdate();
184
182
 
185
- const filteredItems = this.filteredItems ? [...this.filteredItems] : [];
186
- filteredItems.splice(params.page * params.pageSize, items.length, ...items);
187
- this.filteredItems = filteredItems;
183
+ if (!this.opened && !this._isInputFocused()) {
184
+ this._commitValue();
185
+ }
186
+ }
188
187
 
189
- if (!this.opened && !this._isInputFocused()) {
190
- this._commitValue();
191
- }
188
+ /**
189
+ * Clears the cached pages and reloads data from dataprovider when needed.
190
+ */
191
+ clearCache() {
192
+ if (!this.dataProvider) {
193
+ return;
194
+ }
192
195
 
193
- if (size !== undefined) {
194
- this.size = size;
195
- }
196
+ this._dataProviderController.clearCache();
196
197
 
197
- delete this._pendingRequests[page];
198
+ this._hasData = false;
198
199
 
199
- if (Object.keys(this._pendingRequests).length === 0) {
200
- this.loading = false;
201
- }
202
- };
200
+ this.requestContentUpdate();
203
201
 
204
- this._pendingRequests[page] = callback;
205
- // Set the `loading` flag only after marking the request as pending
206
- // to prevent the same page from getting requested multiple times
207
- // as a result of `__loadingChanged` in the scroller which requests
208
- // a virtualizer update which in turn may trigger a data provider page request.
209
- this.loading = true;
210
- this.dataProvider(params, callback);
202
+ if (this._shouldFetchData()) {
203
+ this._dataProviderController.loadFirstPage();
204
+ }
211
205
  }
212
206
 
213
207
  /** @private */
214
- _getPageForIndex(index) {
215
- return Math.floor(index / this.pageSize);
208
+ _sizeChanged(size = 0) {
209
+ const { rootCache } = this._dataProviderController;
210
+ // When the size update originates from the developer,
211
+ // sync the new size with the controller and trigger
212
+ // a content update to re-render the scroller.
213
+ if (rootCache.size !== size) {
214
+ rootCache.size = size;
215
+ this.requestContentUpdate();
216
+ }
216
217
  }
217
218
 
218
219
  /**
219
- * Clears the cached pages and reloads data from dataprovider when needed.
220
+ * @private
221
+ * @override
220
222
  */
221
- clearCache() {
223
+ _filteredItemsChanged(items) {
222
224
  if (!this.dataProvider) {
223
- return;
225
+ return super._filteredItemsChanged(items);
224
226
  }
225
227
 
226
- this._pendingRequests = {};
227
- const filteredItems = [];
228
- for (let i = 0; i < (this.size || 0); i++) {
229
- filteredItems.push(this.__placeHolder);
230
- }
231
- this.filteredItems = filteredItems;
232
-
233
- if (this._shouldFetchData()) {
234
- this._forceNextRequest = false;
235
- this._loadPage(0);
236
- } else {
237
- this._forceNextRequest = true;
228
+ const { rootCache } = this._dataProviderController;
229
+ // When the items update originates from the developer,
230
+ // sync the new items with the controller and trigger
231
+ // a content update to re-render the scroller.
232
+ if (rootCache.items !== items) {
233
+ rootCache.items = items;
234
+ this.requestContentUpdate();
238
235
  }
239
236
  }
240
237
 
241
- /** @private */
242
- _sizeChanged(size = 0) {
243
- const filteredItems = (this.filteredItems || []).slice(0, size);
244
- for (let i = 0; i < size; i++) {
245
- filteredItems[i] = filteredItems[i] !== undefined ? filteredItems[i] : this.__placeHolder;
238
+ /** @override */
239
+ requestContentUpdate() {
240
+ if (this.dataProvider) {
241
+ const { rootCache } = this._dataProviderController;
242
+
243
+ // Sync the controller's size with the component.
244
+ // They can be out of sync after, for example,
245
+ // the controller received new data.
246
+ if ((this.size || 0) !== rootCache.size) {
247
+ this.size = rootCache.size;
248
+ }
249
+
250
+ // Sync the controller's items with the component.
251
+ // They can be out of sync after, for example,
252
+ // the controller received new data.
253
+ if (this.filteredItems !== rootCache.items) {
254
+ this.filteredItems = rootCache.items;
255
+ }
256
+
257
+ // Sync the controller's loading state with the component.
258
+ this.loading = this._dataProviderController.isLoading();
259
+
260
+ // Set a copy of the controller's items as the dropdown items
261
+ // to trigger an update of the focused index in _setDropdownItems.
262
+ this._setDropdownItems([...this.filteredItems]);
246
263
  }
247
- this.filteredItems = filteredItems;
248
264
 
249
- // Cleans up the redundant pending requests for pages > size
250
- // Refers to https://github.com/vaadin/vaadin-flow-components/issues/229
251
- this._flushPendingRequests(size);
265
+ super.requestContentUpdate();
252
266
  }
253
267
 
254
268
  /** @private */
@@ -257,6 +271,8 @@ export const ComboBoxDataProviderMixin = (superClass) =>
257
271
  this.pageSize = oldPageSize;
258
272
  throw new Error('`pageSize` value must be an integer > 0');
259
273
  }
274
+
275
+ this._dataProviderController.setPageSize(pageSize);
260
276
  this.clearCache();
261
277
  }
262
278
 
@@ -266,6 +282,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
266
282
  this.dataProvider = oldDataProvider;
267
283
  });
268
284
 
285
+ this._dataProviderController.setDataProvider(dataProvider);
269
286
  this.clearCache();
270
287
  }
271
288
 
@@ -274,8 +291,6 @@ export const ComboBoxDataProviderMixin = (superClass) =>
274
291
  if (this.items !== undefined && this.dataProvider !== undefined) {
275
292
  restoreOldValueCallback();
276
293
  throw new Error('Using `items` and `dataProvider` together is not supported');
277
- } else if (this.dataProvider && !this.filteredItems) {
278
- this.filteredItems = [];
279
294
  }
280
295
  }
281
296
 
@@ -294,28 +309,4 @@ export const ComboBoxDataProviderMixin = (superClass) =>
294
309
  }
295
310
  }
296
311
  }
297
-
298
- /**
299
- * This method cleans up the page callbacks which refers to the
300
- * non-existing pages, i.e. which item indexes are greater than the
301
- * changed size.
302
- * This case is basically happens when:
303
- * 1. Users scroll fast to the bottom and combo box generates the
304
- * redundant page request/callback
305
- * 2. Server side uses undefined size lazy loading and suddenly reaches
306
- * the exact size which is on the range edge
307
- * (for default page size = 50, it will be 100, 200, 300, ...).
308
- * @param size the new size of items
309
- * @private
310
- */
311
- _flushPendingRequests(size) {
312
- if (this._pendingRequests) {
313
- const lastPage = Math.ceil(size / this.pageSize);
314
- Object.entries(this._pendingRequests).forEach(([page, callback]) => {
315
- if (parseInt(page) >= lastPage) {
316
- callback([], size);
317
- }
318
- });
319
- }
320
- }
321
312
  };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2015 - 2024 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+ import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
8
+ import type { ComboBoxDataProviderMixinClass } from './vaadin-combo-box-data-provider-mixin.js';
9
+ import type { ComboBoxMixinClass } from './vaadin-combo-box-mixin.js';
10
+
11
+ export declare function ComboBoxLightMixin<TItem, T extends Constructor<HTMLElement>>(
12
+ base: T,
13
+ ): Constructor<ComboBoxDataProviderMixinClass<TItem>> &
14
+ Constructor<ComboBoxLightMixinClass> &
15
+ Constructor<ComboBoxMixinClass<TItem>> &
16
+ Constructor<ValidateMixinClass> &
17
+ T;
18
+
19
+ export declare class ComboBoxLightMixinClass {
20
+ /**
21
+ * Name of the two-way data-bindable property representing the
22
+ * value of the custom input field.
23
+ * @attr {string} attr-for-value
24
+ */
25
+ attrForValue: string;
26
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2015 - 2024 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { dashToCamelCase } from '@polymer/polymer/lib/utils/case-map.js';
7
+ import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
8
+ import { ValidateMixin } from '@vaadin/field-base/src/validate-mixin.js';
9
+ import { ComboBoxDataProviderMixin } from './vaadin-combo-box-data-provider-mixin.js';
10
+ import { ComboBoxMixin } from './vaadin-combo-box-mixin.js';
11
+
12
+ /**
13
+ * @polymerMixin
14
+ * @mixes ComboBoxDataProviderMixin
15
+ * @mixes ComboBoxMixin
16
+ * @mixes ValidateMixin
17
+ */
18
+ export const ComboBoxLightMixin = (superClass) =>
19
+ class ComboBoxLightMixinClass extends ComboBoxDataProviderMixin(ComboBoxMixin(ValidateMixin(superClass))) {
20
+ static get properties() {
21
+ return {
22
+ /**
23
+ * Name of the two-way data-bindable property representing the
24
+ * value of the custom input field.
25
+ * @attr {string} attr-for-value
26
+ * @type {string}
27
+ */
28
+ attrForValue: {
29
+ type: String,
30
+ value: 'value',
31
+ },
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Used by `InputControlMixin` as a reference to the clear button element.
37
+ * @protected
38
+ * @return {!HTMLElement}
39
+ */
40
+ get clearElement() {
41
+ return this.querySelector('.clear-button');
42
+ }
43
+
44
+ /**
45
+ * Override this getter from `InputMixin` to allow using
46
+ * an arbitrary property name instead of `value`
47
+ * for accessing the input element's value.
48
+ *
49
+ * @protected
50
+ * @override
51
+ * @return {string}
52
+ */
53
+ get _inputElementValueProperty() {
54
+ return dashToCamelCase(this.attrForValue);
55
+ }
56
+
57
+ /**
58
+ * @protected
59
+ * @override
60
+ * @return {HTMLInputElement | undefined}
61
+ */
62
+ get _nativeInput() {
63
+ const input = this.inputElement;
64
+
65
+ if (input) {
66
+ // Support `<input class="input">`
67
+ if (input instanceof HTMLInputElement) {
68
+ return input;
69
+ }
70
+
71
+ // Support `<input>` in light DOM (e.g. `vaadin-text-field`)
72
+ const slottedInput = input.querySelector('input');
73
+ if (slottedInput) {
74
+ return slottedInput;
75
+ }
76
+
77
+ if (input.shadowRoot) {
78
+ // Support `<input>` in Shadow DOM (e.g. `mwc-textfield`)
79
+ const shadowInput = input.shadowRoot.querySelector('input');
80
+ if (shadowInput) {
81
+ return shadowInput;
82
+ }
83
+ }
84
+ }
85
+
86
+ return undefined;
87
+ }
88
+
89
+ /** @protected */
90
+ ready() {
91
+ super.ready();
92
+
93
+ this._toggleElement = this.querySelector('.toggle-button');
94
+
95
+ // Wait until the slotted input DOM is ready
96
+ afterNextRender(this, () => {
97
+ this._setInputElement(this.querySelector('vaadin-text-field,.input'));
98
+ this._revertInputValue();
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Returns true if the current input value satisfies all constraints (if any).
104
+ * @return {boolean}
105
+ */
106
+ checkValidity() {
107
+ if (this.inputElement && this.inputElement.validate) {
108
+ return this.inputElement.validate();
109
+ }
110
+ return super.checkValidity();
111
+ }
112
+
113
+ /**
114
+ * @protected
115
+ * @override
116
+ */
117
+ _isClearButton(event) {
118
+ return (
119
+ super._isClearButton(event) ||
120
+ (event.type === 'input' && !event.isTrusted) || // Fake input event dispatched by clear button
121
+ event.composedPath()[0].getAttribute('part') === 'clear-button'
122
+ );
123
+ }
124
+
125
+ /**
126
+ * @protected
127
+ * @override
128
+ */
129
+ _shouldRemoveFocus(event) {
130
+ const isBlurringControlButtons = event.target === this._toggleElement || event.target === this.clearElement;
131
+ const isFocusingInputElement = event.relatedTarget && event.relatedTarget === this._nativeInput;
132
+
133
+ // prevent closing the overlay when moving focus from clear or toggle buttons to the internal input
134
+ if (isBlurringControlButtons && isFocusingInputElement) {
135
+ return false;
136
+ }
137
+
138
+ return super._shouldRemoveFocus(event);
139
+ }
140
+ };
@@ -11,6 +11,7 @@ import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.j
11
11
  import type { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
12
12
  import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
13
13
  import type { ComboBoxDataProviderMixinClass } from './vaadin-combo-box-data-provider-mixin.js';
14
+ import type { ComboBoxLightMixinClass } from './vaadin-combo-box-light-mixin.js';
14
15
  import type { ComboBoxDefaultItem, ComboBoxMixinClass } from './vaadin-combo-box-mixin.js';
15
16
  export {
16
17
  ComboBoxDataProvider,
@@ -126,13 +127,6 @@ export interface ComboBoxLightEventMap<TItem> extends HTMLElementEventMap {
126
127
  * @fires {CustomEvent} validated - Fired whenever the field is validated.
127
128
  */
128
129
  declare class ComboBoxLight<TItem = ComboBoxDefaultItem> extends HTMLElement {
129
- /**
130
- * Name of the two-way data-bindable property representing the
131
- * value of the custom input field.
132
- * @attr {string} attr-for-value
133
- */
134
- attrForValue: string;
135
-
136
130
  addEventListener<K extends keyof ComboBoxLightEventMap<TItem>>(
137
131
  type: K,
138
132
  listener: (this: ComboBoxLight<TItem>, ev: ComboBoxLightEventMap<TItem>[K]) => void,
@@ -148,6 +142,7 @@ declare class ComboBoxLight<TItem = ComboBoxDefaultItem> extends HTMLElement {
148
142
 
149
143
  interface ComboBoxLight<TItem = ComboBoxDefaultItem>
150
144
  extends ComboBoxDataProviderMixinClass<TItem>,
145
+ ComboBoxLightMixinClass,
151
146
  ComboBoxMixinClass<TItem>,
152
147
  KeyboardMixinClass,
153
148
  InputMixinClass,