@vaadin/combo-box 24.4.0-alpha1 → 24.4.0-dev.223e39f050

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/combo-box",
3
- "version": "24.4.0-alpha1",
3
+ "version": "24.4.0-dev.223e39f050",
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-alpha1",
42
- "@vaadin/component-base": "24.4.0-alpha1",
43
- "@vaadin/field-base": "24.4.0-alpha1",
44
- "@vaadin/input-container": "24.4.0-alpha1",
45
- "@vaadin/item": "24.4.0-alpha1",
46
- "@vaadin/lit-renderer": "24.4.0-alpha1",
47
- "@vaadin/overlay": "24.4.0-alpha1",
48
- "@vaadin/vaadin-lumo-styles": "24.4.0-alpha1",
49
- "@vaadin/vaadin-material-styles": "24.4.0-alpha1",
50
- "@vaadin/vaadin-themable-mixin": "24.4.0-alpha1"
41
+ "@vaadin/a11y-base": "24.4.0-dev.223e39f050",
42
+ "@vaadin/component-base": "24.4.0-dev.223e39f050",
43
+ "@vaadin/field-base": "24.4.0-dev.223e39f050",
44
+ "@vaadin/input-container": "24.4.0-dev.223e39f050",
45
+ "@vaadin/item": "24.4.0-dev.223e39f050",
46
+ "@vaadin/lit-renderer": "24.4.0-dev.223e39f050",
47
+ "@vaadin/overlay": "24.4.0-dev.223e39f050",
48
+ "@vaadin/vaadin-lumo-styles": "24.4.0-dev.223e39f050",
49
+ "@vaadin/vaadin-material-styles": "24.4.0-dev.223e39f050",
50
+ "@vaadin/vaadin-themable-mixin": "24.4.0-dev.223e39f050"
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-alpha1",
55
+ "@vaadin/text-field": "24.4.0-dev.223e39f050",
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": "3e2ed41c99d618ff7def2734fd863c21c85775a3"
63
+ "gitHead": "5e2e3bfc811c95aed9354235fab93fdbf43eb354"
64
64
  }
@@ -3,6 +3,8 @@
3
3
  * Copyright (c) 2015 - 2023 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(),
@@ -78,10 +73,33 @@ export const ComboBoxDataProviderMixin = (superClass) =>
78
73
  ];
79
74
  }
80
75
 
76
+ constructor() {
77
+ super();
78
+
79
+ /** @type {DataProviderController} */
80
+ this._dataProviderController = new DataProviderController(this, {
81
+ size: this.size,
82
+ pageSize: this.pageSize,
83
+ getItemId: (item) => get(this.itemIdPath, item),
84
+ placeholder: this.__placeHolder,
85
+ dataProvider: this.dataProvider ? this.dataProvider.bind(this) : null,
86
+ dataProviderParams: () => ({ filter: this.filter }),
87
+ });
88
+ }
89
+
81
90
  /** @protected */
82
91
  ready() {
83
92
  super.ready();
93
+
94
+ this._dataProviderController.addEventListener('page-requested', this.__onDataProviderPageRequested.bind(this));
95
+ this._dataProviderController.addEventListener('page-received', this.__onDataProviderPageReceived.bind(this));
96
+ this._dataProviderController.addEventListener('page-loaded', this.__onDataProviderPageLoaded.bind(this));
97
+
84
98
  this._scroller.addEventListener('index-requested', (e) => {
99
+ if (!this._shouldFetchData()) {
100
+ return;
101
+ }
102
+
85
103
  const index = e.detail.index;
86
104
  const currentScrollerPos = e.detail.currentScrollerPos;
87
105
  const allowedIndexRange = Math.floor(this.pageSize * 1.5);
@@ -95,10 +113,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
95
113
  }
96
114
 
97
115
  if (index !== undefined) {
98
- const page = this._getPageForIndex(index);
99
- if (this._shouldLoadPage(page)) {
100
- this._loadPage(page);
101
- }
116
+ this._dataProviderController.ensureFlatIndexLoaded(index);
102
117
  }
103
118
  });
104
119
  }
@@ -113,19 +128,14 @@ export const ComboBoxDataProviderMixin = (superClass) =>
113
128
  if (this.__previousDataProviderFilter !== filter) {
114
129
  this.__previousDataProviderFilter = filter;
115
130
 
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
131
+ this._keepOverlayOpened = true;
122
132
  this.size = undefined;
123
-
124
133
  this.clearCache();
134
+ this._keepOverlayOpened = false;
125
135
  }
126
136
  }
127
137
 
128
- /** @private */
138
+ /** @protected */
129
139
  _shouldFetchData() {
130
140
  if (!this.dataProvider) {
131
141
  return false;
@@ -136,8 +146,12 @@ export const ComboBoxDataProviderMixin = (superClass) =>
136
146
 
137
147
  /** @private */
138
148
  _ensureFirstPage(opened) {
139
- if (opened && this._shouldLoadPage(0)) {
140
- this._loadPage(0);
149
+ if (!this._shouldFetchData()) {
150
+ return;
151
+ }
152
+
153
+ if (opened && !this._dataProviderController.hasData) {
154
+ this._dataProviderController.loadFirstPage();
141
155
  }
142
156
  }
143
157
 
@@ -151,68 +165,24 @@ export const ComboBoxDataProviderMixin = (superClass) =>
151
165
  }
152
166
 
153
167
  /** @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;
168
+ __onDataProviderPageRequested() {
169
+ this.loading = true;
165
170
  }
166
171
 
167
172
  /** @private */
168
- _loadPage(page) {
169
- // Make sure same page isn't requested multiple times.
170
- if (this._pendingRequests[page] || !this.dataProvider) {
171
- return;
172
- }
173
-
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
- }
184
-
185
- const filteredItems = this.filteredItems ? [...this.filteredItems] : [];
186
- filteredItems.splice(params.page * params.pageSize, items.length, ...items);
187
- this.filteredItems = filteredItems;
188
-
189
- if (!this.opened && !this._isInputFocused()) {
190
- this._commitValue();
191
- }
192
-
193
- if (size !== undefined) {
194
- this.size = size;
195
- }
196
-
197
- delete this._pendingRequests[page];
198
-
199
- if (Object.keys(this._pendingRequests).length === 0) {
200
- this.loading = false;
201
- }
202
- };
203
-
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);
173
+ __onDataProviderPageReceived() {
174
+ this.requestContentUpdate();
211
175
  }
212
176
 
213
177
  /** @private */
214
- _getPageForIndex(index) {
215
- return Math.floor(index / this.pageSize);
178
+ __onDataProviderPageLoaded() {
179
+ if (!this.opened && !this._isInputFocused()) {
180
+ this._commitValue();
181
+ }
182
+
183
+ if (!this._dataProviderController.isLoading()) {
184
+ this.loading = false;
185
+ }
216
186
  }
217
187
 
218
188
  /**
@@ -223,32 +193,74 @@ export const ComboBoxDataProviderMixin = (superClass) =>
223
193
  return;
224
194
  }
225
195
 
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;
196
+ this._dataProviderController.clearCache();
197
+
198
+ this.requestContentUpdate();
232
199
 
233
200
  if (this._shouldFetchData()) {
234
- this._forceNextRequest = false;
235
- this._loadPage(0);
236
- } else {
237
- this._forceNextRequest = true;
201
+ this._dataProviderController.loadFirstPage();
238
202
  }
239
203
  }
240
204
 
241
205
  /** @private */
242
206
  _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;
207
+ const { rootCache } = this._dataProviderController;
208
+ // When the size update originates from the developer,
209
+ // sync the new size with the controller and trigger
210
+ // a content update to re-render the scroller.
211
+ if (rootCache.size !== size) {
212
+ rootCache.size = size;
213
+ this.requestContentUpdate();
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @private
219
+ * @override
220
+ */
221
+ _filteredItemsChanged(items) {
222
+ if (!this.dataProvider) {
223
+ return super._filteredItemsChanged(items);
246
224
  }
247
- this.filteredItems = filteredItems;
248
225
 
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);
226
+ const { rootCache } = this._dataProviderController;
227
+ // When the items update originates from the developer,
228
+ // sync the new items with the controller and trigger
229
+ // a content update to re-render the scroller.
230
+ if (rootCache.items !== items) {
231
+ rootCache.items = items;
232
+ this.requestContentUpdate();
233
+ }
234
+ }
235
+
236
+ /** @override */
237
+ requestContentUpdate() {
238
+ if (this.dataProvider) {
239
+ const { rootCache } = this._dataProviderController;
240
+
241
+ // Sync the controller's size with the component.
242
+ // They can be out of sync after, for example,
243
+ // the controller received new data.
244
+ if ((this.size || 0) !== rootCache.size) {
245
+ this.size = rootCache.size;
246
+ }
247
+
248
+ // Sync the controller's items with the component.
249
+ // They can be out of sync after, for example,
250
+ // the controller received new data.
251
+ if (this.filteredItems !== rootCache.items) {
252
+ this.filteredItems = rootCache.items;
253
+ }
254
+
255
+ // Sync the controller's loading state with the component.
256
+ this.loading = this._dataProviderController.isLoading();
257
+
258
+ // Set a copy of the controller's items as the dropdown items
259
+ // to trigger an update of the focused index in _setDropdownItems.
260
+ this._setDropdownItems([...this.filteredItems]);
261
+ }
262
+
263
+ super.requestContentUpdate();
252
264
  }
253
265
 
254
266
  /** @private */
@@ -257,6 +269,8 @@ export const ComboBoxDataProviderMixin = (superClass) =>
257
269
  this.pageSize = oldPageSize;
258
270
  throw new Error('`pageSize` value must be an integer > 0');
259
271
  }
272
+
273
+ this._dataProviderController.setPageSize(pageSize);
260
274
  this.clearCache();
261
275
  }
262
276
 
@@ -266,6 +280,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
266
280
  this.dataProvider = oldDataProvider;
267
281
  });
268
282
 
283
+ this._dataProviderController.setDataProvider(dataProvider);
269
284
  this.clearCache();
270
285
  }
271
286
 
@@ -274,8 +289,6 @@ export const ComboBoxDataProviderMixin = (superClass) =>
274
289
  if (this.items !== undefined && this.dataProvider !== undefined) {
275
290
  restoreOldValueCallback();
276
291
  throw new Error('Using `items` and `dataProvider` together is not supported');
277
- } else if (this.dataProvider && !this.filteredItems) {
278
- this.filteredItems = [];
279
292
  }
280
293
  }
281
294
 
@@ -294,28 +307,4 @@ export const ComboBoxDataProviderMixin = (superClass) =>
294
307
  }
295
308
  }
296
309
  }
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
310
  };
@@ -259,7 +259,7 @@ export const ComboBoxMixin = (subclass) =>
259
259
  static get observers() {
260
260
  return [
261
261
  '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
262
- '_openedOrItemsChanged(opened, _dropdownItems, loading)',
262
+ '_openedOrItemsChanged(opened, _dropdownItems, loading, _keepOverlayOpened)',
263
263
  '_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
264
264
  ];
265
265
  }
@@ -487,10 +487,10 @@ export const ComboBoxMixin = (subclass) =>
487
487
  }
488
488
 
489
489
  /** @private */
490
- _openedOrItemsChanged(opened, items, loading) {
490
+ _openedOrItemsChanged(opened, items, loading, keepOverlayOpened) {
491
491
  // Close the overlay if there are no items to display.
492
492
  // See https://github.com/vaadin/vaadin-combo-box/pull/964
493
- this._overlayOpened = !!(opened && (loading || (items && items.length)));
493
+ this._overlayOpened = !!(opened && (keepOverlayOpened || loading || (items && items.length)));
494
494
  }
495
495
 
496
496
  /** @private */
@@ -1113,6 +1113,10 @@ export const ComboBoxMixin = (subclass) =>
1113
1113
  this.items = oldItems;
1114
1114
  });
1115
1115
 
1116
+ if (this.dataProvider) {
1117
+ return;
1118
+ }
1119
+
1116
1120
  if (items) {
1117
1121
  this.filteredItems = items.slice(0);
1118
1122
  } else if (oldItems) {