UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

880 lines (879 loc) • 25.7 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 { Build, h, Host } from "@stencil/core"; import { getElementDir, getElementProp, toAriaBoolean, nodeListToArray } from "../../utils/dom"; import { guid } from "../../utils/guid"; import { connectInteractive, disconnectInteractive, updateHostInteraction } from "../../utils/interactive"; import { createObserver } from "../../utils/observers"; import { CSS, ICONS } from "./resources"; import { connectLocalized, disconnectLocalized } from "../../utils/locale"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; /** * Tab-titles are optionally individually closable. */ /** * @slot - A slot for adding text. */ export class TabTitle { constructor() { //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- this.closeClickHandler = () => { this.closeTabTitleAndNotify(); }; //-------------------------------------------------------------------------- // // Private State/Props // //-------------------------------------------------------------------------- /** watches for changing text content */ this.mutationObserver = createObserver("mutation", () => this.updateHasText()); this.resizeObserver = createObserver("resize", () => { this.calciteInternalTabIconChanged.emit(); }); this.guid = `calcite-tab-title-${guid()}`; this.selected = false; this.closable = false; this.closed = false; this.disabled = false; this.iconEnd = undefined; this.iconFlipRtl = undefined; this.iconStart = undefined; this.layout = undefined; this.position = undefined; this.scale = undefined; this.bordered = false; this.tab = undefined; this.messages = undefined; this.messageOverrides = undefined; this.controls = undefined; this.defaultMessages = undefined; this.effectiveLocale = undefined; this.hasText = false; } selectedHandler() { if (this.selected) { this.emitActiveTab(false); } } onMessagesChange() { /* wired up by t9n util */ } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectInteractive(this); connectLocalized(this); connectMessages(this); this.setupTextContentObserver(); this.parentTabNavEl = this.el.closest("calcite-tab-nav"); this.parentTabsEl = this.el.closest("calcite-tabs"); } disconnectedCallback() { this.mutationObserver?.disconnect(); // Dispatching to body in order to be listened by other elements that are still connected to the DOM. document.body?.dispatchEvent(new CustomEvent("calciteTabTitleUnregister", { detail: this.el })); this.resizeObserver?.disconnect(); disconnectInteractive(this); disconnectLocalized(this); disconnectMessages(this); } async componentWillLoad() { await setUpMessages(this); if (Build.isBrowser) { this.updateHasText(); } if (this.tab && this.selected) { this.emitActiveTab(false); } } componentWillRender() { if (this.parentTabsEl) { this.layout = this.parentTabsEl.layout; this.position = this.parentTabsEl.position; this.scale = this.parentTabsEl.scale; this.bordered = this.parentTabsEl.bordered; } // handle case when tab-nav is only parent if (!this.parentTabsEl && this.parentTabNavEl) { this.position = getElementProp(this.parentTabNavEl, "position", this.position); this.scale = getElementProp(this.parentTabNavEl, "scale", this.scale); } } render() { const { el, closed } = this; const id = el.id || this.guid; const iconStartEl = (h("calcite-icon", { class: { [CSS.titleIcon]: true, [CSS.iconStart]: true }, flipRtl: this.iconFlipRtl === "start" || this.iconFlipRtl === "both", icon: this.iconStart, scale: this.scale === "l" ? "m" : "s" })); const iconEndEl = (h("calcite-icon", { class: { [CSS.titleIcon]: true, [CSS.iconEnd]: true }, flipRtl: this.iconFlipRtl === "end" || this.iconFlipRtl === "both", icon: this.iconEnd, scale: this.scale === "l" ? "m" : "s" })); return (h(Host, { "aria-controls": this.controls, "aria-selected": toAriaBoolean(this.selected), id: id, role: "tab", tabIndex: this.selected ? 0 : -1 }, h("div", { class: { container: true, [CSS.iconPresent]: !!this.iconStart || !!this.iconEnd }, hidden: closed, // eslint-disable-next-line react/jsx-sort-props ref: (el) => this.resizeObserver?.observe(el) }, h("div", { class: { [CSS.content]: true, [CSS.contentHasText]: this.hasText } }, this.iconStart ? iconStartEl : null, h("slot", null), this.iconEnd ? iconEndEl : null), this.renderCloseButton()))); } renderCloseButton() { const { closable, messages } = this; return closable ? (h("button", { "aria-label": messages.close, class: CSS.closeButton, disabled: false, key: CSS.closeButton, onClick: this.closeClickHandler, title: messages.close, type: "button", // eslint-disable-next-line react/jsx-sort-props ref: (el) => (this.closeButtonEl = el) }, h("calcite-icon", { icon: ICONS.close, scale: this.scale === "l" ? "m" : "s" }))) : null; } async componentDidLoad() { this.calciteInternalTabTitleRegister.emit(await this.getTabIdentifier()); } componentDidRender() { updateHostInteraction(this, () => { return this.selected; }); } //-------------------------------------------------------------------------- // // Event Listeners // //-------------------------------------------------------------------------- internalTabChangeHandler(event) { const targetTabsEl = event .composedPath() .find((el) => el.tagName === "CALCITE-TABS"); if (targetTabsEl !== this.parentTabsEl) { return; } if (this.tab) { this.selected = this.tab === event.detail.tab; } else { this.getTabIndex().then((index) => { this.selected = index === event.detail.tab; }); } event.stopPropagation(); } onClick() { if (this.disabled) { return; } this.emitActiveTab(); } keyDownHandler(event) { switch (event.key) { case " ": case "Enter": if (!event.composedPath().includes(this.closeButtonEl)) { this.emitActiveTab(); event.preventDefault(); } break; case "ArrowRight": event.preventDefault(); if (getElementDir(this.el) === "ltr") { this.calciteInternalTabsFocusNext.emit(); } else { this.calciteInternalTabsFocusPrevious.emit(); } break; case "ArrowLeft": event.preventDefault(); if (getElementDir(this.el) === "ltr") { this.calciteInternalTabsFocusPrevious.emit(); } else { this.calciteInternalTabsFocusNext.emit(); } break; case "Home": event.preventDefault(); this.calciteInternalTabsFocusFirst.emit(); break; case "End": event.preventDefault(); this.calciteInternalTabsFocusLast.emit(); break; } } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** * Returns the index of the title within the `calcite-tab-nav`. */ async getTabIndex() { return Array.prototype.indexOf.call(nodeListToArray(this.el.parentElement.children).filter((el) => el.matches("calcite-tab-title")), this.el); } /** * @internal */ async getTabIdentifier() { return this.tab ? this.tab : this.getTabIndex(); } /** * @param tabIds * @param titleIds * @internal */ async updateAriaInfo(tabIds = [], titleIds = []) { this.controls = tabIds[titleIds.indexOf(this.el.id)] || null; } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } updateHasText() { this.hasText = this.el.textContent.trim().length > 0; } setupTextContentObserver() { this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); } emitActiveTab(userTriggered = true) { if (this.disabled || this.closed) { return; } const payload = { tab: this.tab }; this.calciteInternalTabsActivate.emit(payload); if (userTriggered) { // emit in the next frame to let internal events sync up requestAnimationFrame(() => this.calciteTabsActivate.emit()); } } closeTabTitleAndNotify() { this.closed = true; this.calciteInternalTabsClose.emit({ tab: this.tab }); this.calciteTabsClose.emit(); } static get is() { return "calcite-tab-title"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["tab-title.scss"] }; } static get styleUrls() { return { "$": ["tab-title.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "selected": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the component and its respective `calcite-tab` contents are selected.\n\nOnly one tab can be selected within the `calcite-tabs` parent." }, "attribute": "selected", "reflect": true, "defaultValue": "false" }, "closable": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, a close button is added to the component." }, "attribute": "closable", "reflect": true, "defaultValue": "false" }, "closed": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, does not display or position the component." }, "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" }, "iconEnd": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies an icon to display at the end of the component." }, "attribute": "icon-end", "reflect": true }, "iconFlipRtl": { "type": "string", "mutable": false, "complexType": { "original": "FlipContext", "resolved": "\"both\" | \"end\" | \"start\"", "references": { "FlipContext": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`\"rtl\"`)." }, "attribute": "icon-flip-rtl", "reflect": true }, "iconStart": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies an icon to display at the start of the component." }, "attribute": "icon-start", "reflect": true }, "layout": { "type": "string", "mutable": true, "complexType": { "original": "TabLayout", "resolved": "\"center\" | \"inline\"", "references": { "TabLayout": { "location": "import", "path": "../tabs/interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "layout", "reflect": true }, "position": { "type": "string", "mutable": true, "complexType": { "original": "TabPosition", "resolved": "\"bottom\" | \"top\"", "references": { "TabPosition": { "location": "import", "path": "../tabs/interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "position", "reflect": true }, "scale": { "type": "string", "mutable": true, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "scale", "reflect": true }, "bordered": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "bordered", "reflect": true, "defaultValue": "false" }, "tab": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies a unique name for the component.\n\nWhen specified, use the same value on the `calcite-tab`." }, "attribute": "tab", "reflect": true }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "TabTitleMessages", "resolved": "{ close: string; }", "references": { "TabTitleMessages": { "location": "import", "path": "./assets/tab-title/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<TabTitleMessages>", "resolved": "{ close?: string; }", "references": { "Partial": { "location": "global" }, "TabTitleMessages": { "location": "import", "path": "./assets/tab-title/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } } }; } static get states() { return { "controls": {}, "defaultMessages": {}, "effectiveLocale": {}, "hasText": {} }; } static get events() { return [{ "method": "calciteTabsActivate", "name": "calciteTabsActivate", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when a `calcite-tab` is selected." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabsActivate", "name": "calciteInternalTabsActivate", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "see", "text": "[TabChangeEventDetail](https://github.com/Esri/calcite-components/blob/master/src/components/tab/interfaces.ts#L1)" }, { "name": "internal", "text": undefined }], "text": "Fires when a `calcite-tab` is selected (`event.details`)." }, "complexType": { "original": "TabChangeEventDetail", "resolved": "TabChangeEventDetail", "references": { "TabChangeEventDetail": { "location": "import", "path": "../tab/interfaces" } } } }, { "method": "calciteTabsClose", "name": "calciteTabsClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when a `calcite-tab` is closed." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabsClose", "name": "calciteInternalTabsClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "see", "text": "[TabChangeEventDetail](https://github.com/Esri/calcite-components/blob/master/src/components/tab/interfaces.ts)" }, { "name": "internal", "text": undefined }], "text": "Fires when `calcite-tab` is closed (`event.details`)." }, "complexType": { "original": "TabCloseEventDetail", "resolved": "TabCloseEventDetail", "references": { "TabCloseEventDetail": { "location": "import", "path": "../tab/interfaces" } } } }, { "method": "calciteInternalTabsFocusNext", "name": "calciteInternalTabsFocusNext", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabsFocusPrevious", "name": "calciteInternalTabsFocusPrevious", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabsFocusFirst", "name": "calciteInternalTabsFocusFirst", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabsFocusLast", "name": "calciteInternalTabsFocusLast", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalTabTitleRegister", "name": "calciteInternalTabTitleRegister", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "TabID", "resolved": "number | string", "references": { "TabID": { "location": "import", "path": "../tabs/interfaces" } } } }, { "method": "calciteInternalTabIconChanged", "name": "calciteInternalTabIconChanged", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get methods() { return { "getTabIndex": { "complexType": { "signature": "() => Promise<number>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<number>" }, "docs": { "text": "Returns the index of the title within the `calcite-tab-nav`.", "tags": [] } }, "getTabIdentifier": { "complexType": { "signature": "() => Promise<TabID>", "parameters": [], "references": { "Promise": { "location": "global" }, "TabID": { "location": "import", "path": "../tabs/interfaces" } }, "return": "Promise<TabID>" }, "docs": { "text": "", "tags": [{ "name": "internal", "text": undefined }] } }, "updateAriaInfo": { "complexType": { "signature": "(tabIds?: string[], titleIds?: string[]) => Promise<void>", "parameters": [{ "tags": [{ "name": "param", "text": "tabIds" }], "text": "" }, { "tags": [{ "name": "param", "text": "titleIds" }], "text": "" }], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [{ "name": "param", "text": "tabIds" }, { "name": "param", "text": "titleIds" }, { "name": "internal", "text": undefined }] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "selected", "methodName": "selectedHandler" }, { "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } static get listeners() { return [{ "name": "calciteInternalTabChange", "method": "internalTabChangeHandler", "target": "body", "capture": false, "passive": false }, { "name": "click", "method": "onClick", "target": undefined, "capture": false, "passive": false }, { "name": "keydown", "method": "keyDownHandler", "target": undefined, "capture": false, "passive": false }]; } }