diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridCellFocusPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridCellFocusPage.java new file mode 100644 index 00000000000..81c2df64d95 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridCellFocusPage.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2021 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 com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +/** + * Page created for testing purposes. Not suitable for demos. + * + * @author Vaadin Ltd. + */ +@Route("vaadin-grid/grid-cell-focus-page") +public class GridCellFocusPage extends Div { + + public static final String ID_GRID = "cell-focus-grid"; + public static final String ID_ITEM_RESULT = "cell-focus-item-result"; + public static final String ID_COLUMN_RESULT = "cell-focus-column-result"; + public static final String ID_SECTION_RESULT = "cell-focus-section-result"; + + public static final String KEY_FIRST_COLUMN = "first-column"; + public static final String KEY_SECOND_COLUMN = "second-column"; + public static final String KEY_THIRD_COLUMN = "third-column"; + + public static final String NO_ITEM = "-- no item --"; + public static final String NO_COLUMN = "-- no column --"; + public static final String NO_SECTION = "-- no section --"; + + public GridCellFocusPage() { + setSizeFull(); + + Grid grid = new Grid<>(); + grid.setId(ID_GRID); + + grid.addColumn(s -> s + "1") + .setKey(KEY_FIRST_COLUMN) + .setHeader("First column header") + .setFooter("First column footer"); + grid.addColumn(s -> s + "2") + .setKey(KEY_SECOND_COLUMN) + .setHeader("Second column header") + .setFooter("Second column footer"); + grid.addColumn(s -> s + "3") + .setKey(KEY_THIRD_COLUMN) + .setHeader("Third column header") + .setFooter("Third column footer"); + + grid.setItems("A", "B", "C", "D"); + + Span itemResult = new Span(); + itemResult.setId(ID_ITEM_RESULT); + + Span colResult = new Span(); + colResult.setId(ID_COLUMN_RESULT); + + Span sectionResult = new Span(); + sectionResult.setId(ID_SECTION_RESULT); + + add(itemResult, colResult, sectionResult, grid); + + grid.addCellFocusListener(event -> { + String item = event + .getItem() + .orElse(NO_ITEM); + + String column = event + .getColumn() + .map(Column::getKey) + .orElse(NO_COLUMN); + + itemResult.setText(item); + colResult.setText(column); + sectionResult.setText(event.getSection().getClientSideName()); + }); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridCellFocusPageIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridCellFocusPageIT.java new file mode 100644 index 00000000000..e8880d048e1 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridCellFocusPageIT.java @@ -0,0 +1,114 @@ +/* + * Copyright 2000-2021 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 org.junit.Assert; +import org.junit.Test; + +import com.vaadin.flow.component.grid.CellFocusEvent; +import com.vaadin.flow.component.grid.testbench.GridElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.AbstractComponentIT; + +/** + * IT for grid's Flow based cell focus event. + * + * @author Vaadin Ltd + */ +@TestPath("vaadin-grid/grid-cell-focus-page") +public class GridCellFocusPageIT extends AbstractComponentIT { + + @Test + public void focusBodyCell() { + open(); + + getGrid().getCell(0, 0).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, "A"); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_FIRST_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.BODY.getClientSideName()); + + getGrid().getCell(1, 0).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, "B"); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_FIRST_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.BODY.getClientSideName()); + + getGrid().getCell(2, 1).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, "C"); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_SECOND_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.BODY.getClientSideName()); + } + + @Test + public void focusHeaderCell() { + open(); + + getGrid().getHeaderCell(0).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, + GridCellFocusPage.NO_ITEM); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_FIRST_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.HEADER.getClientSideName()); + + getGrid().getHeaderCell(1).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, + GridCellFocusPage.NO_ITEM); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_SECOND_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.HEADER.getClientSideName()); + } + + @Test + public void focusFooterCell() { + open(); + + getGrid().getFooterCell(0).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, + GridCellFocusPage.NO_ITEM); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_FIRST_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.FOOTER.getClientSideName()); + + getGrid().getFooterCell(1).focus(); + assertTextResult(GridCellFocusPage.ID_ITEM_RESULT, + GridCellFocusPage.NO_ITEM); + assertTextResult(GridCellFocusPage.ID_COLUMN_RESULT, + GridCellFocusPage.KEY_SECOND_COLUMN); + assertTextResult(GridCellFocusPage.ID_SECTION_RESULT, + CellFocusEvent.GridSection.FOOTER.getClientSideName()); + } + + private GridElement getGrid() { + return $(GridElement.class).id(GridCellFocusPage.ID_GRID); + } + + private void assertTextResult(String resultFieldId, String expectedResult) { + String text = $(TestBenchElement.class) + .id(resultFieldId) + .getText(); + + Assert.assertEquals(expectedResult, text); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/CellFocusEvent.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/CellFocusEvent.java new file mode 100644 index 00000000000..d02546b6a72 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/CellFocusEvent.java @@ -0,0 +1,166 @@ +/* + * Copyright 2000-2021 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; + +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.DomEvent; +import com.vaadin.flow.component.EventData; + +/** + * Event fired when a cell in the Grid is focused. + * + * @param the grid bean type + * @author Vaadin Ltd + *

+ * @see Grid#addCellFocusListener(com.vaadin.flow.component.ComponentEventListener) + */ +@DomEvent("grid-cell-focus") +public class CellFocusEvent extends ComponentEvent> { + + private final transient T item; + private final Grid.Column column; + private final GridSection section; + + /** + * Creates a new cell focus event. + * + * @param source the source component + * @param fromClient true if the event originated from the + * client + */ + public CellFocusEvent(Grid source, boolean fromClient, + @EventData("event.detail.itemKey") String itemKey, + @EventData("event.detail.internalColumnId") + String internalColumnId, + @EventData("event.detail.section") String sectionName) { + super(source, fromClient); + + item = source.getDataCommunicator().getKeyMapper().get(itemKey); + column = source.getColumnByInternalId(internalColumnId); + section = GridSection.ofClientSideName(sectionName); + } + + /** + * Indicates, if the clicked cell is part of the grid's body section. + * + * @return is a body cell + */ + public boolean isBodyCell() { + return section == GridSection.BODY; + } + + /** + * Indicates, if the clicked cell is part of the grid's header section. + * + * @return is a header cell + */ + public boolean isHeaderCell() { + return section == GridSection.HEADER; + } + + /** + * Indicates, if the clicked cell is part of the grid's footer section. + * + * @return is a footer cell + */ + public boolean isFooterCell() { + return section == GridSection.FOOTER; + } + + /** + * Returns the grid section, where this cell is located. Never null. + * @return section + */ + public GridSection getSection() { + return section; + } + + /** + * Returns the item represented by the focused cell. Is empty, when the + * focused cell is not a body cell. + * + * @return item or empty + */ + public Optional getItem() { + return Optional.ofNullable(item); + } + + /** + * Returns the column represented by the focused cell. Is empty, when the + * focused cell is a header group (a + * cell with a cellspan > 1). + * + * @return column or empty + */ + public Optional> getColumn() { + return Optional.ofNullable(column); + } + + /** + * An enum representing the different sections of a grid. + */ + public enum GridSection { + /** + * Header section. + */ + HEADER("header"), + + /** + * Body section. + */ + BODY("body"), + + /** + * Footer section. + */ + FOOTER("footer"); + + + private final String clientSideName; + + GridSection(String clientSideName) { + this.clientSideName = clientSideName; + } + + /** + * Returns the matching {@link GridSection} for the given client side name. + * An unknown client side name will lead to an exception. + * @param clientSideName client side name to lookup + * @throws IllegalArgumentException on an unknown client side section name + * @return matching section instance + */ + public static GridSection ofClientSideName(String clientSideName) { + for (GridSection section : values()) { + if (Objects.equals(clientSideName, section.getClientSideName())) { + return section; + } + } + + throw new IllegalArgumentException("Unknown section client side section name: " + clientSideName); + } + + /** + * Returns the client side name of the section. + * @return client side name + */ + public String getClientSideName() { + return clientSideName; + } + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java index cd10ff86a6b..22e779593cf 100755 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java @@ -3499,6 +3499,38 @@ public Registration addItemDoubleClickListener( (ComponentEventListener) Objects.requireNonNull(listener)); } + /** + * Adds a listener to the grid that will be notified, when a cell has been + * focused. + *

+ * The listener will be notified, when + *


+ * The listener will not be notified, when + * + * + * @param listener + * the listener to add, not null + * @return a handle that can be used for removing the listener + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Registration addCellFocusListener( + ComponentEventListener> listener) { + return addListener(CellFocusEvent.class, + (ComponentEventListener) Objects.requireNonNull(listener)); + } + /** * Enables or disables the vertical scrolling on the Grid web component. By * default, the scrolling is enabled. diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.js b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.js index 176bb5b8ae0..bc5144d5a3e 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.js +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.js @@ -992,6 +992,29 @@ import { ItemCache } from '@vaadin/vaadin-grid/src/vaadin-grid-data-provider-mix })); })); + grid.addEventListener('cell-focus', tryCatchWrapper(e => { + const eventContext = grid.getEventContext(e); + const expectedSectionValues = ['header', 'body', 'footer']; + + if(expectedSectionValues.indexOf(eventContext.section) === -1) { + throw new Error('Unexpected value for section: ' + eventContext.section); + } + + grid.dispatchEvent(new CustomEvent('grid-cell-focus', { + detail: { + itemKey: eventContext.item + ? eventContext.item.key + : null, + + internalColumnId: eventContext.column + ? eventContext.column._flowId + : null, + + section: eventContext.section + } + })); + })); + function _fireClickEvent(event, eventName) { if (grid.$connector.activeItem) { event.itemKey = grid.$connector.activeItem.key;