Skip to content

feat: pass clicked column to grid context menu dynamic content handler - #10331

Open
totally-not-ai[bot] wants to merge 6 commits into
mainfrom
fix/grid-context-menu-dynamic-handler-column
Open

totally-not-ai[bot] wants to merge 6 commits into
mainfrom
fix/grid-context-menu-dynamic-handler-column

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review risk: two-way door — additive API on GridContextMenu; the only behavior change is that the context menu target (item key / column id) is updated before the dynamic content handler runs.

Summary

The dynamic content handler of GridContextMenu only received the clicked item, and the clicked column could not be reliably read from inside it: the grid's _contextMenuTargetColumnId property was updated by a separate server call that is processed after the handler runs, so the handler always saw the previously clicked column (or none on the first click).

This adds a handler overload that receives the clicked column directly, which enables context menus whose items depend on the column that was right-clicked:

contextMenu.setDynamicContentHandler((person, column) -> {
    contextMenu.removeAll();
    if (column == nameColumn) {
        contextMenu.addItem("Call", e -> call(person));
    } else if (column == addressColumn) {
        contextMenu.addItem("Show on map", e -> showOnMap(person));
    }
    return true;
});

What changed

  • New GridContextMenu#setDynamicContentHandler(SerializableBiPredicate<T, Grid.Column<T>>) with a matching getColumnDynamicContentHandler() getter. The column is null when the click doesn't target an application column (e.g. the selection column), the item is null when it doesn't target an item (e.g. a header). The column is resolved from its internal id, so it works without Column#setId.
  • The two setDynamicContentHandler overloads replace each other; getDynamicContentHandler() returns null when the column-aware handler is set.
  • The item-only setDynamicContentHandler(SerializablePredicate) and getDynamicContentHandler() are deprecated for removal in favor of the new overload. Their behavior is unchanged.
  • Before running any dynamic content handler, the grid's context menu target item key and column id are now updated from the before-open event, fixing the ordering issue for existing handlers that read them.
  • The grid connector now includes the column's internal id in the context menu before-open detail.
  • Source compatibility note: a literal setDynamicContentHandler(null) call is now ambiguous and needs a cast.

Test summary

  • Unit tests: target column is up to date inside the handler; the new handler receives the clicked item and column, and null for non-application columns; the overloads replace each other.
  • Connector test: before-open detail includes the internal column id.
  • Integration test: menu contents differ depending on the right-clicked column in a real browser.

API Changes

// com.vaadin.flow.component.grid.contextmenu.GridContextMenu
// Added
public void setDynamicContentHandler(SerializableBiPredicate<T, Grid.Column<T>> dynamicContentHandler)
public SerializableBiPredicate<T, Grid.Column<T>> getColumnDynamicContentHandler()

// Changed
- public SerializablePredicate<T> getDynamicContentHandler()
+ @Deprecated(since = "25.4", forRemoval = true) public SerializablePredicate<T> getDynamicContentHandler()
- public void setDynamicContentHandler(SerializablePredicate<T> dynamicContentHandler)
+ @Deprecated(since = "25.4", forRemoval = true) public void setDynamicContentHandler(SerializablePredicate<T> dynamicContentHandler)

Fixes #2258

🤖 Generated with Claude Code

totally-not-ai Bot and others added 2 commits October 3, 2026 06:18
…ntent handler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a GridContextMenu#setDynamicContentHandler overload whose callback
receives both the clicked item and the clicked column. Also update the
context menu target item and column before running the dynamic content
handler, so that they are no longer one click behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -276,6 +284,50 @@ public SerializablePredicate<T> getDynamicContentHandler() {
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.

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.

totally-not-ai Bot and others added 2 commits October 4, 2026 05:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* @since 25.4
*/
public void setDynamicContentHandler(
SerializableBiPredicate<T, Grid.Column<T>> dynamicContentHandler) {

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.

Should probably pass a record here and not two separate items - if that had been done originally then this change would be backwards compatible.. but that wouldn't be possible now would it?

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- Right, it can't be done under the same method name. A setDynamicContentHandler(SerializablePredicate<Context<T>>) overload has the same erasure as the existing SerializablePredicate<T> one, so it doesn't compile. A dedicated functional interface that takes a record would compile, but it is also single-argument, so every existing item -> ... lambda would become ambiguous between the two overloads and existing code would stop compiling. That's why I used a two-argument BiPredicate.

A record-based API would need a new name, for example setDynamicContentProvider(context -> ...) with record DynamicContentContext<T>(T item, Grid.Column<T> column), and both setDynamicContentHandler overloads would be deprecated. That would let us add more context later (such as the clicked section or header row) without adding more overloads. I can switch the PR to that if you prefer it over the (item, column) overload.

@Artur-
Artur- marked this pull request as ready for review October 4, 2026 06:08

@vaadin-review-bot vaadin-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the changes — left 1 comment.

Finding
📐 New setter has no getter, and the only existing getter is deprecated for removal

* context menu, or {@code null} to remove it
* @since 25.4
*/
public void setDynamicContentHandler(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📐 New setter has no getter, and the only existing getter is deprecated for removal

CONVENTIONS.md states: "Pair public setters with a matching getter."

After this change there is no way to read back a handler set through the new overload: getDynamicContentHandler() returns null for it and is itself marked forRemoval. Once the deprecated pair is gone, the dynamic content handler becomes write-only.

A separately named getter (for example getColumnDynamicContentHandler()) avoids the erasure clash that blocks reusing the name.

As a side effect today, getDynamicContentHandler() != null is no longer a reliable "has a dynamic handler" check.

GridContextMenu.java:336 · conventions

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.

I added getColumnDynamicContentHandler(), which returns the handler set through the (item, column) overload. It returns null when the item-only handler is set, the same way getDynamicContentHandler() returns null for the column-aware one. The deprecated getter's @deprecated tag now points to it. The tests now check that each getter returns the handler set through its own overload, and that setting one handler clears the other.

totally-not-ai Bot and others added 2 commits October 4, 2026 06:17
…andler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GridContextMenu dynamicContentHandler called before _contextMenuTargetColumnId is set by Grid.updateContextMenuTargetItem

2 participants