UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

694 lines (693 loc) • 22.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 { h, Host } from "@stencil/core"; import { connectForm, disconnectForm, HiddenFormInputSlot } from "../../utils/form"; import { connectLabel, disconnectLabel, getLabelText } from "../../utils/label"; import { slotChangeHasAssignedElement, toAriaBoolean } from "../../utils/dom"; import { CSS, SLOTS, RESIZE_TIMEOUT } from "./resources"; import { connectLocalized, disconnectLocalized, numberStringFormatter } from "../../utils/locale"; import { createObserver } from "../../utils/observers"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import { throttle } from "lodash-es"; import { connectInteractive, disconnectInteractive, updateHostInteraction } from "../../utils/interactive"; /** * @slot - A slot for adding text. * @slot footer-start - A slot for adding content to the start of the component's footer. * @slot footer-end - A slot for adding content to the end of the component's footer. */ export class TextArea { constructor() { this.handleInput = (event) => { this.value = event.target["value"]; this.calciteTextAreaInput.emit(); }; this.handleChange = () => { this.calciteTextAreaChange.emit(); }; this.contentSlotChangeHandler = () => { if (!this.value) { const nodes = this.el.childNodes; nodes.forEach((el) => { if (el.nodeName === "#text") { this.value = el.nodeValue.trim(); } }); } }; this.renderCharacterLimit = () => { return this.maxLength ? (h("span", { class: CSS.characterLimit }, h("span", { class: { [CSS.characterOverLimit]: this.value?.length > this.maxLength } }, this.getLocalizedCharacterLength()), "/", numberStringFormatter.localize(this.maxLength.toString()))) : null; }; this.resizeObserver = createObserver("resize", async () => { await componentLoaded(this); const { textAreaHeight, textAreaWidth, elHeight, elWidth, footerHeight, footerWidth } = this.getHeightandWidthOfElements(); if (footerWidth > 0 && footerWidth !== textAreaWidth) { this.footerEl.style.width = `${textAreaWidth}px`; } if (elWidth !== textAreaWidth || elHeight !== textAreaHeight + (footerHeight || 0)) { this.setHeightAndWidthToAuto(); } }); // height and width are set to auto here to avoid overlapping on to neighboring elements in the layout when user starts resizing. // throttle is used to avoid flashing of textarea when user resizes. this.setHeightAndWidthToAuto = throttle(() => { if (this.resize === "vertical" || this.resize === "both") { this.el.style.height = "auto"; } if (this.resize === "horizontal" || this.resize === "both") { this.el.style.width = "auto"; } }, RESIZE_TIMEOUT, { leading: false }); this.setTextAreaEl = (el) => { this.textAreaEl = el; this.resizeObserver.observe(el); }; this.autofocus = false; this.columns = undefined; this.disabled = false; this.form = undefined; this.groupSeparator = false; this.label = undefined; this.maxLength = undefined; this.messages = undefined; this.name = undefined; this.numberingSystem = undefined; this.placeholder = undefined; this.readOnly = false; this.required = false; this.resize = "both"; this.rows = undefined; this.scale = "m"; this.value = undefined; this.wrap = "soft"; this.messageOverrides = undefined; this.defaultMessages = undefined; this.endSlotHasElements = undefined; this.startSlotHasElements = undefined; this.effectiveLocale = ""; } onMessagesChange() { /* wired up by t9n util */ } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectInteractive(this); connectLabel(this); connectForm(this); connectLocalized(this); connectMessages(this); } async componentWillLoad() { setUpLoadableComponent(this); await setUpMessages(this); } componentDidLoad() { setComponentLoaded(this); } componentDidRender() { updateHostInteraction(this); this.setTextAreaHeight(); } disconnectedCallback() { disconnectInteractive(this); disconnectLabel(this); disconnectForm(this); disconnectLocalized(this); disconnectMessages(this); this.resizeObserver?.disconnect(); } render() { const hasFooter = this.startSlotHasElements || this.endSlotHasElements || !!this.maxLength; return (h(Host, null, h("textarea", { "aria-invalid": toAriaBoolean(this.value?.length > this.maxLength), "aria-label": getLabelText(this), autofocus: this.autofocus, class: { [CSS.readOnly]: this.readOnly, [CSS.textAreaInvalid]: this.value?.length > this.maxLength, [CSS.footerSlotted]: this.endSlotHasElements && this.startSlotHasElements, [CSS.blockSizeFull]: !hasFooter, [CSS.borderColor]: !hasFooter }, cols: this.columns, disabled: this.disabled, name: this.name, onChange: this.handleChange, onInput: this.handleInput, placeholder: this.placeholder, readonly: this.readOnly, required: this.required, rows: this.rows, value: this.value, wrap: this.wrap, // eslint-disable-next-line react/jsx-sort-props ref: this.setTextAreaEl }), h("span", { class: { [CSS.content]: true } }, h("slot", { onSlotchange: this.contentSlotChangeHandler })), h("footer", { class: { [CSS.footer]: true, [CSS.readOnly]: this.readOnly, [CSS.hide]: !hasFooter }, ref: (el) => (this.footerEl = el) }, h("div", { class: { [CSS.container]: true, [CSS.footerEndSlotOnly]: !this.startSlotHasElements && this.endSlotHasElements } }, h("slot", { name: SLOTS.footerStart, onSlotchange: (event) => (this.startSlotHasElements = slotChangeHasAssignedElement(event)) }), h("slot", { name: SLOTS.footerEnd, onSlotchange: (event) => (this.endSlotHasElements = slotChangeHasAssignedElement(event)) })), this.renderCharacterLimit()), h(HiddenFormInputSlot, { component: this }))); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { await componentLoaded(this); this.textAreaEl.focus(); } /** Selects the text of the component's `value`. */ async selectText() { await componentLoaded(this); this.textAreaEl.select(); } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- onFormReset() { this.value = this.defaultValue; } onLabelClick() { this.setFocus(); } getLocalizedCharacterLength() { numberStringFormatter.numberFormatOptions = { locale: this.effectiveLocale, numberingSystem: this.numberingSystem, signDisplay: "never", useGrouping: this.groupSeparator }; return numberStringFormatter.localize(this.value ? this.value.length.toString() : "0"); } syncHiddenFormInput(input) { input.setCustomValidity(""); if (this.value?.length > this.maxLength) { input.setCustomValidity(this.messages.tooLong); } } setTextAreaHeight() { const { textAreaHeight, elHeight, footerHeight } = this.getHeightandWidthOfElements(); if (footerHeight > 0 && textAreaHeight + footerHeight != elHeight) { this.textAreaEl.style.height = `${elHeight - footerHeight}px`; } } getHeightandWidthOfElements() { const { height: textAreaHeight, width: textAreaWidth } = this.textAreaEl.getBoundingClientRect(); const { height: elHeight, width: elWidth } = this.el.getBoundingClientRect(); const { height: footerHeight, width: footerWidth } = this.footerEl?.getBoundingClientRect(); return { textAreaHeight, textAreaWidth, elHeight, elWidth, footerHeight, footerWidth }; } static get is() { return "calcite-text-area"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["text-area.scss"] }; } static get styleUrls() { return { "$": ["text-area.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "autofocus": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[autofocus](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus)" }], "text": "When `true`, the component is focused on page load. Only one element can contain `autofocus`. If multiple elements have `autofocus`, the first element will receive focus." }, "attribute": "autofocus", "reflect": true, "defaultValue": "false" }, "columns": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[cols](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-cols)" }], "text": "Specifies the component's number of columns." }, "attribute": "columns", "reflect": true }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[disabled](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled)" }], "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 }, "groupSeparator": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, number values are displayed with a group separator corresponding to the language and country format." }, "attribute": "group-separator", "reflect": true, "defaultValue": "false" }, "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 }, "maxLength": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[maxlength](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-maxlength)" }], "text": "Specifies the maximum number of characters allowed." }, "attribute": "max-length", "reflect": true }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "TextAreaMessages", "resolved": "{ invalid: string; tooLong: string; longText: string; }", "references": { "TextAreaMessages": { "location": "import", "path": "./assets/text-area/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Made into a prop for testing purposes only" } }, "name": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[name](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-name)" }], "text": "Specifies the name of the component." }, "attribute": "name", "reflect": true }, "numberingSystem": { "type": "string", "mutable": false, "complexType": { "original": "NumberingSystem", "resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"", "references": { "NumberingSystem": { "location": "import", "path": "../../utils/locale" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the Unicode numeral system used by the component for localization." }, "attribute": "numbering-system", "reflect": false }, "placeholder": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[placeholder](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-placeholder)" }], "text": "Specifies the placeholder text for the component." }, "attribute": "placeholder", "reflect": false }, "readOnly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "readonly", "text": undefined }, { "name": "mdn", "text": "[readOnly](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly)" }], "text": "When `true`, the component's `value` can be read, but cannot be modified." }, "attribute": "read-only", "reflect": true, "defaultValue": "false" }, "required": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[required]https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required" }], "text": "When `true`, the component must have a value in order for the form to submit." }, "attribute": "required", "reflect": true, "defaultValue": "false" }, "resize": { "type": "string", "mutable": false, "complexType": { "original": "\"both\" | \"horizontal\" | \"vertical\" | \"none\"", "resolved": "\"both\" | \"horizontal\" | \"none\" | \"vertical\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies if the component is resizable." }, "attribute": "resize", "reflect": true, "defaultValue": "\"both\"" }, "rows": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[rows](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-rows)" }], "text": "Specifies the component's number of rows." }, "attribute": "rows", "reflect": true }, "scale": { "type": "string", "mutable": false, "complexType": { "original": "\"l\" | \"m\" | \"s\"", "resolved": "\"l\" | \"m\" | \"s\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the size of the component." }, "attribute": "scale", "reflect": true, "defaultValue": "\"m\"" }, "value": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The component's value." }, "attribute": "value", "reflect": false }, "wrap": { "type": "string", "mutable": false, "complexType": { "original": "\"soft\" | \"hard\"", "resolved": "\"hard\" | \"soft\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[wrap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-wrap)" }], "text": "Specifies the wrapping mechanism for the text." }, "attribute": "wrap", "reflect": true, "defaultValue": "\"soft\"" }, "messageOverrides": { "type": "unknown", "mutable": true, "complexType": { "original": "Partial<TextAreaMessages>", "resolved": "{ invalid?: string; tooLong?: string; longText?: string; }", "references": { "Partial": { "location": "global" }, "TextAreaMessages": { "location": "import", "path": "./assets/text-area/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } } }; } static get states() { return { "defaultMessages": {}, "endSlotHasElements": {}, "startSlotHasElements": {}, "effectiveLocale": {} }; } static get events() { return [{ "method": "calciteTextAreaInput", "name": "calciteTextAreaInput", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Fires each time a new `value` is typed." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteTextAreaChange", "name": "calciteTextAreaChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Fires each time a new `value` is typed and committed." }, "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.", "tags": [] } }, "selectText": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Selects the text of the component's `value`.", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } }