@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
341 lines (340 loc) • 16.8 kB
JavaScript
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified.
See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details.
v3.2.1 */
import { c as customElement } from "../../chunks/runtime.js";
import { keyed } from "lit-html/directives/keyed.js";
import { html } from "lit";
import { createRef, ref } from "lit-html/directives/ref.js";
import { LitElement, createEvent, safeClassMap, setAttribute } from "@arcgis/lumina";
import { d as defaultOffsetDistance, r as reposition, a as disconnectFloatingUI, f as filterValidFlipPlacements, c as connectFloatingUI, h as hideFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js";
import { u as isKeyboardTriggeredClick, h as focusFirstTabbable, t as toAriaBoolean, q as queryElementRoots } from "../../chunks/dom.js";
import { g as guid } from "../../chunks/guid.js";
import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js";
import { H as Heading } from "../../chunks/Heading.js";
import { c as componentFocusable, g as getIconScale } from "../../chunks/component.js";
import { c as createObserver } from "../../chunks/observers.js";
import { F as FloatingArrow } from "../../chunks/FloatingArrow.js";
import { u as useT9n } from "../../chunks/useT9n.js";
import { u as useFocusTrap } from "../../chunks/useFocusTrap.js";
import { i as isActivationKey } from "../../chunks/key.js";
import { css } from "@lit/reactive-element/css-tag.js";
class PopoverManager {
constructor() {
this.registeredElements = /* @__PURE__ */ new Map();
this.registeredElementCount = 0;
this.queryPopover = (composedPath) => {
const { registeredElements } = this;
const registeredElement = composedPath.find((pathEl) => registeredElements.has(pathEl));
return registeredElements.get(registeredElement);
};
this.togglePopovers = (event) => {
const composedPath = event.composedPath();
const togglePopover = this.queryPopover(composedPath);
if (togglePopover && !togglePopover.triggerDisabled) {
togglePopover.open = !togglePopover.open;
}
Array.from(this.registeredElements.values()).filter(
(popover) => popover !== togglePopover && popover.autoClose && popover.open && !composedPath.includes(popover)
).forEach((popover) => popover.open = false);
};
this.keyDownHandler = (event) => {
if (event.defaultPrevented) {
return;
}
if (event.key === "Escape") {
this.closeAllPopovers();
} else if (isActivationKey(event.key)) {
this.togglePopovers(event);
}
};
this.clickHandler = (event) => {
if (isKeyboardTriggeredClick(event) || event.defaultPrevented) {
return;
}
this.togglePopovers(event);
};
}
// --------------------------------------------------------------------------
//
// Public Methods
//
// --------------------------------------------------------------------------
registerElement(referenceEl, popover) {
this.registeredElementCount++;
this.registeredElements.set(referenceEl, popover);
if (this.registeredElementCount === 1) {
this.addListeners();
}
}
unregisterElement(referenceEl) {
if (this.registeredElements.delete(referenceEl)) {
this.registeredElementCount--;
}
if (this.registeredElementCount === 0) {
this.removeListeners();
}
}
closeAllPopovers() {
Array.from(this.registeredElements.values()).forEach((popover) => popover.open = false);
}
addListeners() {
window.addEventListener("click", this.clickHandler);
window.addEventListener("keydown", this.keyDownHandler);
}
removeListeners() {
window.removeEventListener("click", this.clickHandler);
window.removeEventListener("keydown", this.keyDownHandler);
}
}
const CSS = {
positionContainer: "position-container",
container: "container",
closeButtonContainer: "close-button-container",
closeButton: "close-button",
content: "content",
hasHeader: "has-header",
header: "header",
headerContainer: "header-container",
heading: "heading"
};
const defaultPopoverPlacement = "auto";
const ARIA_CONTROLS = "aria-controls";
const ARIA_EXPANDED = "aria-expanded";
const styles = css`:host{display:contents;--calcite-floating-ui-z-index: var(--calcite-popover-z-index, var(--calcite-z-index-popup))}.position-container{inline-size:max-content;display:none;max-inline-size:100vw;max-block-size:100vh;inset-block-start:0;left:0;z-index:var(--calcite-floating-ui-z-index)}.position-container .calcite-floating-ui-anim{position:relative;transition:var(--calcite-floating-ui-transition);transition-property:inset,left,opacity;opacity:0;box-shadow:0 0 16px #00000029;z-index:var(--calcite-z-index);border-radius:.25rem}.position-container[data-placement^=bottom] .calcite-floating-ui-anim{inset-block-start:-5px}.position-container[data-placement^=top] .calcite-floating-ui-anim{inset-block-start:5px}.position-container[data-placement^=left] .calcite-floating-ui-anim{left:5px}.position-container[data-placement^=right] .calcite-floating-ui-anim{left:-5px}.position-container[data-placement] .calcite-floating-ui-anim--active{opacity:1;inset-block-start:0;left:0}.calcite-floating-ui-arrow{pointer-events:none;position:absolute;z-index:calc(var(--calcite-z-index) * -1);fill:var(--calcite-color-foreground-1)}.calcite-floating-ui-arrow__stroke{stroke:var(--calcite-color-border-3)}:host([scale=s]) .heading{padding:.5rem .75rem;font-size:var(--calcite-font-size--1);line-height:1.375}:host([scale=m]) .heading{padding:.75rem 1rem;font-size:var(--calcite-font-size-0);line-height:1.375}:host([scale=l]) .heading{padding:1rem 1.25rem;font-size:var(--calcite-font-size-1);line-height:1.375}.position-container .calcite-floating-ui-anim{border-width:1px;border-style:solid;background-color:var(--calcite-popover-background-color, var(--calcite-color-foreground-1));border-color:var(--calcite-popover-border-color, var(--calcite-color-border-3));border-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round))}.calcite-floating-ui-arrow{fill:var(--calcite-popover-background-color, var(--calcite-color-foreground-1))}.calcite-floating-ui-arrow__stroke{stroke:var(--calcite-popover-border-color, var(--calcite-color-border-3))}.header{display:flex;flex:1 1 auto;align-items:stretch;justify-content:flex-start;border-width:0px;border-bottom-width:1px;border-style:solid;border-block-end-color:var(--calcite-popover-border-color, var(--calcite-color-border-3))}.heading{margin:0;display:block;flex:1 1 auto;align-self:center;white-space:normal;font-weight:var(--calcite-font-weight-medium);word-wrap:break-word;word-break:break-word;color:var(--calcite-popover-text-color, var(--calcite-color-text-1))}.header-container{position:relative;display:flex;height:100%;flex-direction:row;flex-wrap:nowrap;border-radius:.25rem;color:var(--calcite-popover-text-color, var(--calcite-color-text-1))}.header-container.has-header{flex-direction:column}.content{display:flex;height:100%;width:100%;flex-direction:column;flex-wrap:nowrap;align-self:center;word-wrap:break-word;word-break:break-word}.close-button-container{display:flex;overflow:hidden;flex:0 0 auto;border-start-end-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round));border-end-end-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round));--calcite-action-corner-radius-start-end: var(--calcite-popover-corner-radius, var(--calcite-corner-radius-sharp));--calcite-action-corner-radius-end-end: var(--calcite-popover-corner-radius, var(--calcite-corner-radius-sharp))}::slotted(calcite-panel),::slotted(calcite-flow){height:100%}:host([hidden]){display:none}[hidden]{display:none}`;
const manager = new PopoverManager();
class Popover extends LitElement {
constructor() {
super(...arguments);
this.closeButtonEl = createRef();
this.focusTrap = useFocusTrap({
triggerProp: "open",
focusTrapOptions: {
allowOutsideClick: true,
escapeDeactivates: (event) => {
if (!event.defaultPrevented) {
this.open = false;
event.preventDefault();
}
return false;
}
}
})(this);
this.guid = `calcite-popover-${guid()}`;
this.hasLoaded = false;
this.mutationObserver = createObserver("mutation", () => this.focusTrap.updateContainerElements());
this.transitionProp = "opacity";
this.messages = useT9n();
this.floatingLayout = "vertical";
this.autoClose = false;
this.closable = false;
this.flipDisabled = false;
this.focusTrapDisabled = false;
this.offsetDistance = defaultOffsetDistance;
this.offsetSkidding = 0;
this.open = false;
this.overlayPositioning = "absolute";
this.placement = defaultPopoverPlacement;
this.pointerDisabled = false;
this.scale = "m";
this.triggerDisabled = false;
this.calcitePopoverBeforeClose = createEvent({ cancelable: false });
this.calcitePopoverBeforeOpen = createEvent({ cancelable: false });
this.calcitePopoverClose = createEvent({ cancelable: false });
this.calcitePopoverOpen = createEvent({ cancelable: false });
}
static {
this.properties = { floatingLayout: [16, {}, { state: true }], referenceEl: [16, {}, { state: true }], autoClose: [7, {}, { reflect: true, type: Boolean }], closable: [7, {}, { reflect: true, type: Boolean }], flipDisabled: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], focusTrapDisabled: [7, {}, { reflect: true, type: Boolean }], focusTrapOptions: [0, {}, { attribute: false }], heading: 1, headingLevel: [11, {}, { type: Number, reflect: true }], label: 1, messageOverrides: [0, {}, { attribute: false }], offsetDistance: [11, {}, { type: Number, reflect: true }], offsetSkidding: [11, {}, { reflect: true, type: Number }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], placement: [3, {}, { reflect: true }], pointerDisabled: [7, {}, { reflect: true, type: Boolean }], referenceElement: 1, scale: [3, {}, { reflect: true }], triggerDisabled: [7, {}, { reflect: true, type: Boolean }] };
}
static {
this.styles = styles;
}
async reposition(delayed = false) {
const { referenceEl, placement, overlayPositioning, flipDisabled, filteredFlipPlacements, offsetDistance, offsetSkidding, arrowEl, floatingEl } = this;
return reposition(this, {
floatingEl,
referenceEl,
overlayPositioning,
placement,
flipDisabled,
flipPlacements: filteredFlipPlacements,
offsetDistance,
offsetSkidding,
arrowEl,
type: "popover"
}, delayed);
}
async setFocus() {
await componentFocusable(this);
this.requestUpdate();
focusFirstTabbable(this.el);
}
async updateFocusTrapElements() {
this.focusTrap.updateContainerElements();
}
connectedCallback() {
super.connectedCallback();
this.mutationObserver?.observe(this.el, { childList: true, subtree: true });
this.setFilteredPlacements();
requestAnimationFrame(() => this.setUpReferenceElement(this.hasLoaded));
}
willUpdate(changes) {
if (changes.has("flipPlacements")) {
this.flipPlacementsHandler();
}
if (changes.has("open") && (this.hasUpdated || this.open !== false)) {
this.openHandler();
}
if (changes.has("offsetDistance") && (this.hasUpdated || this.offsetDistance !== defaultOffsetDistance) || changes.has("offsetSkidding") && (this.hasUpdated || this.offsetSkidding !== 0) || changes.has("overlayPositioning") && (this.hasUpdated || this.overlayPositioning !== "absolute") || changes.has("placement") && (this.hasUpdated || this.placement !== defaultPopoverPlacement)) {
this.reposition(true);
}
if (changes.has("referenceElement")) {
this.referenceElementHandler();
}
}
loaded() {
if (this.referenceElement && !this.referenceEl) {
this.setUpReferenceElement();
}
this.hasLoaded = true;
}
disconnectedCallback() {
super.disconnectedCallback();
this.mutationObserver?.disconnect();
this.removeReferences();
disconnectFloatingUI(this);
}
flipPlacementsHandler() {
this.setFilteredPlacements();
this.reposition(true);
}
openHandler() {
onToggleOpenCloseComponent(this);
this.reposition(true);
this.setExpandedAttr();
}
referenceElementHandler() {
this.setUpReferenceElement();
this.reposition(true);
}
setFloatingEl(el) {
this.floatingEl = el;
if (el) {
requestAnimationFrame(() => this.setUpReferenceElement());
}
}
setTransitionEl(el) {
if (!el) {
return;
}
this.transitionEl = el;
}
setFilteredPlacements() {
const { el, flipPlacements } = this;
this.filteredFlipPlacements = flipPlacements ? filterValidFlipPlacements(flipPlacements, el) : null;
}
setUpReferenceElement(warn = true) {
this.removeReferences();
this.referenceEl = this.getReferenceElement();
connectFloatingUI(this);
const { el, referenceElement, referenceEl } = this;
if (warn && referenceElement && !referenceEl) {
console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, {
el
});
}
this.addReferences();
}
getId() {
return this.el.id || this.guid;
}
setExpandedAttr() {
const { referenceEl, open } = this;
if (!referenceEl) {
return;
}
if ("setAttribute" in referenceEl) {
referenceEl.setAttribute(ARIA_EXPANDED, toAriaBoolean(open));
}
}
addReferences() {
const { referenceEl } = this;
if (!referenceEl) {
return;
}
const id = this.getId();
if ("setAttribute" in referenceEl) {
referenceEl.setAttribute(ARIA_CONTROLS, id);
}
manager.registerElement(referenceEl, this.el);
this.setExpandedAttr();
}
removeReferences() {
const { referenceEl } = this;
if (!referenceEl) {
return;
}
if ("removeAttribute" in referenceEl) {
referenceEl.removeAttribute(ARIA_CONTROLS);
referenceEl.removeAttribute(ARIA_EXPANDED);
}
manager.unregisterElement(referenceEl);
}
getReferenceElement() {
const { referenceElement, el } = this;
return (typeof referenceElement === "string" ? queryElementRoots(el, { id: referenceElement }) : referenceElement) || null;
}
hide() {
this.open = false;
}
onBeforeOpen() {
this.calcitePopoverBeforeOpen.emit();
}
onOpen() {
this.calcitePopoverOpen.emit();
this.focusTrap.activate();
}
onBeforeClose() {
this.calcitePopoverBeforeClose.emit();
}
onClose() {
this.calcitePopoverClose.emit();
hideFloatingUI(this);
this.focusTrap.deactivate();
}
storeArrowEl(el) {
this.arrowEl = el;
this.reposition(true);
}
renderCloseButton() {
const { messages, closable } = this;
return closable ? keyed(CSS.closeButtonContainer, html`<div class=${safeClassMap(CSS.closeButtonContainer)}><calcite-action appearance=transparent class=${safeClassMap(CSS.closeButton)} @click=${this.hide} .scale=${this.scale} .text=${messages.close} ${ref(this.closeButtonEl)}><calcite-icon icon=x .scale=${getIconScale(this.scale)}></calcite-icon></calcite-action></div>`) : null;
}
renderHeader() {
const { heading, headingLevel } = this;
const headingNode = heading ? Heading({ class: CSS.heading, level: headingLevel, children: heading }) : null;
return headingNode ? keyed(CSS.header, html`<div class=${safeClassMap(CSS.header)}>${headingNode}${this.renderCloseButton()}</div>`) : null;
}
render() {
const { referenceEl, heading, label, open, pointerDisabled, floatingLayout } = this;
const displayed = referenceEl && open;
const hidden = !displayed;
const arrowNode = !pointerDisabled ? keyed("floating-arrow", FloatingArrow({ floatingLayout, ref: this.storeArrowEl })) : null;
this.el.inert = hidden;
this.el.ariaLabel = label;
this.el.ariaLive = "polite";
setAttribute(this.el, "id", this.getId());
this.el.role = "dialog";
return html`<div class=${safeClassMap(CSS.positionContainer)} ${ref(this.setFloatingEl)}><div class=${safeClassMap({
[CSS.container]: true,
[FloatingCSS.animation]: true,
[FloatingCSS.animationActive]: displayed
})} ${ref(this.setTransitionEl)}>${arrowNode}<div class=${safeClassMap({
[CSS.hasHeader]: !!heading,
[CSS.headerContainer]: true
})}>${this.renderHeader()}<div class=${safeClassMap(CSS.content)}><slot></slot></div>${!heading ? this.renderCloseButton() : null}</div></div></div>`;
}
}
customElement("calcite-popover", Popover);
export {
Popover
};