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[]