UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

662 lines (661 loc) • 23.1 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 } from "@stencil/core"; import { connectConditionalSlotComponent, disconnectConditionalSlotComponent } from "../../utils/conditionalSlot"; import { getElementDir, getSlotted, isPrimaryPointerButton, slotChangeGetAssignedElements } from "../../utils/dom"; import { connectLocalized, disconnectLocalized } from "../../utils/locale"; import { clamp } from "../../utils/math"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import { CSS, SLOTS } from "./resources"; import { CSS_UTILITY } from "../../utils/resources"; /** * @slot - A slot for adding custom content. * @slot action-bar - A slot for adding a `calcite-action-bar` to the component. */ export class ShellPanel { constructor() { this.initialContentWidth = null; this.initialContentHeight = null; this.initialClientX = null; this.initialClientY = null; this.contentWidthMax = null; this.contentWidthMin = null; this.contentHeightMax = null; this.contentHeightMin = null; this.step = 1; this.stepMultiplier = 10; this.actionBars = []; this.storeContentEl = (contentEl) => { this.contentEl = contentEl; }; this.getKeyAdjustedSize = (event) => { const { key } = event; const { el, step, stepMultiplier, layout, contentWidthMin, contentWidthMax, initialContentWidth, initialContentHeight, contentHeightMin, contentHeightMax, position } = this; const multipliedStep = step * stepMultiplier; const MOVEMENT_KEYS = [ "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown" ]; if (MOVEMENT_KEYS.indexOf(key) > -1) { event.preventDefault(); } const dir = getElementDir(el); const horizontalKeys = ["ArrowLeft", "ArrowRight"]; const verticalKeys = ["ArrowDown", "ArrowUp"]; const directionFactor = dir === "rtl" && horizontalKeys.includes(key) ? -1 : 1; const increaseKeys = layout === "horizontal" ? position === "end" ? key === verticalKeys[1] || key === horizontalKeys[0] : key === verticalKeys[0] || key === horizontalKeys[1] : key === verticalKeys[1] || (position === "end" ? key === horizontalKeys[0] : key === horizontalKeys[1]); if (increaseKeys) { const stepValue = event.shiftKey ? multipliedStep : step; return layout === "horizontal" ? initialContentHeight + directionFactor * stepValue : initialContentWidth + directionFactor * stepValue; } const decreaseKeys = layout === "horizontal" ? position === "end" ? key === verticalKeys[0] || key === horizontalKeys[0] : key === verticalKeys[1] || key === horizontalKeys[1] : key === verticalKeys[0] || (position === "end" ? key === horizontalKeys[1] : key === horizontalKeys[0]); if (decreaseKeys) { const stepValue = event.shiftKey ? multipliedStep : step; return layout === "horizontal" ? initialContentHeight - directionFactor * stepValue : initialContentWidth - directionFactor * stepValue; } if (key === "Home" && layout === "horizontal" && typeof contentHeightMin === "number") { return contentHeightMin; } if (key === "Home" && layout === "vertical" && typeof contentWidthMin === "number") { return contentWidthMin; } if (key === "End" && layout === "horizontal" && typeof contentHeightMax === "number") { return contentHeightMax; } if (key === "End" && layout === "vertical" && typeof contentWidthMax === "number") { return contentWidthMax; } if (key === "PageDown") { return layout === "horizontal" ? initialContentHeight - multipliedStep : initialContentWidth - multipliedStep; } if (key === "PageUp") { return layout === "horizontal" ? initialContentHeight + multipliedStep : initialContentWidth + multipliedStep; } return null; }; this.initialKeydownWidth = (event) => { this.setInitialContentWidth(); const width = this.getKeyAdjustedSize(event); if (typeof width === "number") { this.setContentWidth(width); } }; this.initialKeydownHeight = (event) => { this.setInitialContentHeight(); const height = this.getKeyAdjustedSize(event); if (typeof height === "number") { this.setContentHeight(height); } }; this.separatorKeyDown = (event) => { this.layout === "horizontal" ? this.initialKeydownHeight(event) : this.initialKeydownWidth(event); }; this.separatorPointerMove = (event) => { event.preventDefault(); const { el, layout, initialContentWidth, initialContentHeight, position, initialClientX, initialClientY } = this; const offset = layout === "horizontal" ? event.clientY - initialClientY : event.clientX - initialClientX; const adjustmentDirection = layout === "vertical" && getElementDir(el) === "rtl" ? -1 : 1; const adjustedOffset = layout === "horizontal" ? position === "end" ? -adjustmentDirection * offset : adjustmentDirection * offset : position === "end" ? -adjustmentDirection * offset : adjustmentDirection * offset; layout === "horizontal" ? this.setContentHeight(initialContentHeight + adjustedOffset) : this.setContentWidth(initialContentWidth + adjustedOffset); }; this.separatorPointerUp = (event) => { if (!isPrimaryPointerButton(event)) { return; } event.preventDefault(); document.removeEventListener("pointerup", this.separatorPointerUp); document.removeEventListener("pointermove", this.separatorPointerMove); }; this.setInitialContentHeight = () => { this.initialContentHeight = this.contentEl?.getBoundingClientRect().height; }; this.setInitialContentWidth = () => { this.initialContentWidth = this.contentEl?.getBoundingClientRect().width; }; this.separatorPointerDown = (event) => { if (!isPrimaryPointerButton(event)) { return; } event.preventDefault(); const { separatorEl } = this; separatorEl && document.activeElement !== separatorEl && separatorEl.focus(); if (this.layout === "horizontal") { this.setInitialContentHeight(); this.initialClientY = event.clientY; } else { this.setInitialContentWidth(); this.initialClientX = event.clientX; } document.addEventListener("pointerup", this.separatorPointerUp); document.addEventListener("pointermove", this.separatorPointerMove); }; this.connectSeparator = (separatorEl) => { this.disconnectSeparator(); this.separatorEl = separatorEl; separatorEl?.addEventListener("pointerdown", this.separatorPointerDown); }; this.disconnectSeparator = () => { this.separatorEl?.removeEventListener("pointerdown", this.separatorPointerDown); }; this.setActionBarsLayout = (actionBars) => { actionBars.forEach((actionBar) => (actionBar.layout = this.layout)); }; this.handleActionBarSlotChange = (event) => { const actionBars = slotChangeGetAssignedElements(event).filter((el) => el?.matches("calcite-action-bar")); this.actionBars = actionBars; this.setActionBarsLayout(actionBars); }; this.collapsed = false; this.detached = false; this.displayMode = "dock"; this.detachedHeightScale = undefined; this.heightScale = undefined; this.widthScale = "m"; this.layout = "vertical"; this.position = "start"; this.resizable = false; this.messages = undefined; this.messageOverrides = undefined; this.contentWidth = null; this.contentHeight = null; this.defaultMessages = undefined; this.effectiveLocale = ""; } handleDetached(value) { if (value) { this.displayMode = "float"; } else if (this.displayMode === "float") { this.displayMode = "dock"; } } handleDisplayMode(value) { this.detached = value === "float"; } handleDetachedHeightScale(value) { this.heightScale = value; } handleHeightScale(value) { this.detachedHeightScale = value; } layoutHandler() { this.setActionBarsLayout(this.actionBars); } onMessagesChange() { /* wired up by t9n util */ } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectConditionalSlotComponent(this); connectLocalized(this); connectMessages(this); } async componentWillLoad() { await setUpMessages(this); } disconnectedCallback() { disconnectConditionalSlotComponent(this); this.disconnectSeparator(); disconnectLocalized(this); disconnectMessages(this); } componentDidLoad() { this.updateAriaValues(); } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } // -------------------------------------------------------------------------- // // Render Methods // // -------------------------------------------------------------------------- renderHeader() { const { el } = this; const hasHeader = getSlotted(el, SLOTS.header); return hasHeader ? (h("div", { class: CSS.contentHeader, key: "header" }, h("slot", { name: SLOTS.header }))) : null; } render() { const { collapsed, position, initialContentWidth, initialContentHeight, contentWidth, contentWidthMax, contentWidthMin, contentHeight, contentHeightMax, contentHeightMin, resizable, layout, displayMode } = this; const dir = getElementDir(this.el); const allowResizing = displayMode !== "float" && resizable; const style = allowResizing ? layout === "horizontal" ? contentHeight ? { height: `${contentHeight}px` } : null : contentWidth ? { width: `${contentWidth}px` } : null : null; const separatorNode = !collapsed && allowResizing ? (h("div", { "aria-label": this.messages.resize, "aria-orientation": layout === "horizontal" ? "vertical" : "horizontal", "aria-valuemax": layout == "horizontal" ? contentHeightMax : contentWidthMax, "aria-valuemin": layout == "horizontal" ? contentHeightMin : contentWidthMin, "aria-valuenow": layout == "horizontal" ? contentHeight ?? initialContentHeight : contentWidth ?? initialContentWidth, class: CSS.separator, key: "separator", onKeyDown: this.separatorKeyDown, role: "separator", tabIndex: 0, "touch-action": "none", // eslint-disable-next-line react/jsx-sort-props ref: this.connectSeparator })) : null; const getAnimationDir = () => { if (layout === "horizontal") { return position === "start" ? CSS_UTILITY.calciteAnimateInDown : CSS_UTILITY.calciteAnimateInUp; } else { const isStart = (dir === "ltr" && position === "end") || (dir === "rtl" && position === "start"); return isStart ? CSS_UTILITY.calciteAnimateInLeft : CSS_UTILITY.calciteAnimateInRight; } }; const contentNode = (h("div", { class: { [CSS_UTILITY.rtl]: dir === "rtl", [CSS.content]: true, [CSS.contentOverlay]: displayMode === "overlay", [CSS.contentFloat]: displayMode === "float", [CSS_UTILITY.calciteAnimate]: displayMode === "overlay", [getAnimationDir()]: displayMode === "overlay" }, hidden: collapsed, key: "content", style: style, // eslint-disable-next-line react/jsx-sort-props ref: this.storeContentEl }, this.renderHeader(), h("div", { class: CSS.contentBody }, h("slot", null)), separatorNode)); const actionBarNode = (h("slot", { key: "action-bar", name: SLOTS.actionBar, onSlotchange: this.handleActionBarSlotChange })); const mainNodes = [actionBarNode, contentNode]; if (position === "end") { mainNodes.reverse(); } return h("div", { class: { [CSS.container]: true } }, mainNodes); } // -------------------------------------------------------------------------- // // private Methods // // -------------------------------------------------------------------------- setContentWidth(width) { const { contentWidthMax, contentWidthMin } = this; const roundedWidth = Math.round(width); this.contentWidth = typeof contentWidthMax === "number" && typeof contentWidthMin === "number" ? clamp(roundedWidth, contentWidthMin, contentWidthMax) : roundedWidth; } updateAriaValues() { const { contentEl } = this; const computedStyle = contentEl && getComputedStyle(contentEl); if (!computedStyle) { return; } this.layout === "horizontal" ? this.updateHeights(computedStyle) : this.updateWidths(computedStyle); forceUpdate(this); } setContentHeight(height) { const { contentHeightMax, contentHeightMin } = this; const roundedWidth = Math.round(height); this.contentHeight = typeof contentHeightMax === "number" && typeof contentHeightMin === "number" ? clamp(roundedWidth, contentHeightMin, contentHeightMax) : roundedWidth; } updateWidths(computedStyle) { const max = parseInt(computedStyle.getPropertyValue("max-width"), 10); const min = parseInt(computedStyle.getPropertyValue("min-width"), 10); const valueNow = parseInt(computedStyle.getPropertyValue("width"), 10); if (typeof valueNow === "number" && !isNaN(valueNow)) { this.initialContentWidth = valueNow; } if (typeof max === "number" && !isNaN(max)) { this.contentWidthMax = max; } if (typeof min === "number" && !isNaN(min)) { this.contentWidthMin = min; } } updateHeights(computedStyle) { const max = parseInt(computedStyle.getPropertyValue("max-height"), 10); const min = parseInt(computedStyle.getPropertyValue("min-height"), 10); const valueNow = parseInt(computedStyle.getPropertyValue("height"), 10); if (typeof valueNow === "number" && !isNaN(valueNow)) { this.initialContentHeight = valueNow; } if (typeof max === "number" && !isNaN(max)) { this.contentHeightMax = max; } if (typeof min === "number" && !isNaN(min)) { this.contentHeightMin = min; } } static get is() { return "calcite-shell-panel"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["shell-panel.scss"] }; } static get styleUrls() { return { "$": ["shell-panel.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "collapsed": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, hides the component's content area." }, "attribute": "collapsed", "reflect": true, "defaultValue": "false" }, "detached": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "deprecated", "text": "use `displayMode` instead." }], "text": "When `true`, the content area displays like a floating panel." }, "attribute": "detached", "reflect": true, "defaultValue": "false" }, "displayMode": { "type": "string", "mutable": false, "complexType": { "original": "DisplayMode", "resolved": "\"dock\" | \"float\" | \"overlay\"", "references": { "DisplayMode": { "location": "import", "path": "./interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the display mode - `\"dock\"` (full height, displays adjacent to center content), `\"float\"` (not full height, content is separated detached from `calcite-action-bar`, displays on top of center content),\nor `\"overlay\"` (full height, displays on top of center content)." }, "attribute": "display-mode", "reflect": true, "defaultValue": "\"dock\"" }, "detachedHeightScale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "deprecated", "text": "use `heightScale` instead." }], "text": "When `displayMode` is `float`, specifies the maximum height of the component." }, "attribute": "detached-height-scale", "reflect": true }, "heightScale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `layout` is `horizontal`, or `layout` is `vertical` and `displayMode` is `float`, specifies the maximum height of the component." }, "attribute": "height-scale", "reflect": true }, "widthScale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `layout` is `vertical`, specifies the width of the component." }, "attribute": "width-scale", "reflect": true, "defaultValue": "\"m\"" }, "layout": { "type": "string", "mutable": false, "complexType": { "original": "Extract<\"horizontal\" | \"vertical\", Layout>", "resolved": "\"horizontal\" | \"vertical\"", "references": { "Extract": { "location": "global" }, "Layout": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "The direction of the component." }, "attribute": "layout", "reflect": true, "defaultValue": "\"vertical\"" }, "position": { "type": "string", "mutable": false, "complexType": { "original": "Position", "resolved": "\"end\" | \"start\"", "references": { "Position": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the component's position. Will be flipped when the element direction is right-to-left (`\"rtl\"`)." }, "attribute": "position", "reflect": true, "defaultValue": "\"start\"" }, "resizable": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true` and `displayMode` is not `float`, the component's content area is resizable." }, "attribute": "resizable", "reflect": true, "defaultValue": "false" }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "ShellPanelMessages", "resolved": "{ resize: string; }", "references": { "ShellPanelMessages": { "location": "import", "path": "./assets/shell-panel/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Made into a prop for testing purposes only" } }, "messageOverrides": { "type": "unknown", "mutable": true, "complexType": { "original": "Partial<ShellPanelMessages>", "resolved": "{ resize?: string; }", "references": { "Partial": { "location": "global" }, "ShellPanelMessages": { "location": "import", "path": "./assets/shell-panel/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } } }; } static get states() { return { "contentWidth": {}, "contentHeight": {}, "defaultMessages": {}, "effectiveLocale": {} }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "detached", "methodName": "handleDetached" }, { "propName": "displayMode", "methodName": "handleDisplayMode" }, { "propName": "detachedHeightScale", "methodName": "handleDetachedHeightScale" }, { "propName": "heightScale", "methodName": "handleHeightScale" }, { "propName": "layout", "methodName": "layoutHandler" }, { "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } }