@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
853 lines (852 loc) • 27 kB
JavaScript
/*!
* 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"
}];
}
}