Skip to content

Commit

Permalink
feat: conditional selection of grid items (#6740)
Browse files Browse the repository at this point in the history
* implement items selectable provider for single selection

* allow selectable provider to be null

* hide select all checkbox when using conditional selection

* add select all test cases

* make item selectable check handle undefined values

* update selected items when selecting null from server

* update tests

* handle null items
  • Loading branch information
sissbruecker authored Oct 30, 2024
1 parent 7e54c05 commit ca44217
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.component.grid.it;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.data.selection.SelectionEvent;
import com.vaadin.flow.router.Route;

@Route("vaadin-grid/conditional-selection")
public class ConditionalSelectionPage extends Div {
private final Span selectedItems;

public ConditionalSelectionPage() {
Grid<Integer> grid = new Grid<>();
grid.setItems(IntStream.range(0, 10).boxed().toList());
grid.addColumn(i -> i).setHeader("Item");

selectedItems = new Span();
selectedItems.setId("selected-items");

NativeButton enableSingleSelect = new NativeButton(
"Enable single selection", e -> {
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
grid.addSelectionListener(this::updateSelection);
});
enableSingleSelect.setId("enable-single-selection");

NativeButton enableMultiSelect = new NativeButton(
"Enable multi selection", e -> {
grid.setSelectionMode(Grid.SelectionMode.MULTI);
grid.addSelectionListener(this::updateSelection);
});
enableMultiSelect.setId("enable-multi-selection");

NativeButton disableSelectionFirstFive = new NativeButton(
"Disable selection for first five items", e -> {
grid.setItemSelectableProvider(item -> item >= 5);
});
disableSelectionFirstFive.setId("disable-selection-first-five");

NativeButton allowSelectionFirstFive = new NativeButton(
"Allow selection for first five items", e -> {
grid.setItemSelectableProvider(item -> item < 5);
});
allowSelectionFirstFive.setId("allow-selection-first-five");

add(grid);
add(new Div(enableSingleSelect, enableMultiSelect));
add(new Div(disableSelectionFirstFive, allowSelectionFirstFive));
add(new Div(new Span("Selected items: "), selectedItems));
}

private void updateSelection(
SelectionEvent<Grid<Integer>, Integer> selectionEvent) {
String items = selectionEvent.getAllSelectedItems().stream()
.map(Object::toString).collect(Collectors.joining(","));
selectedItems.setText(items);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.component.grid.it;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.flow.component.grid.testbench.GridElement;
import com.vaadin.flow.testutil.TestPath;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.tests.AbstractComponentIT;

@TestPath("vaadin-grid/conditional-selection")
public class ConditionalSelectionIT extends AbstractComponentIT {
private GridElement grid;

@Before
public void init() {
open();
grid = $(GridElement.class).waitForFirst();
}

@Test
public void singleSelect_clickRow_preventsSelection() {
$("button").id("enable-single-selection").click();
$("button").id("disable-selection-first-five").click();

// Prevents selection of non-selectable item
grid.select(0);
assertSelectedItems(Set.of());

// Allows selection of selectable item
grid.select(5);
assertSelectedItems(Set.of(5));
}

@Test
public void singleSelect_clickRow_preventsDeselection() {
$("button").id("enable-single-selection").click();
grid.select(0);

$("button").id("disable-selection-first-five").click();

// Prevents deselection of non-selectable item
grid.deselect(0);
assertSelectedItems(Set.of(0));

// Allows deselection of selectable item
grid.select(5);
grid.deselect(5);
assertSelectedItems(Set.of());
}

@Test
public void multiSelect_hidesCheckboxes() {
$("button").id("enable-multi-selection").click();
$("button").id("disable-selection-first-five").click();

Assert.assertFalse(getItemCheckbox(0).isDisplayed());
Assert.assertTrue(getItemCheckbox(5).isDisplayed());
}

@Test
public void multiSelect_updateProvider_updatesCheckboxes() {
$("button").id("enable-multi-selection").click();
$("button").id("disable-selection-first-five").click();

Assert.assertFalse(getItemCheckbox(0).isDisplayed());
Assert.assertTrue(getItemCheckbox(5).isDisplayed());

$("button").id("allow-selection-first-five").click();

Assert.assertTrue(getItemCheckbox(0).isDisplayed());
Assert.assertFalse(getItemCheckbox(5).isDisplayed());
}

private TestBenchElement getItemCheckbox(int index) {
return grid.getCell(index, 0).$("vaadin-checkbox").first();
}

private Set<Integer> getServerSelectedItems() {
var items = $("span").id("selected-items").getText();
return items.isEmpty() ? Set.of()
: Stream.of(items.split(",")).map(Integer::parseInt)
.collect(Collectors.toSet());
}

@SuppressWarnings("unchecked")
private Set<Integer> getClientSelectedItems() {
var itemNames = (List<String>) getCommandExecutor().executeScript(
"return arguments[0].selectedItems.map(item => item.col0)",
grid);
return itemNames.stream().map(Integer::parseInt)
.collect(Collectors.toSet());
}

private void assertSelectedItems(Set<Integer> items) {
Assert.assertEquals(items, getServerSelectedItems());
Assert.assertEquals(items, getClientSelectedItems());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,95 @@ describe('grid connector - selection', () => {
expect(grid.$server.deselect).not.to.be.called;
});
});

describe('conditional selection', () => {
let items;

beforeEach(async () => {
items = Array.from({ length: 4 }, (_, i) => ({
key: i.toString(),
name: i.toString(),
selectable: i >= 2
}));
setRootItems(grid.$connector, items);
await nextFrame();
grid.requestContentUpdate();
});

it('should prevent selection of non-selectable items on click', () => {
getBodyCellContent(grid, 0, 0)!.click();
expect(grid.selectedItems).to.be.empty;
expect(grid.$server.select).to.not.be.called;
});

it('should allow selection of selectable items on click', async () => {
getBodyCellContent(grid, 2, 0)!.click();
expect(grid.selectedItems).to.deep.equal([items[2]]);
expect(grid.$server.select).to.be.calledWith(items[2].key);
});

it('should prevent deselection of non-selectable items on click', () => {
grid.$connector.doSelection([items[0]], false);
getBodyCellContent(grid, 0, 0)!.click();
expect(grid.selectedItems).to.deep.equal([items[0]]);
expect(grid.$server.deselect).to.not.be.called;
});

it('should prevent deselection of non-selectable items when clicking another non-selectable item', () => {
grid.$connector.doSelection([items[0]], false);
getBodyCellContent(grid, 1, 0)!.click();
expect(grid.selectedItems).to.deep.equal([items[0]]);
expect(grid.$server.deselect).to.not.be.called;
});

it('should prevent deselection of non-selectable items on row click when active item data is stale', () => {
// item is selectable initially and is selected
grid.$connector.doSelection([items[2]], false);

// update grid items to make the item non-selectable
const updatedItems = items.map((item) => ({ ...item, selectable: false }));
setRootItems(grid.$connector, updatedItems);

// active item still references the original item with selectable: true
expect(grid.activeItem.selectable).to.be.true;

// however clicking the row should not deselect the item
getBodyCellContent(grid, 2, 0)!.click();
expect(grid.selectedItems).to.deep.equal([updatedItems[2]]);
expect(grid.$server.deselect).to.not.be.called;
});

it('should allow deselection of selectable items on row click', () => {
grid.$connector.doSelection([items[2]], false);
getBodyCellContent(grid, 2, 0)!.click();
expect(grid.selectedItems).to.be.empty;
expect(grid.$server.deselect).to.be.calledWith(items[2].key);
});

it('should always allow selection from server', () => {
// non-selectable item
grid.$connector.doSelection([items[0]], false);
expect(grid.selectedItems).to.deep.equal([items[0]]);
expect(grid.activeItem).to.deep.equal(items[0]);

// selectable item
grid.$connector.doSelection([items[2]], false);
expect(grid.selectedItems).to.deep.equal([items[2]]);
expect(grid.activeItem).to.deep.equal(items[2]);
})

it('should always allow deselection from server', () => {
// non-selectable item
grid.$connector.doSelection([items[0]], false);
grid.$connector.doDeselection([items[0]], false);
expect(grid.selectedItems).to.deep.equal([]);

// selectable item
grid.$connector.doSelection([items[2]], false);
grid.$connector.doDeselection([items[2]], false);
expect(grid.selectedItems).to.deep.equal([]);
})
});
});

describe('none selection mode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type Item = {
key: string;
name?: string;
price?: number,
selectable?: boolean;
selected?: boolean;
detailsOpened?: boolean;
style?: Record<string, string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ public AbstractGridMultiSelectionModel(Grid<T> grid) {
this::clientDeselectAll);
selectAllCheckBoxVisibility = SelectAllCheckboxVisibility.DEFAULT;

selectionColumn
.setSelectAllCheckBoxVisibility(isSelectAllCheckboxVisible());
updateSelectAllCheckBoxVisibility();

if (grid.getElement().getNode().isAttached()) {
this.insertSelectionColumn(grid, selectionColumn);
Expand All @@ -89,6 +88,11 @@ public AbstractGridMultiSelectionModel(Grid<T> grid) {
}
}

void updateSelectAllCheckBoxVisibility() {
selectionColumn
.setSelectAllCheckBoxVisibility(isSelectAllCheckboxVisible());
}

private void insertSelectionColumn(Grid<T> grid,
GridSelectionColumn selectionColumn) {
grid.getElement().insertChild(0, selectionColumn.getElement());
Expand All @@ -105,7 +109,8 @@ protected void remove() {

@Override
public void selectFromClient(T item) {
if (isSelected(item)) {
boolean selectable = getGrid().isItemSelectable(item);
if (isSelected(item) || !selectable) {
return;
}

Expand All @@ -131,7 +136,8 @@ public void selectFromClient(T item) {

@Override
public void deselectFromClient(T item) {
if (!isSelected(item)) {
boolean selectable = getGrid().isItemSelectable(item);
if (!isSelected(item) || !selectable) {
return;
}

Expand Down Expand Up @@ -320,6 +326,10 @@ public SelectAllCheckboxVisibility getSelectAllCheckboxVisibility() {

@Override
public boolean isSelectAllCheckboxVisible() {
if (getGrid().getItemSelectableProvider() != null) {
return false;
}

switch (selectAllCheckBoxVisibility) {
case DEFAULT:
return getGrid().getDataCommunicator().getDataProvider()
Expand Down Expand Up @@ -376,8 +386,8 @@ protected abstract void fireSelectionEvent(
SelectionEvent<Grid<T>, T> event);

protected void clientSelectAll() {
// ignore call if the checkbox is hidden
if (!isSelectAllCheckboxVisible()) {
// ignore event if the checkBox was meant to be hidden
return;
}
Stream<T> allItemsStream;
Expand Down Expand Up @@ -439,8 +449,8 @@ private Stream<T> fetchAllDescendants(T parent,
}

protected void clientDeselectAll() {
// ignore call if the checkbox is hidden
if (!isSelectAllCheckboxVisible()) {
// ignore event if the checkBox was meant to be hidden
return;
}
doUpdateSelection(Collections.emptySet(), getSelectedItems(), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public AbstractGridSingleSelectionModel(Grid<T> grid) {

@Override
public void selectFromClient(T item) {
if (isSelected(item)) {
boolean selectable = getGrid().isItemSelectable(item);
if (isSelected(item) || !selectable) {
return;
}
doSelect(item, true);
Expand All @@ -78,8 +79,9 @@ public void select(T item) {

@Override
public void deselectFromClient(T item) {
if (isSelected(item) && isDeselectAllowed()) {
selectFromClient(null);
boolean selectable = getGrid().isItemSelectable(item);
if (isSelected(item) && selectable && isDeselectAllowed()) {
doSelect(null, true);
}
}

Expand Down
Loading

0 comments on commit ca44217

Please sign in to comment.