Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: conditional selectability of grid items #7974

Merged
merged 12 commits into from
Oct 23, 2024
30 changes: 28 additions & 2 deletions packages/grid/src/vaadin-grid-selection-column-base-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
checkbox.__item = item;
checkbox.__rendererChecked = selected;
checkbox.checked = selected;

const isSelectable = this._grid.__isItemSelectable(item);
checkbox.readonly = !isSelectable;
checkbox.hidden = !isSelectable && !selected;
Comment on lines +149 to +151
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rolf specifically suggested to use this behavior.

}

/**
Expand Down Expand Up @@ -245,9 +249,18 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
_onCellKeyDown(e) {
const target = e.composedPath()[0];
// Toggle on Space without having to enter interaction mode first
if (e.keyCode === 32 && (target === this._headerCell || (this._cells.includes(target) && !this.autoSelect))) {
if (e.keyCode !== 32) {
return;
}
if (target === this._headerCell) {
if (this.selectAll) {
this._deselectAll();
} else {
this._selectAll();
}
} else if (this._cells.includes(target) && !this.autoSelect) {
const checkbox = target._content.firstElementChild;
checkbox.checked = !checkbox.checked;
this.__toggleItem(checkbox.__item);
}
}

Expand Down Expand Up @@ -358,6 +371,19 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
*/
_deselectItem(_item) {}

/**
* Toggles the selected state of the given item.
* @param item the item to toggle
* @private
*/
__toggleItem(item) {
if (this._grid._isSelected(item)) {
this._deselectItem(item);
} else {
this._selectItem(item);
}
}

/**
* IOS needs indeterminate + checked at the same time
* @private
Expand Down
25 changes: 17 additions & 8 deletions packages/grid/src/vaadin-grid-selection-column-mixin.js
tomivirkki marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const GridSelectionColumnMixin = (superClass) =>
super();

this.__boundOnActiveItemChanged = this.__onActiveItemChanged.bind(this);
this.__boundOnDataProviderChanged = this.__onDataProviderChanged.bind(this);
this.__boundUpdateSelectAllVisibility = this.__updateSelectAllVisibility.bind(this);
this.__boundOnSelectedItemsChanged = this.__onSelectedItemsChanged.bind(this);
}

/** @protected */
disconnectedCallback() {
this._grid.removeEventListener('active-item-changed', this.__boundOnActiveItemChanged);
this._grid.removeEventListener('data-provider-changed', this.__boundOnDataProviderChanged);
this._grid.removeEventListener('data-provider-changed', this.__boundUpdateSelectAllVisibility);
this._grid.removeEventListener('is-item-selectable-changed', this.__boundUpdateSelectAllVisibility);
this._grid.removeEventListener('filter-changed', this.__boundOnSelectedItemsChanged);
this._grid.removeEventListener('selected-items-changed', this.__boundOnSelectedItemsChanged);

Expand All @@ -49,7 +50,8 @@ export const GridSelectionColumnMixin = (superClass) =>
super.connectedCallback();
if (this._grid) {
this._grid.addEventListener('active-item-changed', this.__boundOnActiveItemChanged);
this._grid.addEventListener('data-provider-changed', this.__boundOnDataProviderChanged);
this._grid.addEventListener('data-provider-changed', this.__boundUpdateSelectAllVisibility);
this._grid.addEventListener('is-item-selectable-changed', this.__boundUpdateSelectAllVisibility);
this._grid.addEventListener('filter-changed', this.__boundOnSelectedItemsChanged);
this._grid.addEventListener('selected-items-changed', this.__boundOnSelectedItemsChanged);
}
Expand Down Expand Up @@ -111,7 +113,9 @@ export const GridSelectionColumnMixin = (superClass) =>
* @override
*/
_selectItem(item) {
this._grid.selectItem(item);
if (this._grid.__isItemSelectable(item)) {
this._grid.selectItem(item);
}
}

/**
Expand All @@ -123,7 +127,9 @@ export const GridSelectionColumnMixin = (superClass) =>
* @override
*/
_deselectItem(item) {
this._grid.deselectItem(item);
if (this._grid.__isItemSelectable(item)) {
this._grid.deselectItem(item);
}
}

/** @private */
Expand All @@ -132,7 +138,7 @@ export const GridSelectionColumnMixin = (superClass) =>
if (this.autoSelect) {
const item = activeItem || this.__previousActiveItem;
if (item) {
this._grid._toggleItem(item);
this.__toggleItem(item);
}
}
this.__previousActiveItem = activeItem;
Expand Down Expand Up @@ -164,8 +170,11 @@ export const GridSelectionColumnMixin = (superClass) =>
}

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

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/grid/src/vaadin-grid-selection-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,25 @@ export declare class SelectionMixinClass<TItem> {
* @param item The item object
*/
deselectItem(item: TItem): void;

/**
* A function to check whether a specific item in the grid may be
* selected or deselected by the user. Used by the selection column to
* conditionally enable to disable checkboxes for individual items. This
* function does not prevent programmatic selection/deselection of
* items. Changing the function does not modify the currently selected
* items.
*
* Configuring this function hides the select all checkbox of the grid
* selection column, which means users can not select or deselect all
* items anymore, nor do they get feedback on whether all items are
* selected or not.
*
* Receives an item instance and should return a boolean indicating
* whether users may change the selection state of that item.
*
* @param item The item object
* @return Whether the item is selectable
*/
isItemSelectable: (item: TItem) => boolean;
}
56 changes: 40 additions & 16 deletions packages/grid/src/vaadin-grid-selection-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ export const SelectionMixin = (superClass) =>
sync: true,
},

/**
* A function to check whether a specific item in the grid may be
* selected or deselected by the user. Used by the selection column to
* conditionally enable to disable checkboxes for individual items. This
* function does not prevent programmatic selection/deselection of
* items. Changing the function does not modify the currently selected
* items.
*
* Configuring this function hides the select all checkbox of the grid
* selection column, which means users can not select or deselect all
* items anymore, nor do they get feedback on whether all items are
* selected or not.
*
* Receives an item instance and should return a boolean indicating
* whether users may change the selection state of that item.
*
* @type {(item: !GridItem) => boolean}
*/
isItemSelectable: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other similar APIs, such as part name generator or isCellEditable, use a row model instance (see __getRowModel). However that only works if you have a row instance available, whereas this can also be called for items that don't have a rendered row. Not sure if we need to align this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this and decided to only pass the item. Technically we could construct a row model object from the item itself before passing it to the function, but that would potentially require iterating over all grid caches to figure out the flat row index, or whether an item is a nested one. If we later on want to add support for select all, we'll have to run this function in some way or another over all grid items, at which point this could become a performance issue.

type: Function,
notify: true,
},

/**
* Set of selected item ids
* @private
Expand All @@ -34,7 +57,7 @@ export const SelectionMixin = (superClass) =>
}

static get observers() {
return ['__selectedItemsChanged(itemIdPath, selectedItems)'];
return ['__selectedItemsChanged(itemIdPath, selectedItems, isItemSelectable)'];
}

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

/**
* Determines whether the selection state of an item may be changed by the
* user.
*
* @private
*/
__isItemSelectable(item) {
// Item is selectable by default if isItemSelectable is not configured
if (!this.isItemSelectable || !item) {
return true;
}

// Otherwise, check isItemSelectable function
return this.isItemSelectable(item);
}

/**
* Selects the given item.
*
Expand All @@ -70,21 +109,6 @@ export const SelectionMixin = (superClass) =>
}
}

/**
* Toggles the selected state of the given item.
*
* @method toggle
* @param {!GridItem} item The item object
* @protected
*/
_toggleItem(item) {
if (!this._isSelected(item)) {
this.selectItem(item);
} else {
this.deselectItem(item);
}
}

/** @private */
__selectedItemsChanged() {
this.requestContentUpdate();
Expand Down
16 changes: 12 additions & 4 deletions packages/grid/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,24 @@ export const getContainerCellContent = (container, row, col) => {
return getCellContent(getContainerCell(container, row, col));
};

export const getHeaderCellContent = (grid, row, col) => {
export const getHeaderCell = (grid, row, col) => {
const container = grid.$.header;
return getContainerCellContent(container, row, col);
return getContainerCell(container, row, col);
};

export const getBodyCellContent = (grid, row, col) => {
export const getHeaderCellContent = (grid, row, col) => {
return getCellContent(getHeaderCell(grid, row, col));
};

export const getBodyCell = (grid, row, col) => {
const physicalItems = getPhysicalItems(grid);
const physicalRow = physicalItems.find((item) => item.index === row);
const cells = getRowCells(physicalRow);
return getCellContent(cells[col]);
return cells[col];
};

export const getBodyCellContent = (grid, row, col) => {
return getCellContent(getBodyCell(grid, row, col));
};

export const fire = (type, detail, options) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/grid/test/selectable-provider-lit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import '../theme/lumo/lit-all-imports.js';
import '../src/lit-all-imports.js';
import './selectable-provider.common.js';
2 changes: 2 additions & 0 deletions packages/grid/test/selectable-provider-polymer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import '../all-imports.js';
import './selectable-provider.common.js';
Loading