UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

853 lines (852 loc) • 27 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { forceUpdate, h, Host } from "@stencil/core"; import { connectFloatingUI, defaultOffsetDistance, disconnectFloatingUI, filterComputedPlacements, FloatingCSS, reposition } from "../../utils/floating-ui"; import { activateFocusTrap, connectFocusTrap, deactivateFocusTrap, updateFocusTrapElements } from "../../utils/focusTrapComponent"; import { ARIA_CONTROLS, ARIA_EXPANDED, CSS, defaultPopoverPlacement } from "./resources"; import { focusFirstTabbable, queryElementRoots, toAriaBoolean } from "../../utils/dom"; import { guid } from "../../utils/guid"; import { connectOpenCloseComponent, disconnectOpenCloseComponent } from "../../utils/openCloseComponent"; import { Heading } from "../functional/Heading"; import { connectLocalized, disconnectLocalized } from "../../utils/locale"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import PopoverManager from "./PopoverManager"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { createObserver } from "../../utils/observers"; import { FloatingArrow } from "../functional/FloatingArrow"; const manager = new PopoverManager(); /** * @slot - A slot for adding custom content. */ export class Popover { constructor() { // -------------------------------------------------------------------------- // // Private Properties // // -------------------------------------------------------------------------- this.mutationObserver = createObserver("mutation", () => this.updateFocusTrapElements()); this.guid = `calcite-popover-${guid()}`; this.openTransitionProp = "opacity"; this.hasLoaded = false; // -------------------------------------------------------------------------- // // Private Methods // // -------------------------------------------------------------------------- this.setTransitionEl = (el) => { this.transitionEl = el; connectOpenCloseComponent(this); }; this.setFilteredPlacements = () => { const { el, flipPlacements } = this; this.filteredFlipPlacements = flipPlacements ? filterComputedPlacements(flipPlacements, el) : null; }; this.setUpReferenceElement = (warn = true) => { this.removeReferences(); this.effectiveReferenceElement = this.getReferenceElement(); connectFloatingUI(this, this.effectiveReferenceElement, this.el); const { el, referenceElement, effectiveReferenceElement } = this; if (warn && referenceElement && !effectiveReferenceElement) { console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, { el }); } this.addReferences(); }; this.getId = () => { return this.el.id || this.guid; }; this.setExpandedAttr = () => { const { effectiveReferenceElement, open } = this; if (!effectiveReferenceElement) { return; } if ("setAttribute" in effectiveReferenceElement) { effectiveReferenceElement.setAttribute(ARIA_EXPANDED, toAriaBoolean(open)); } }; this.addReferences = () => { const { effectiveReferenceElement } = this; if (!effectiveReferenceElement) { return; } const id = this.getId(); if ("setAttribute" in effectiveReferenceElement) { effectiveReferenceElement.setAttribute(ARIA_CONTROLS, id); } manager.registerElement(effectiveReferenceElement, this.el); this.setExpandedAttr(); }; this.removeReferences = () => { const { effectiveReferenceElement } = this; if (!effectiveReferenceElement) { return; } if ("removeAttribute" in effectiveReferenceElement) { effectiveReferenceElement.removeAttribute(ARIA_CONTROLS); effectiveReferenceElement.removeAttribute(ARIA_EXPANDED); } manager.unregisterElement(effectiveReferenceElement); }; this.hide = () => { this.open = false; }; this.storeArrowEl = (el) => { this.arrowEl = el; this.reposition(true); }; this.autoClose = false; this.closable = false; this.flipDisabled = false; this.focusTrapDisabled = false; this.pointerDisabled = false; this.flipPlacements = undefined; this.heading = undefined; this.headingLevel = undefined; this.label = undefined; this.messageOverrides = undefined; this.messages = undefined; this.offsetDistance = defaultOffsetDistance; this.offsetSkidding = 0; this.open = false; this.overlayPositioning = "absolute"; this.placement = defaultPopoverPlacement; this.referenceElement = undefined; this.scale = "m"; this.triggerDisabled = false; this.effectiveLocale = ""; this.floatingLayout = "vertical"; this.effectiveReferenceElement = undefined; this.defaultMessages = undefined; } handlefocusTrapDisabled(focusTrapDisabled) { if (!this.open) { return; } focusTrapDisabled ? deactivateFocusTrap(this) : activateFocusTrap(this); } flipPlacementsHandler() { this.setFilteredPlacements(); this.reposition(true); } onMessagesChange() { /* wired up by t9n util */ } offsetDistanceOffsetHandler() { this.reposition(true); } offsetSkiddingHandler() { this.reposition(true); } openHandler(value) { if (value) { this.reposition(true); } this.setExpandedAttr(); } overlayPositioningHandler() { this.reposition(true); } placementHandler() { this.reposition(true); } referenceElementHandler() { this.setUpReferenceElement(); this.reposition(true); } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } // -------------------------------------------------------------------------- // // Lifecycle // // -------------------------------------------------------------------------- connectedCallback() { this.setFilteredPlacements(); connectLocalized(this); connectMessages(this); connectOpenCloseComponent(this); this.setUpReferenceElement(this.hasLoaded); connectFocusTrap(this); } async componentWillLoad() { await setUpMessages(this); setUpLoadableComponent(this); } componentDidLoad() { setComponentLoaded(this); if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); } this.reposition(); this.hasLoaded = true; } disconnectedCallback() { this.removeReferences(); disconnectLocalized(this); disconnectMessages(this); disconnectFloatingUI(this, this.effectiveReferenceElement, this.el); disconnectOpenCloseComponent(this); deactivateFocusTrap(this); } // -------------------------------------------------------------------------- // // Public Methods // // -------------------------------------------------------------------------- /** * Updates the position of the component. * * @param delayed */ async reposition(delayed = false) { const { el, effectiveReferenceElement, placement, overlayPositioning, flipDisabled, filteredFlipPlacements, offsetDistance, offsetSkidding, arrowEl } = this; return reposition(this, { floatingEl: el, referenceEl: effectiveReferenceElement, overlayPositioning, placement, flipDisabled, flipPlacements: filteredFlipPlacements, offsetDistance, offsetSkidding, arrowEl, type: "popover" }, delayed); } /** * Sets focus on the component's first focusable element. */ async setFocus() { await componentLoaded(this); forceUpdate(this.el); focusFirstTabbable(this.el); } /** * Updates the element(s) that are used within the focus-trap of the component. */ async updateFocusTrapElements() { updateFocusTrapElements(this); } getReferenceElement() { const { referenceElement, el } = this; return ((typeof referenceElement === "string" ? queryElementRoots(el, { id: referenceElement }) : referenceElement) || null); } onBeforeOpen() { this.calcitePopoverBeforeOpen.emit(); } onOpen() { this.calcitePopoverOpen.emit(); activateFocusTrap(this); } onBeforeClose() { this.calcitePopoverBeforeClose.emit(); } onClose() { this.calcitePopoverClose.emit(); deactivateFocusTrap(this); } // -------------------------------------------------------------------------- // // Render Methods // // -------------------------------------------------------------------------- renderCloseButton() { const { messages, closable } = this; return closable ? (h("div", { class: CSS.closeButtonContainer, key: CSS.closeButtonContainer }, h("calcite-action", { appearance: "transparent", class: CSS.closeButton, onClick: this.hide, scale: this.scale, text: messages.close, // eslint-disable-next-line react/jsx-sort-props ref: (closeButtonEl) => (this.closeButtonEl = closeButtonEl) }, h("calcite-icon", { icon: "x", scale: this.scale === "l" ? "m" : this.scale })))) : null; } renderHeader() { const { heading, headingLevel } = this; const headingNode = heading ? (h(Heading, { class: CSS.heading, level: headingLevel }, heading)) : null; return headingNode ? (h("div", { class: CSS.header, key: CSS.header }, headingNode, this.renderCloseButton())) : null; } render() { const { effectiveReferenceElement, heading, label, open, pointerDisabled, floatingLayout } = this; const displayed = effectiveReferenceElement && open; const hidden = !displayed; const arrowNode = !pointerDisabled ? (h(FloatingArrow, { floatingLayout: floatingLayout, key: "floating-arrow", // eslint-disable-next-line react/jsx-sort-props ref: this.storeArrowEl })) : null; return (h(Host, { "aria-hidden": toAriaBoolean(hidden), "aria-label": label, "aria-live": "polite", "calcite-hydrated-hidden": hidden, id: this.getId(), role: "dialog" }, h("div", { class: { [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: displayed }, // eslint-disable-next-line react/jsx-sort-props ref: this.setTransitionEl }, arrowNode, h("div", { class: { [CSS.hasHeader]: !!heading, [CSS.container]: true } }, this.renderHeader(), h("div", { class: CSS.content }, h("slot", null)), !heading ? this.renderCloseButton() : null)))); } static get is() { return "calcite-popover"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["popover.scss"] }; } static get styleUrls() { return { "$": ["popover.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "autoClose": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, clicking outside of the component automatically closes open `calcite-popover`s." }, "attribute": "auto-close", "reflect": true, "defaultValue": "false" }, "closable": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, display a close button within the component." }, "attribute": "closable", "reflect": true, "defaultValue": "false" }, "flipDisabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, prevents flipping the component's placement when overlapping its `referenceElement`." }, "attribute": "flip-disabled", "reflect": true, "defaultValue": "false" }, "focusTrapDisabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, prevents focus trapping." }, "attribute": "focus-trap-disabled", "reflect": true, "defaultValue": "false" }, "pointerDisabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, removes the caret pointer." }, "attribute": "pointer-disabled", "reflect": true, "defaultValue": "false" }, "flipPlacements": { "type": "unknown", "mutable": false, "complexType": { "original": "EffectivePlacement[]", "resolved": "Placement[]", "references": { "EffectivePlacement": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines the available placements that can be used when a flip occurs." } }, "heading": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The component header text." }, "attribute": "heading", "reflect": false }, "headingLevel": { "type": "number", "mutable": false, "complexType": { "original": "HeadingLevel", "resolved": "1 | 2 | 3 | 4 | 5 | 6", "references": { "HeadingLevel": { "location": "import", "path": "../functional/Heading" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the number at which section headings should start." }, "attribute": "heading-level", "reflect": true }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "Accessible name for the component." }, "attribute": "label", "reflect": false }, "messageOverrides": { "type": "unknown", "mutable": true, "complexType": { "original": "Partial<PopoverMessages>", "resolved": "{ close?: string; }", "references": { "Partial": { "location": "global" }, "PopoverMessages": { "location": "import", "path": "./assets/popover/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "PopoverMessages", "resolved": "{ close: string; }", "references": { "PopoverMessages": { "location": "import", "path": "./assets/popover/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Made into a prop for testing purposes only" } }, "offsetDistance": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "default", "text": "6" }], "text": "Offsets the position of the popover away from the `referenceElement`." }, "attribute": "offset-distance", "reflect": true, "defaultValue": "defaultOffsetDistance" }, "offsetSkidding": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Offsets the position of the component along the `referenceElement`." }, "attribute": "offset-skidding", "reflect": true, "defaultValue": "0" }, "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, displays and positions the component." }, "attribute": "open", "reflect": true, "defaultValue": "false" }, "overlayPositioning": { "type": "string", "mutable": false, "complexType": { "original": "OverlayPositioning", "resolved": "\"absolute\" | \"fixed\"", "references": { "OverlayPositioning": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Determines the type of positioning to use for the overlaid content.\n\nUsing `\"absolute\"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.\n\n`\"fixed\"` value should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `\"fixed\"`." }, "attribute": "overlay-positioning", "reflect": true, "defaultValue": "\"absolute\"" }, "placement": { "type": "string", "mutable": false, "complexType": { "original": "LogicalPlacement", "resolved": "\"auto\" | \"top\" | \"right\" | \"bottom\" | \"left\" | \"top-start\" | \"top-end\" | \"right-start\" | \"right-end\" | \"bottom-start\" | \"bottom-end\" | \"left-start\" | \"left-end\" | \"auto-start\" | \"auto-end\" | \"leading-start\" | \"leading\" | \"leading-end\" | \"trailing-end\" | \"trailing\" | \"trailing-start\"", "references": { "LogicalPlacement": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Determines where the component will be positioned relative to the `referenceElement`." }, "attribute": "placement", "reflect": true, "defaultValue": "defaultPopoverPlacement" }, "referenceElement": { "type": "string", "mutable": false, "complexType": { "original": "ReferenceElement | string", "resolved": "Element | VirtualElement | string", "references": { "ReferenceElement": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": true, "optional": false, "docs": { "tags": [], "text": "The `referenceElement` used to position the component according to its `placement` value. Setting to an `HTMLElement` is preferred so the component does not need to query the DOM. However, a string `id` of the reference element can also be used." }, "attribute": "reference-element", "reflect": false }, "scale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the size of the component." }, "attribute": "scale", "reflect": true, "defaultValue": "\"m\"" }, "triggerDisabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, disables automatically toggling the component when its `referenceElement` has been triggered.\n\nThis property can be set to `true` to manage when the component is open." }, "attribute": "trigger-disabled", "reflect": true, "defaultValue": "false" } }; } static get states() { return { "effectiveLocale": {}, "floatingLayout": {}, "effectiveReferenceElement": {}, "defaultMessages": {} }; } static get events() { return [{ "method": "calcitePopoverBeforeClose", "name": "calcitePopoverBeforeClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is requested to be closed and before the closing transition begins." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calcitePopoverClose", "name": "calcitePopoverClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is closed and animation is complete." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calcitePopoverBeforeOpen", "name": "calcitePopoverBeforeOpen", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is added to the DOM but not rendered, and before the opening transition begins." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calcitePopoverOpen", "name": "calcitePopoverOpen", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is open and animation is complete." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get methods() { return { "reposition": { "complexType": { "signature": "(delayed?: boolean) => Promise<void>", "parameters": [{ "tags": [{ "name": "param", "text": "delayed" }], "text": "" }], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Updates the position of the component.", "tags": [{ "name": "param", "text": "delayed" }] } }, "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets focus on the component's first focusable element.", "tags": [] } }, "updateFocusTrapElements": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Updates the element(s) that are used within the focus-trap of the component.", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "focusTrapDisabled", "methodName": "handlefocusTrapDisabled" }, { "propName": "flipPlacements", "methodName": "flipPlacementsHandler" }, { "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "offsetDistance", "methodName": "offsetDistanceOffsetHandler" }, { "propName": "offsetSkidding", "methodName": "offsetSkiddingHandler" }, { "propName": "open", "methodName": "openHandler" }, { "propName": "overlayPositioning", "methodName": "overlayPositioningHandler" }, { "propName": "placement", "methodName": "placementHandler" }, { "propName": "referenceElement", "methodName": "referenceElementHandler" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } }