ContextMenuPresentation.java
package com.vaadin.demo.component.contextmenu;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.contextmenu.GridContextMenu;
import com.vaadin.flow.component.grid.contextmenu.GridMenuItem;
import com.vaadin.flow.component.grid.contextmenu.GridSubMenu;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Hr;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import java.util.List;
import java.util.Random;
@Route("context-menu-presentation")
public class ContextMenuPresentation extends Div {
private List<Person> people = DataService.getPeople(10);
private Random random = new Random(1);
public ContextMenuPresentation() {
Grid<Person> grid = new Grid();
grid.setAllRowsVisible(true);
grid.setItems(people.subList(0, 5));
grid.addColumn(person -> person.getFullName()).setHeader("Applicant");
grid.addColumn(person -> person.getEmail()).setHeader("Email");
grid.addColumn(person -> person.getAddress().getPhone())
.setHeader("Phone number");
// tag::snippet1[]
GridContextMenu<Person> menu = grid.addContextMenu();
// end::snippet1[]
GridMenuItem<Person> open = menu.addItem("Open", event -> {
});
open.addComponentAsFirst(createIcon(VaadinIcon.FILE_SEARCH));
GridMenuItem<Person> assign = menu.addItem("Assign");
assign.addComponentAsFirst(createIcon(VaadinIcon.USER_CHECK));
GridSubMenu<Person> assignSubMenu = assign.getSubMenu();
people.subList(5, 10).forEach(person -> {
assignSubMenu.addItem(createPersonItem(person), event -> {
});
});
menu.addSeparator();
GridMenuItem<Person> delete = menu.addItem("Delete", event -> {
});
delete.addComponentAsFirst(createIcon(VaadinIcon.TRASH));
add(grid);
}
private Component createIcon(VaadinIcon vaadinIcon) {
Icon icon = vaadinIcon.create();
icon.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("margin-inline-end", "var(--lumo-space-s")
.set("padding", "var(--lumo-space-xs");
return icon;
}
private Component createPersonItem(Person person) {
Avatar avatar = new Avatar();
avatar.setImage(person.getPictureUrl());
avatar.setName(person.getFirstName());
Span name = new Span(person.getFullName());
Span apps = new Span(getApplicationCount());
apps.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
VerticalLayout verticalLayout = new VerticalLayout(name, apps);
verticalLayout.setPadding(false);
verticalLayout.setSpacing(false);
HorizontalLayout horizontalLayout = new HorizontalLayout(avatar,
verticalLayout);
horizontalLayout.setAlignItems(FlexComponent.Alignment.CENTER);
horizontalLayout.getStyle().set("line-height",
"var(--lumo-line-height-m)");
return horizontalLayout;
}
private String getApplicationCount() {
// Randomised dummy data
return random.nextInt(20) + 1 + " applications";
}
}
context-menu-presentation.tsx
import '@vaadin/icons';
import React, { useEffect, useRef } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Avatar } from '@vaadin/react-components/Avatar.js';
import { ContextMenu, type ContextMenuItem } from '@vaadin/react-components/ContextMenu.js';
import { Grid, type GridElement } from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { Icon } from '@vaadin/react-components/Icon.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
function Item({ person }: { person: Person }) {
return (
<HorizontalLayout
style={{ alignItems: 'center', lineHeight: 'var(--lumo-line-height-m)' }}
theme="spacing"
>
<Avatar img={person.pictureUrl} name={`${person.firstName} ${person.lastName}`} />
<VerticalLayout>
<span>
{person.firstName} {person.lastName}
</span>
<span
style={{ color: 'var(--lumo-secondary-text-color)', fontSize: 'var(--lumo-font-size-s)' }}
>
{Math.floor(Math.random() * 20) + 1} applications
</span>
</VerticalLayout>
</HorizontalLayout>
);
}
function createItem(iconName: string, text: string) {
return (
<>
<Icon
icon={iconName}
style={{
color: 'var(--lumo-secondary-text-color)',
marginInlineEnd: 'var(--lumo-space-s)',
padding: 'var(--lumo-space-xs)',
}}
/>
{text}
</>
);
}
function renderApplicant({ item }: { item: Person }) {
return (
<span>
{item.firstName} {item.lastName}
</span>
);
}
function Example() {
const gridItems = useSignal<Person[]>([]);
const items = useSignal<ContextMenuItem[]>([]);
const gridRef = useRef<GridElement>(null);
useEffect(() => {
getPeople({ count: 5 }).then(({ people }) => {
gridItems.value = people;
// tag::snippet[]
const contextMenuItems: ContextMenuItem[] = [
{ component: createItem('vaadin:file-search', 'Open') },
{
component: createItem('vaadin:user-check', 'Assign'),
children: [
{
component: <Item person={people[0]} />,
},
{
component: <Item person={people[1]} />,
},
{
component: <Item person={people[2]} />,
},
{
component: <Item person={people[3]} />,
},
{
component: <Item person={people[4]} />,
},
],
},
{ component: 'hr' },
{ component: createItem('vaadin:trash', 'Delete') },
];
items.value = contextMenuItems;
// end::snippet[]
});
}, []);
useEffect(() => {
const grid = gridRef.current;
if (grid) {
// Workaround: Prevent opening context menu on header row.
// @ts-expect-error vaadin-contextmenu isn't a GridElement event.
grid.addEventListener('vaadin-contextmenu', (e) => {
if (grid.getEventContext(e).section !== 'body') {
e.stopPropagation();
}
});
}
}, [gridRef.current]);
// tag::snippet[]
return (
<ContextMenu items={items.value}>
<Grid allRowsVisible items={gridItems.value} ref={gridRef}>
<GridColumn header="Applicant" renderer={renderApplicant} />
<GridColumn path="email" />
<GridColumn header="Phone number" path="address.phone" />
</Grid>
</ContextMenu>
);
// end::snippet[]
}
context-menu-presentation.ts
import '@vaadin/avatar';
import '@vaadin/context-menu';
import '@vaadin/grid';
import '@vaadin/horizontal-layout';
import '@vaadin/icon';
import '@vaadin/icons';
import '@vaadin/vertical-layout';
import { html, LitElement, render } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { ContextMenuItem } from '@vaadin/context-menu';
import type { Grid } from '@vaadin/grid';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('context-menu-presentation')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private gridItems: Person[] = [];
@state()
private items: ContextMenuItem[] | undefined;
// tag::snippet[]
protected override async firstUpdated() {
const { people } = await getPeople({ count: 10 });
this.gridItems = people.slice(0, 5);
const itemsArray = this.createItemsArray(people.slice(5, 10));
this.items = [
{ component: this.createItem('vaadin:file-search', 'Open') },
{
component: this.createItem('vaadin:user-check', 'Assign'),
children: [
{ component: itemsArray[0] },
{ component: itemsArray[1] },
{ component: itemsArray[2] },
{ component: itemsArray[3] },
{ component: itemsArray[4] },
],
},
{ component: 'hr' },
{ component: this.createItem('vaadin:trash', 'Delete') },
];
}
// end::snippet[]
protected override render() {
return html`
<!-- tag::snippethtml[] -->
<vaadin-context-menu .items=${this.items}>
<vaadin-grid
all-rows-visible
.items=${this.gridItems}
@vaadin-contextmenu=${this.onContextMenu}
>
<vaadin-grid-column
header="Applicant"
${columnBodyRenderer<Person>(
(person) => html`<span>${person.firstName} ${person.lastName}</span>`,
[]
)}
></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column header="Phone number" path="address.phone"></vaadin-grid-column>
</vaadin-grid>
</vaadin-context-menu>
<!-- end::snippethtml[] -->
`;
}
createItemsArray(people: Person[]) {
return people.map((person, index) => {
const item = document.createElement('vaadin-context-menu-item');
if (index === 0) {
item.setAttribute('selected', '');
}
render(
html`
<vaadin-horizontal-layout
style="align-items: center; line-height: var(--lumo-line-height-m)"
theme="spacing"
>
<vaadin-avatar
.img=${person.pictureUrl}
.name=${`${person.firstName} ${person.lastName}`}
></vaadin-avatar>
<vaadin-vertical-layout>
<span> ${person.firstName} ${person.lastName} </span>
<span
style="color: var(--lumo-secondary-text-color); font-size: var(--lumo-font-size-s);"
>
${Math.floor(Math.random() * 20) + 1} applications
</span>
</vaadin-vertical-layout>
</vaadin-horizontal-layout>
`,
item
);
return item;
});
}
createItem(iconName: string, text: string) {
const item = document.createElement('vaadin-context-menu-item');
const icon = document.createElement('vaadin-icon');
icon.style.color = 'var(--lumo-secondary-text-color)';
icon.style.marginInlineEnd = 'var(--lumo-space-s)';
icon.style.padding = 'var(--lumo-space-xs)';
icon.setAttribute('icon', iconName);
item.appendChild(icon);
if (text) {
item.appendChild(document.createTextNode(text));
}
return item;
}
onContextMenu(e: MouseEvent) {
// Prevent opening context menu on header row.
const target = e.currentTarget as Grid<Person>;
if (target.getEventContext(e).section !== 'body') {
e.stopPropagation();
}
}
}