diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java
index b72d960a94f..7087ef275e6 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java
@@ -27,6 +27,7 @@ public class DynamicContextMenuGridPage extends Div {
public DynamicContextMenuGridPage() {
gridWithDynamicContextMenu();
+ gridWithDynamicContentProvider();
}
private void gridWithDynamicContextMenu() {
@@ -53,4 +54,30 @@ private void gridWithDynamicContextMenu() {
grid.setId("grid-with-dynamic-context-menu");
add(grid);
}
+
+ private void gridWithDynamicContentProvider() {
+ final Grid grid = new Grid<>();
+ Grid.Column nameColumn = grid.addColumn(Person::getFirstName)
+ .setHeader("Name");
+ grid.addColumn(Person::getAge).setHeader("Age");
+
+ grid.setItems(IntStream.range(0, 50)
+ .mapToObj(i -> new Person("Person " + i, i)).toList());
+
+ GridContextMenu contextMenu = grid.addContextMenu();
+
+ contextMenu.setDynamicContentProvider(context -> {
+ Person person = context.item();
+ contextMenu.removeAll();
+ if (person != null) {
+ contextMenu.addItem(context.column() == nameColumn
+ ? "Call " + person.getFirstName()
+ : "Age " + person.getAge());
+ }
+ return person != null;
+ });
+
+ grid.setId("grid-with-dynamic-content-provider");
+ add(grid);
+ }
}
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridIT.java
index 0993f311798..6c95c65b9a1 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridIT.java
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridIT.java
@@ -36,7 +36,7 @@ public class DynamicContextMenuGridIT extends AbstractComponentIT {
@Before
public void init() {
open();
- grid = $(GridElement.class).first();
+ grid = $(GridElement.class).id("grid-with-dynamic-context-menu");
verifyClosed();
}
@@ -76,6 +76,25 @@ public void shouldOpenContextMenuWhenClickingOnTheEdgeOfCell() {
getContextMenu().getDomProperty("innerText"));
}
+ @Test
+ public void dynamicContentProvider_menuContentDependsOnClickedColumn() {
+ GridElement columnGrid = $(GridElement.class)
+ .id("grid-with-dynamic-content-provider");
+
+ columnGrid.getCell(5, 1).contextClick();
+ verifyOpened();
+ Assert.assertEquals("Age 5",
+ getContextMenu().getDomProperty("innerText"));
+
+ $("body").first().click();
+ verifyClosed();
+
+ columnGrid.getCell(5, 0).contextClick();
+ verifyOpened();
+ Assert.assertEquals("Call Person 5",
+ getContextMenu().getDomProperty("innerText"));
+ }
+
private TestBenchElement getContextMenu() {
return $("vaadin-context-menu").first();
}
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-context-menu.test.ts b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-context-menu.test.ts
index 34bcdfebb3a..18202172fbe 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-context-menu.test.ts
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-context-menu.test.ts
@@ -23,6 +23,7 @@ describe('grid connector - context menu', () => {
column = grid.querySelector('vaadin-grid-column')!;
column.id = 'name-column';
+ column._flowId = 'col0';
setRootItems(grid.$connector, [
{ key: '0', name: 'foo' },
@@ -41,14 +42,14 @@ describe('grid connector - context menu', () => {
});
it('should return item key and column id in before-open detail', () => {
- let detail: { key: string, columnId: string };
+ let detail: { key: string; columnId: string; internalColumnId: string };
// The detail is resolved while the source event is still being dispatched
grid.addEventListener('contextmenu', (e) => {
detail = grid.getContextMenuBeforeOpenDetail(new CustomEvent('contextmenu', { detail: { sourceEvent: e } }));
});
getBodyCellContent(grid, 0, 1)!.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, composed: true }));
- expect(detail!).to.deep.equal({ key: '0', columnId: 'name-column' });
+ expect(detail!).to.deep.equal({ key: '0', columnId: 'name-column', internalColumnId: 'col0' });
});
it('should prevent context menu on selection column left click', () => {
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenu.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenu.java
index 00acfa044e4..2d1ca82e105 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenu.java
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenu.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.flow.component.grid.contextmenu;
+import java.io.Serializable;
import java.util.List;
import java.util.Optional;
@@ -44,6 +45,7 @@ public class GridContextMenu extends
implements HasGridMenuItems {
private SerializablePredicate dynamicContentHandler;
+ private SerializablePredicate> dynamicContentProvider;
/**
* Event that is fired when a {@link GridMenuItem} is clicked inside a
@@ -135,6 +137,27 @@ public Optional getColumnId() {
}
+ /**
+ * The context of a context-click on a {@link Grid}, passed to the
+ * {@link #setDynamicContentProvider(SerializablePredicate) dynamic content
+ * provider}.
+ *
+ * @param item
+ * the item in the Grid that was the target of the context-click,
+ * or {@code null} if the context-click didn't target any item
+ * (eg. if targeting a header)
+ * @param column
+ * the column in the Grid that was the target of the
+ * context-click, or {@code null} if the context-click didn't
+ * target any application column (eg. selection column)
+ * @param
+ * the grid bean type
+ * @since 25.4
+ */
+ public record DynamicContentContext(T item,
+ Grid.Column column) implements Serializable {
+ }
+
/**
* Creates an empty context menu to be used with a Grid.
*/
@@ -247,9 +270,15 @@ public Registration addGridContextMenuOpenedListener(
*
*
* @return the callback function that is executed before opening the context
- * menu, or {@code null} if not specified.
+ * menu, or {@code null} if not specified or if a dynamic content
+ * provider was set with
+ * {@link #setDynamicContentProvider(SerializablePredicate)}.
* @since 4.1
+ * @deprecated Use {@link #getDynamicContentProvider()} together with
+ * {@link #setDynamicContentProvider(SerializablePredicate)},
+ * which also receives the clicked column.
*/
+ @Deprecated(since = "25.4", forRemoval = true)
public SerializablePredicate getDynamicContentHandler() {
return dynamicContentHandler;
}
@@ -267,15 +296,80 @@ public SerializablePredicate getDynamicContentHandler() {
* The boolean return value of this callback specifies if the context menu
* will be opened.
*
+ *
+ * Replaces any provider set with
+ * {@link #setDynamicContentProvider(SerializablePredicate)}.
+ *
*
* @param dynamicContentHandler
* the callback function that will be executed before opening the
* context menu.
* @since 4.1
+ * @deprecated Use {@link #setDynamicContentProvider(SerializablePredicate)}
+ * instead, which also receives the clicked column.
*/
+ @Deprecated(since = "25.4", forRemoval = true)
public void setDynamicContentHandler(
SerializablePredicate dynamicContentHandler) {
this.dynamicContentHandler = dynamicContentHandler;
+ this.dynamicContentProvider = null;
+ }
+
+ /**
+ * Gets the callback function that is executed before the context menu is
+ * opened to dynamically provide its contents.
+ *
+ * @return the callback function that is executed before opening the context
+ * menu, or {@code null} if not specified or if a handler was set
+ * with {@link #setDynamicContentHandler(SerializablePredicate)}.
+ * @see #setDynamicContentProvider(SerializablePredicate)
+ * @since 25.4
+ */
+ public SerializablePredicate> getDynamicContentProvider() {
+ return dynamicContentProvider;
+ }
+
+ /**
+ * Sets a callback that is executed before the context menu is opened to
+ * dynamically provide its contents.
+ *
+ *
+ * The callback receives a {@link DynamicContentContext} with the clicked
+ * item and column. This is useful when the context menu items depend on
+ * what was clicked, for example:
+ *
+ *
+ *
+ * contextMenu.setDynamicContentProvider(context -> {
+ * contextMenu.removeAll();
+ * Person person = context.item();
+ * if (context.column() == nameColumn) {
+ * contextMenu.addItem("Call", e -> call(person));
+ * } else if (context.column() == addressColumn) {
+ * contextMenu.addItem("Show on map", e -> showOnMap(person));
+ * }
+ * return true;
+ * });
+ *
+ *
+ *
+ * The boolean return value of this callback specifies if the context menu
+ * will be opened.
+ *
+ *
+ * Replaces any handler set with
+ * {@link #setDynamicContentHandler(SerializablePredicate)}.
+ *
+ *
+ * @param dynamicContentProvider
+ * the callback function that will be executed before opening the
+ * context menu, or {@code null} to remove it
+ * @since 25.4
+ */
+ public void setDynamicContentProvider(
+ SerializablePredicate> dynamicContentProvider) {
+ this.dynamicContentProvider = dynamicContentProvider;
+ this.dynamicContentHandler = null;
}
/**
@@ -283,10 +377,26 @@ public void setDynamicContentHandler(
*
* @since 25.0
*/
+ @SuppressWarnings("removal")
@Override
protected boolean onBeforeOpenMenu(ObjectNode eventDetail) {
Grid grid = (Grid) getTarget();
String key = eventDetail.get("key").asString();
+ String columnId = eventDetail.get("columnId").asString();
+
+ // The grid connector also reports the target with a separate server
+ // call, which is processed after this event. Update the target here so
+ // that it is up to date in the dynamic content handlers.
+ grid.getElement().setProperty("_contextMenuTargetItemKey", key);
+ grid.getElement().setProperty("_contextMenuTargetColumnId", columnId);
+
+ if (dynamicContentProvider != null) {
+ final T item = grid.getDataCommunicator().getKeyMapper().get(key);
+ final Grid.Column column = getColumnByInternalId(grid,
+ eventDetail.get("internalColumnId").asString());
+ return dynamicContentProvider
+ .test(new DynamicContentContext<>(item, column));
+ }
if (getDynamicContentHandler() != null) {
final T item = grid.getDataCommunicator().getKeyMapper().get(key);
@@ -295,4 +405,12 @@ protected boolean onBeforeOpenMenu(ObjectNode eventDetail) {
return super.onBeforeOpenMenu(eventDetail);
}
+
+ private static Grid.Column getColumnByInternalId(Grid grid,
+ String internalId) {
+ return grid.getColumns().stream()
+ .filter(column -> internalId
+ .equals(column.getElement().getProperty("_flowId")))
+ .findFirst().orElse(null);
+ }
}
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/gridConnector.ts b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/gridConnector.ts
index c647c2d0b30..08c57989726 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/gridConnector.ts
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/gridConnector.ts
@@ -512,7 +512,8 @@ export class GridConnector {
const eventContext = grid.getEventContext(sourceEvent);
const key = eventContext.item?.key || '';
const columnId = eventContext.column?.id || '';
- return { key, columnId };
+ const internalColumnId = eventContext.column?._flowId ?? '';
+ return { key, columnId, internalColumnId };
};
grid.preventContextMenu = (event) => {
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/vaadin-grid-types.d.ts b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/vaadin-grid-types.d.ts
index 429a2e93039..971842023b8 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/vaadin-grid-types.d.ts
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/vaadin-grid/vaadin-grid-types.d.ts
@@ -93,7 +93,11 @@ export interface FlowGridInternals {
_isDetailsOpened(item: Item | undefined): boolean;
isItemSelectable(item: Item | null | undefined): boolean;
_mapSorters(): GridSorterDefinition[];
- getContextMenuBeforeOpenDetail(event: CustomEvent<{ sourceEvent?: Event }>): { key: string; columnId: string };
+ getContextMenuBeforeOpenDetail(event: CustomEvent<{ sourceEvent?: Event }>): {
+ key: string;
+ columnId: string;
+ internalColumnId: string;
+ };
preventContextMenu(event: MouseEvent): boolean;
}
diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTest.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTest.java
index af4894fe9b2..3c04c752759 100644
--- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTest.java
+++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTest.java
@@ -16,6 +16,7 @@
package com.vaadin.flow.component.grid.contextmenu;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -25,13 +26,18 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.contextmenu.ContextMenu;
+import com.vaadin.flow.component.contextmenu.ContextMenuBase;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.contextmenu.MenuManager;
import com.vaadin.flow.component.contextmenu.SubMenu;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.contextmenu.GridContextMenu.GridContextMenuItemClickEvent;
import com.vaadin.flow.component.html.NativeButton;
+import com.vaadin.flow.dom.DomEvent;
+import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.function.SerializableRunnable;
+import com.vaadin.flow.internal.JacksonUtils;
+import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
class GridContextMenuTest {
@@ -136,4 +142,101 @@ void multipleContextMenus_clearTargetOfOne_getContextMenusKeepsOthers() {
Assertions.assertEquals(List.of(first, third), grid.getContextMenus());
}
+
+ @Test
+ void dynamicContentHandler_targetColumnIdIsUpdatedBeforeHandlerRuns() {
+ Grid grid = new Grid<>();
+ grid.addColumn(item -> item).setId("first");
+ grid.addColumn(item -> item).setId("second");
+ GridContextMenu contextMenu = grid.addContextMenu();
+
+ AtomicReference columnIdInHandler = new AtomicReference<>();
+ contextMenu.setDynamicContentHandler(item -> {
+ columnIdInHandler.set(grid.getElement()
+ .getProperty("_contextMenuTargetColumnId"));
+ return false;
+ });
+
+ fireBeforeOpenEvent(grid, "second");
+ Assertions.assertEquals("second", columnIdInHandler.get());
+
+ fireBeforeOpenEvent(grid, "first");
+ Assertions.assertEquals("first", columnIdInHandler.get());
+ }
+
+ @Test
+ void dynamicContentProvider_receivesClickedItemAndColumn() {
+ Grid grid = new Grid<>();
+ grid.setItems("foo", "bar");
+ Grid.Column first = grid.addColumn(item -> item);
+ Grid.Column second = grid.addColumn(item -> item);
+ GridContextMenu contextMenu = grid.addContextMenu();
+
+ AtomicReference> contextInProvider = new AtomicReference<>();
+ contextMenu.setDynamicContentProvider(context -> {
+ contextInProvider.set(context);
+ return false;
+ });
+
+ String barKey = grid.getDataCommunicator().getKeyMapper().key("bar");
+ fireBeforeOpenEvent(grid, barKey, "", getInternalId(second));
+ Assertions.assertEquals("bar", contextInProvider.get().item());
+ Assertions.assertSame(second, contextInProvider.get().column());
+
+ fireBeforeOpenEvent(grid, "", "", getInternalId(first));
+ Assertions.assertNull(contextInProvider.get().item());
+ Assertions.assertSame(first, contextInProvider.get().column());
+
+ // Not an application column, e.g. the selection column
+ fireBeforeOpenEvent(grid, barKey, "", "");
+ Assertions.assertEquals("bar", contextInProvider.get().item());
+ Assertions.assertNull(contextInProvider.get().column());
+ }
+
+ @Test
+ void dynamicContentProviderAndHandler_replaceEachOther() {
+ GridContextMenu contextMenu = new Grid()
+ .addContextMenu();
+ SerializablePredicate handler = item -> true;
+ SerializablePredicate> provider = context -> true;
+
+ contextMenu.setDynamicContentHandler(handler);
+ Assertions.assertSame(handler, contextMenu.getDynamicContentHandler());
+
+ contextMenu.setDynamicContentProvider(provider);
+ Assertions.assertSame(provider,
+ contextMenu.getDynamicContentProvider());
+ Assertions.assertNull(contextMenu.getDynamicContentHandler());
+
+ AtomicReference called = new AtomicReference<>(false);
+ contextMenu.setDynamicContentProvider(context -> {
+ called.set(true);
+ return false;
+ });
+ contextMenu.setDynamicContentHandler(item -> false);
+ Assertions.assertNull(contextMenu.getDynamicContentProvider());
+ fireBeforeOpenEvent((Grid>) contextMenu.getTarget(), "", "", "");
+ Assertions.assertFalse(called.get());
+ }
+
+ private static String getInternalId(Grid.Column> column) {
+ return column.getElement().getProperty("_flowId");
+ }
+
+ private static void fireBeforeOpenEvent(Grid> grid, String columnId) {
+ fireBeforeOpenEvent(grid, "", columnId, "");
+ }
+
+ private static void fireBeforeOpenEvent(Grid> grid, String key,
+ String columnId, String internalColumnId) {
+ var detail = JacksonUtils.createObjectNode();
+ detail.put("key", key);
+ detail.put("columnId", columnId);
+ detail.put("internalColumnId", internalColumnId);
+ var eventData = JacksonUtils.createObjectNode();
+ eventData.set(ContextMenuBase.EVENT_DETAIL, detail);
+ grid.getElement().getNode().getFeature(ElementListenerMap.class)
+ .fireEvent(new DomEvent(grid.getElement(),
+ "vaadin-context-menu-before-open", eventData));
+ }
}