From fd2e1e363859d40eb75777c8c2b57ce6bc98571e Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Wed, 4 Sep 2024 12:25:33 +0300 Subject: [PATCH] docs: add Popover documentation and live examples (#3587) --- articles/components/popover/index.adoc | 565 ++++++++++++++++++ articles/components/popover/styling.adoc | 58 ++ .../popover/popover-anchored-dialog.ts | 125 ++++ .../demo/component/popover/popover-arrow.ts | 39 ++ .../popover/popover-dropdown-field.ts | 157 +++++ .../popover/popover-interactive-tooltip.ts | 52 ++ .../demo/component/popover/popover-modal.ts | 40 ++ .../popover/popover-notification-panel.ts | 127 ++++ .../component/popover/popover-positioning.ts | 72 +++ .../component/popover/popover-user-menu.ts | 98 +++ .../popover/react/popover-anchored-dialog.tsx | 102 ++++ .../component/popover/react/popover-arrow.tsx | 21 + .../popover/react/popover-dropdown-field.tsx | 124 ++++ .../react/popover-interactive-tooltip.tsx | 33 + .../component/popover/react/popover-modal.tsx | 19 + .../react/popover-notification-panel.tsx | 109 ++++ .../popover/react/popover-positioning.tsx | 56 ++ .../popover/react/popover-user-menu.tsx | 102 ++++ frontend/demo/init.ts | 1 + frontend/themes/docs/document.css | 1 + .../docs/popover-notification-panel.css | 7 + .../popover/PopoverAnchoredDialog.java | 117 ++++ .../demo/component/popover/PopoverArrow.java | 36 ++ .../popover/PopoverDropdownField.java | 125 ++++ .../popover/PopoverInteractiveTooltip.java | 54 ++ .../demo/component/popover/PopoverModal.java | 31 + .../popover/PopoverNotificationPanel.java | 115 ++++ .../component/popover/PopoverPositioning.java | 58 ++ .../component/popover/PopoverUserMenu.java | 102 ++++ 29 files changed, 2546 insertions(+) create mode 100644 articles/components/popover/index.adoc create mode 100644 articles/components/popover/styling.adoc create mode 100644 frontend/demo/component/popover/popover-anchored-dialog.ts create mode 100644 frontend/demo/component/popover/popover-arrow.ts create mode 100644 frontend/demo/component/popover/popover-dropdown-field.ts create mode 100644 frontend/demo/component/popover/popover-interactive-tooltip.ts create mode 100644 frontend/demo/component/popover/popover-modal.ts create mode 100644 frontend/demo/component/popover/popover-notification-panel.ts create mode 100644 frontend/demo/component/popover/popover-positioning.ts create mode 100644 frontend/demo/component/popover/popover-user-menu.ts create mode 100644 frontend/demo/component/popover/react/popover-anchored-dialog.tsx create mode 100644 frontend/demo/component/popover/react/popover-arrow.tsx create mode 100644 frontend/demo/component/popover/react/popover-dropdown-field.tsx create mode 100644 frontend/demo/component/popover/react/popover-interactive-tooltip.tsx create mode 100644 frontend/demo/component/popover/react/popover-modal.tsx create mode 100644 frontend/demo/component/popover/react/popover-notification-panel.tsx create mode 100644 frontend/demo/component/popover/react/popover-positioning.tsx create mode 100644 frontend/demo/component/popover/react/popover-user-menu.tsx create mode 100644 frontend/themes/docs/popover-notification-panel.css create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverAnchoredDialog.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverArrow.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverDropdownField.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverInteractiveTooltip.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverModal.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverPositioning.java create mode 100644 src/main/java/com/vaadin/demo/component/popover/PopoverUserMenu.java diff --git a/articles/components/popover/index.adoc b/articles/components/popover/index.adoc new file mode 100644 index 0000000000..0812c9b6fd --- /dev/null +++ b/articles/components/popover/index.adoc @@ -0,0 +1,565 @@ +--- +tab-title: Usage +layout: tabbed-page +title: Popover +description: Popover is a generic overlay whose position is anchored to an element in the UI. +page-links: + - 'API: https://cdn.vaadin.com/vaadin-web-components/{moduleNpmVersion:@vaadin/popover}/#/elements/vaadin-popover[TypeScript] / https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/component/popover/Popover.html[Java]' + - 'Source: https://github.com/vaadin/web-components/tree/v{moduleNpmVersion:@vaadin/popover}/packages/popover[TypeScript] / https://github.com/vaadin/flow-components/tree/{moduleMavenVersion:com.vaadin:vaadin}/vaadin-popover-flow-parent[Java]' +--- + += [since:com.vaadin:vaadin@V24.5]#Popover# + +A generic overlay whose position is anchored to an element in the UI. + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-notification-panel.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-notification-panel.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +Popovers support focusable, interactive content, and can be used to build virtually any type of anchored overlays from custom drop-down fields and drop-down buttons to and interactive tooltips. + +The popover’s position is anchored to an element in the UI, called the *target element*. + +Popovers differ from <<../dialog#,Dialogs>> in that they are visually anchored to a target element, and they differ from <<../tooltip#,Tooltips>> in that they can be focused and support rich, interactive content. + +== Opening and Closing + +Popovers can be configured to open and close based on different pointer and keyboard based triggers. See <> for examples. + +=== Opening Triggers + +Three target element triggers can be configured to open the popover: + +- *Click*: clicking the target element, or pressing kbd:[Space] when the target element has focus (*default*) +- *Hover*: hovering over the target element +- *Focus*: focusing the target element + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setOpenOnClick(false); +popover.setOpenOnHover(true); +popover.setOpenOnFocus(true); +---- + +[source,typescript] +---- + + +---- + +[source,tsx] +---- + + +---- + +-- + +All three triggers can be enabled simultaneously. If no opening trigger is enabled, the popover can only be opened programmatically. + +The hover and focus opening triggers have a configurable opening delay. + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setHoverDelay(500); +popover.setFocusDelay(500); +---- + +[source,html] +---- + + +---- + +[source,tsx] +---- + + +---- + +-- + +=== Closing Triggers + +The following triggers close the popover by default: + +- *Target click*: Clicking the target element (non-modal popovers only) +- *Outside click*: clicking anywhere outside the overlay (can be disabled) +- *Esc*: pressing kbd:[Esc] (can be disabled) + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setCloseOnEsc(false); +popover.setCloseOnOutsideClick(false); +---- + +[source,html] +---- + + +---- + +[source,tsx] +---- + + +---- + +-- + +Additionally: + +- When opened on hover, the popover closes on mouseout, i.e. when the pointer leaves the target element and the overlay. +- When opened on focus, the popover closes on blur, i.e. when focus is no longer on the target element or in the overlay. + +The mouseout closing trigger has a configurable delay. + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setHideDelay(500); +---- + +[source,html] +---- + + +---- + +[source,tsx] +---- + + +---- + +-- + + +=== Auto Focus + +Keyboard focus can be automatically moved to the Popover when it opens. This is recommended for popovers with interactive content that the user is expected to interact with. Modal popovers have auto-focus behavior by default. Popovers opened with hover or focus should not use auto-focus. + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setAutofocus(true); +---- + +[source,html] +---- + + +---- + +[source,tsx] +---- + + +---- + +-- + + +== Positioning + +By default, popovers open below their target element, horizontally centered to its midpoint, but the positioning options allow this to be changed to any edge or corner, depending on what is most appropriate for the use case. + +The popover’s position is automatically maintained if the target element scrolls within the viewport. If there is insufficient space in the viewport for the desired positioning, the popover automatically shifts to fit within the viewport. + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-positioning.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverPositioning.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-positioning.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +=== Target Gap + +The distance between the popover and the target element can be customized by setting the following CSS properties on the Popover component: + +- `--vaadin-popover-offset-top` +- `--vaadin-popover-offset-bottom` +- `--vaadin-popover-offset-start` +- `--vaadin-popover-offset-end` + +=== Target Arrow + +Popovers can render a wedge-shaped arrow tip pointing at the target element. + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-arrow.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverArrow.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-arrow.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +== Configuring Delays + +The delay before popover opens can be configured separately for hover and focus. There is no delay +before the popover appears on click or when opening programmatically. + +The delay before popover closes -- when the pointer leaves the target element -- can also be configured separately. On blur, though, the popover is closed immediately to avoid confusion when focusing another element. + +[.example.show-code] +-- +ifdef::flow[] +[source,java] +---- +// Global delay configuration: +Popover.setDefaultFocusDelay(2000); +Popover.setDefaultHoverDelay(1000); +Popover.setDefaultHideDelay(1000); + +// Overriding delays for a particular popover: +Popover popover = new Popover(); +popover.setHoverDelay(0); +---- +endif::[] + +ifdef::lit[] +[source,typescript] +---- +import { Popover } from '@vaadin/popover'; + +// Global delay configuration: +Popover.setDefaultFocusDelay(2000); +Popover.setDefaultHoverDelay(1000); +Popover.setDefaultHideDelay(1000); + +// Overriding delays for a particular popover: + +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +import { Popover } from '@vaadin/popover'; + +// Global delay configuration: +Popover.setDefaultFocusDelay(2000); +Popover.setDefaultHoverDelay(1000); +Popover.setDefaultHideDelay(1000); + +// Overriding delays for a particular popover: + +---- +endif::[] + +-- + + +== Dimensions + +By default, the Popover’s size is determined by its contents, but an explicit width and height can be set on the Popover itself. + +Contents that exceed the width of the popover will scroll. + +The maximum width of popovers is limited to the width of the viewport, minus a small margin that can be customized with CSS by overriding the `inset` property of the `vaadin-popover-overlay` element. + +== Modality + +A modal Popover blocks the user from interacting with the rest of the user interface while open, automatically moves focus from the target element to the Popover, and traps keyboard focus within it. + +When combined with an outside click closing trigger, modality prevents accidentally triggering other UI elements when clicking outside the Popover to close it. + +By default, modal popovers do not render a modality curtain (or _backdrop_), but one can be enabled separately. A modality curtain can be useful for de-emphasizing the UI in the background and to give a visual indication that the rest of the UI is blocked from user interaction. + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-modal.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverModal.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-modal.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + + +== Accessibility + +By default, the Popover overlay has the ARIA role `dialog`. This can be changed to another role to provide appropriate semantics for the type of content and interaction in the popover (see <> for examples). + +- `menu`: when the content is a list of actions or links +- `listbox`: when the content is a list form which you can select one or more items +- `grid`: when the content is a tabular structure from which you select an item +- `tree`: when the content is a hierarchical list + +Remember that, unlike <<../tooltip#,Tooltip>>, the contents of a Popover are _not_ automatically announced by screen readers when it opens. Consider using a live region to announce non-interactive popovers, and ensure keyboard access to interactive popovers. + +The target element is automatically applied `aria-controls` (with a reference to the Popover overlay), `aria-haspopup` (with the overlay’s role as the value), and `aria-expanded` (set to `true` when open, and to `false` otherwise). + +An accessible name can be provided for the overlay using the ARIA label API: + +[.example.show-code] +-- + +[source,java] +---- + +Popover popover = new Popover(); +popover.setAriaLabel("Label"); +// OR +popover.setAriaLabelledby("label-element-id"); +---- + +[source,html] +---- + + + + +---- + +[source,tsx] +---- + + + +---- + +-- + +== Typical Use Cases + +Here are a few examples of common use cases for the Popover component with recommended configurations. + +=== Drop-Down Field + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-dropdown-field.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverDropdownField.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-dropdown-field.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +- Opens on click, focus +- Closes on kbd:[Esc], blur, outside click, target click, and programmatically upon selection +- Modal, no modality curtain (drop-down fields typically don’t have curtains) +- Auto-focused +- ARIA role `dialog` +- ARIA label _“Select a date range”_ + +=== User Menu + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-user-menu.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverUserMenu.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-user-menu.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +- Opens on click +- Closes on kbd:[Esc], outside click, target click +- Modal, no modality curtain (small popovers like this usually don’t need them) +- ARIA role `menu` (the overlay’s content is a menu; although there are non-list elements, they are not interactive, nor do they need to be announced by screen readers) +- ARIA label _“User menu”_ + +Note: if the popover only needs to contain menu items, consider using a <<../menu-bar#,Menu Bar>> or <<../context-menu#,Context Menu>> instead. + +=== Notification Panel + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-notification-panel.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-notification-panel.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +- Opens on click +- Closes on kbd:[Esc], outside click, target click +- Modal, no modality curtain (small popovers like this usually don’t need them) +- ARIA role `dialog` (although mainly a list, there interactive non-list elements as well) +- ARIA labelled-by pointing to heading in overlay +- Arrow variant + +=== Rich, Interactive Tooltip + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-interactive-tooltip.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverInteractiveTooltip.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-interactive-tooltip.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + +- Opens on hover, focus +- Closes on kbd:[Esc], outside click (as well as mouseout and blur) +- Non-modal +- Not auto-focused +- ARIA role `dialog` (`tooltip` would be invalid due to interactive contents) +- ARIA labelled-by pointing to heading in overlay +- Positioned above target +- Arrow variant + +=== Anchored Modal Dialog + +[.example] +-- + +[source,html] +---- +include::{root}/frontend/demo/component/popover/popover-anchored-dialog.ts[render,tags=snippet,indent=0,group=Lit] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/popover/PopoverAnchoredDialog.java[render,tags=snippet,indent=0,group=Flow] +---- + +[source,tsx] +---- +include::{root}/frontend/demo/component/popover/react/popover-anchored-dialog.tsx[render,tags=snippet,indent=0,group=React] +---- +-- + + +- Opens on click +- Closes on kbd:[Esc], outside click +- Modal, with curtain +- Auto-focused +- ARIA role `dialog` +- ARIA labelled-by pointing to heading in the overlay + +Note: if the dialog doesn’t benefit from being anchored-positioned to another element, consider using a <<../dialog#,Dialog>> instead. + + +== Related Components + +[cols="1,2"] +|=== +|Component |Usage Recommendation + +|<<../dialog#,Dialog>> +|Use instead of Popover if there is no need to visually associate it with another UI element. +Modal dialogs are a better option for transactional operations. +Modal dialogs provide full-stack modality (e.g. keyboard shortcuts defined in the UI behind are blocked while the Dialog is open) + + +|<<../tooltip#,Tooltip>> +|Use instead of Popover if the content is just text. + +|<<../context-menu#,Context Menu>> +|Use instead of Popover if the content is a list of actions or toggles. + +|<<../menu-bar#,Menu Bar>> +|Same as Context Menu, but with built-in trigger buttons. +|=== diff --git a/articles/components/popover/styling.adoc b/articles/components/popover/styling.adoc new file mode 100644 index 0000000000..3b2652edad --- /dev/null +++ b/articles/components/popover/styling.adoc @@ -0,0 +1,58 @@ +--- +title: Styling +description: Styling API reference for the Popover component. +order: 50 +--- += Styling + + +include::../_styling-section-theming-props.adoc[tag=style-properties] + +[cols="1,2,2"] +|=== +| Feature | Property | Default Value + +|Arrow size +|`--vaadin-popover-arrow-size` +|`0.5rem` + +|Top offset +|`--vaadin-popover-offset-top` +|`var(--lumo-space-xs)` + +|Bottom offset +|`--vaadin-popover-offset-bottom` +|`var(--lumo-space-xs)` + +|Start offset +|`--vaadin-popover-offset-start` +|`var(--lumo-space-xs)` + +|End offset +|`--vaadin-popover-offset-end` +|`var(--lumo-space-xs)` + +|=== + + +include::../_styling-section-intros.adoc[tag=selectors] + + +Root element:: `vaadin-popover-overlay` + + +=== States + +Non-modal:: `vaadin-popover-overlay++++++**[modeless]**` + + +=== Parts + +Modality curtain (backdrop):: `vaadin-popover-overlay++++++**::part(backdrop)**` +Popover surface:: `vaadin-popover-overlay++++++**::part(overlay)**` +Content wrapper:: `vaadin-popover-overlay++++++**::part(content)**` +Arrow element:: `vaadin-popover-overlay++++++**::part(arrow)**` + +=== Style Variants + +Arrow:: `vaadin-popover++++++**[theme~="arrow"]**` diff --git a/frontend/demo/component/popover/popover-anchored-dialog.ts b/frontend/demo/component/popover/popover-anchored-dialog.ts new file mode 100644 index 0000000000..6dabb1ad5c --- /dev/null +++ b/frontend/demo/component/popover/popover-anchored-dialog.ts @@ -0,0 +1,125 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/checkbox-group'; +import '@vaadin/grid'; +import '@vaadin/horizontal-layout'; +import '@vaadin/icon'; +import '@vaadin/icons'; +import '@vaadin/popover'; +import type { CheckboxChangeEvent } from '@vaadin/checkbox'; +import { popoverRenderer } from '@vaadin/popover/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'; + +const DEFAULT_COLUMNS = [ + { label: 'First name', key: 'firstName', visible: true }, + { label: 'Last name', key: 'lastName', visible: true }, + { label: 'Email', key: 'email', visible: true }, + { label: 'Phone', key: 'address.phone', visible: false }, + { label: 'Birthday', key: 'birthday', visible: false }, + { label: 'Profession', key: 'profession', visible: true }, +]; + +@customElement('popover-anchored-dialog') +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; + } + + // tag::snippet[] + @state() + private items: Person[] = []; + + @state() + private gridColumns = [...DEFAULT_COLUMNS]; + + protected override async firstUpdated() { + const { people } = await getPeople(); + this.items = people; + } + + protected override render() { + return html` + + + Employees + + + + + + + + + + ${this.gridColumns.map( + (column) => html` + + ` + )} + + `; + } + + // tag::snippet[] + popoverRenderer() { + const visibleColumns = this.gridColumns + .filter((column) => column.visible) + .map((column) => column.key); + + return html` +

Configure columns

+ + ${this.gridColumns.map( + (column) => html` + + ` + )} + + + Show all + Reset + + `; + } + // end::snippet[] + + onCheckboxChange(event: CheckboxChangeEvent) { + const idx = this.gridColumns.findIndex(({ key }) => key === event.target.value); + this.gridColumns = this.gridColumns.map((column, index) => ({ + ...column, + visible: idx === index ? event.target.checked : column.visible, + })); + } + + showAllColumns() { + this.gridColumns = this.gridColumns.map((column) => ({ ...column, visible: true })); + } + + resetColumns() { + this.gridColumns = this.gridColumns.map((column, idx) => ({ + ...column, + visible: DEFAULT_COLUMNS[idx].visible, + })); + } +} diff --git a/frontend/demo/component/popover/popover-arrow.ts b/frontend/demo/component/popover/popover-arrow.ts new file mode 100644 index 0000000000..881269b5a4 --- /dev/null +++ b/frontend/demo/component/popover/popover-arrow.ts @@ -0,0 +1,39 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/icon'; +import '@vaadin/popover'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('popover-arrow') +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; + } + + protected override render() { + return html` + + + + + + + `; + } + + popoverRenderer() { + return html`
No new notifications
`; + } +} diff --git a/frontend/demo/component/popover/popover-dropdown-field.ts b/frontend/demo/component/popover/popover-dropdown-field.ts new file mode 100644 index 0000000000..1340299c90 --- /dev/null +++ b/frontend/demo/component/popover/popover-dropdown-field.ts @@ -0,0 +1,157 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/date-picker'; +import '@vaadin/horizontal-layout'; +import '@vaadin/icon'; +import '@vaadin/popover'; +import '@vaadin/select'; +import '@vaadin/text-field'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; +import type { DatePickerChangeEvent } from '@vaadin/date-picker'; +import type { PopoverOpenedChangedEvent, PopoverTrigger } from '@vaadin/popover'; +import type { SelectChangeEvent } from '@vaadin/select'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { applyTheme } from 'Frontend/generated/theme'; +import { formatISO, subMonths, subWeeks, subYears } from 'date-fns'; + +@customElement('popover-dropdown-field') +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() + range = ''; + + @state() + from = ''; + + @state() + to = ''; + + @state() + opened = false; + + @state() + trigger: PopoverTrigger[] = ['click', 'focus']; + + @state() + presets = [ + { label: 'Today', value: 'today' }, + { label: 'Last week', value: 'last-week' }, + { label: 'Last month', value: 'last-month' }, + { label: 'Year to date', value: 'year-to-date' }, + { label: 'Last year', value: 'last-year' }, + { label: 'Past 5 years', value: 'past-5-years' }, + ]; + + protected override render() { + return html` + + + + + + + `; + } + + // tag::snippet[] + popoverRenderer() { + return html` + + + +
+ +
+ `; + } + // end::snippet[] + + onFromChange(event: DatePickerChangeEvent) { + this.range = ''; + this.from = event.target.value; + } + + onToChange(event: DatePickerChangeEvent) { + this.range = ''; + this.to = event.target.value; + } + + onOpenedChanged(event: PopoverOpenedChangedEvent) { + this.opened = event.detail.value; + } + + onRangeChange(event: SelectChangeEvent) { + this.range = event.target.value; + this.to = this.formatDate(new Date()); + + switch (event.target.value) { + case 'today': + this.from = this.formatDate(new Date()); + break; + case 'last-week': + this.from = this.formatDate(subWeeks(new Date(), 1)); + break; + case 'last-month': + this.from = this.formatDate(subMonths(new Date(), 1)); + break; + case 'year-to-date': + this.from = this.formatDate(new Date(new Date().getFullYear(), 0, 1)); + break; + case 'last-year': + this.from = this.formatDate(subYears(new Date(), 1)); + break; + case 'past-5-years': + this.from = this.formatDate(subYears(new Date(), 5)); + break; + default: + // Do nothing + } + + this.opened = false; + } + + formatDate(date: Date) { + return formatISO(date, { representation: 'date' }); + } +} diff --git a/frontend/demo/component/popover/popover-interactive-tooltip.ts b/frontend/demo/component/popover/popover-interactive-tooltip.ts new file mode 100644 index 0000000000..f1934efcf6 --- /dev/null +++ b/frontend/demo/component/popover/popover-interactive-tooltip.ts @@ -0,0 +1,52 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/integer-field'; +import '@vaadin/popover'; +import type { PopoverTrigger } from '@vaadin/popover'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('popover-interactive-tooltip') +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() + trigger: PopoverTrigger[] = ['hover', 'focus']; + + protected override render() { + return html` + + + + + `; + } + + cvvRenderer() { + return html` +

Card Verification Value

+
+ A three or four digit code, usually printed on the back of the card, next to, or at the end + of, the signature strip. +
+ + See where to find CVV on different cards + + `; + } +} diff --git a/frontend/demo/component/popover/popover-modal.ts b/frontend/demo/component/popover/popover-modal.ts new file mode 100644 index 0000000000..dc5b5cb4c8 --- /dev/null +++ b/frontend/demo/component/popover/popover-modal.ts @@ -0,0 +1,40 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/popover'; +import '@vaadin/text-field'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('popover-modal') +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; + } + + protected override render() { + return html` + Discount + + + + `; + } + + popoverRenderer() { + return html` + + Apply + `; + } +} diff --git a/frontend/demo/component/popover/popover-notification-panel.ts b/frontend/demo/component/popover/popover-notification-panel.ts new file mode 100644 index 0000000000..1edaaec6b9 --- /dev/null +++ b/frontend/demo/component/popover/popover-notification-panel.ts @@ -0,0 +1,127 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/horizontal-layout'; +import '@vaadin/icon'; +import '@vaadin/message-list'; +import '@vaadin/popover'; +import '@vaadin/tabsheet'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; +import type { MessageListItem } from '@vaadin/message-list'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { format, subMinutes } from 'date-fns'; +import { applyTheme } from 'Frontend/generated/theme'; +import { getPeople } from 'Frontend/demo/domain/DataService'; + +@customElement('popover-notifications-panel') +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() + allNotifications: MessageListItem[] = []; + + @state() + unreadNotifications: MessageListItem[] = []; + + protected override async firstUpdated() { + const { people } = await getPeople({ count: 4 }); + + this.unreadNotifications = [ + { + userName: `${people[0].firstName} ${people[0].lastName}`, + userImg: people[0].pictureUrl, + time: format(subMinutes(new Date(), 20), 'HH:mm'), + text: 'Could you send me the latest TPS report from the ACME project?', + }, + { + userName: `${people[1].firstName} ${people[1].lastName}`, + userImg: people[1].pictureUrl, + time: format(subMinutes(new Date(), 30), 'HH:mm'), + text: 'Hey, are we on track for the trade show next month?', + }, + { + userName: `${people[2].firstName} ${people[2].lastName}`, + userImg: people[2].pictureUrl, + time: format(subMinutes(new Date(), 35), 'HH:mm'), + text: `TPS reports look good! I'm going to pass it on to Alliyah next.`, + }, + ]; + this.allNotifications = [ + ...this.unreadNotifications, + { + userName: `${people[3].firstName} ${people[3].lastName}`, + userImg: people[3].pictureUrl, + time: format(subMinutes(new Date(), 55), 'HH:mm'), + text: 'Hi, are you going to attend the brainstorming session tomorrow?', + }, + ]; + } + + protected override render() { + return html` + + + + + + + `; + } + + notificationsRenderer() { + return html` + +

Notifications

+ + Mark all read + +
+ + + Unread + All + +
+ ${this.unreadNotifications.length + ? html` + + ` + : 'No unread notifications'} +
+
+ +
+
+ `; + } + + markAllRead() { + this.unreadNotifications = []; + } +} diff --git a/frontend/demo/component/popover/popover-positioning.ts b/frontend/demo/component/popover/popover-positioning.ts new file mode 100644 index 0000000000..760579b5c0 --- /dev/null +++ b/frontend/demo/component/popover/popover-positioning.ts @@ -0,0 +1,72 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/button'; +import '@vaadin/popover'; +import '@vaadin/select'; +import '@vaadin/vertical-layout'; +import type { PopoverPosition } from '@vaadin/popover'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import type { SelectChangeEvent } from '@vaadin/select'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('popover-positioning') +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() + items = [ + { label: 'Top Start', value: 'top-start' }, + { label: 'Top', value: 'top' }, + { label: 'Top End', value: 'top-end' }, + { label: 'Bottom Start', value: 'bottom-start' }, + { label: 'Bottom', value: 'bottom' }, + { label: 'Bottom End', value: 'bottom-end' }, + { label: 'Start Top', value: 'start-top' }, + { label: 'Start', value: 'start' }, + { label: 'Start Bottom', value: 'start-bottom' }, + { label: 'End Top', value: 'end-top' }, + { label: 'End', value: 'end' }, + { label: 'End Bottom', value: 'end-bottom' }, + ]; + + @state() + position: PopoverPosition = 'bottom'; + + protected override render() { + return html` + + + Open + + + + + + + `; + } + + popoverRenderer() { + return html`Popover content`; + } + + onPositionChange(event: SelectChangeEvent) { + this.position = event.target.value as PopoverPosition; + } +} diff --git a/frontend/demo/component/popover/popover-user-menu.ts b/frontend/demo/component/popover/popover-user-menu.ts new file mode 100644 index 0000000000..de52582b21 --- /dev/null +++ b/frontend/demo/component/popover/popover-user-menu.ts @@ -0,0 +1,98 @@ +import 'Frontend/demo/init'; // hidden-source-line + +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import '@vaadin/avatar'; +import '@vaadin/button'; +import '@vaadin/horizontal-layout'; +import '@vaadin/popover'; +import '@vaadin/vertical-layout'; +import { popoverRenderer } from '@vaadin/popover/lit.js'; +import { applyTheme } from 'Frontend/generated/theme'; +import { getPeople } from 'Frontend/demo/domain/DataService'; +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; + +@customElement('popover-user-menu') +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 person: Person | undefined; + + protected override async firstUpdated() { + const { people } = await getPeople({ count: 1 }); + this.person = people[0]; + } + + protected override render() { + return html` + + + + + + + + + `; + } + + // NOTE + // We are using inline styles here to keep the example simple. + // We recommend placing CSS in a separate style sheet and + // encapsulating the styling in a new component. + userMenuRenderer() { + const { firstName, lastName, pictureUrl } = this.person ?? {}; + const nickName = `@${firstName}${lastName}`.toLowerCase(); + + return html` + + + +
${firstName} ${lastName}
+
${nickName}
+
+
+ + + User profile + + + Preferences + + + Sign out + + + `; + } +} diff --git a/frontend/demo/component/popover/react/popover-anchored-dialog.tsx b/frontend/demo/component/popover/react/popover-anchored-dialog.tsx new file mode 100644 index 0000000000..4f232946f0 --- /dev/null +++ b/frontend/demo/component/popover/react/popover-anchored-dialog.tsx @@ -0,0 +1,102 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useEffect } from 'react'; +import { useComputed, useSignal } from '@vaadin/hilla-react-signals'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; +import { getPeople } from 'Frontend/demo/domain/DataService'; +import { + Button, + Checkbox, + CheckboxGroup, + Grid, + GridColumn, + HorizontalLayout, + Icon, + Popover, +} from '@vaadin/react-components'; +import '@vaadin/icons'; + +type ColumnConfig = { label: string; key: string; visible: boolean }; + +const DEFAULT_COLUMNS: ColumnConfig[] = [ + { label: 'First name', key: 'firstName', visible: true }, + { label: 'Last name', key: 'lastName', visible: true }, + { label: 'Email', key: 'email', visible: true }, + { label: 'Phone', key: 'address.phone', visible: false }, + { label: 'Birthday', key: 'birthday', visible: false }, + { label: 'Profession', key: 'profession', visible: true }, +]; + +function Example() { + useSignals(); // hidden-source-line + const items = useSignal([]); + const columns = useSignal([...DEFAULT_COLUMNS]); + const visibleColumns = useComputed(() => + columns.value.filter((column) => column.visible).map((column) => column.key) + ); + + useEffect(() => { + getPeople().then(({ people }) => { + items.value = people; + }); + }, []); + + return ( + <> + + Employees + + + + {/* tag::snippet[] */} + +

Configure columns

+ + {columns.value.map((item) => ( + { + const idx = columns.value.findIndex(({ key }) => key === event.target.value); + columns.value = columns.value.map((column, index) => ({ + ...column, + visible: idx === index ? event.target.checked : column.visible, + })); + }} + > + ))} + + + + + +
+ {/* end::snippet[] */} + + + {columns.value.map((item) => ( + + + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/popover/react/popover-arrow.tsx b/frontend/demo/component/popover/react/popover-arrow.tsx new file mode 100644 index 0000000000..1ac46e01cd --- /dev/null +++ b/frontend/demo/component/popover/react/popover-arrow.tsx @@ -0,0 +1,21 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; +import { Button, Icon, Popover } from '@vaadin/react-components'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; + +function Example() { + // tag::snippet[] + return ( + <> + + +
No new notifications
+
+ + ); + // end::snippet[] +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/popover/react/popover-dropdown-field.tsx b/frontend/demo/component/popover/react/popover-dropdown-field.tsx new file mode 100644 index 0000000000..8e99cc0324 --- /dev/null +++ b/frontend/demo/component/popover/react/popover-dropdown-field.tsx @@ -0,0 +1,124 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; +import { useSignal } from '@vaadin/hilla-react-signals'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { + DatePicker, + HorizontalLayout, + Icon, + Popover, + Select, + type SelectChangeEvent, + TextField, +} from '@vaadin/react-components'; +import { formatISO, subMonths, subWeeks, subYears } from 'date-fns'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; + +function Example() { + useSignals(); // hidden-source-line + const opened = useSignal(false); + const range = useSignal(''); + const from = useSignal(''); + const to = useSignal(''); + + const formatDate = (date: Date) => formatISO(date, { representation: 'date' }); + + const presets = [ + { label: 'Today', value: 'today' }, + { label: 'Last week', value: 'last-week' }, + { label: 'Last month', value: 'last-month' }, + { label: 'Year to date', value: 'year-to-date' }, + { label: 'Last year', value: 'last-year' }, + { label: 'Past 5 years', value: 'past-5-years' }, + ]; + + const onPresetChange = (event: SelectChangeEvent) => { + range.value = event.target.value; + to.value = formatDate(new Date()); + + switch (event.target.value) { + case 'today': + from.value = formatDate(new Date()); + break; + case 'last-week': + from.value = formatDate(subWeeks(new Date(), 1)); + break; + case 'last-month': + from.value = formatDate(subMonths(new Date(), 1)); + break; + case 'year-to-date': + from.value = formatDate(new Date(new Date().getFullYear(), 0, 1)); + break; + case 'last-year': + from.value = formatDate(subYears(new Date(), 1)); + break; + case 'past-5-years': + from.value = formatDate(subYears(new Date(), 5)); + break; + default: + // Do nothing + } + + opened.value = false; + }; + + return ( + <> + + + + {/* tag::snippet[] */} + { + opened.value = e.detail.value; + }} + > + { + position.value = e.target.value as PopoverPosition; + }} + /> + + + {/* tag::snippet[] */} + + Popover content + + {/* end::snippet[] */} + + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/popover/react/popover-user-menu.tsx b/frontend/demo/component/popover/react/popover-user-menu.tsx new file mode 100644 index 0000000000..95b885b4ff --- /dev/null +++ b/frontend/demo/component/popover/react/popover-user-menu.tsx @@ -0,0 +1,102 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useEffect } from 'react'; +import { useSignal } from '@vaadin/hilla-react-signals'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { + Avatar, + Button, + HorizontalLayout, + Popover, + VerticalLayout, +} from '@vaadin/react-components'; +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 person = useSignal(undefined); + + useEffect(() => { + getPeople({ count: 1 }).then(({ people }) => { + person.value = people[0]; + }); + }, []); + + const { firstName, lastName, pictureUrl } = person.value ?? {}; + const nickName = `@${firstName}${lastName}`.toLowerCase(); + + return ( + <> + + + + {/* tag::snippet[] */} + + + + +
+ {firstName} {lastName} +
+
{nickName}
+
+
+ + + User profile + + + Preferences + + + Sign out + + +
+ {/* end::snippet[] */} + + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/init.ts b/frontend/demo/init.ts index 24797f57ab..b5cb359689 100644 --- a/frontend/demo/init.ts +++ b/frontend/demo/init.ts @@ -4,6 +4,7 @@ import '@vaadin/polymer-legacy-adapter/style-modules.js'; import './init-flow-namespace'; import './init-flow-components'; import '../generated/vaadin-featureflags'; +import '../generated/theme-docs.global.generated.js'; import client from 'Frontend/generated/connect-client.default'; import { applyTheme } from 'Frontend/generated/theme'; diff --git a/frontend/themes/docs/document.css b/frontend/themes/docs/document.css index 65d8b3f452..c8ea284c4c 100644 --- a/frontend/themes/docs/document.css +++ b/frontend/themes/docs/document.css @@ -1,2 +1,3 @@ @import './combo-box-item-class-name.css'; @import './multi-select-combo-box-item-class-name.css'; +@import './popover-notification-panel.css'; diff --git a/frontend/themes/docs/popover-notification-panel.css b/frontend/themes/docs/popover-notification-panel.css new file mode 100644 index 0000000000..e5f7ca1ddc --- /dev/null +++ b/frontend/themes/docs/popover-notification-panel.css @@ -0,0 +1,7 @@ +vaadin-message-list.notifications vaadin-message { + padding: var(--lumo-space-xs) var(--lumo-space-s); +} + +vaadin-message-list.notifications vaadin-message::part(message) { + font-size: 14px; +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverAnchoredDialog.java b/src/main/java/com/vaadin/demo/component/popover/PopoverAnchoredDialog.java new file mode 100644 index 0000000000..1a9afa18b3 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverAnchoredDialog.java @@ -0,0 +1,117 @@ +package com.vaadin.demo.component.popover; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +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.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.checkbox.CheckboxGroupVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.html.Span; +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.popover.Popover; +import com.vaadin.flow.component.popover.PopoverPosition; +import com.vaadin.flow.data.renderer.LocalDateRenderer; +import com.vaadin.flow.router.Route; + +@Route("popover-anchored-dialog") +public class PopoverAnchoredDialog extends Div { + + public PopoverAnchoredDialog() { + Grid grid = new Grid<>(Person.class, false); + grid.addColumn(Person::getFirstName).setKey("firstName") + .setHeader("First name"); + grid.addColumn(Person::getLastName).setKey("lastName") + .setHeader("Last name"); + grid.addColumn(Person::getEmail).setKey("email").setHeader("Email"); + grid.addColumn(person -> person.getAddress().getPhone()).setKey("phone") + .setHeader("Phone"); + grid.addColumn(new LocalDateRenderer<>( + PopoverAnchoredDialog::getPersonBirthday, "yyyy-MM-dd")) + .setKey("birthday").setHeader("Birthday"); + grid.addColumn(Person::getProfession).setKey("profession") + .setHeader("Profession"); + + grid.setItems(DataService.getPeople()); + + Span title = new Span("Employees"); + title.getStyle().set("font-weight", "bold"); + + Button button = new Button(VaadinIcon.GRID_H.create()); + button.addThemeVariants(ButtonVariant.LUMO_ICON); + button.setAriaLabel("Show / hide columns"); + + HorizontalLayout headerLayout = new HorizontalLayout(title, button); + headerLayout.setAlignItems(FlexComponent.Alignment.BASELINE); + headerLayout.setFlexGrow(1, title); + + // tag::snippet[] + Popover popover = new Popover(); + popover.setModal(true); + popover.setBackdropVisible(true); + popover.setPosition(PopoverPosition.BOTTOM_END); + popover.setTarget(button); + + H4 heading = new H4("Configure columns"); + heading.getStyle().set("margin", "0"); + + List columns = List.of("firstName", "lastName", "email", + "phone", "birthday", "profession"); + + CheckboxGroup group = new CheckboxGroup<>(); + group.addThemeVariants(CheckboxGroupVariant.LUMO_VERTICAL); + group.setItems(columns); + group.setItemLabelGenerator((item) -> { + String label = StringUtils + .join(StringUtils.splitByCharacterTypeCamelCase(item), " "); + return StringUtils.capitalize(label.toLowerCase()); + }); + group.addValueChangeListener((e) -> { + columns.stream().forEach((key) -> { + grid.getColumnByKey(key).setVisible(e.getValue().contains(key)); + }); + }); + + Set defaultColumns = Set.of("firstName", "lastName", "email", + "profession"); + group.setValue(defaultColumns); + + Button showAll = new Button("Show all", (e) -> { + group.setValue(new HashSet(columns)); + }); + + Button reset = new Button("Reset", (e) -> { + group.setValue(defaultColumns); + }); + + HorizontalLayout footer = new HorizontalLayout(showAll, reset); + footer.setSpacing(false); + footer.getThemeList().add("spacing-xs"); + + popover.add(heading, group, footer); + // end::snippet[] + + add(headerLayout, grid, popover); + } + + private static LocalDate getPersonBirthday(Person person) { + return person.getBirthday().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate(); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverArrow.java b/src/main/java/com/vaadin/demo/component/popover/PopoverArrow.java new file mode 100644 index 0000000000..78b654a711 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverArrow.java @@ -0,0 +1,36 @@ +package com.vaadin.demo.component.popover; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.popover.PopoverVariant; +import com.vaadin.flow.theme.lumo.LumoIcon; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line + +@Route("popover-arrow") +public class PopoverArrow extends Div { + + public PopoverArrow() { + Icon icon = LumoIcon.BELL.create(); + Button button = new Button(icon); + button.addThemeVariants(ButtonVariant.LUMO_ICON); + button.setAriaLabel("Notifications"); + + // tag::snippet[] + Popover popover = new Popover(); + popover.addThemeVariants(PopoverVariant.ARROW); + // end::snippet[] + + popover.setTarget(button); + Div content = new Div("No new notifications"); + popover.add(content); + + add(button, popover); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverDropdownField.java b/src/main/java/com/vaadin/demo/component/popover/PopoverDropdownField.java new file mode 100644 index 0000000000..850e1aace2 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverDropdownField.java @@ -0,0 +1,125 @@ +package com.vaadin.demo.component.popover; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; + +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.theme.lumo.LumoIcon; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line + +@Route("popover-dropdown-field") +public class PopoverDropdownField extends Div { + + private LocalDate from; + private LocalDate to; + + private Select rangeSelector; + private DatePicker fromPicker; + private DatePicker toPicker; + private Popover popover; + + public PopoverDropdownField() { + // tag::snippet[] + TextField field = new TextField("Search date range"); + field.setWidth("340px"); + Icon icon = LumoIcon.DROPDOWN.create(); + field.setSuffixComponent(icon); + + popover = new Popover(); + popover.setModal(true); + popover.setWidth("325px"); + popover.setAriaLabel("Select a date range"); + popover.setOpenOnFocus(true); + popover.setFocusDelay(0); + popover.setTarget(field); + // end::snippet[] + + rangeSelector = new Select<>(); + rangeSelector.setLabel("Common ranges"); + rangeSelector.setPlaceholder("Select preset"); + rangeSelector.setWidth("100%"); + rangeSelector.setItems("Today", "Last week", "Last month", + "Year to date", "Last year", "Past 5 years"); + rangeSelector.addValueChangeListener((e) -> { + updateRange(e.getValue()); + }); + + fromPicker = new DatePicker("From"); + fromPicker.setWidth("150px"); + fromPicker.addValueChangeListener((e) -> { + from = e.getValue(); + updateValue(field); + if (e.isFromClient()) { + rangeSelector.setValue(""); + } + }); + + toPicker = new DatePicker("To"); + toPicker.setWidth("150px"); + toPicker.addValueChangeListener((e) -> { + to = e.getValue(); + updateValue(field); + if (e.isFromClient()) { + rangeSelector.setValue(""); + } + }); + + HorizontalLayout layout = new HorizontalLayout(fromPicker, new Div("−"), + toPicker); + layout.setSpacing(false); + layout.getThemeList().add("spacing-s"); + layout.setAlignItems(FlexComponent.Alignment.BASELINE); + + popover.add(rangeSelector, layout); + + add(field, popover); + } + + private void updateValue(TextField field) { + field.setValue(from != null && to != null + ? from.toString() + " - " + to.toString() + : ""); + } + + private void updateRange(String value) { + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + to = today; + toPicker.setValue(to); + + switch (value) { + case "Today": + from = today; + break; + case "Last week": + from = today.minusWeeks(1); + break; + case "Last month": + from = today.minusMonths(1); + break; + case "Year to date": + from = today.with(TemporalAdjusters.firstDayOfYear()); + break; + case "Last year": + from = today.minusYears(1); + break; + case "Past 5 years": + from = today.minusYears(5); + break; + } + + fromPicker.setValue(from); + popover.setOpened(false); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverInteractiveTooltip.java b/src/main/java/com/vaadin/demo/component/popover/PopoverInteractiveTooltip.java new file mode 100644 index 0000000000..ae3d3fca11 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverInteractiveTooltip.java @@ -0,0 +1,54 @@ +package com.vaadin.demo.component.popover; + +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.AnchorTarget; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.popover.PopoverPosition; +import com.vaadin.flow.component.popover.PopoverVariant; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line + +@Route("popover-interactive-tooltip") +public class PopoverInteractiveTooltip extends Div { + + public PopoverInteractiveTooltip() { + // tag::snippet[] + IntegerField cvv = new IntegerField("CVV"); + cvv.setWidth("60px"); + + Popover popover = new Popover(); + popover.addThemeVariants(PopoverVariant.ARROW); + popover.setPosition(PopoverPosition.TOP); + popover.setOpenOnClick(false); + popover.setOpenOnHover(true); + popover.setOpenOnFocus(true); + + H3 header = new H3("Card Verification Value"); + header.setId("cvv-heading"); + header.getStyle().set("margin", "0"); + header.getStyle().set("font-size", "1rem"); + + Div content = new Div( + "A three or four digit code, usually printed on the back of the card, " + + "next to, or at the end of, the signature strip."); + content.getStyle().set("max-width", "300px"); + + Anchor link = new Anchor("https://www.cvvnumber.com/cvv.html", + "See where to find CVV on different cards", AnchorTarget.BLANK); + + popover.add(header, content, link); + popover.setAriaLabelledBy("cvv-heading"); + popover.setTarget(cvv); + + add(cvv, popover); + // end::snippet[] + } + + public static class Exporter extends // hidden-source-line + DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverModal.java b/src/main/java/com/vaadin/demo/component/popover/PopoverModal.java new file mode 100644 index 0000000000..881f2e16ca --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverModal.java @@ -0,0 +1,31 @@ +package com.vaadin.demo.component.popover; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line + +@Route("popover-modal") +public class PopoverModal extends Div { + + public PopoverModal() { + // tag::snippet[] + Popover popover = new Popover(); + popover.setModal(true); + popover.setBackdropVisible(true); + // end::snippet[] + TextField code = new TextField("Discount code"); + Button apply = new Button("Apply"); + popover.add(code, apply); + + Button button = new Button("Discount"); + popover.setTarget(button); + + add(button, popover); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java b/src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java new file mode 100644 index 0000000000..8d6f3908ca --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverNotificationPanel.java @@ -0,0 +1,115 @@ +package com.vaadin.demo.component.popover; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +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.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.messages.MessageList; +import com.vaadin.flow.component.messages.MessageListItem; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.popover.PopoverPosition; +import com.vaadin.flow.component.popover.PopoverVariant; +import com.vaadin.flow.component.tabs.TabSheet; +import com.vaadin.flow.component.tabs.TabSheetVariant; +import com.vaadin.flow.theme.lumo.LumoIcon; +import com.vaadin.flow.router.Route; + +@Route("popover-notification-panel") +public class PopoverNotificationPanel extends Div { + + public PopoverNotificationPanel() { + Button button = new Button(LumoIcon.BELL.create()); + button.addThemeVariants(ButtonVariant.LUMO_ICON); + button.setAriaLabel("Notifications"); + + // tag::snippet[] + Popover popover = new Popover(); + popover.setTarget(button); + popover.setWidth("300px"); + popover.addThemeVariants(PopoverVariant.ARROW, + PopoverVariant.LUMO_NO_PADDING); + popover.setPosition(PopoverPosition.BOTTOM); + popover.setAriaLabelledBy("notifications-heading"); + // end::snippet[] + + List people = DataService.getPeople(5); + + MessageList unreadList = new MessageList(); + unreadList.addClassName("notifications"); + + MessageList allList = new MessageList(); + allList.addClassName("notifications"); + + MessageListItem message1 = new MessageListItem( + "Could you send me the latest TPS report from the ACME project?", + minutesAgo(20), people.get(0).getFullName(), + people.get(0).getPictureUrl()); + + MessageListItem message2 = new MessageListItem( + "Hey, are we on track for the trade show next month?", + minutesAgo(30), people.get(1).getFullName(), + people.get(1).getPictureUrl()); + + MessageListItem message3 = new MessageListItem( + "TPS reports look good! I'm going to pass it on to Alliyah next.", + minutesAgo(35), people.get(2).getFullName(), + people.get(2).getPictureUrl()); + + MessageListItem message4 = new MessageListItem( + "Hi, are you going to attend the brainstorming session tomorrow?", + minutesAgo(55), people.get(3).getFullName(), + people.get(3).getPictureUrl()); + + unreadList.setItems(Arrays.asList(message1, message2, message3)); + allList.setItems(Arrays.asList(message1, message2, message3, message4)); + + TabSheet tabSheet = new TabSheet(); + tabSheet.addThemeVariants(TabSheetVariant.LUMO_NO_PADDING); + + Div unreadContent = new Div(); + unreadContent.add(unreadList); + + tabSheet.add("Unread", unreadContent); + tabSheet.add("All", new Div(allList)); + + H4 heading = new H4("Notifications"); + heading.setId("notifications-heading"); + heading.getStyle().set("margin", "0"); + + Button markRead = new Button("Mark all read", (e) -> { + unreadContent.remove(unreadList); + unreadContent.add(new Div("No new notifications")); + }); + markRead.getStyle().set("margin-inline-start", "auto"); + + HorizontalLayout layout = new HorizontalLayout(heading, markRead); + layout.setSpacing(false); + layout.setAlignItems(FlexComponent.Alignment.BASELINE); + layout.getStyle().set("padding", + "var(--lumo-space-s) var(--lumo-space-s) 0"); + + popover.add(layout, tabSheet); + + add(button, popover); + } + + private Instant minutesAgo(int minutes) { + return LocalDateTime.now(ZoneOffset.UTC).minusMinutes(minutes) + .toInstant(ZoneOffset.UTC); + } + + public static class Exporter + extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverPositioning.java b/src/main/java/com/vaadin/demo/component/popover/PopoverPositioning.java new file mode 100644 index 0000000000..3d5b4d7236 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverPositioning.java @@ -0,0 +1,58 @@ +package com.vaadin.demo.component.popover; + +import java.util.Arrays; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.component.popover.PopoverPosition; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line + +@Route("popover-positioning") +public class PopoverPositioning extends VerticalLayout { + + public PopoverPositioning() { + setPadding(true); + setSpacing(true); + setAlignItems(Alignment.CENTER); + + Button button = new Button("Open"); + button.getStyle().set("--vaadin-button-height", "3rem"); + button.getStyle().set("margin-top", "2rem"); + + Select select = new Select<>(); + select.setLabel("Position"); + select.setItems(Arrays.asList(PopoverPosition.values())); + select.setValue(PopoverPosition.BOTTOM); + + select.setItemLabelGenerator((item) -> { + String label = ""; + String[] parts = item.toString().split("_"); + for (String p : parts) { + label += p.substring(0, 1) + p.substring(1).toLowerCase() + " "; + } + return label.trim(); + }); + + Popover popover = new Popover(); + popover.setTarget(button); + Div content = new Div("Popover content"); + popover.add(content); + + // tag::snippet[] + select.addValueChangeListener(event -> { + PopoverPosition position = event.getValue(); + popover.setPosition(position); + }); + // end::snippet[] + + add(button, select, popover); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/popover/PopoverUserMenu.java b/src/main/java/com/vaadin/demo/component/popover/PopoverUserMenu.java new file mode 100644 index 0000000000..64ea19de8e --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/popover/PopoverUserMenu.java @@ -0,0 +1,102 @@ +package com.vaadin.demo.component.popover; + +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.avatar.Avatar; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.popover.Popover; +import com.vaadin.flow.router.Route; + +@Route("popover-user-menu") +public class PopoverUserMenu extends HorizontalLayout { + + private Person person = DataService.getPeople(1).get(0); + + public PopoverUserMenu() { + setSpacing(false); + getStyle().set("background", "var(--lumo-contrast-5pct)"); + + // tag::snippet[] + String name = person.getFirstName() + " " + person.getLastName(); + String pictureUrl = person.getPictureUrl(); + + Avatar avatar = new Avatar(name); + avatar.setImage(pictureUrl); + avatar.getStyle().set("display", "block"); + avatar.getElement().setAttribute("tabindex", "-1"); + + Button button = new Button(avatar); + button.addThemeVariants(ButtonVariant.LUMO_ICON, + ButtonVariant.LUMO_TERTIARY_INLINE); + button.getStyle().set("margin", "var(--lumo-space-s)"); + button.getStyle().set("margin-inline-start", "auto"); + button.getStyle().set("border-radius", "50%"); + + Popover popover = new Popover(); + popover.setModal(true); + popover.setOverlayRole("menu"); + popover.setAriaLabel("User menu"); + popover.setTarget(button); + // end::snippet[] + + HorizontalLayout userInfo = new HorizontalLayout(); + userInfo.getStyle().set("background", "var(--lumo-contrast-5pct)"); + userInfo.getStyle().set("padding", "var(--lumo-space-s)"); + userInfo.getStyle().set("margin", "calc(var(--lumo-space-s) * -1)"); + + Avatar userAvatar = new Avatar(name); + userAvatar.setImage(pictureUrl); + userAvatar.getStyle().set("align-self", "center"); + userAvatar.getElement().setAttribute("tabindex", "-1"); + + VerticalLayout nameLayout = new VerticalLayout(); + nameLayout.setSpacing(false); + nameLayout.setPadding(false); + nameLayout.getStyle().set("margin-top", "var(--lumo-space-s)"); + + Div fullName = new Div(name); + fullName.getStyle().set("font-weight", "bold"); + fullName.getStyle().set("line-height", "1"); + Div nickName = new Div("@" + person.getFirstName().toLowerCase() + + person.getLastName().toLowerCase()); + nameLayout.add(fullName, nickName); + + userInfo.add(userAvatar, nameLayout); + + VerticalLayout linksLayout = new VerticalLayout(); + linksLayout.setSpacing(false); + linksLayout.setPadding(false); + linksLayout.getStyle().set("margin-top", "var(--lumo-space-s)"); + linksLayout.getStyle().set("align-items", "stretch"); + + Anchor profile = new Anchor("#", "User profile"); + profile.getElement().setAttribute("role", "menuitem"); + profile.getStyle().set("padding", "var(--lumo-space-xs)"); + profile.getStyle().set("text-decoration", "none"); + + Anchor preferences = new Anchor("#", "Preferences"); + preferences.getElement().setAttribute("role", "menuitem"); + preferences.getStyle().set("padding", "var(--lumo-space-xs)"); + preferences.getStyle().set("text-decoration", "none"); + + Anchor signOut = new Anchor("#", "Sign out"); + signOut.getElement().setAttribute("role", "menuitem"); + signOut.getStyle().set("padding", "var(--lumo-space-xs)"); + signOut.getStyle().set("text-decoration", "none"); + + linksLayout.add(profile, preferences, signOut); + + popover.add(userInfo, linksLayout); + + add(button, popover); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +}