@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
694 lines (693 loc) • 22.7 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 { 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"
}];
}
}