PageSourceSearch

https://ux-components.fcsamerica.com/dist/src/components/text-area/text-area.js

js fcsamerica.com collected 2026-10-02 05:03:27 UTC 16,941 bytes, 424 lines download raw bytes

1/*!
2 * pivot v1.2.0
3 * FCSA Pivot Design System
4 * https://careers.fcsamerica.com/
5 */
6import { __decorate } from '../../../node_modules/tslib/tslib.es6.js';
7import '../../../node_modules/@lit/reactive-element/reactive-element.js';
8import { nothing as E, html as x } from '../../../node_modules/lit-html/lit-html.js';
9import '../../../node_modules/lit-element/lit-element.js';
10import { customElement as t } from '../../../node_modules/@lit/reactive-element/decorators/custom-element.js';
11import { property as n } from '../../../node_modules/@lit/reactive-element/decorators/property.js';
12import { state as r } from '../../../node_modules/@lit/reactive-element/decorators/state.js';
13import { ifDefined as o } from '../../../node_modules/lit-html/directives/if-defined.js';
14import { unsafeSVG as o$1 } from '../../../node_modules/lit-html/directives/unsafe-svg.js';
15import { nanoid } from '../../../node_modules/nanoid/index.browser.js';
16import { styles } from './text-area.css.js';
17import { styles as styles$1 } from '../field-label/field-label.css.js';
18import { PivotBase } from '../base/base.js';
19import { errorIcon } from './constants.js';
20import { renderFieldLabel } from '../field-label/render-field-label.js';
21import '../field-helper-message/field-helper-message.js';
22import { FormAssociationController } from '../base/form-association-controller.js';
23
24const tagName = "pivot-text-area";
25/* eslint-disable jsdoc/valid-types */
26/**
27 * A multi-line text input component with optional character count and helper text.
28 *
29 * **Accessibility:** In the error state (`pivot-error`, `pivot-error-text`, or a
30 * displayed native validation message), `aria-invalid="true"` is applied
31 * to the native textarea (WCAG 4.1.2). When helper or error text is present, the
32 * native textarea is linked to the `pivot-field-helper-message` via `aria-describedby`
33 * so assistive technology announces the message on focus (WCAG 1.3.1). The field
34 * label is rendered by the shared `renderFieldLabel` helper directly into this
35 * component's own shadow root — the same tree scope as the `<textarea>` — so its
36 * `<label for>` resolves to a working native label association and clicking or
37 * tapping the label focuses the textarea. The textarea's accessible name is still
38 * set explicitly via `aria-label` — `pivot-aria-label` if provided, otherwise the
39 * visible `pivot-label` text — since `aria-label` takes precedence over the
40 * associated `<label>` in accessible-name computation, letting `pivot-aria-label`
41 * override the visible text when the two need to differ (WCAG 4.1.2).
42 *
43 * @element pivot-text-area
44 *
45 * @fires pivot-text-area-input - Fired on every keystroke with the current value in `detail.value`.
46 *
47 * @slot field-label-tooltip - Optional tooltip content rendered beside the field label
48 *
49 * @csspart container - The outer wrapper element
50 * @csspart textarea - The native textarea element
51 *
52 * @cssprop --component-field-gap - Gap between the label, textarea, and helper/error message regions
53 * @cssprop --component-field-label-gap - Gap between the label text, optional/required indicator, and tooltip icon
54 * @cssprop --global-interaction-md - Minimum block size (height) of the textarea
55 * @cssprop --component-field-control-input-placeholder - Placeholder text color
56 * @cssprop --component-field-control-border-color-error - Border color of the textarea in the error state
57 * @cssprop --component-field-control-general-inset-y - Vertical padding reserved inside the textarea for the character counter when shown
58 * @cssprop --component-input-field-inset-y - Vertical position of the character counter within the textarea
59 * @cssprop --component-input-field-inset-x - Horizontal position of the character counter within the textarea
60 * @cssprop --component-field-helper-gap - Gap between the icon and text within the character counter
61 * @cssprop --global-color-text-on-light-error-color - Text color of the required-field indicator, and of the character counter when the limit is exceeded
62 */
63let PivotTextArea = class PivotTextArea extends PivotBase {
64    /**
65     * Imported CSS styles from text-area.css
66     */
67    static { this.styles = [
68        PivotBase.convertCssForComponent(tagName, styles),
69        PivotBase.convertCssForComponent(tagName, styles$1),
70    ]; }
71    static { this.formAssociated = true; }
72    constructor() {
73        super();
74        this._formController = new FormAssociationController(this);
75        /**
76         * The accessible name for the textarea. The field label is rendered by the
77         * shared `renderFieldLabel` helper directly into this component's own
78         * shadow root, in the same tree scope as the `<textarea>`, so its
79         * `<label for>` does resolve to a working native label association — but
80         * the textarea's `aria-label` still takes precedence in accessible-name
81         * computation, and falls back to the visible `pivotLabel` text
82         * automatically when this is unset, so this only needs to be set to
83         * override that text.
84         *
85         * @type {string}
86         * @attr pivot-aria-label
87         * @default ""
88         */
89        this.pivotAriaLabel = "";
90        /**
91         * This will create an attribute: pivot-name="value"
92         *
93         * @type {string}
94         * @attr pivot-name
95         * @default
96         */
97        this.pivotName = "";
98        /**
99         * This will create an attribute: pivot-id="value"
100         *
101         * @type {string}
102         * @attr pivot-id
103         * @default ""
104         */
105        this.pivotId = "";
106        /**
107         * Placeholder text shown inside the textarea when empty.
108         *
109         * @type {string | null}
110         * @attr pivot-placeholder
111         * @default null
112         */
113        this.pivotPlaceholder = null;
114        /**
115         * Helper text displayed below the textarea. Shown automatically when a value is provided.
116         *
117         * @type {string}
118         * @attr pivot-helper-text
119         * @default ""
120         */
121        this.pivotHelperText = "";
122        /**
123         * Error message displayed below the textarea. Setting a value puts the
124         * component into its error state and takes priority over both
125         * `pivot-helper-text` and the browser's native validation message.
126         *
127         * @type {string}
128         * @attr pivot-error-text
129         * @default ""
130         */
131        this.pivotErrorText = "";
132        /**
133         * Puts the component into an error state, applying error styling and
134         * `aria-invalid="true"`. Does not affect the element's validity. The error
135         * message itself comes from `pivot-error-text` or, after a failed
136         * validation check, the browser's native validation message.
137         *
138         * @type {boolean}
139         * @attr pivot-error
140         * @default false
141         */
142        this.pivotError = false;
143        /**
144         * Marks the field as optional, displaying an "(optional)" label next to the field label.
145         *
146         * @type {boolean}
147         * @attr pivot-optional
148         * @default false
149         */
150        this.pivotOptional = false;
151        /**
152         * Hides the required asterisk, optional text, and tooltip on the field label.
153         * Set by parent components (e.g. `pivot-radio`, `pivot-checkbox`) that show them on their legend.
154         *
155         * @type {boolean}
156         * @default false
157         */
158        this.pivotHideLabelIndicators = false;
159        /**
160         * Whether the field-label-tooltip slot has assigned content.
161         *
162         * @type {boolean}
163         * @default false
164         */
165        this.hasFieldLabelTooltip = false;
166        /**
167         * Disables the textarea, preventing user interaction.
168         *
169         * @type {boolean}
170         * @attr pivot-disabled
171         * @default false
172         */
173        this.pivotDisabled = false;
174        /**
175         * Maximum number of characters. When set, a character counter is shown automatically.
176         * The counter enters an error state when the limit is exceeded.
177         *
178         * @type {number | null}
179         * @attr pivot-max-length
180         * @default null
181         */
182        this.pivotMaxLength = null;
183        /**
184         * Marks the field as required.
185         *
186         * @type {boolean}
187         * @attr pivot-required
188         * @default false
189         */
190        this.pivotRequired = false;
191        /**
192         * Number of visible text rows.
193         *
194         * @type {number}
195         * @attr pivot-rows
196         * @default 4
197         */
198        this.pivotRows = 4;
199        /**
200         * Current character count, updated on every input event.
201         *
202         * @type {number}
203         * @default 0
204         */
205        this._charCount = 0;
206        this.uniqueID = nanoid(10);
207    }
208    connectedCallback() {
209        super.connectedCallback?.();
210        this.prepareSlots();
211    }
212    // Restores slot attributes stripped by SSG tools (e.g. WebC) at build time.
213    prepareSlots() {
214        for (const child of Array.from(this.children)) {
215            if (child.tagName === "PIVOT-FIELD-LABEL-TOOLTIP" &&
216                !child.getAttribute("slot")) {
217                child.setAttribute("slot", "field-label-tooltip");
218            }
219        }
220    }
221    get _isOverLimit() {
222        return (this.pivotMaxLength !== null && this._charCount > this.pivotMaxLength);
223    }
224    willUpdate(changedProperties) {
225        super.willUpdate(changedProperties);
226        if (!this.pivotName.trim()) {
227            throw new Error("pivot-text-area requires pivot-name.");
228        }
229        this.hasFieldLabelTooltip = Array.from(this.children).some((child) => child.slot === "field-label-tooltip" ||
230            child.tagName === "PIVOT-FIELD-LABEL-TOOLTIP");
231        // Drives the CSS-only error state; keeps `pivotError` free for consumer control.
232        this.toggleAttribute("has-error", this.pivotError ||
233            !!this.pivotErrorText ||
234            !!this._formController.nativeErrorText);
235    }
236    updated(changedProperties) {
237        super.updated(changedProperties);
238        if (changedProperties.has("pivotName")) {
239            if (this.pivotName) {
240                this.setAttribute("name", this.pivotName);
241            }
242            else {
243                this.removeAttribute("name");
244            }
245        }
246        this.syncFormState();
247    }
248    /**
249     * Syncs the component's form value and validity state with ElementInternals.
250     * Called after each render and after user input so the associated form
251     * always has an accurate value and constraint-validation state.
252     *
253     * @returns {void}
254     */
255    syncFormState() {
256        const textarea = this.renderRoot?.querySelector("textarea");
257        const value = textarea?.value ?? "";
258        this._formController.setFormValue(value);
259        if (textarea) {
260            if (textarea.validity.valid) {
261                this._formController.setValidity({});
262            }
263            else {
264                this._formController.setValidity({
265                    badInput: textarea.validity.badInput,
266                    customError: textarea.validity.customError,
267                    tooLong: textarea.validity.tooLong,
268                    tooShort: textarea.validity.tooShort,
269                    valueMissing: textarea.validity.valueMissing,
270                }, textarea.validationMessage, textarea);
271            }
272        }
273    }
274    /**
275     * Handles native textarea input events, updating the character count
276     * and syncing form state.
277     *
278     * @param {Event} event - The input event.
279     * @returns {void}
280     */
281    handleInput(event) {
282        const target = event.target;
283        if (!(target instanceof HTMLTextAreaElement))
284            return;
285        this._charCount = target.value.length;
286        this.syncFormState();
287        this.dispatchEvent(new CustomEvent("pivot-text-area-input", {
288            detail: { value: target.value },
289            bubbles: true,
290            composed: true,
291        }));
292    }
293    /**
294     * Resets the textarea and form state when the associated form resets.
295     *
296     * @returns {void}
297     */
298    formResetCallback() {
299        const textarea = this.renderRoot?.querySelector("textarea");
300        if (textarea)
301            textarea.value = "";
302        this._charCount = 0;
303        this._formController.setFormValue("");
304        this._formController.setValidity({});
305    }
306    checkValidity() {
307        return this._formController.checkValidity();
308    }
309    reportValidity() {
310        return this._formController.reportValidity();
311    }
312    formDisabledCallback(disabled) {
313        return this._formController.formDisabledCallback(disabled);
314    }
315    render() {
316        const showCharCount = this.pivotMaxLength !== null;
317        const showCounterError = showCharCount && this._isOverLimit;
318        const helperId = `${this.pivotName}-helper`;
319        const controlId = this.pivotId || `${this.pivotName}-${this.uniqueID}`;
320        const nativeError = this._formController.nativeErrorText;
321        const showError = this.pivotError || !!this.pivotErrorText || !!nativeError;
322        const errorText = this.pivotErrorText || nativeError;
323        const hasMessage = !!(this.pivotHelperText || errorText);
324        return x ` <div class="text-area-container" part="container">
325      ${renderFieldLabel({
326            id: controlId,
327            pivotLabel: this.pivotLabel,
328            pivotOptional: this.pivotOptional,
329            pivotRequired: this.pivotRequired,
330            hasFieldLabelTooltip: this.hasFieldLabelTooltip,
331            hideIndicators: this.pivotHideLabelIndicators,
332        })}
333      <div
334        class="text-area-input-wrapper${showCharCount ? " has-char-count" : ""}"
335      >
336        <textarea
337          id="${controlId}"
338          part="textarea"
339          name=${o(this.pivotName || undefined)}
340          placeholder=${o(this.pivotPlaceholder ?? undefined)}
341          rows=${this.pivotRows}
342          ?disabled=${this.pivotDisabled}
343          ?required=${this.pivotRequired}
344          aria-label=${o(this.pivotAriaLabel || this.pivotLabel || undefined)}
345          @input=${this.handleInput}
346          aria-invalid=${showError ? "true" : "false"}
347          aria-describedby=${o(hasMessage ? helperId : undefined)}
348        ></textarea>
349        ${showCharCount
350            ? x `<span
351              class="text-area-char-count${showCounterError ? " error" : ""}"
352              aria-live="polite"
353            >
354              ${showCounterError ? o$1(errorIcon) : E}
355              ${this._charCount} / ${this.pivotMaxLength}
356            </span>`
357            : E}
358      </div>
359
360      ${this.pivotHelperText || hasMessage
361            ? x ` <pivot-field-helper-message
362            id="${helperId}"
363            .pivotError=${showError}
364            .pivotErrorText=${errorText}
365            .pivotHelperText=${this.pivotHelperText}
366          ></pivot-field-helper-message>`
367            : E}
368    </div>`;
369    }
370};
371__decorate([
372    n({ type: String, attribute: "pivot-label" })
373], PivotTextArea.prototype, "pivotLabel", void 0);
374__decorate([
375    n({ type: String, attribute: "pivot-aria-label" })
376], PivotTextArea.prototype, "pivotAriaLabel", void 0);
377__decorate([
378    n({ type: String, attribute: "pivot-name" })
379], PivotTextArea.prototype, "pivotName", void 0);
380__decorate([
381    n({ type: String, attribute: "pivot-id" })
382], PivotTextArea.prototype, "pivotId", void 0);
383__decorate([
384    n({ type: String, attribute: "pivot-placeholder" })
385], PivotTextArea.prototype, "pivotPlaceholder", void 0);
386__decorate([
387    n({ type: String, attribute: "pivot-helper-text" })
388], PivotTextArea.prototype, "pivotHelperText", void 0);
389__decorate([
390    n({ type: String, attribute: "pivot-error-text" })
391], PivotTextArea.prototype, "pivotErrorText", void 0);
392__decorate([
393    n({ type: Boolean, attribute: "pivot-error" })
394], PivotTextArea.prototype, "pivotError", void 0);
395__decorate([
396    n({ type: Boolean, attribute: "pivot-optional" })
397], PivotTextArea.prototype, "pivotOptional", void 0);
398__decorate([
399    n({ type: Boolean, attribute: false })
400], PivotTextArea.prototype, "pivotHideLabelIndicators", void 0);
401__decorate([
402    r()
403], PivotTextArea.prototype, "hasFieldLabelTooltip", void 0);
404__decorate([
405    n({ type: Boolean, attribute: "pivot-disabled" })
406], PivotTextArea.prototype, "pivotDisabled", void 0);
407__decorate([
408    n({ type: Number, attribute: "pivot-max-length" })
409], PivotTextArea.prototype, "pivotMaxLength", void 0);
410__decorate([
411    n({ type: Boolean, attribute: "pivot-required" })
412], PivotTextArea.prototype, "pivotRequired", void 0);
413__decorate([
414    n({ type: Number, attribute: "pivot-rows" })
415], PivotTextArea.prototype, "pivotRows", void 0);
416__decorate([
417    r()
418], PivotTextArea.prototype, "_charCount", void 0);
419PivotTextArea = __decorate([
420    t(tagName)
421], PivotTextArea);
422
423export { PivotTextArea };
424//# sourceMappingURL=text-area.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.