@vaadin/grid 22.0.0-beta2 → 22.0.2

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/grid",
3
- "version": "22.0.0-beta2",
3
+ "version": "22.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -40,19 +40,19 @@
40
40
  "dependencies": {
41
41
  "@open-wc/dedupe-mixin": "^1.3.0",
42
42
  "@polymer/polymer": "^3.0.0",
43
- "@vaadin/checkbox": "22.0.0-beta2",
44
- "@vaadin/component-base": "22.0.0-beta2",
45
- "@vaadin/text-field": "22.0.0-beta2",
46
- "@vaadin/vaadin-lumo-styles": "22.0.0-beta2",
47
- "@vaadin/vaadin-material-styles": "22.0.0-beta2",
48
- "@vaadin/vaadin-themable-mixin": "22.0.0-beta2"
43
+ "@vaadin/checkbox": "^22.0.2",
44
+ "@vaadin/component-base": "^22.0.2",
45
+ "@vaadin/text-field": "^22.0.2",
46
+ "@vaadin/vaadin-lumo-styles": "^22.0.2",
47
+ "@vaadin/vaadin-material-styles": "^22.0.2",
48
+ "@vaadin/vaadin-themable-mixin": "^22.0.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@esm-bundle/chai": "^4.3.4",
52
- "@vaadin/polymer-legacy-adapter": "22.0.0-beta2",
53
- "@vaadin/testing-helpers": "^0.3.0",
52
+ "@vaadin/polymer-legacy-adapter": "^22.0.2",
53
+ "@vaadin/testing-helpers": "^0.3.2",
54
54
  "lit": "^2.0.0",
55
55
  "sinon": "^9.2.0"
56
56
  },
57
- "gitHead": "f13833683e6667f6ca6678452db14aa6b7eac4a4"
57
+ "gitHead": "df21370c4a655a38eac11f79686021ab3b0887ad"
58
58
  }
@@ -97,7 +97,9 @@ export const KeyboardNavigationMixin = (superClass) =>
97
97
 
98
98
  /** @private */
99
99
  get __rowFocusMode() {
100
- return this.__isRow(this._itemsFocusable);
100
+ return (
101
+ this.__isRow(this._itemsFocusable) || this.__isRow(this._headerFocusable) || this.__isRow(this._footerFocusable)
102
+ );
101
103
  }
102
104
 
103
105
  set __rowFocusMode(value) {
@@ -561,7 +563,10 @@ export const KeyboardNavigationMixin = (superClass) =>
561
563
 
562
564
  index += step;
563
565
  while (index >= 0 && index <= tabOrder.length - 1) {
564
- const rowElement = this.__rowFocusMode ? tabOrder[index] : tabOrder[index].parentNode;
566
+ let rowElement = tabOrder[index];
567
+ if (rowElement && !this.__rowFocusMode) {
568
+ rowElement = tabOrder[index].parentNode;
569
+ }
565
570
 
566
571
  if (!rowElement || rowElement.hidden) {
567
572
  index += step;
@@ -592,9 +597,9 @@ export const KeyboardNavigationMixin = (superClass) =>
592
597
  // assigned with a new index since last focus, probably because of
593
598
  // scrolling. Focus the row for the stored focused item index instead.
594
599
  const columnIndex = Array.from(targetRow.children).indexOf(this._itemsFocusable);
595
- const focusedItemRow = Array.from(this.$.items.children).filter(
596
- (row) => row.index === this._focusedItemIndex
597
- )[0];
600
+ const focusedItemRow = Array.from(this.$.items.children).find(
601
+ (row) => !row.hidden && row.index === this._focusedItemIndex
602
+ );
598
603
  if (focusedItemRow) {
599
604
  itemsFocusTarget = focusedItemRow.children[columnIndex];
600
605
  }
@@ -778,19 +783,27 @@ export const KeyboardNavigationMixin = (superClass) =>
778
783
 
779
784
  /** @protected */
780
785
  _resetKeyboardNavigation() {
781
- if (!this.__isValidFocusable(this._headerFocusable) && this.$.header.firstElementChild) {
782
- this._headerFocusable = Array.from(this.$.header.firstElementChild.children).filter((el) => !el.hidden)[0];
783
- }
786
+ // Header / footer
787
+ ['header', 'footer'].forEach((section) => {
788
+ if (!this.__isValidFocusable(this[`_${section}Focusable`])) {
789
+ const firstVisibleRow = [...this.$[section].children].find((row) => row.offsetHeight);
790
+ const firstVisibleCell = firstVisibleRow ? [...firstVisibleRow.children].find((cell) => !cell.hidden) : null;
791
+ if (firstVisibleRow && firstVisibleCell) {
792
+ this[`_${section}Focusable`] = this.__rowFocusMode ? firstVisibleRow : firstVisibleCell;
793
+ }
794
+ }
795
+ });
784
796
 
797
+ // Body
785
798
  if (!this.__isValidFocusable(this._itemsFocusable) && this.$.items.firstElementChild) {
786
- const firstVisibleIndexRow = this.__getFirstVisibleItem();
787
- if (firstVisibleIndexRow) {
788
- this._itemsFocusable = Array.from(firstVisibleIndexRow.children).filter((el) => !el.hidden)[0];
789
- }
790
- }
799
+ const firstVisibleRow = this.__getFirstVisibleItem();
800
+ const firstVisibleCell = firstVisibleRow ? [...firstVisibleRow.children].find((cell) => !cell.hidden) : null;
791
801
 
792
- if (!this.__isValidFocusable(this._footerFocusable) && this.$.footer.firstElementChild) {
793
- this._footerFocusable = Array.from(this.$.footer.firstElementChild.children).filter((el) => !el.hidden)[0];
802
+ if (firstVisibleCell && firstVisibleRow) {
803
+ // Reset memoized column
804
+ delete this._focusedColumnOrder;
805
+ this._itemsFocusable = this.__rowFocusMode ? firstVisibleRow : firstVisibleCell;
806
+ }
794
807
  }
795
808
  }
796
809
 
@@ -184,7 +184,11 @@ class GridSelectionColumn extends GridColumn {
184
184
  return;
185
185
  }
186
186
 
187
- this._grid.selectedItems = selectAll && Array.isArray(this._grid.items) ? this.__getRootLevelItems() : [];
187
+ if (selectAll && Array.isArray(this._grid.items)) {
188
+ this.__withFilteredItemsArray((items) => (this._grid.selectedItems = items));
189
+ } else {
190
+ this._grid.selectedItems = [];
191
+ }
188
192
  }
189
193
 
190
194
  /**
@@ -250,26 +254,22 @@ class GridSelectionColumn extends GridColumn {
250
254
  this.__previousActiveItem = activeItem;
251
255
  }
252
256
 
253
- /** @private */
254
- __getRootLevelItems() {
255
- const rootCache = this._grid._cache;
256
- return [...Array(rootCache.size)].map((_, idx) => rootCache.items[idx]);
257
- }
258
-
259
257
  /** @private */
260
258
  __onSelectedItemsChanged() {
261
259
  this._selectAllChangeLock = true;
262
260
  if (Array.isArray(this._grid.items)) {
263
- if (!this._grid.selectedItems.length) {
264
- this.selectAll = false;
265
- this.__indeterminate = false;
266
- } else if (this.__arrayContains(this._grid.selectedItems, this.__getRootLevelItems())) {
267
- this.selectAll = true;
268
- this.__indeterminate = false;
269
- } else {
270
- this.selectAll = false;
271
- this.__indeterminate = true;
272
- }
261
+ this.__withFilteredItemsArray((items) => {
262
+ if (!this._grid.selectedItems.length) {
263
+ this.selectAll = false;
264
+ this.__indeterminate = false;
265
+ } else if (this.__arrayContains(this._grid.selectedItems, items)) {
266
+ this.selectAll = true;
267
+ this.__indeterminate = false;
268
+ } else {
269
+ this.selectAll = false;
270
+ this.__indeterminate = true;
271
+ }
272
+ });
273
273
  }
274
274
  this._selectAllChangeLock = false;
275
275
  }
@@ -278,6 +278,22 @@ class GridSelectionColumn extends GridColumn {
278
278
  __onDataProviderChanged() {
279
279
  this.__selectAllHidden = !Array.isArray(this._grid.items);
280
280
  }
281
+
282
+ /**
283
+ * Assuming the grid uses an items array data provider, fetches all the filtered items
284
+ * from the data provider and invokes the callback with the resulting array.
285
+ *
286
+ * @private
287
+ */
288
+ __withFilteredItemsArray(callback) {
289
+ const params = {
290
+ page: 0,
291
+ pageSize: Infinity,
292
+ sortOrders: [],
293
+ filters: this._grid._mapFilters()
294
+ };
295
+ this._grid.dataProvider(params, (items) => callback(items));
296
+ }
281
297
  }
282
298
 
283
299
  customElements.define(GridSelectionColumn.is, GridSelectionColumn);
@@ -253,20 +253,16 @@ export interface GridEventMap<TItem> extends HTMLElementEventMap, GridCustomEven
253
253
  * in the second argument of the data provider callback:__
254
254
  *
255
255
  * ```javascript
256
- * grid.dataProvider = function(params, callback) {
257
- * const url = 'https://api.example/data' +
258
- * '?page=' + params.page + // the requested page index
259
- * '&per_page=' + params.pageSize; // number of items on the page
260
- * const xhr = new XMLHttpRequest();
261
- * xhr.onload = function() {
262
- * const response = JSON.parse(xhr.responseText);
263
- * callback(
264
- * response.employees, // requested page of items
265
- * response.totalSize // total number of items
266
- * );
267
- * };
268
- * xhr.open('GET', url, true);
269
- * xhr.send();
256
+ * grid.dataProvider = ({page, pageSize}, callback) => {
257
+ * // page: the requested page index
258
+ * // pageSize: number of items on one page
259
+ * const url = `https://api.example/data?page=${page}&per_page=${pageSize}`;
260
+ *
261
+ * fetch(url)
262
+ * .then((res) => res.json())
263
+ * .then(({ employees, totalSize }) => {
264
+ * callback(employees, totalSize);
265
+ * });
270
266
  * };
271
267
  * ```
272
268
  *
@@ -274,17 +270,12 @@ export interface GridEventMap<TItem> extends HTMLElementEventMap, GridCustomEven
274
270
  *
275
271
  * ```javascript
276
272
  * grid.size = 200; // The total number of items
277
- * grid.dataProvider = function(params, callback) {
278
- * const url = 'https://api.example/data' +
279
- * '?page=' + params.page + // the requested page index
280
- * '&per_page=' + params.pageSize; // number of items on the page
281
- * const xhr = new XMLHttpRequest();
282
- * xhr.onload = function() {
283
- * const response = JSON.parse(xhr.responseText);
284
- * callback(response.employees);
285
- * };
286
- * xhr.open('GET', url, true);
287
- * xhr.send();
273
+ * grid.dataProvider = ({page, pageSize}, callback) => {
274
+ * const url = `https://api.example/data?page=${page}&per_page=${pageSize}`;
275
+ *
276
+ * fetch(url)
277
+ * .then((res) => res.json())
278
+ * .then((resJson) => callback(resJson.employees));
288
279
  * };
289
280
  * ```
290
281
  *
@@ -7,6 +7,7 @@ import './vaadin-grid-column.js';
7
7
  import './vaadin-grid-styles.js';
8
8
  import { beforeNextRender } from '@polymer/polymer/lib/utils/render-status.js';
9
9
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
10
+ import { isAndroid, isFirefox, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
10
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
11
12
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
12
13
  import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
@@ -28,15 +29,6 @@ import { SelectionMixin } from './vaadin-grid-selection-mixin.js';
28
29
  import { SortMixin } from './vaadin-grid-sort-mixin.js';
29
30
  import { StylingMixin } from './vaadin-grid-styling-mixin.js';
30
31
 
31
- const TOUCH_DEVICE = (() => {
32
- try {
33
- document.createEvent('TouchEvent');
34
- return true;
35
- } catch (e) {
36
- return false;
37
- }
38
- })();
39
-
40
32
  /**
41
33
  * `<vaadin-grid>` is a free, high quality data grid / data table Web Component. The content of the
42
34
  * the grid can be populated by using renderer callback function.
@@ -147,20 +139,16 @@ const TOUCH_DEVICE = (() => {
147
139
  * in the second argument of the data provider callback:__
148
140
  *
149
141
  * ```javascript
150
- * grid.dataProvider = function(params, callback) {
151
- * const url = 'https://api.example/data' +
152
- * '?page=' + params.page + // the requested page index
153
- * '&per_page=' + params.pageSize; // number of items on the page
154
- * const xhr = new XMLHttpRequest();
155
- * xhr.onload = function() {
156
- * const response = JSON.parse(xhr.responseText);
157
- * callback(
158
- * response.employees, // requested page of items
159
- * response.totalSize // total number of items
160
- * );
161
- * };
162
- * xhr.open('GET', url, true);
163
- * xhr.send();
142
+ * grid.dataProvider = ({page, pageSize}, callback) => {
143
+ * // page: the requested page index
144
+ * // pageSize: number of items on one page
145
+ * const url = `https://api.example/data?page=${page}&per_page=${pageSize}`;
146
+ *
147
+ * fetch(url)
148
+ * .then((res) => res.json())
149
+ * .then(({ employees, totalSize }) => {
150
+ * callback(employees, totalSize);
151
+ * });
164
152
  * };
165
153
  * ```
166
154
  *
@@ -168,17 +156,12 @@ const TOUCH_DEVICE = (() => {
168
156
  *
169
157
  * ```javascript
170
158
  * grid.size = 200; // The total number of items
171
- * grid.dataProvider = function(params, callback) {
172
- * const url = 'https://api.example/data' +
173
- * '?page=' + params.page + // the requested page index
174
- * '&per_page=' + params.pageSize; // number of items on the page
175
- * const xhr = new XMLHttpRequest();
176
- * xhr.onload = function() {
177
- * const response = JSON.parse(xhr.responseText);
178
- * callback(response.employees);
179
- * };
180
- * xhr.open('GET', url, true);
181
- * xhr.send();
159
+ * grid.dataProvider = ({page, pageSize}, callback) => {
160
+ * const url = `https://api.example/data?page=${page}&per_page=${pageSize}`;
161
+ *
162
+ * fetch(url)
163
+ * .then((res) => res.json())
164
+ * .then((resJson) => callback(resJson.employees));
182
165
  * };
183
166
  * ```
184
167
  *
@@ -326,33 +309,31 @@ class Grid extends ElementMixin(
326
309
  /** @private */
327
310
  _safari: {
328
311
  type: Boolean,
329
- value: /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
312
+ value: isSafari
330
313
  },
331
314
 
332
315
  /** @private */
333
316
  _ios: {
334
317
  type: Boolean,
335
- value:
336
- (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) ||
337
- (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
318
+ value: isIOS
338
319
  },
339
320
 
340
321
  /** @private */
341
322
  _firefox: {
342
323
  type: Boolean,
343
- value: navigator.userAgent.toLowerCase().indexOf('firefox') > -1
324
+ value: isFirefox
344
325
  },
345
326
 
346
327
  /** @private */
347
328
  _android: {
348
329
  type: Boolean,
349
- value: /android/i.test(navigator.userAgent)
330
+ value: isAndroid
350
331
  },
351
332
 
352
333
  /** @private */
353
334
  _touchDevice: {
354
335
  type: Boolean,
355
- value: TOUCH_DEVICE
336
+ value: isTouch
356
337
  },
357
338
 
358
339
  /**
@@ -512,6 +493,9 @@ class Grid extends ElementMixin(
512
493
 
513
494
  // If the focused cell's parent row got hidden by the size change, focus the corresponding new cell
514
495
  cellCoordinates && cell.parentElement.hidden && this.__focusBodyCell(cellCoordinates);
496
+
497
+ // Make sure the body has a tabbable element
498
+ this._resetKeyboardNavigation();
515
499
  }
516
500
  }
517
501
 
@@ -532,42 +516,72 @@ class Grid extends ElementMixin(
532
516
  }
533
517
  }
534
518
 
519
+ /** @private */
520
+ __getIntrinsicWidth(col) {
521
+ const initialWidth = col.width;
522
+ const initialFlexGrow = col.flexGrow;
523
+
524
+ col.width = 'auto';
525
+ col.flexGrow = 0;
526
+
527
+ // Note: _allCells only contains cells which are currently rendered in DOM
528
+ const width = col._allCells
529
+ .filter((cell) => {
530
+ // Exclude body cells that are out of the visible viewport
531
+ return !this.$.items.contains(cell) || this._isInViewport(cell.parentElement);
532
+ })
533
+ .reduce((width, cell) => {
534
+ // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering)
535
+ return Math.max(width, cell.offsetWidth + 1);
536
+ }, 0);
537
+
538
+ col.flexGrow = initialFlexGrow;
539
+ col.width = initialWidth;
540
+
541
+ return width;
542
+ }
543
+
544
+ /** @private */
545
+ __getDistributedWidth(col, innerColumn) {
546
+ if (col == null || col === this) return 0;
547
+
548
+ const columnWidth = Math.max(this.__getIntrinsicWidth(col), this.__getDistributedWidth(col.parentElement, col));
549
+
550
+ // we're processing a regular grid-column and not a grid-column-group
551
+ if (!innerColumn) {
552
+ return columnWidth;
553
+ }
554
+
555
+ // At the end, the width of each vaadin-grid-column-group is determined by the sum of the width of its children.
556
+ // Here we determine how much space the vaadin-grid-column-group actually needs to render properly and then we distribute that space
557
+ // to its children, so when we actually do the summation it will be rendered properly.
558
+ // Check out vaadin-grid-column-group:_updateFlexAndWidth
559
+ const columnGroup = col;
560
+ const columnGroupWidth = columnWidth;
561
+ const sumOfWidthOfAllChildColumns = columnGroup._visibleChildColumns
562
+ .map((col) => this.__getIntrinsicWidth(col))
563
+ .reduce((sum, curr) => sum + curr, 0);
564
+
565
+ const extraNecessarySpaceForGridColumnGroup = Math.max(0, columnGroupWidth - sumOfWidthOfAllChildColumns);
566
+
567
+ // The distribution of the extra necessary space is done according to the intrinsic width of each child column.
568
+ // Lets say we need 100 pixels of extra space for the grid-column-group to render properly
569
+ // it has two grid-column children, |100px|300px| in total 400px
570
+ // the first column gets 25px of the additional space (100/400)*100 = 25
571
+ // the second column gets the 75px of the additional space (300/400)*100 = 75
572
+ const proportionOfExtraSpace = this.__getIntrinsicWidth(innerColumn) / sumOfWidthOfAllChildColumns;
573
+ const shareOfInnerColumnFromNecessaryExtraSpace = proportionOfExtraSpace * extraNecessarySpaceForGridColumnGroup;
574
+
575
+ return this.__getIntrinsicWidth(innerColumn) + shareOfInnerColumnFromNecessaryExtraSpace;
576
+ }
577
+
535
578
  /**
536
579
  * @param {!Array<!GridColumn>} cols the columns to auto size based on their content width
537
580
  * @private
538
581
  */
539
582
  _recalculateColumnWidths(cols) {
540
- // Note: The `cols.forEach()` loops below could be implemented as a single loop but this has been
541
- // split for performance reasons to batch these similar actions [write/read] together to avoid
542
- // unnecessary layout trashing.
543
-
544
- // [write] Set automatic width for all cells (breaks column alignment)
545
583
  cols.forEach((col) => {
546
- col.width = 'auto';
547
- col._origFlexGrow = col.flexGrow;
548
- col.flexGrow = 0;
549
- });
550
- // [read] Measure max cell width in each column
551
- cols.forEach((col) => {
552
- col._currentWidth = 0;
553
- // Note: _allCells only contains cells which are currently rendered in DOM
554
- col._allCells
555
- .filter((c) => {
556
- // Exclude body cells that are out of the visible viewport
557
- return !this.$.items.contains(c) || this._isInViewport(c.parentElement);
558
- })
559
- .forEach((c) => {
560
- // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering)
561
- const cellWidth = c.offsetWidth + 1;
562
- col._currentWidth = Math.max(col._currentWidth, cellWidth);
563
- });
564
- });
565
- // [write] Set column widths to fit widest measured content
566
- cols.forEach((col) => {
567
- col.width = `${col._currentWidth}px`;
568
- col.flexGrow = col._origFlexGrow;
569
- col._currentWidth = undefined;
570
- col._origFlexGrow = undefined;
584
+ col.width = `${this.__getDistributedWidth(col)}px`;
571
585
  });
572
586
  }
573
587
 
@@ -791,6 +805,9 @@ class Grid extends ElementMixin(
791
805
  if (row.hidden !== !visibleRowCells.length) {
792
806
  row.hidden = !visibleRowCells.length;
793
807
  }
808
+
809
+ // Make sure the section has a tabbable element
810
+ this._resetKeyboardNavigation();
794
811
  }
795
812
 
796
813
  /** @private */
@@ -858,6 +875,7 @@ class Grid extends ElementMixin(
858
875
  this._a11yUpdateHeaderRows();
859
876
  this._a11yUpdateFooterRows();
860
877
  this.__updateFooterPositioning();
878
+ this.generateCellClassNames();
861
879
  }
862
880
 
863
881
  __updateFooterPositioning() {
@@ -60,6 +60,10 @@ registerStyles(
60
60
 
61
61
  /* Focus-ring */
62
62
 
63
+ [part~='row'] {
64
+ position: relative;
65
+ }
66
+
63
67
  [part~='row']:focus,
64
68
  [part~='cell']:focus {
65
69
  outline: none;
@@ -118,6 +118,10 @@ registerStyles(
118
118
 
119
119
  /* Keyboard navigation */
120
120
 
121
+ [part~='row'] {
122
+ position: relative;
123
+ }
124
+
121
125
  [part~='row']:focus,
122
126
  [part~='cell']:focus {
123
127
  outline: none;