Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

public DynamicContextMenuGridPage() {
gridWithDynamicContextMenu();
gridWithDynamicContentProvider();
}

private void gridWithDynamicContextMenu() {
Expand All @@ -53,4 +54,30 @@
grid.setId("grid-with-dynamic-context-menu");
add(grid);
}

private void gridWithDynamicContentProvider() {
final Grid<Person> grid = new Grid<>();
Grid.Column<Person> 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<Person> contextMenu = grid.addContextMenu();

contextMenu.setDynamicContentProvider(context -> {
Person person = context.item();
contextMenu.removeAll();
if (person != null) {
contextMenu.addItem(context.column() == nameColumn
? "Call " + person.getFirstName()

Check warning on line 74 in vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add parentheses to make the operator precedence explicit.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AaEFwUHgA1EgyIR1QLwx&open=AaEFwUHgA1EgyIR1QLwx&pullRequest=10331
: "Age " + person.getAge());

Check warning on line 75 in vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridPage.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add parentheses to make the operator precedence explicit.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AaEAeNqsyIjIAJjAzpb_&open=AaEAeNqsyIjIAJjAzpb_&pullRequest=10331
}
return person != null;
});

grid.setId("grid-with-dynamic-content-provider");
add(grid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
@Before
public void init() {
open();
grid = $(GridElement.class).first();
grid = $(GridElement.class).id("grid-with-dynamic-context-menu");
verifyClosed();
}

Expand Down Expand Up @@ -76,6 +76,25 @@
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();

Check warning on line 89 in vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/contextmenu/DynamicContextMenuGridIT.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated method, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AaEAeNgyyIjIAJjAzpb9&open=AaEAeNgyyIjIAJjAzpb9&pullRequest=10331
verifyClosed();

columnGrid.getCell(5, 0).contextClick();
verifyOpened();
Assert.assertEquals("Call Person 5",
getContextMenu().getDomProperty("innerText"));
}

private TestBenchElement getContextMenu() {
return $("vaadin-context-menu").first();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.vaadin.flow.component.grid.contextmenu;

import java.io.Serializable;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -44,6 +45,7 @@
implements HasGridMenuItems<T> {

private SerializablePredicate<T> dynamicContentHandler;
private SerializablePredicate<DynamicContentContext<T>> dynamicContentProvider;

/**
* Event that is fired when a {@link GridMenuItem} is clicked inside a
Expand All @@ -64,7 +66,7 @@
grid = (Grid<T>) getSource().getContextMenu().getTarget();
item = Optional.ofNullable(grid.getDataCommunicator().getKeyMapper()
.get(grid.getElement()
.getProperty("_contextMenuTargetItemKey")));

Check failure on line 69 in vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenu.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "_contextMenuTargetItemKey" 3 times.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow-components&issues=AaEAeNrpyIjIAJjAzpcA&open=AaEAeNrpyIjIAJjAzpcA&pullRequest=10331
}

/**
Expand Down Expand Up @@ -135,6 +137,27 @@

}

/**
* 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 <T>
* the grid bean type
* @since 25.4
*/
public record DynamicContentContext<T>(T item,
Grid.Column<T> column) implements Serializable {
}

/**
* Creates an empty context menu to be used with a Grid.
*/
Expand Down Expand Up @@ -247,9 +270,15 @@
* </p>
*
* @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<T> getDynamicContentHandler() {
return dynamicContentHandler;
}
Expand All @@ -267,26 +296,107 @@
* The boolean return value of this callback specifies if the context menu
* will be opened.
* </p>
* <p>
* Replaces any provider set with
* {@link #setDynamicContentProvider(SerializablePredicate)}.
* </p>
*
* @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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be deprecated for removal then, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Artur- Yes, I've deprecated it for removal now. Both setDynamicContentHandler(SerializablePredicate) and getDynamicContentHandler() have @Deprecated(since = "25.4", forRemoval = true), and the @deprecated tag points to the (item, column) overload. The getter has no replacement, because the two overloads can't share a getter name. Behavior is unchanged and the existing tests still cover the old setter.

SerializablePredicate<T> 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<DynamicContentContext<T>> getDynamicContentProvider() {
return dynamicContentProvider;
}

/**
* Sets a callback that is executed before the context menu is opened to
* dynamically provide its contents.
*
* <p>
* 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:
* </p>
*
* <pre>
* contextMenu.setDynamicContentProvider(context -&gt; {
* contextMenu.removeAll();
* Person person = context.item();
* if (context.column() == nameColumn) {
* contextMenu.addItem("Call", e -&gt; call(person));
* } else if (context.column() == addressColumn) {
* contextMenu.addItem("Show on map", e -&gt; showOnMap(person));
* }
* return true;
* });
* </pre>
*
* <p>
* The boolean return value of this callback specifies if the context menu
* will be opened.
* </p>
* <p>
* Replaces any handler set with
* {@link #setDynamicContentHandler(SerializablePredicate)}.
* </p>
*
* @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<DynamicContentContext<T>> dynamicContentProvider) {
this.dynamicContentProvider = dynamicContentProvider;
this.dynamicContentHandler = null;
}

/**
* {@inheritDoc}
*
* @since 25.0
*/
@SuppressWarnings("removal")
@Override
protected boolean onBeforeOpenMenu(ObjectNode eventDetail) {
Grid<T> grid = (Grid<T>) 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<T> 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);
Expand All @@ -295,4 +405,12 @@

return super.onBeforeOpenMenu(eventDetail);
}

private static <T> Grid.Column<T> getColumnByInternalId(Grid<T> grid,
String internalId) {
return grid.getColumns().stream()
.filter(column -> internalId
.equals(column.getElement().getProperty("_flowId")))
.findFirst().orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why isn't columnId enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Artur- columnId is the column's DOM id, which is only set when the application calls Column#setId. It is empty by default and isn't guaranteed to be unique. So it can't reliably be mapped back to a Grid.Column, and handlers for columns without an id would always get null. _flowId is the internal id that Grid assigns to every column, and the server keeps a map from it to the column. The item click and cell focus events already use it for the same reason. columnId stays in the event detail because GridContextMenuOpenedEvent#getColumnId() and the _contextMenuTargetColumnId property still expose it.

};

grid.preventContextMenu = (event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading
Loading