All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.notification.Notification Maven / Gradle / Ivy

/*
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.vaadin.flow.component.notification;

import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.HasComponents;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.component.shared.internal.OverlayClassListProxy;
import com.vaadin.flow.dom.ClassList;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementDetachEvent;
import com.vaadin.flow.dom.ElementDetachListener;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.internal.StateTree;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.shared.Registration;

/**
 * Notifications are used to provide feedback to the user. They communicate
 * information about activities, processes, and events in the application.
 *
 * @author Vaadin Ltd
 */
@Tag("vaadin-notification")
@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.6.0")
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
@NpmPackage(value = "@vaadin/notification", version = "24.6.0")
@JsModule("@vaadin/notification/src/vaadin-notification.js")
@JsModule("./flow-component-renderer.js")
public class Notification extends Component implements HasComponents, HasStyle,
        HasThemeVariant {

    private static final int DEFAULT_DURATION = 5000;
    private static final Position DEFAULT_POSITION = Position.BOTTOM_START;
    private static final String OPENED_PROPERTY = "opened";
    private static final String OPENED_CHANGED_EVENT = "opened-changed";

    private boolean autoAddedToTheUi = false;

    private Registration afterProgrammaticNavigationListenerRegistration;

    /**
     * Enumeration of all available positions for notification component
     */
    public enum Position {
        TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH;

        private final String clientName;

        Position() {
            this.clientName = name().toLowerCase(Locale.ENGLISH).replace('_',
                    '-');
        }

        /**
         * Gets name that is used in the client side representation of the
         * component.
         *
         * @return the name used in the client side representation of the
         *         component.
         */
        public String getClientName() {
            return clientName;
        }

        /**
         * Creates {@link Position} from the client side representation property
         * name
         *
         * @param clientName
         *            the client side representation of the property
         * @return corresponding {@link Position}
         */
        static Position fromClientName(String clientName) {
            return clientName == null ? null
                    : Position.valueOf(clientName.replace('-', '_')
                            .toUpperCase(Locale.ENGLISH));
        }
    }

    /**
     * Assigns a renderer function to the notification.
     *
     * If the Web Component has {@code text} property defined, it will be used
     * as the text content of the notification.
     *
     * Otherwise, the child nodes of this.container will be included in the
     * notification.
     */
    private void configureRenderer() {
        String appId = UI.getCurrent() != null
                ? UI.getCurrent().getInternals().getAppId()
                : "ROOT";

        //@formatter:off
        getElement().executeJs(
            "this.renderer = (root) => {" +
            "  if (this.text) {" +
            "    root.textContent = this.text;" +
            "  } else {" +
            "    Vaadin.FlowComponentHost.setChildNodes($0, this.virtualChildNodeIds, root)" +
            "  }" +
            "}", appId);
        //@formatter:on
    }

    /**
     * Default constructor. Create an empty notification with component support
     * and non-auto-closing
     * 

* Note: To mix text and child components in notification that also supports * child components, use the {@link Text} component for the textual parts. */ public Notification() { initBaseElementsAndListeners(); setPosition(DEFAULT_POSITION); setDuration(0); } /** * Creates a Notification with the given String rendered as its HTML text, * that does not close automatically. * * @param text * the text of the Notification */ public Notification(String text) { this(text, 0, DEFAULT_POSITION); } /** * Creates a Notification with given String rendered as its HTML text and * given Integer rendered as its duration. *

* Set to {@code 0} or a negative number to disable the notification * auto-closing. * * @param text * the text of the Notification * @param duration * the duration in milliseconds to show the notification */ public Notification(String text, int duration) { this(text, duration, DEFAULT_POSITION); } /** * Creates a Notification with given text String, duration and position *

* Set to {@code 0} or a negative number to disable the notification * auto-closing. * * @param text * the text of the notification * @param duration * the duration in milliseconds to show the notification * @param position * the position of the notification. Valid enumerate values are * TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, * BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH */ public Notification(String text, int duration, Position position) { initBaseElementsAndListeners(); setText(text); setDuration(duration); setPosition(position); } /** * Creates a Notification with given text String, duration, position and * assertive state. *

* Set to {@code 0} or a negative number to disable the notification * auto-closing. * * @param text * the text of the notification * @param duration * the duration in milliseconds to show the notification * @param position * the position of the notification. Valid enumerate values are * TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, * BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH * @param assertive * whether the notification should have {@code aria-live} * attribute set to {@code assertive} or {@code polite} */ public Notification(String text, int duration, Position position, boolean assertive) { this(text, duration, position); setAssertive(assertive); } /** * Creates a notification with given components inside. *

* Note: To mix text and child components in a component that also supports * child components, use the {@link Text} component for the textual parts. * * @param components * the components inside the notification * @see #add(Component...) */ public Notification(Component... components) { this(); add(components); } private void initBaseElementsAndListeners() { getElement().addPropertyChangeListener(OPENED_PROPERTY, event -> fireEvent( new OpenedChangeEvent(this, event.isUserOriginated()))); getElement().addEventListener(OPENED_CHANGED_EVENT, event -> removeAutoAdded()); } /** * Removes the notification from its parent if it was added automatically. */ private void removeAutoAdded() { if (autoAddedToTheUi && !isOpened()) { autoAddedToTheUi = false; getElement().removeFromParent(); } } /** * Shows a notification in the current page with given text, duration, * position and assertive state. * * @param text * the text of the Notification * @param duration * the duration in milliseconds to show the notification * @param position * the position of the notification. Valid enumerate values are * TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, * BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH * @param assertive * whether the notification should have {@code aria-live} * attribute set to {@code assertive} or {@code polite} * @return the notification */ public static Notification show(String text, int duration, Position position, boolean assertive) { Notification notification = new Notification(text, duration, position, assertive); notification.open(); return notification; } /** * Shows a notification in the current page with given text, duration and * position. * * @param text * the text of the Notification * @param duration * the duration in milliseconds to show the notification * @param position * the position of the notification. Valid enumerate values are * TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, * BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH * @return the notification */ public static Notification show(String text, int duration, Position position) { return show(text, duration, position, false); } /** * Shows a notification in the current page with given text. *

* This is the convenience method for {@link #show(String, int, Position)} * which uses default web-component values for duration (which is 5000 ms) * and position ({@literal Position.BOTTOM_START}). * * * @param text * the text of the Notification * @return the notification */ public static Notification show(String text) { return show(text, DEFAULT_DURATION, DEFAULT_POSITION); } /** * Set the text of the notification with given String *

* NOTE: When mixing this method with {@link #Notification()} and * {@link #Notification(Component...)}. Method will remove all the * components from the notification. * * @param text * the text of the Notification */ public void setText(String text) { removeAll(); this.getElement().setProperty("text", text); this.getElement().callJsFunction("requestContentUpdate"); } /** * Set position of the notification. *

* * @param position * the position of the notification. Valid enumerate values are * {@code TOP_STRETCH, TOP_START, TOP_CENTER, TOP_END, MIDDLE, BOTTOM_START, BOTTOM_CENTER, BOTTOM_END, BOTTOM_STRETCH}, * not {@code null} */ public void setPosition(Position position) { String positionName = position.getClientName(); getElement().setProperty("position", positionName == null ? "" : positionName); } /** *

* Description copied from corresponding location in WebComponent: *

*

* Alignment of the notification in the viewport Valid values are * {@code top-stretch|top-start|top-center|top-end|middle|bottom-start|bottom-center|bottom-end|bottom-stretch} *

* This property is not synchronized automatically from the client side, so * the returned value may not be the same as in client side. *

*

* The default position value is {@literal Position.BOTTOM_START}. * * @return the {@link Position} property from the webcomponent */ public Position getPosition() { String position = getElement().getProperty("position"); return Optional.ofNullable(position).map(Position::fromClientName) .orElse(DEFAULT_POSITION); } /** * Opens the notification. */ public void open() { setOpened(true); } /** * Closes the notification. *

* Note: This method also removes the notification component from the DOM * after closing it, unless you have added the component manually. */ public void close() { setOpened(false); } /** * Adds the given components into this notification. *

* The elements in the DOM will not be children of the * {@code } element, but will be inserted into an * overlay that is attached into the {@code }. *

* NOTE: When mixing this method with {@link #Notification(String)}, * {@link #Notification(String, int)} and * {@link #Notification(String, int, Position)} method will remove the text * content. * * @param components * the components to add */ @Override public void add(Collection components) { HasComponents.super.add(components); configureComponentRenderer(); } /** * Adds the given component into this notification at the given index. *

* The element in the DOM will not be child of the * {@code } element, but will be inserted into an * overlay that is attached into the {@code }. *

* NOTE: When mixing this method with {@link #Notification(String)}, * {@link #Notification(String, int)} and * {@link #Notification(String, int, Position)} method will remove the text * content. * * @param index * the index, where the component will be added. * @param component * the component to add */ @Override public void addComponentAtIndex(int index, Component component) { HasComponents.super.addComponentAtIndex(index, component); configureComponentRenderer(); } /** * Opens or closes the notification. *

* Note: You don't need to add the component anywhere before opening it. * Since {@code }'s location in the DOM doesn't really * matter, opening a notification will automatically add it to the * {@code } if it's not yet attached anywhere. * * @param opened * {@code true} to open the notification, {@code false} to close * it */ public void setOpened(boolean opened) { UI ui = UI.getCurrent(); if (ui == null) { throw new IllegalStateException("UI instance is not available. " + "It means that you are calling this method " + "out of a normal workflow where it's always implicitly set. " + "That may happen if you call the method from the custom thread without " + "'UI::access' or from tests without proper initialization."); } StateTree.ExecutionRegistration addToUiRegistration = ui .beforeClientResponse(ui, context -> { if (isOpened() && getElement().getNode().getParent() == null) { ui.addToModalComponent(this); autoAddedToTheUi = true; } if (afterProgrammaticNavigationListenerRegistration != null) { afterProgrammaticNavigationListenerRegistration .remove(); } }); if (ui.getSession() != null) { afterProgrammaticNavigationListenerRegistration = ui .addAfterNavigationListener(event -> { if (event.getLocationChangeEvent() .getTrigger() == NavigationTrigger.PROGRAMMATIC) { addToUiRegistration.remove(); afterProgrammaticNavigationListenerRegistration .remove(); } }); } getElement().setProperty(OPENED_PROPERTY, opened); } /** * True if the notification is currently displayed. *

* This property is synchronized automatically from client side when an * {@code opened-changed} event happens. * * @return the {@code opened} property from the webcomponent */ @Synchronize(property = "opened", value = "opened-changed") public boolean isOpened() { return getElement().getProperty(OPENED_PROPERTY, false); } /** * {@code opened-changed} event is sent when the notification opened state * changes. */ public static class OpenedChangeEvent extends ComponentEvent { private final boolean opened; public OpenedChangeEvent(Notification source, boolean fromClient) { super(source, fromClient); this.opened = source.isOpened(); } public boolean isOpened() { return opened; } } /** * Adds a listener for {@code opened-changed} events fired by the * webcomponent. * * @param listener * the listener * @return a {@link Registration} for removing the event listener */ public Registration addOpenedChangeListener( ComponentEventListener listener) { return addListener(OpenedChangeEvent.class, listener); } /** * The duration in milliseconds to show the notification. Set to {@code 0} * or a negative number to disable the notification auto-closing. * * @param duration * the value to set */ public void setDuration(int duration) { getElement().setProperty("duration", duration); } /** *

* The duration in milliseconds to show the notification. Set to {@code 0} * or a negative number to disable the notification auto-closing. *

* This property is not synchronized automatically from the client side, so * the returned value may not be the same as in client side. *

* * @return the {@code duration} property from the webcomponent */ public int getDuration() { return getElement().getProperty("duration", 0); } /** * When true, the notification card has {@code aria-live} attribute set to * {@code assertive} instead of {@code polite}. This makes screen readers * announce the notification content immediately when it appears. * * @param assertive * the value to set */ public void setAssertive(boolean assertive) { getElement().setProperty("assertive", assertive); } /** * When true, the notification card has {@code aria-live} attribute set to * {@code assertive} instead of {@code polite}. This makes screen readers * announce the notification content immediately when it appears. * * @return the {@code assertive} property from the webcomponent */ public boolean isAssertive() { return getElement().getProperty("assertive", false); } /** * {@inheritDoc} *

* Note: To listen for opening the notification, you should use * {@link #addOpenedChangeListener(ComponentEventListener)}. */ @Override public Registration addAttachListener( ComponentEventListener listener) { return super.addAttachListener(listener); } /** * {@inheritDoc} *

* Note: To listen for closing the notification, you should use * {@link #addOpenedChangeListener(ComponentEventListener)}, as the * component is not necessarily removed from the DOM when closing. */ @Override public Registration addDetachListener( ComponentEventListener listener) { return super.addDetachListener(listener); } private void configureComponentRenderer() { this.getElement().removeProperty("text"); updateVirtualChildNodeIds(); } private Map childDetachListenerMap = new HashMap<>(); // Must not use lambda here as that would break serialization. See // https://github.com/vaadin/flow-components/issues/5597 private ElementDetachListener childDetachListener = new ElementDetachListener() { @Override public void onDetach(ElementDetachEvent e) { var child = e.getSource(); var childDetachedFromContainer = !getElement().getChildren() .anyMatch(containerChild -> Objects.equals(child, containerChild)); if (childDetachedFromContainer) { // The child was removed from the notification // Remove the registration for the child detach listener childDetachListenerMap.get(child).remove(); childDetachListenerMap.remove(child); configureComponentRenderer(); } } }; /** * Updates the virtualChildNodeIds property of the notification element. *

* This method is called whenever the notification's child components * change. *

* Also calls {@code requestContentUpdate} on the notification element to * trigger the content update. */ private void updateVirtualChildNodeIds() { // Add detach listeners (child may be removed with removeFromParent()) getElement().getChildren().forEach(child -> { if (!childDetachListenerMap.containsKey(child)) { childDetachListenerMap.put(child, child.addDetachListener(childDetachListener)); } }); this.getElement().setPropertyList("virtualChildNodeIds", getElement().getChildren() .map(element -> element.getNode().getId()) .collect(Collectors.toList())); this.getElement().callJsFunction("requestContentUpdate"); } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); getElement().executeJs( "Vaadin.FlowComponentHost.patchVirtualContainer(this)"); configureRenderer(); updateVirtualChildNodeIds(); } @Override protected void onDetach(DetachEvent detachEvent) { super.onDetach(detachEvent); // When reloading a page using preserve on refresh, the notification // should keep its opened state. To prevent it from auto-closing, delay // the auto-closing logic to before client response, which is not called // when reloading the page. This also prevents an exception when trying // to remove an auto-added notification from its parent. detachEvent.getUI().beforeClientResponse(this, executionContext -> { // Close the notification, and remove it from its parent if it was // auto-added. This ensures that the notification doesn't re-open // itself when its parent, for example a dialog, gets attached // again. setOpened(false); removeAutoAdded(); }); } /** * Sets the CSS class names of the notification overlay element. This method * overwrites any previous set class names. * * @param className * a space-separated string of class names to set, or * null to remove all class names */ @Override public void setClassName(String className) { getClassNames().clear(); if (className != null) { addClassNames(className.split(" ")); } } @Override public ClassList getClassNames() { return new OverlayClassListProxy(this); } /** * @throws UnsupportedOperationException * Notification does not support adding styles to card element */ @Override public Style getStyle() { throw new UnsupportedOperationException( "Notification does not support adding styles to card element"); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy