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

org.patternfly.component.popover.Popover Maven / Gradle / Ivy

There is a newer version: 0.2.11
Show newest version
/*
 *  Copyright 2023 Red Hat
 *
 *  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
 *
 *      https://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 org.patternfly.component.popover;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;

import org.jboss.elemento.Attachable;
import org.jboss.elemento.By;
import org.jboss.elemento.Elements;
import org.jboss.elemento.Id;
import org.jboss.elemento.logger.Logger;
import org.patternfly.component.BaseComponent;
import org.patternfly.component.Closeable;
import org.patternfly.component.ComponentType;
import org.patternfly.component.Severity;
import org.patternfly.component.WithIcon;
import org.patternfly.component.button.Button;
import org.patternfly.core.Aria;
import org.patternfly.handler.CloseHandler;
import org.patternfly.popper.Modifiers;
import org.patternfly.popper.Placement;
import org.patternfly.popper.Popper;
import org.patternfly.popper.PopperBuilder;
import org.patternfly.popper.TriggerAction;
import org.patternfly.style.Modifiers.NoPadding;

import elemental2.dom.Element;
import elemental2.dom.Event;
import elemental2.dom.HTMLDivElement;
import elemental2.dom.HTMLElement;
import elemental2.dom.MutationRecord;
import elemental2.dom.Node;

import static elemental2.dom.DomGlobal.document;
import static java.util.Arrays.asList;
import static org.jboss.elemento.Elements.div;
import static org.jboss.elemento.Elements.failSafeRemoveFromParent;
import static org.jboss.elemento.Elements.insertBefore;
import static org.jboss.elemento.Elements.insertFirst;
import static org.jboss.elemento.Elements.removeChildrenFrom;
import static org.jboss.elemento.Elements.span;
import static org.jboss.elemento.EventType.click;
import static org.patternfly.component.button.Button.button;
import static org.patternfly.component.popover.PopoverBody.popoverBody;
import static org.patternfly.component.popover.PopoverFooter.popoverFooter;
import static org.patternfly.component.popover.PopoverHeader.popoverHeader;
import static org.patternfly.core.Aria.describedBy;
import static org.patternfly.core.Aria.label;
import static org.patternfly.core.Aria.labelledBy;
import static org.patternfly.core.Aria.modal;
import static org.patternfly.core.Attributes.role;
import static org.patternfly.core.Roles.dialog;
import static org.patternfly.handler.CloseHandler.fireEvent;
import static org.patternfly.handler.CloseHandler.shouldClose;
import static org.patternfly.icon.IconSets.fas.times;
import static org.patternfly.popper.Placement.auto;
import static org.patternfly.popper.Placement.top;
import static org.patternfly.style.Classes.arrow;
import static org.patternfly.style.Classes.close;
import static org.patternfly.style.Classes.component;
import static org.patternfly.style.Classes.content;
import static org.patternfly.style.Classes.icon;
import static org.patternfly.style.Classes.modifier;
import static org.patternfly.style.Classes.popover;
import static org.patternfly.style.Classes.screenReader;
import static org.patternfly.style.Classes.title;
import static org.patternfly.style.Classes.widthAuto;

/**
 * A popover is in-app messaging that provides more information on specific product areas. Popovers display content in a new
 * window that overlays the current page. Unlike modals, popovers don't block the current page.
 *
 * @see https://www.patternfly.org/components/popover
 */
public class Popover extends BaseComponent implements
        Closeable,
        NoPadding,
        WithIcon,
        Attachable {

    // ------------------------------------------------------ factory

    public static Popover popover() {
        return new Popover(null);
    }

    public static Popover popover(By trigger) {
        return new Popover(() -> Elements.find(document.body, trigger));
    }

    public static Popover popover(HTMLElement trigger) {
        return new Popover(() -> trigger);
    }

    public static Popover popover(Supplier trigger) {
        return new Popover(trigger);
    }

    // ------------------------------------------------------ instance

    private static final Logger logger = Logger.getLogger(Popover.class.getName());

    public static final int ANIMATION_DURATION = 300;
    public static final int ENTRY_DELAY = 300;
    public static final int EXIT_DELAY = 300;
    public static final int DISTANCE = 25;
    public static final int Z_INDEX = 9999;

    private final HTMLElement contentElement;
    private final Set triggerActions;
    private final List> closeHandler;
    private Supplier trigger;
    private boolean flip;
    private boolean showClose;
    private int distance;
    private int animationDuration;
    private int zIndex;
    private Popper popper;
    private Placement placement;
    private Button closeButton;
    private Severity severity;
    private PopoverHeader header;
    private HTMLElement screenReaderElement;
    private HTMLElement iconContainer;

    Popover(Supplier trigger) {
        super(ComponentType.Popover, div().css(component(popover))
                .style("display", "none")
                .attr(role, dialog)
                .aria(modal, true)
                .element());

        this.trigger = trigger;
        this.triggerActions = EnumSet.of(TriggerAction.click);
        this.closeHandler = new ArrayList<>();
        this.flip = true;
        this.showClose = true;
        this.placement = top;
        this.zIndex = Z_INDEX;
        this.distance = DISTANCE;
        this.animationDuration = ANIMATION_DURATION;

        String bodyId = Id.unique(componentType().id, "body");
        add(div().css(component(popover, arrow)));
        add(contentElement = div().css(component(popover, content))
                .element());
        aria(describedBy, bodyId);

        Attachable.register(this, this);
    }

    @Override
    public void attach(MutationRecord mutationRecord) {
        if (showClose) {
            closable();
        } else {
            failSafeRemoveFromParent(closeButton);
        }

        if (trigger != null) {
            HTMLElement triggerElement = trigger.get();
            if (triggerElement != null) {
                popper = new PopperBuilder(componentType().componentName, triggerElement, element())
                        .animationDuration(animationDuration)
                        .zIndex(zIndex)
                        .placement(placement)
                        .addModifier(Modifiers.offset(distance),
                                Modifiers.noOverflow(),
                                Modifiers.hide(),
                                Modifiers.flip(placement == auto || flip),
                                Modifiers.placement(),
                                Modifiers.eventListeners(false))
                        .registerHandler(triggerActions, this::show, this::close)
                        .removePopperOnTriggerDetach()
                        .build();
            } else {
                logger.error("Unable to find trigger element for popover %o", element());
            }
        } else {
            logger.error("No trigger element defined for popover %o", element());
        }
    }

    @Override
    public void detach(MutationRecord mutationRecord) {
        popper.cleanup();
    }

    // ------------------------------------------------------ add

    public Popover addHeader(String header) {
        return add(popoverHeader().textContent(header));
    }

    public Popover addHeader(PopoverHeader header) {
        return add(header);
    }

    // override to append to the right container!
    public Popover add(PopoverHeader header) {
        this.header = header;
        contentElement.appendChild(header.element());
        aria(labelledBy, header.headerId);
        return this;
    }

    public Popover addBody(String body) {
        return add(popoverBody().textContent(body));
    }

    public Popover addBody(PopoverBody body) {
        return add(body);
    }

    // override to append to the right container!
    public Popover add(PopoverBody body) {
        contentElement.appendChild(body.element());
        return this;
    }

    public Popover addFooter(String footer) {
        return add(popoverFooter().textContent(footer));
    }

    public Popover addFooter(PopoverFooter footer) {
        return add(footer);
    }

    // override to append to the right container!
    public Popover add(PopoverFooter footer) {
        contentElement.appendChild(footer.element());
        return this;
    }

    // ------------------------------------------------------ builder

    public Popover appendToBody() {
        return appendTo(document.body);
    }

    public Popover appendTo(Node node) {
        if (node != null) {
            node.appendChild(element());
        }
        return this;
    }

    public Popover animationDuration(int animationDuration) {
        this.animationDuration = animationDuration;
        return this;
    }

    public Popover autoWidth() {
        return css(modifier(widthAuto));
    }

    public Popover closable() {
        return closable(null);
    }

    public Popover closable(CloseHandler closeHandler) {
        if (closeButton == null) {
            insertFirst(contentElement, div().css(component(popover, close))
                    .add(closeButton = button()
                            .plain()
                            .icon(times())
                            .aria(label, "Close")
                            .on(click, event -> close(event, true)))
                    .element());
        }
        return onClose(closeHandler);
    }

    public Popover distance(int distance) {
        this.distance = distance;
        return this;
    }

    public Popover flip(boolean flip) {
        this.flip = flip;
        return this;
    }

    @Override
    public Popover icon(Element icon) {
        removeIcon();
        failSafeIconContainer().appendChild(icon);
        return this;
    }

    @Override
    public Popover removeIcon() {
        failSafeRemoveFromParent(iconContainer);
        return this;
    }

    public Popover noClose() {
        this.showClose = false;
        return this;
    }

    public Popover placement(Placement placement) {
        if (placement == auto) {
            flip = true;
        }
        this.placement = placement;
        return this;
    }

    public Popover severity(Severity severity) {
        return severity(severity, severity.aria);
    }

    public Popover severity(Severity severity, String screenReaderText) {
        if (this.severity != null) {
            element().classList.remove(this.severity.status.modifier());
        }
        this.severity = severity;
        css(severity.status.modifier());
        removeChildrenFrom(failSafeIconContainer());
        icon((severity.icon.get().element()));
        failSafeScreenReaderElement().textContent = screenReaderText;
        return this;
    }

    public Popover trigger(String trigger) {
        return trigger(() -> Elements.find(document.body, By.selector(trigger)));
    }

    public Popover trigger(By trigger) {
        return trigger(() -> Elements.find(document.body, trigger));
    }

    public Popover trigger(HTMLElement trigger) {
        return trigger(() -> trigger);
    }

    public Popover trigger(Supplier trigger) {
        this.trigger = trigger;
        return this;
    }

    public Popover triggerActions(TriggerAction... triggerActions) {
        if (triggerActions != null) {
            this.triggerActions.clear();
            this.triggerActions.addAll(asList(triggerActions));
        }
        return this;
    }

    public Popover zIndex(int zIndex) {
        this.zIndex = zIndex;
        return this;
    }

    @Override
    public Popover that() {
        return this;
    }

    // ------------------------------------------------------ aria

    /**
     * Accessible label for the popover, required when header is not present.
     */
    public Popover ariaLabel(String label) {
        return aria(Aria.label, label);
    }

    /**
     * Accessible label for the close button.
     */
    public Popover ariaCloseLabel(String label) {
        if (closeButton != null) {
            closeButton.aria(Aria.label, label);
        }
        return this;
    }

    // ------------------------------------------------------ events

    @Override
    public Popover onClose(CloseHandler closeHandler) {
        if (closeButton == null) {
            this.closeHandler.add(closeHandler);
        }
        return this;
    }

    // ------------------------------------------------------ api

    public void show() {
        show(new Event(""));
    }

    public void show(Event event) {
        popper.show(null);
    }

    @Override
    public void close(Event event, boolean fireEvent) {
        if (shouldClose(this, closeHandler, event, fireEvent)) {
            popper.hide(() -> fireEvent(this, closeHandler, event, fireEvent));
        }
    }

    // ------------------------------------------------------ internal

    private HTMLElement failSafeHeaderElement() {
        if (header == null) {
            add(popoverHeader());
        }
        return header.element();
    }

    private HTMLElement failSafeIconContainer() {
        if (iconContainer == null) {
            insertBefore(iconContainer = span().css(component(popover, title, icon)).element(), failSafeHeaderElement());
        }
        return iconContainer;
    }

    private HTMLElement failSafeScreenReaderElement() {
        if (screenReaderElement == null) {
            insertFirst(failSafeHeaderElement(), screenReaderElement = span().css(screenReader).element());
        }
        return screenReaderElement;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy