@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
594 lines (593 loc) • 21 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 { Fragment, h } from "@stencil/core";
import { focusFirstTabbable, slotChangeGetAssignedElements, toAriaBoolean } from "../../utils/dom";
import { connectInteractive, disconnectInteractive, updateHostInteraction } from "../../utils/interactive";
import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable";
import { createObserver } from "../../utils/observers";
import { SLOTS as ACTION_MENU_SLOTS } from "../action-menu/resources";
import { Heading } from "../functional/Heading";
import { CSS, ICONS, SLOTS } from "./resources";
import { connectLocalized, disconnectLocalized } from "../../utils/locale";
import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n";
/**
* @slot - A slot for adding custom content.
* @slot action-bar - A slot for adding a `calcite-action-bar` to the component.
* @slot header-actions-start - A slot for adding actions or content to the start side of the header.
* @slot header-actions-end - A slot for adding actions or content to the end side of the header.
* @slot header-content - A slot for adding custom content to the header.
* @slot header-menu-actions - A slot for adding an overflow menu with actions inside a `calcite-dropdown`.
* @slot fab - A slot for adding a `calcite-fab` (floating action button) to perform an action.
* @slot footer-actions - [Deprecated] Use the `"footer"` slot instead. A slot for adding `calcite-button`s to the component's footer.
* @slot footer - A slot for adding custom content to the footer.
*/
export class Panel {
constructor() {
this.resizeObserver = createObserver("resize", () => this.resizeHandler());
// --------------------------------------------------------------------------
//
// Private Methods
//
// --------------------------------------------------------------------------
this.resizeHandler = () => {
const { panelScrollEl } = this;
if (!panelScrollEl ||
typeof panelScrollEl.scrollHeight !== "number" ||
typeof panelScrollEl.offsetHeight !== "number") {
return;
}
panelScrollEl.tabIndex = panelScrollEl.scrollHeight > panelScrollEl.offsetHeight ? 0 : -1;
};
this.setContainerRef = (node) => {
this.containerEl = node;
};
this.setCloseRef = (node) => {
this.closeButtonEl = node;
};
this.setBackRef = (node) => {
this.backButtonEl = node;
};
this.panelKeyDownHandler = (event) => {
if (this.closable && event.key === "Escape" && !event.defaultPrevented) {
this.close();
event.preventDefault();
}
};
this.close = () => {
this.closed = true;
this.calcitePanelClose.emit();
};
this.panelScrollHandler = () => {
this.calcitePanelScroll.emit();
};
this.handleHeaderActionsStartSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasStartActions = !!elements.length;
};
this.handleHeaderActionsEndSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasEndActions = !!elements.length;
};
this.handleHeaderMenuActionsSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasMenuItems = !!elements.length;
};
this.handleActionBarSlotChange = (event) => {
const actionBars = slotChangeGetAssignedElements(event).filter((el) => el?.matches("calcite-action-bar"));
actionBars.forEach((actionBar) => (actionBar.layout = "horizontal"));
this.hasActionBar = !!actionBars.length;
};
this.handleHeaderContentSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasHeaderContent = !!elements.length;
};
this.handleFooterSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasFooterContent = !!elements.length;
};
this.handleFooterActionsSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasFooterActions = !!elements.length;
};
this.handleFabSlotChange = (event) => {
const elements = event.target.assignedElements({
flatten: true
});
this.hasFab = !!elements.length;
};
this.setPanelScrollEl = (el) => {
this.panelScrollEl = el;
this.resizeObserver?.disconnect();
if (el) {
this.resizeObserver?.observe(el);
this.resizeHandler();
}
};
this.closed = false;
this.disabled = false;
this.closable = false;
this.headingLevel = undefined;
this.loading = false;
this.heading = undefined;
this.description = undefined;
this.menuOpen = false;
this.messageOverrides = undefined;
this.messages = undefined;
this.hasStartActions = false;
this.hasEndActions = false;
this.hasMenuItems = false;
this.hasHeaderContent = false;
this.hasActionBar = false;
this.hasFooterContent = false;
this.hasFooterActions = false;
this.hasFab = false;
this.defaultMessages = undefined;
this.effectiveLocale = "";
}
onMessagesChange() {
/* wired up by t9n util */
}
//--------------------------------------------------------------------------
//
// Lifecycle
//
//--------------------------------------------------------------------------
connectedCallback() {
connectInteractive(this);
connectLocalized(this);
connectMessages(this);
}
async componentWillLoad() {
setUpLoadableComponent(this);
await setUpMessages(this);
}
componentDidLoad() {
setComponentLoaded(this);
}
componentDidRender() {
updateHostInteraction(this);
}
disconnectedCallback() {
disconnectInteractive(this);
disconnectLocalized(this);
disconnectMessages(this);
this.resizeObserver?.disconnect();
}
effectiveLocaleChange() {
updateMessages(this, this.effectiveLocale);
}
// --------------------------------------------------------------------------
//
// Methods
//
// --------------------------------------------------------------------------
/**
* Sets focus on the component's first focusable element.
*/
async setFocus() {
await componentLoaded(this);
focusFirstTabbable(this.containerEl);
}
/**
* Scrolls the component's content to a specified set of coordinates.
*
* @example
* myCalciteFlowItem.scrollContentTo({
* left: 0, // Specifies the number of pixels along the X axis to scroll the window or element.
* top: 0, // Specifies the number of pixels along the Y axis to scroll the window or element
* behavior: "auto" // Specifies whether the scrolling should animate smoothly (smooth), or happen instantly in a single jump (auto, the default value).
* });
* @param options
*/
async scrollContentTo(options) {
this.panelScrollEl?.scrollTo(options);
}
// --------------------------------------------------------------------------
//
// Render Methods
//
// --------------------------------------------------------------------------
renderHeaderContent() {
const { heading, headingLevel, description, hasHeaderContent } = this;
const headingNode = heading ? (h(Heading, { class: CSS.heading, level: headingLevel }, heading)) : null;
const descriptionNode = description ? h("span", { class: CSS.description }, description) : null;
return !hasHeaderContent && (headingNode || descriptionNode) ? (h("div", { class: CSS.headerContent, key: "header-content" }, headingNode, descriptionNode)) : null;
}
renderActionBar() {
return (h("div", { class: CSS.actionBarContainer, hidden: !this.hasActionBar }, h("slot", { name: SLOTS.actionBar, onSlotchange: this.handleActionBarSlotChange })));
}
/**
* Allows user to override the entire header-content node.
*/
renderHeaderSlottedContent() {
return (h("div", { class: CSS.headerContent, hidden: !this.hasHeaderContent, key: "slotted-header-content" }, h("slot", { name: SLOTS.headerContent, onSlotchange: this.handleHeaderContentSlotChange })));
}
renderHeaderStartActions() {
const { hasStartActions } = this;
return (h("div", { class: { [CSS.headerActionsStart]: true, [CSS.headerActions]: true }, hidden: !hasStartActions, key: "header-actions-start" }, h("slot", { name: SLOTS.headerActionsStart, onSlotchange: this.handleHeaderActionsStartSlotChange })));
}
renderHeaderActionsEnd() {
const { close, hasEndActions, messages, closable, hasMenuItems } = this;
const text = messages.close;
const closableNode = closable ? (h("calcite-action", { "aria-label": text, "data-test": "close", icon: ICONS.close, onClick: close, text: text,
// eslint-disable-next-line react/jsx-sort-props
ref: this.setCloseRef })) : null;
const slotNode = (h("slot", { name: SLOTS.headerActionsEnd, onSlotchange: this.handleHeaderActionsEndSlotChange }));
const showContainer = hasEndActions || closableNode || hasMenuItems;
return (h("div", { class: { [CSS.headerActionsEnd]: true, [CSS.headerActions]: true }, hidden: !showContainer, key: "header-actions-end" }, slotNode, this.renderMenu(), closableNode));
}
renderMenu() {
const { hasMenuItems, messages, menuOpen } = this;
return (h("calcite-action-menu", { flipPlacements: ["top", "bottom"], hidden: !hasMenuItems, key: "menu", label: messages.options, open: menuOpen, placement: "bottom-end" }, h("calcite-action", { icon: ICONS.menu, slot: ACTION_MENU_SLOTS.trigger, text: messages.options }), h("slot", { name: SLOTS.headerMenuActions, onSlotchange: this.handleHeaderMenuActionsSlotChange })));
}
renderHeaderNode() {
const { hasHeaderContent, hasStartActions, hasEndActions, closable, hasMenuItems } = this;
const headerContentNode = this.renderHeaderContent();
const showHeader = hasHeaderContent ||
headerContentNode ||
hasStartActions ||
hasEndActions ||
closable ||
hasMenuItems;
return (h("header", { class: CSS.header, hidden: !showHeader }, this.renderHeaderStartActions(), this.renderHeaderSlottedContent(), headerContentNode, this.renderHeaderActionsEnd()));
}
renderFooterNode() {
const { hasFooterContent, hasFooterActions } = this;
const showFooter = hasFooterContent || hasFooterActions;
return (h("footer", { class: CSS.footer, hidden: !showFooter }, h("slot", { key: "footer-slot", name: SLOTS.footer, onSlotchange: this.handleFooterSlotChange }), h("slot", { key: "footer-actions-slot", name: SLOTS.footerActions, onSlotchange: this.handleFooterActionsSlotChange })));
}
renderContent() {
const { hasFab } = this;
const defaultSlotNode = h("slot", { key: "default-slot" });
const containerNode = hasFab ? (h("section", { class: CSS.contentContainer }, defaultSlotNode)) : (defaultSlotNode);
return (h("div", { class: {
[CSS.contentWrapper]: true,
[CSS.contentContainer]: !hasFab,
[CSS.contentHeight]: hasFab
}, onScroll: this.panelScrollHandler,
// eslint-disable-next-line react/jsx-sort-props
ref: this.setPanelScrollEl }, containerNode, this.renderFab()));
}
renderFab() {
return (h("div", { class: CSS.fabContainer, hidden: !this.hasFab }, h("slot", { name: SLOTS.fab, onSlotchange: this.handleFabSlotChange })));
}
render() {
const { loading, panelKeyDownHandler, closed, closable } = this;
const panelNode = (h("article", { "aria-busy": toAriaBoolean(loading), class: CSS.container, hidden: closed, onKeyDown: panelKeyDownHandler, tabIndex: closable ? 0 : -1,
// eslint-disable-next-line react/jsx-sort-props
ref: this.setContainerRef }, this.renderHeaderNode(), this.renderActionBar(), this.renderContent(), this.renderFooterNode()));
return (h(Fragment, null, loading ? h("calcite-scrim", { loading: loading }) : null, panelNode));
}
static get is() { return "calcite-panel"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["panel.scss"]
};
}
static get styleUrls() {
return {
"$": ["panel.css"]
};
}
static get assetsDirs() { return ["assets"]; }
static get properties() {
return {
"closed": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, the component will be hidden."
},
"attribute": "closed",
"reflect": true,
"defaultValue": "false"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, interaction is prevented and the component is displayed with lower opacity."
},
"attribute": "disabled",
"reflect": true,
"defaultValue": "false"
},
"closable": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, displays a close button in the trailing side of the header."
},
"attribute": "closable",
"reflect": true,
"defaultValue": "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
},
"loading": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, a busy indicator is displayed."
},
"attribute": "loading",
"reflect": true,
"defaultValue": "false"
},
"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
},
"description": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "A description for the component."
},
"attribute": "description",
"reflect": false
},
"menuOpen": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, the action menu items in the `header-menu-actions` slot are open."
},
"attribute": "menu-open",
"reflect": true,
"defaultValue": "false"
},
"messageOverrides": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "Partial<PanelMessages>",
"resolved": "{ close?: string; options?: string; }",
"references": {
"Partial": {
"location": "global"
},
"PanelMessages": {
"location": "import",
"path": "./assets/panel/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": "PanelMessages",
"resolved": "{ close: string; options: string; }",
"references": {
"PanelMessages": {
"location": "import",
"path": "./assets/panel/t9n"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "internal",
"text": undefined
}],
"text": "Made into a prop for testing purposes only"
}
}
};
}
static get states() {
return {
"hasStartActions": {},
"hasEndActions": {},
"hasMenuItems": {},
"hasHeaderContent": {},
"hasActionBar": {},
"hasFooterContent": {},
"hasFooterActions": {},
"hasFab": {},
"defaultMessages": {},
"effectiveLocale": {}
};
}
static get events() {
return [{
"method": "calcitePanelClose",
"name": "calcitePanelClose",
"bubbles": true,
"cancelable": false,
"composed": true,
"docs": {
"tags": [],
"text": "Fires when the close button is clicked."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "calcitePanelScroll",
"name": "calcitePanelScroll",
"bubbles": true,
"cancelable": false,
"composed": true,
"docs": {
"tags": [],
"text": "Fires when the content is scrolled."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}];
}
static get methods() {
return {
"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": []
}
},
"scrollContentTo": {
"complexType": {
"signature": "(options?: ScrollToOptions) => Promise<void>",
"parameters": [{
"tags": [{
"name": "param",
"text": "options"
}],
"text": ""
}],
"references": {
"Promise": {
"location": "global"
},
"ScrollToOptions": {
"location": "global"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Scrolls the component's content to a specified set of coordinates.",
"tags": [{
"name": "example",
"text": "myCalciteFlowItem.scrollContentTo({\n left: 0, // Specifies the number of pixels along the X axis to scroll the window or element.\n top: 0, // Specifies the number of pixels along the Y axis to scroll the window or element\n behavior: \"auto\" // Specifies whether the scrolling should animate smoothly (smooth), or happen instantly in a single jump (auto, the default value).\n});"
}, {
"name": "param",
"text": "options"
}]
}
}
};
}
static get elementRef() { return "el"; }
static get watchers() {
return [{
"propName": "messageOverrides",
"methodName": "onMessagesChange"
}, {
"propName": "effectiveLocale",
"methodName": "effectiveLocaleChange"
}];
}
}