diff --git a/articles/components/context-menu/index.adoc b/articles/components/context-menu/index.adoc
index 3d8410ed6a..a883a665a5 100644
--- a/articles/components/context-menu/index.adoc
+++ b/articles/components/context-menu/index.adoc
@@ -153,6 +153,43 @@ endif::[]
--
+[role="since:com.vaadin:vaadin@V25.2"]
+== Tooltips
+
+Menu items can be configured with tooltips to provide additional information.
+
+[.example,themes="lumo,aura"]
+--
+
+ifdef::lit[]
+[source,typescript]
+----
+include::{root}/frontend/demo/component/contextmenu/context-menu-tooltip.ts[render,tag=snippet,indent=0,group=Lit]
+
+...
+
+include::{root}/frontend/demo/component/contextmenu/context-menu-tooltip.ts[render,tag=snippethtml,indent=0,group=Lit]
+----
+endif::[]
+
+ifdef::flow[]
+[source,java]
+----
+include::{root}/src/main/java/com/vaadin/demo/component/contextmenu/ContextMenuTooltip.java[render,tags=snippet,indent=0,group=Flow]
+----
+endif::[]
+
+ifdef::react[]
+[source,tsx]
+----
+include::{root}/frontend/demo/component/contextmenu/react/context-menu-tooltip.tsx[render,tags=snippet,indent=0,group=React]
+----
+endif::[]
+--
+
+See the <<../tooltip#,Tooltips documentation page>> for details on tooltip configuration.
+
+
== Custom Items
You can customize the items to include more than a single line of text.
diff --git a/articles/components/menu-bar/index.adoc b/articles/components/menu-bar/index.adoc
index 410ab68556..45d44bade5 100644
--- a/articles/components/menu-bar/index.adoc
+++ b/articles/components/menu-bar/index.adoc
@@ -306,7 +306,7 @@ endif::[]
== Tooltips
-Tooltips can be configured on top-level items to provide additional information, especially for icon-only items. When a top-level item is disabled, the corresponding tooltip isn't shown.
+Both top-level and [since:com.vaadin:vaadin@V25.2]#sub-menu items# can be configured with tooltips to provide additional context, such as labels for icon-only menu buttons.
[.example,themes="lumo,aura"]
--
diff --git a/frontend/demo/component/contextmenu/context-menu-tooltip.ts b/frontend/demo/component/contextmenu/context-menu-tooltip.ts
new file mode 100644
index 0000000000..da7c4c51a9
--- /dev/null
+++ b/frontend/demo/component/contextmenu/context-menu-tooltip.ts
@@ -0,0 +1,71 @@
+import 'Frontend/demo/init'; // hidden-source-line
+import '@vaadin/context-menu';
+import '@vaadin/grid';
+import '@vaadin/tooltip';
+import { html, LitElement } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import type { Grid } from '@vaadin/grid';
+import { getPeople } from 'Frontend/demo/domain/DataService';
+import { applyTheme } from 'Frontend/demo/theme';
+import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
+
+@customElement('context-menu-tooltip')
+export class Example extends LitElement {
+ protected override createRenderRoot() {
+ const root = super.createRenderRoot();
+ applyTheme(root);
+ return root;
+ }
+
+ // tag::snippet[]
+ @state()
+ private items = [
+ { text: 'Edit', tooltip: 'Edit selected person' },
+ {
+ text: 'Share',
+ children: [
+ { text: 'Copy link', tooltip: 'Copy a shareable link to the clipboard' },
+ {
+ text: 'Email',
+ tooltip: 'Send the contact details by email',
+ tooltipPosition: 'end',
+ },
+ ],
+ },
+ ];
+ // end::snippet[]
+
+ @state()
+ private gridItems: Person[] = [];
+
+ protected override async firstUpdated() {
+ this.gridItems = (await getPeople({ count: 5 })).people;
+ }
+
+ protected override render() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ onContextMenu(e: MouseEvent) {
+ // Prevent opening context menu on header row.
+ const target = e.currentTarget as Grid;
+ if (target.getEventContext(e).section !== 'body') {
+ e.stopPropagation();
+ }
+ }
+}
diff --git a/frontend/demo/component/contextmenu/react/context-menu-tooltip.tsx b/frontend/demo/component/contextmenu/react/context-menu-tooltip.tsx
new file mode 100644
index 0000000000..5b8492e970
--- /dev/null
+++ b/frontend/demo/component/contextmenu/react/context-menu-tooltip.tsx
@@ -0,0 +1,64 @@
+import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
+import React, { useEffect, useRef } from 'react';
+import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line
+import { useSignal } from '@vaadin/hilla-react-signals';
+import { ContextMenu, Tooltip } from '@vaadin/react-components';
+import { Grid, type GridElement } from '@vaadin/react-components/Grid.js';
+import { GridColumn } from '@vaadin/react-components/GridColumn.js';
+import { getPeople } from 'Frontend/demo/domain/DataService';
+import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
+
+function Example() {
+ useSignals(); // hidden-source-line
+ const gridItems = useSignal([]);
+ const gridRef = useRef(null);
+
+ useEffect(() => {
+ getPeople({ count: 5 }).then(({ people }) => {
+ gridItems.value = people;
+ });
+ }, []);
+
+ 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[]
+ const items = [
+ { text: 'Edit', tooltip: 'Edit selected person' },
+ {
+ text: 'Share',
+ children: [
+ { text: 'Copy link', tooltip: 'Copy a shareable link to the clipboard' },
+ {
+ text: 'Email',
+ tooltip: 'Send the contact details by email',
+ tooltipPosition: 'end',
+ },
+ ],
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ // end::snippet[]
+}
+
+export default reactExample(Example); // hidden-source-line
diff --git a/frontend/demo/component/menubar/menu-bar-tooltip.ts b/frontend/demo/component/menubar/menu-bar-tooltip.ts
index 1f6b11ed31..adf462dda9 100644
--- a/frontend/demo/component/menubar/menu-bar-tooltip.ts
+++ b/frontend/demo/component/menubar/menu-bar-tooltip.ts
@@ -29,14 +29,18 @@ export class Example extends LitElement {
{
component: this.createItem('folder'),
tooltip: 'Move',
+ children: [
+ { text: 'To folder…', tooltip: 'Choose a destination folder' },
+ { text: 'To archive', tooltip: 'Move to archive', tooltipPosition: 'end' },
+ ],
},
{
component: this.createItem('copy'),
tooltip: 'Duplicate',
},
{
- component: this.createItem('archive'),
- tooltip: 'Archive',
+ component: this.createItem('trash'),
+ tooltip: 'Delete',
disabled: true,
},
];
diff --git a/frontend/demo/component/menubar/react/menu-bar-tooltip.tsx b/frontend/demo/component/menubar/react/menu-bar-tooltip.tsx
index 3a9e18523b..8bd36e606e 100644
--- a/frontend/demo/component/menubar/react/menu-bar-tooltip.tsx
+++ b/frontend/demo/component/menubar/react/menu-bar-tooltip.tsx
@@ -1,9 +1,7 @@
import '@vaadin/icons';
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
import React from 'react';
-import { Icon } from '@vaadin/react-components/Icon.js';
-import { MenuBar, type MenuBarItem } from '@vaadin/react-components/MenuBar.js';
-import { Tooltip } from '@vaadin/react-components/Tooltip.js';
+import { Icon, MenuBar, type MenuBarItem, Tooltip } from '@vaadin/react-components';
function createItem(iconName: string) {
return ;
@@ -14,9 +12,16 @@ function Example() {
const items: MenuBarItem[] = [
{ component: createItem('eye'), tooltip: 'View' },
{ component: createItem('pencil'), tooltip: 'Edit' },
- { component: createItem('folder'), tooltip: 'Move' },
+ {
+ component: createItem('folder'),
+ tooltip: 'Move',
+ children: [
+ { text: 'To folder…', tooltip: 'Choose a destination folder' },
+ { text: 'To archive', tooltip: 'Move to archive', tooltipPosition: 'end' },
+ ],
+ },
{ component: createItem('copy'), tooltip: 'Duplicate' },
- { component: createItem('archive'), tooltip: 'Archive', disabled: true },
+ { component: createItem('trash'), tooltip: 'Delete', disabled: true },
];
return (
diff --git a/src/main/java/com/vaadin/demo/component/contextmenu/ContextMenuTooltip.java b/src/main/java/com/vaadin/demo/component/contextmenu/ContextMenuTooltip.java
new file mode 100644
index 0000000000..9e337584b0
--- /dev/null
+++ b/src/main/java/com/vaadin/demo/component/contextmenu/ContextMenuTooltip.java
@@ -0,0 +1,49 @@
+package com.vaadin.demo.component.contextmenu;
+
+import com.vaadin.demo.DemoExporter; // hidden-source-line
+import com.vaadin.demo.domain.DataService;
+import com.vaadin.demo.domain.Person;
+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.shared.Tooltip.TooltipPosition;
+import com.vaadin.flow.router.Route;
+
+import java.util.List;
+
+@Route("context-menu-tooltip")
+public class ContextMenuTooltip extends Div {
+
+ private List people = DataService.getPeople(5);
+
+ public ContextMenuTooltip() {
+ Grid grid = new Grid();
+ grid.setAllRowsVisible(true);
+ grid.setItems(people);
+
+ grid.addColumn(Person::getFirstName).setHeader("First name");
+ grid.addColumn(Person::getLastName).setHeader("Last name");
+ grid.addColumn(Person::getEmail).setHeader("Email");
+
+ // tag::snippet[]
+ GridContextMenu menu = grid.addContextMenu();
+ GridMenuItem edit = menu.addItem("Edit");
+ edit.setTooltipText("Edit selected person");
+
+ GridMenuItem share = menu.addItem("Share");
+ GridSubMenu shareSubMenu = share.getSubMenu();
+ shareSubMenu.addItem("Copy link")
+ .setTooltipText("Copy a shareable link to the clipboard");
+ GridMenuItem email = shareSubMenu.addItem("Email");
+ email.setTooltipText("Send the contact details by email");
+ email.setTooltipPosition(TooltipPosition.END);
+ // end::snippet[]
+
+ add(grid);
+ }
+
+ public static class Exporter extends DemoExporter { // hidden-source-line
+ } // hidden-source-line
+}
diff --git a/src/main/java/com/vaadin/demo/component/menubar/MenuBarTooltip.java b/src/main/java/com/vaadin/demo/component/menubar/MenuBarTooltip.java
index 9948991be9..a53ab3cc5d 100644
--- a/src/main/java/com/vaadin/demo/component/menubar/MenuBarTooltip.java
+++ b/src/main/java/com/vaadin/demo/component/menubar/MenuBarTooltip.java
@@ -1,11 +1,13 @@
package com.vaadin.demo.component.menubar;
import com.vaadin.flow.component.contextmenu.MenuItem;
+import com.vaadin.flow.component.contextmenu.SubMenu;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
+import com.vaadin.flow.component.shared.Tooltip.TooltipPosition;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.DemoExporter; // hidden-source-line
@@ -18,11 +20,18 @@ public MenuBarTooltip() {
createIconItem(menuBar, VaadinIcon.EYE, "View");
createIconItem(menuBar, VaadinIcon.PENCIL, "Edit");
- createIconItem(menuBar, VaadinIcon.FOLDER, "Move");
+
+ MenuItem move = createIconItem(menuBar, VaadinIcon.FOLDER, "Move");
+ SubMenu moveSubMenu = move.getSubMenu();
+ moveSubMenu.addItem("To folder…")
+ .setTooltipText("Choose a destination folder");
+ MenuItem toArchive = moveSubMenu.addItem("To archive");
+ toArchive.setTooltipText("Move to archive");
+ toArchive.setTooltipPosition(TooltipPosition.END);
+
createIconItem(menuBar, VaadinIcon.COPY, "Duplicate");
- MenuItem archive = createIconItem(menuBar, VaadinIcon.ARCHIVE,
- "Archive");
- archive.setEnabled(false);
+ MenuItem delete = createIconItem(menuBar, VaadinIcon.TRASH, "Delete");
+ delete.setEnabled(false);
// end::snippet[]
add(menuBar);
}
@@ -31,7 +40,8 @@ public MenuBarTooltip() {
private MenuItem createIconItem(MenuBar menu, VaadinIcon iconName,
String tooltipText) {
Icon icon = new Icon(iconName);
- MenuItem item = menu.addItem(icon, tooltipText);
+ MenuItem item = menu.addItem(icon);
+ item.setTooltipText(tooltipText);
return item;
}
// end::createIcon[]