Skip to content

Commit 2bae93a

Browse files
authored
feat: conditional selectability of grid items (#7974)
* basic isItemSelectable implementation * handle select all checkbox * fix default renderer * fix more select all cases * make checkboxes readonly or hidden * reuse selectable check * add TS types * cleanup * simplify select all logic * fix select all check * hide select all when using conditional selection
1 parent c318c01 commit 2bae93a

8 files changed

+341
-30
lines changed

packages/grid/src/vaadin-grid-selection-column-base-mixin.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
145145
checkbox.__item = item;
146146
checkbox.__rendererChecked = selected;
147147
checkbox.checked = selected;
148+
149+
const isSelectable = this._grid.__isItemSelectable(item);
150+
checkbox.readonly = !isSelectable;
151+
checkbox.hidden = !isSelectable && !selected;
148152
}
149153

150154
/**
@@ -245,9 +249,18 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
245249
_onCellKeyDown(e) {
246250
const target = e.composedPath()[0];
247251
// Toggle on Space without having to enter interaction mode first
248-
if (e.keyCode === 32 && (target === this._headerCell || (this._cells.includes(target) && !this.autoSelect))) {
252+
if (e.keyCode !== 32) {
253+
return;
254+
}
255+
if (target === this._headerCell) {
256+
if (this.selectAll) {
257+
this._deselectAll();
258+
} else {
259+
this._selectAll();
260+
}
261+
} else if (this._cells.includes(target) && !this.autoSelect) {
249262
const checkbox = target._content.firstElementChild;
250-
checkbox.checked = !checkbox.checked;
263+
this.__toggleItem(checkbox.__item);
251264
}
252265
}
253266

@@ -358,6 +371,19 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
358371
*/
359372
_deselectItem(_item) {}
360373

374+
/**
375+
* Toggles the selected state of the given item.
376+
* @param item the item to toggle
377+
* @private
378+
*/
379+
__toggleItem(item) {
380+
if (this._grid._isSelected(item)) {
381+
this._deselectItem(item);
382+
} else {
383+
this._selectItem(item);
384+
}
385+
}
386+
361387
/**
362388
* IOS needs indeterminate + checked at the same time
363389
* @private

packages/grid/src/vaadin-grid-selection-column-mixin.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ export const GridSelectionColumnMixin = (superClass) =>
3030
super();
3131

3232
this.__boundOnActiveItemChanged = this.__onActiveItemChanged.bind(this);
33-
this.__boundOnDataProviderChanged = this.__onDataProviderChanged.bind(this);
33+
this.__boundUpdateSelectAllVisibility = this.__updateSelectAllVisibility.bind(this);
3434
this.__boundOnSelectedItemsChanged = this.__onSelectedItemsChanged.bind(this);
3535
}
3636

3737
/** @protected */
3838
disconnectedCallback() {
3939
this._grid.removeEventListener('active-item-changed', this.__boundOnActiveItemChanged);
40-
this._grid.removeEventListener('data-provider-changed', this.__boundOnDataProviderChanged);
40+
this._grid.removeEventListener('data-provider-changed', this.__boundUpdateSelectAllVisibility);
41+
this._grid.removeEventListener('is-item-selectable-changed', this.__boundUpdateSelectAllVisibility);
4142
this._grid.removeEventListener('filter-changed', this.__boundOnSelectedItemsChanged);
4243
this._grid.removeEventListener('selected-items-changed', this.__boundOnSelectedItemsChanged);
4344

@@ -49,7 +50,8 @@ export const GridSelectionColumnMixin = (superClass) =>
4950
super.connectedCallback();
5051
if (this._grid) {
5152
this._grid.addEventListener('active-item-changed', this.__boundOnActiveItemChanged);
52-
this._grid.addEventListener('data-provider-changed', this.__boundOnDataProviderChanged);
53+
this._grid.addEventListener('data-provider-changed', this.__boundUpdateSelectAllVisibility);
54+
this._grid.addEventListener('is-item-selectable-changed', this.__boundUpdateSelectAllVisibility);
5355
this._grid.addEventListener('filter-changed', this.__boundOnSelectedItemsChanged);
5456
this._grid.addEventListener('selected-items-changed', this.__boundOnSelectedItemsChanged);
5557
}
@@ -111,7 +113,9 @@ export const GridSelectionColumnMixin = (superClass) =>
111113
* @override
112114
*/
113115
_selectItem(item) {
114-
this._grid.selectItem(item);
116+
if (this._grid.__isItemSelectable(item)) {
117+
this._grid.selectItem(item);
118+
}
115119
}
116120

117121
/**
@@ -123,7 +127,9 @@ export const GridSelectionColumnMixin = (superClass) =>
123127
* @override
124128
*/
125129
_deselectItem(item) {
126-
this._grid.deselectItem(item);
130+
if (this._grid.__isItemSelectable(item)) {
131+
this._grid.deselectItem(item);
132+
}
127133
}
128134

129135
/** @private */
@@ -132,7 +138,7 @@ export const GridSelectionColumnMixin = (superClass) =>
132138
if (this.autoSelect) {
133139
const item = activeItem || this.__previousActiveItem;
134140
if (item) {
135-
this._grid._toggleItem(item);
141+
this.__toggleItem(item);
136142
}
137143
}
138144
this.__previousActiveItem = activeItem;
@@ -164,8 +170,11 @@ export const GridSelectionColumnMixin = (superClass) =>
164170
}
165171

166172
/** @private */
167-
__onDataProviderChanged() {
168-
this._selectAllHidden = !Array.isArray(this._grid.items);
173+
__updateSelectAllVisibility() {
174+
// Hide select all checkbox when we can not easily determine the select all checkbox state:
175+
// - When using a custom data provider
176+
// - When using conditional selection, where users may not select all items
177+
this._selectAllHidden = !Array.isArray(this._grid.items) || !!this._grid.isItemSelectable;
169178
}
170179

171180
/**

packages/grid/src/vaadin-grid-selection-mixin.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,25 @@ export declare class SelectionMixinClass<TItem> {
2828
* @param item The item object
2929
*/
3030
deselectItem(item: TItem): void;
31+
32+
/**
33+
* A function to check whether a specific item in the grid may be
34+
* selected or deselected by the user. Used by the selection column to
35+
* conditionally enable to disable checkboxes for individual items. This
36+
* function does not prevent programmatic selection/deselection of
37+
* items. Changing the function does not modify the currently selected
38+
* items.
39+
*
40+
* Configuring this function hides the select all checkbox of the grid
41+
* selection column, which means users can not select or deselect all
42+
* items anymore, nor do they get feedback on whether all items are
43+
* selected or not.
44+
*
45+
* Receives an item instance and should return a boolean indicating
46+
* whether users may change the selection state of that item.
47+
*
48+
* @param item The item object
49+
* @return Whether the item is selectable
50+
*/
51+
isItemSelectable: (item: TItem) => boolean;
3152
}

packages/grid/src/vaadin-grid-selection-mixin.js

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ export const SelectionMixin = (superClass) =>
2222
sync: true,
2323
},
2424

25+
/**
26+
* A function to check whether a specific item in the grid may be
27+
* selected or deselected by the user. Used by the selection column to
28+
* conditionally enable to disable checkboxes for individual items. This
29+
* function does not prevent programmatic selection/deselection of
30+
* items. Changing the function does not modify the currently selected
31+
* items.
32+
*
33+
* Configuring this function hides the select all checkbox of the grid
34+
* selection column, which means users can not select or deselect all
35+
* items anymore, nor do they get feedback on whether all items are
36+
* selected or not.
37+
*
38+
* Receives an item instance and should return a boolean indicating
39+
* whether users may change the selection state of that item.
40+
*
41+
* @type {(item: !GridItem) => boolean}
42+
*/
43+
isItemSelectable: {
44+
type: Function,
45+
notify: true,
46+
},
47+
2548
/**
2649
* Set of selected item ids
2750
* @private
@@ -34,7 +57,7 @@ export const SelectionMixin = (superClass) =>
3457
}
3558

3659
static get observers() {
37-
return ['__selectedItemsChanged(itemIdPath, selectedItems)'];
60+
return ['__selectedItemsChanged(itemIdPath, selectedItems, isItemSelectable)'];
3861
}
3962

4063
/**
@@ -46,6 +69,22 @@ export const SelectionMixin = (superClass) =>
4669
return this.__selectedKeys.has(this.getItemId(item));
4770
}
4871

72+
/**
73+
* Determines whether the selection state of an item may be changed by the
74+
* user.
75+
*
76+
* @private
77+
*/
78+
__isItemSelectable(item) {
79+
// Item is selectable by default if isItemSelectable is not configured
80+
if (!this.isItemSelectable || !item) {
81+
return true;
82+
}
83+
84+
// Otherwise, check isItemSelectable function
85+
return this.isItemSelectable(item);
86+
}
87+
4988
/**
5089
* Selects the given item.
5190
*
@@ -70,21 +109,6 @@ export const SelectionMixin = (superClass) =>
70109
}
71110
}
72111

73-
/**
74-
* Toggles the selected state of the given item.
75-
*
76-
* @method toggle
77-
* @param {!GridItem} item The item object
78-
* @protected
79-
*/
80-
_toggleItem(item) {
81-
if (!this._isSelected(item)) {
82-
this.selectItem(item);
83-
} else {
84-
this.deselectItem(item);
85-
}
86-
}
87-
88112
/** @private */
89113
__selectedItemsChanged() {
90114
this.requestContentUpdate();

packages/grid/test/helpers.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,24 @@ export const getContainerCellContent = (container, row, col) => {
158158
return getCellContent(getContainerCell(container, row, col));
159159
};
160160

161-
export const getHeaderCellContent = (grid, row, col) => {
161+
export const getHeaderCell = (grid, row, col) => {
162162
const container = grid.$.header;
163-
return getContainerCellContent(container, row, col);
163+
return getContainerCell(container, row, col);
164164
};
165165

166-
export const getBodyCellContent = (grid, row, col) => {
166+
export const getHeaderCellContent = (grid, row, col) => {
167+
return getCellContent(getHeaderCell(grid, row, col));
168+
};
169+
170+
export const getBodyCell = (grid, row, col) => {
167171
const physicalItems = getPhysicalItems(grid);
168172
const physicalRow = physicalItems.find((item) => item.index === row);
169173
const cells = getRowCells(physicalRow);
170-
return getCellContent(cells[col]);
174+
return cells[col];
175+
};
176+
177+
export const getBodyCellContent = (grid, row, col) => {
178+
return getCellContent(getBodyCell(grid, row, col));
171179
};
172180

173181
export const fire = (type, detail, options) => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import '../theme/lumo/lit-all-imports.js';
2+
import '../src/lit-all-imports.js';
3+
import './selectable-provider.common.js';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import '../all-imports.js';
2+
import './selectable-provider.common.js';

0 commit comments

Comments
 (0)