import { LitElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element'; import { classMap } from 'lit-html/directives/class-map'; import { ifDefined } from 'lit-html/directives/if-defined'; import { event, EventEmitter, tag, watch } from '../../internal/decorators'; import styles from 'sass:./textarea.scss'; import { renderFormControl } from '../../internal/form-control'; import { hasSlot } from '../../internal/slot'; let id = 0; /** * @since 2.0 * @status stable * * @slot label - The textarea's label. Alternatively, you can use the label prop. * @slot help-text - Help text that describes how to use the input. * * @part base - The component's base wrapper. * @part form-control - The form control that wraps the label, textarea, and help text. * @part label - The textarea label. * @part textarea - The textarea control. * @part help-text - The textarea help text. */ @tag('sl-textarea') export default class SlTextarea extends LitElement { static styles = unsafeCSS(styles); @query('.textarea__control') input: HTMLTextAreaElement; private helpTextId = `textarea-help-text-${id}`; private inputId = `textarea-${++id}`; private labelId = `textarea-label-${id}`; private resizeObserver: ResizeObserver; @internalProperty() private hasFocus = false; @internalProperty() private hasHelpTextSlot = false; @internalProperty() private hasLabelSlot = false; /** The textarea's size. */ @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; /** The textarea's name attribute. */ @property() name: string; /** The textarea's value attribute. */ @property() value = ''; /** The textarea's label. Alternatively, you can use the label slot. */ @property() label: string; /** The textarea's help text. Alternatively, you can use the help-text slot. */ @property({ attribute: 'help-text' }) helpText: string; /** The textarea's placeholder text. */ @property() placeholder = ''; /** The number of rows to display by default. */ @property({ type: Number }) rows = 4; /** Controls how the textarea can be resized. */ @property() resize: 'none' | 'vertical' | 'auto' = 'vertical'; /** Disables the textarea. */ @property({ type: Boolean, reflect: true }) disabled = false; /** Makes the textarea readonly. */ @property({ type: Boolean, reflect: true }) readonly = false; /** The minimum length of input that will be considered valid. */ @property({ type: Number }) minlength: number; /** The maximum length of input that will be considered valid. */ @property({ type: Number }) maxlength: number; /** A pattern to validate input against. */ @property() pattern: string; /** Makes the textarea a required field. */ @property({ type: Boolean, reflect: true }) required = false; /** * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, * `required`, `minlength`, and `maxlength` using the browser's constraint validation API. */ @property({ type: Boolean, reflect: true }) invalid = false; /** The textarea's autocaptialize attribute. */ @property() autocapitalize: string; /** The textarea's autocorrect attribute. */ @property() autocorrect: string; /** The textarea's autocomplete attribute. */ @property() autocomplete: string; /** The textarea's autofocus attribute. */ @property({ type: Boolean }) autofocus: boolean; /** Enables spell checking on the textarea. */ @property({ type: Boolean }) spellcheck: boolean; /** The textarea's inputmode attribute. */ @property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** Emitted when the control's value changes. */ @event('sl-change') slChange: EventEmitter; /** Emitted when the control receives input. */ @event('sl-input') slInput: EventEmitter; /** Emitted when the control gains focus. */ @event('sl-focus') slFocus: EventEmitter; /** Emitted when the control loses focus. */ @event('sl-blur') slBlur: EventEmitter; connectedCallback() { super.connectedCallback(); this.handleSlotChange = this.handleSlotChange.bind(this); this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); this.handleSlotChange(); } firstUpdated() { this.setTextareaHeight(); this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); this.resizeObserver.observe(this.input); } disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.unobserve(this.input); this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); } /** Sets focus on the textarea. */ setFocus(options?: FocusOptions) { this.input.focus(options); } /** Removes focus from the textarea. */ removeFocus() { this.input.blur(); } /** Selects all the text in the textarea. */ select() { return this.input.select(); } /** Sets the start and end positions of the text selection (0-based). */ setSelectionRange( selectionStart: number, selectionEnd: number, selectionDirection: 'forward' | 'backward' | 'none' = 'none' ) { return this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection); } /** Replaces a range of text with a new string. */ setRangeText( replacement: string, start: number, end: number, selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve' ) { this.input.setRangeText(replacement, start, end, selectMode); if (this.value !== this.input.value) { this.value = this.input.value; this.slInput.emit(); } if (this.value !== this.input.value) { this.value = this.input.value; this.setTextareaHeight(); this.slInput.emit(); this.slChange.emit(); } } /** Checks for validity and shows the browser's validation message if the control is invalid. */ reportValidity() { return this.input.reportValidity(); } /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ setCustomValidity(message: string) { this.input.setCustomValidity(message); this.invalid = !this.input.checkValidity(); } handleChange() { this.value = this.input.value; this.slChange.emit(); } handleInput() { this.value = this.input.value; this.setTextareaHeight(); this.slInput.emit(); } handleBlur() { this.hasFocus = false; this.slBlur.emit(); } handleFocus() { this.hasFocus = true; this.slFocus.emit(); } @watch('rows') handleRowsChange() { this.setTextareaHeight(); } @watch('helpText') @watch('label') handleSlotChange() { this.hasHelpTextSlot = hasSlot(this, 'help-text'); this.hasLabelSlot = hasSlot(this, 'label'); } @watch('value') handleValueChange() { this.invalid = !this.input.checkValidity(); } setTextareaHeight() { if (this.resize === 'auto') { this.input.style.height = 'auto'; this.input.style.height = this.input.scrollHeight + 'px'; } else { (this.input.style.height as string | undefined) = undefined; } } render() { return renderFormControl( { inputId: this.inputId, label: this.label, labelId: this.labelId, hasLabelSlot: this.hasLabelSlot, helpTextId: this.helpTextId, helpText: this.helpText, hasHelpTextSlot: this.hasHelpTextSlot, size: this.size }, html`
` ); } } declare global { interface HTMLElementTagNameMap { 'sl-textarea': SlTextarea; } }