UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

658 lines (657 loc) • 20.5 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 } from "@stencil/core"; import { findAssociatedForm, resetForm, submitForm } from "../../utils/form"; import { connectInteractive, disconnectInteractive, updateHostInteraction } from "../../utils/interactive"; import { connectLabel, disconnectLabel, getLabelText } from "../../utils/label"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { connectLocalized, disconnectLocalized } from "../../utils/locale"; import { createObserver } from "../../utils/observers"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import { CSS } from "./resources"; /** Passing a 'href' will render an anchor link, instead of a button. Role will be set to link, or button, depending on this. */ /** It is the consumers responsibility to add aria information, rel, target, for links, and any button attributes for form submission */ /** @slot - A slot for adding text. */ export class Button { constructor() { /** watches for changing text content */ this.mutationObserver = createObserver("mutation", () => this.updateHasContent()); this.resizeObserver = createObserver("resize", () => this.setTooltipText()); // act on a requested or nearby form based on type this.handleClick = () => { const { type } = this; if (this.href) { return; } // this.type refers to type attribute, not child element type if (type === "submit") { submitForm(this); } else if (type === "reset") { resetForm(this); } }; this.setTooltipText = () => { const { contentEl } = this; if (contentEl) { this.tooltipText = contentEl.offsetWidth < contentEl.scrollWidth ? this.el.innerText : null; } }; this.setChildEl = (el) => { this.childEl = el; if (el) { this.resizeObserver?.observe(el); } }; this.alignment = "center"; this.appearance = "solid"; this.label = undefined; this.kind = "brand"; this.disabled = false; this.form = undefined; this.href = undefined; this.iconEnd = undefined; this.iconFlipRtl = undefined; this.iconStart = undefined; this.loading = false; this.name = undefined; this.rel = undefined; this.round = false; this.scale = "m"; this.splitChild = false; this.target = undefined; this.type = "button"; this.width = "auto"; this.messages = undefined; this.messageOverrides = undefined; this.hasContent = false; this.hasLoader = false; this.effectiveLocale = ""; this.defaultMessages = undefined; this.tooltipText = undefined; } loadingChanged(newValue, oldValue) { if (!!newValue && !oldValue) { this.hasLoader = true; } if (!newValue && !!oldValue) { window.setTimeout(() => { this.hasLoader = false; }, 300); } } onMessagesChange() { /** referred in t9n util */ } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- async connectedCallback() { connectInteractive(this); connectLocalized(this); connectMessages(this); this.hasLoader = this.loading; this.setupTextContentObserver(); connectLabel(this); this.formEl = findAssociatedForm(this); } disconnectedCallback() { this.mutationObserver?.disconnect(); disconnectInteractive(this); disconnectLabel(this); disconnectLocalized(this); disconnectMessages(this); this.resizeObserver?.disconnect(); this.formEl = null; } async componentWillLoad() { setUpLoadableComponent(this); if (Build.isBrowser) { this.updateHasContent(); await setUpMessages(this); } } componentDidLoad() { setComponentLoaded(this); this.setTooltipText(); } componentDidRender() { updateHostInteraction(this); } render() { const childElType = this.href ? "a" : "button"; const Tag = childElType; const loaderNode = this.hasLoader ? (h("div", { class: CSS.buttonLoader }, h("calcite-loader", { class: this.loading ? CSS.loadingIn : CSS.loadingOut, inline: true, label: this.messages.loading, scale: this.scale === "l" ? "m" : "s" }))) : null; const noStartEndIcons = !this.iconStart && !this.iconEnd; const iconStartEl = (h("calcite-icon", { class: { [CSS.icon]: 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.icon]: true, [CSS.iconEnd]: true }, flipRtl: this.iconFlipRtl === "end" || this.iconFlipRtl === "both", icon: this.iconEnd, scale: this.scale === "l" ? "m" : "s" })); const contentEl = (h("span", { class: CSS.content, ref: (el) => (this.contentEl = el) }, h("slot", null))); return (h(Tag, { "aria-label": getLabelText(this), class: { [CSS.buttonPadding]: noStartEndIcons, [CSS.buttonPaddingShrunk]: !noStartEndIcons, [CSS.contentSlotted]: this.hasContent, [CSS.iconStartEmpty]: !this.iconStart, [CSS.iconEndEmpty]: !this.iconEnd }, disabled: this.disabled || this.loading, href: childElType === "a" && this.href, name: childElType === "button" && this.name, onClick: this.handleClick, ref: this.setChildEl, rel: childElType === "a" && this.rel, tabIndex: this.disabled || this.loading ? -1 : null, target: childElType === "a" && this.target, title: this.tooltipText, type: childElType === "button" && this.type }, loaderNode, this.iconStart ? iconStartEl : null, this.hasContent ? contentEl : null, this.iconEnd ? iconEndEl : null)); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { await componentLoaded(this); this.childEl?.focus(); } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } updateHasContent() { const slottedContent = this.el.textContent.trim().length > 0 || this.el.childNodes.length > 0; this.hasContent = this.el.childNodes.length === 1 && this.el.childNodes[0]?.nodeName === "#text" ? this.el.textContent?.trim().length > 0 : slottedContent; } setupTextContentObserver() { this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); } //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- onLabelClick() { this.handleClick(); this.setFocus(); } static get is() { return "calcite-button"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["button.scss"] }; } static get styleUrls() { return { "$": ["button.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "alignment": { "type": "string", "mutable": false, "complexType": { "original": "ButtonAlignment", "resolved": "\"center\" | \"end\" | \"icon-end-space-between\" | \"icon-start-space-between\" | \"space-between\" | \"start\"", "references": { "ButtonAlignment": { "location": "import", "path": "./interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the alignment of the component's elements." }, "attribute": "alignment", "reflect": true, "defaultValue": "\"center\"" }, "appearance": { "type": "string", "mutable": false, "complexType": { "original": "Extract<\n \"outline\" | \"outline-fill\" | \"solid\" | \"transparent\",\n Appearance\n >", "resolved": "\"outline\" | \"outline-fill\" | \"solid\" | \"transparent\"", "references": { "Extract": { "location": "global" }, "Appearance": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the appearance style of the component." }, "attribute": "appearance", "reflect": true, "defaultValue": "\"solid\"" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Accessible name for the component." }, "attribute": "label", "reflect": false }, "kind": { "type": "string", "mutable": false, "complexType": { "original": "Extract<\"brand\" | \"danger\" | \"inverse\" | \"neutral\", Kind>", "resolved": "\"brand\" | \"danger\" | \"inverse\" | \"neutral\"", "references": { "Extract": { "location": "global" }, "Kind": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the kind of the component (will apply to border and background if applicable)." }, "attribute": "kind", "reflect": true, "defaultValue": "\"brand\"" }, "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" }, "form": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The ID of the form that will be associated with the component.\n\nWhen not set, the component will be associated with its ancestor form element, if any." }, "attribute": "form", "reflect": true }, "href": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the URL of the linked resource, which can be set as an absolute or relative path." }, "attribute": "href", "reflect": true }, "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 }, "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 and interaction is disabled." }, "attribute": "loading", "reflect": true, "defaultValue": "false" }, "name": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Specifies the name of the component on form submission." }, "attribute": "name", "reflect": true }, "rel": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[rel](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)" }], "text": "Defines the relationship between the `href` value and the current document." }, "attribute": "rel", "reflect": true }, "round": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, adds a round style to the component." }, "attribute": "round", "reflect": true, "defaultValue": "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\"" }, "splitChild": { "type": "any", "mutable": false, "complexType": { "original": "\"primary\" | \"secondary\" | false", "resolved": "\"primary\" | \"secondary\" | boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies if the component is a child of a `calcite-split-button`." }, "attribute": "split-child", "reflect": true, "defaultValue": "false" }, "target": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target)" }], "text": "Specifies where to open the linked document defined in the `href` property." }, "attribute": "target", "reflect": true }, "type": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type)" }], "text": "Specifies the default behavior of the button." }, "attribute": "type", "reflect": true, "defaultValue": "\"button\"" }, "width": { "type": "string", "mutable": false, "complexType": { "original": "Width", "resolved": "\"auto\" | \"full\" | \"half\"", "references": { "Width": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the width of the component." }, "attribute": "width", "reflect": true, "defaultValue": "\"auto\"" }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "ButtonMessages", "resolved": "{ loading: string; }", "references": { "ButtonMessages": { "location": "import", "path": "./assets/button/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<ButtonMessages>", "resolved": "{ loading?: string; }", "references": { "Partial": { "location": "global" }, "ButtonMessages": { "location": "import", "path": "./assets/button/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } } }; } static get states() { return { "hasContent": {}, "hasLoader": {}, "effectiveLocale": {}, "defaultMessages": {}, "tooltipText": {} }; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets focus on the component.", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "loading", "methodName": "loadingChanged" }, { "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } }