{"slug":"text-editor","name":"TextEditor","packageName":"@liquidify/react","category":"controls","maturity":"stable","summary":"A user-editable control for entering freeform multi-paragraph text — the canonical glass long-form editor.","importPath":"@liquidify/react/text-editor","sourcePath":"packages/@liquidify/react/src/components/text-editor/text-editor.tsx","specPath":"packages/@liquidify/react/src/components/text-editor/spec.md","docsPath":"../liquidify-page/content/component-source/text-editor.md","documentationStatus":"stable","storyIds":["controls-text-editor--default"],"additionalExports":["LineLimit","LiquidColor","LiquidTintValue","TextEditorInputMode","TextEditorResizable","TextEditorSizing","TintableControlProps"],"anatomy":[],"props":[{"name":"autoComplete","type":"string | undefined","required":false,"description":"Native `autocomplete` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"className","type":"string | undefined","required":false,"description":"Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"defaultValue","type":"string | undefined","required":false,"description":"Uncontrolled seed for the internal value (ignored once {@link TextSurfaceProps.value} is supplied). Read once, on mount.","defaultValue":"`\"\"`","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"disabled","type":"boolean | undefined","required":false,"description":"When `true`, the native `disabled` attribute is set on the inner element: the surface is removed from the tab order and all input is suppressed. `aria-disabled` is NOT set — the native attribute already provides full-suppression semantics.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"id","type":"string | undefined","required":false,"description":"Explicit id for the native element. Falls back to a generated stable id so an external `<label htmlFor>` or error message can target it (A16).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"inputMode","type":"TextInputMode | undefined","required":false,"description":"Native `inputmode` passthrough — hints the on-screen keyboard variant.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"invalid","type":"boolean | undefined","required":false,"description":"Marks the surface invalid — sets `aria-invalid=\"true\"` for AT / validation styling. An explicit `aria-invalid` on the same element always wins (including `false` / `\"grammar\"` / `\"spelling\"`); `invalid` is only the shorthand fallback.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"lineLimit","type":"LineLimit | undefined","required":false,"description":"Row-range bound. `lineLimit.min` is the region's initial height and drives the native `rows` attribute; `lineLimit.max` caps growth under `sizing=\"relative\"` and is inert under the other two modes. See {@link LineLimit} for the sanitisation policy applied at render time.","defaultValue":"`{ min: 6, max: 12 }`","declaredIn":"packages/@liquidify/react/src/components/text-editor/text-editor.tsx"},{"name":"maxLength","type":"number | undefined","required":false,"description":"Native `maxlength` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"minLength","type":"number | undefined","required":false,"description":"Native `minlength` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"name","type":"string | undefined","required":false,"description":"Native form field name. When set, the element participates in form submission and React Hook Form can register it (A16).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onBlur","type":"((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | undefined","required":false,"description":"Low-level native passthrough; fires on native blur of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onChange","type":"((next: string) => void) | undefined","required":false,"description":"Fires with the next string whenever the content changes — the SwiftUI `Binding<String>` write-back analogue. Called in both controlled and uncontrolled modes; never called while {@link TextSurfaceProps.disabled} or {@link TextSurfaceProps.readOnly}.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onFocus","type":"((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | undefined","required":false,"description":"Low-level native passthrough; fires on native focus of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"placeholder","type":"string | undefined","required":false,"description":"Placeholder text shown when the surface is empty — the SwiftUI title-arg analogue (the `prompt` parameter).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"readOnly","type":"boolean | undefined","required":false,"description":"When `true`, the surface stays focusable and its value is submitted, but no edit is possible: the native `readOnly` attribute is set and `onChange` is hard-suppressed (never a silent CSS-only block).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"ref","type":"Ref<HTMLTextAreaElement> | undefined","required":false,"description":"Ref forwarded to the native element — the surface's primary interactive element.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"required","type":"boolean | undefined","required":false,"description":"Marks the surface required — sets the native `required` + `aria-required`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"resizable","type":"TextEditorResizable | undefined","required":false,"description":"Whether the reader may drag the region taller. `\"vertical\"` exposes the native resize handle; the inline axis is never resizable, so the region can never break the enclosing layout's width. Precedence, deliberately documented rather than silently enforced: where the browser supports `field-sizing: content` the UA's auto-sizing wins under `sizing=\"relative\"` / `\"content\"`, which makes the handle inert or puts it in tension with the `lineLimit.max` cap; and a reader's drag writes an inline height that may exceed that cap. The handle is therefore most meaningful with `sizing=\"fixed\"`. It is not gated on the sizing mode — a prop that silently no-ops under three of four values is worse than a documented precedence rule.","defaultValue":"`\"none\"`","declaredIn":"packages/@liquidify/react/src/components/text-editor/text-editor.tsx"},{"name":"sizing","type":"TextEditorSizing | undefined","required":false,"description":"Height behaviour of the region (**Q2 SUPERSEDED** — the editor adopts TextField's multiline height vocabulary rather than a bare `rows` count, so the two surfaces cannot disagree on what a row range means). Resolved via `data-sizing` in CSS.","defaultValue":"`\"fixed\"`","declaredIn":"packages/@liquidify/react/src/components/text-editor/text-editor.tsx"},{"name":"tint","type":"LiquidTintValue | undefined","required":false,"description":"System-accent of the control — drawn from the RESTRICTED {@link LIQUID_COLORS} palette (the thirteen Apple system colours). **Orthogonal to a component's `variant`**: it recolours the accent the surface draws from (focus ring, tinted fill, selection) without changing its variant. Resolved via `data-tint` attribute selectors in CSS (tokens-only, ADR-0003).","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"value","type":"string | undefined","required":false,"description":"Controlled current text value. Passing this switches the surface to controlled mode: the rendered value always reflects this prop and the consumer owns it via {@link TextSurfaceProps.onChange}.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"}],"swiftUIMap":[{"swiftUI":"`TextEditor(text: Binding<String>)`","react":"`value?: string` (controlled) / `defaultValue?: string` (uncontrolled seed) + `onChange?(next: string)`","notes":"String-bearing controlled triad (ADR-0015/0017), wired through the shared `useControllableState` machine exactly as TextField. The `Binding<String>` splits into the controlled/uncontrolled pair; `value !== undefined` selects controlled mode (audit §16). `defaultValue` is read once on mount, default `\"\"`. `onChange` fires the next full string on every edit, in both modes, **never** while `disabled` or `readOnly` (hard-suppressed in the handler, not only via the native attribute — charter §3).","relationship":"direct"},{"swiftUI":"`.glassEffect(in: .rect(cornerRadius: 18))`","react":"*(internal — fixed rect glass shell)*","notes":"Not a consumer prop. `GlassPrimitive material=\"regular\"` shell at the 18pt-equivalent corner radius token (**[RESOLVED Q3]** — `--lq-radius-18`). `material` is not public (ControlProps, ADR-0023).","relationship":"direct"},{"swiftUI":"`.scrollContentBackground(.hidden)`","react":"*(internal)*","notes":"The engine shell owns the background; the native `<textarea>` background is transparent so the glass shows through. Not a consumer prop.","relationship":"direct"},{"swiftUI":"`.frame(height:)` (demo layout: 160pt / 80pt)","react":"`sizing?: TextEditorSizing` + `lineLimit?: LineLimit` (**[Q2 SUPERSEDED]**)","notes":"The demo pins a fixed height per section, and `sizing=\"fixed\"` + `lineLimit.min = 6` (the defaults) reproduce that pinned region exactly — iOS-faithful-first is preserved. What changed is the *vocabulary*: rather than a bare `rows` count, the region speaks the same three-mode height model as TextField's multiline branch (`\"fixed\"` pins and scrolls, `\"relative\"` grows `min`→`max` then scrolls, `\"content\"` grows uncapped), sanitised by the shared `sanitizeLineLimit` against the editor's own `{ min: 6, max: 12 }` default. `lineLimit.min` drives the native `<textarea rows>` attribute. **Not** mapped to `SIZE_SCALE`.","relationship":"direct"},{"swiftUI":"— (web-only)","react":"`resizable?: TextEditorResizable`","notes":"No SwiftUI analogue: `TextEditor` has no user-resize affordance, so the axis is off by default (`\"none\"`) and an undecorated `<TextEditor>` renders exactly what the sim shows. `\"vertical\"` exposes the native drag handle; the inline axis is never resizable, so the region cannot break the enclosing layout's width. See ## Interaction for the documented precedence against `field-sizing`.","relationship":"web-only"},{"swiftUI":"`.disabled(true)`","react":"`disabled?: boolean`","notes":"Inherited from `ControlProps`; native `disabled` on the `<textarea>` (Shape N, charter §3). Removes from tab order and hard-suppresses `onChange` / `onFocus` / `onBlur`.","relationship":"direct"},{"swiftUI":"Accessible name (SwiftUI label)","react":"`aria-label` / `aria-labelledby`","notes":"Required; the caller supplies it. Demo `DemoSection` captions are chrome, not real labels.","relationship":"direct"},{"swiftUI":"— (native form passthroughs)","react":"`readOnly?`, `maxLength?`, `minLength?`, `autoComplete?`, `inputMode?`, `name?`, `id?`, `required?`","notes":"The same native `<textarea>` passthroughs TextField's multiline branch exposes; `readOnly` hard-suppresses `onChange`. `id` falls back to a generated stable id; if the caller supplies one it is used verbatim (audit §16 stable-id rule).","relationship":"direct"},{"swiftUI":"— (validation state)","react":"`invalid?: boolean` + `aria-invalid?` precedence","notes":"Same contract as TextField: `invalid` is shorthand for `aria-invalid=\"true\"`; an explicit `aria-invalid` (incl. `\"grammar\"` / `\"spelling\"` / `false`) always wins, via `ariaInvalid ?? (invalid || undefined)`. `aria-describedby` / `aria-errormessage` forwarded verbatim.","relationship":"direct"},{"swiftUI":"— (low-level native)","react":"`onFocus?(e)`, `onBlur?(e)`","notes":"`FocusEvent<HTMLTextAreaElement>`; never fire while `disabled` (charter §1 rule 2).","relationship":"direct"},{"swiftUI":"— (surface override)","react":"`className?`","notes":"The overridable glass-surface class (ADR-0004/0016).","relationship":"direct"},{"swiftUI":"— (empty state)","react":"`placeholder?: string` (**[RESOLVED Q1]** — adopted)","notes":"SwiftUI `TextEditor` has **no** native placeholder and the demo starts pre-filled; adopted for library consistency with TextField (library-consistency-second, no sim conflict — the demo simply never exercises an empty state). Renders through the native `<textarea>` `placeholder` attribute at the secondary-label token.","relationship":"direct"},{"swiftUI":"`.tint(_:)` (**not demoed**)","react":"`tint?: LiquidTintValue` (**[RESOLVED Q4]** — adopted)","notes":"TextField exposes tint for caret / selection; the TextEditor demo never exercises it, but TextEditor mirrors TextField's tint surface (extends `TintableControlProps`, default `\"blue\"`).","relationship":"direct"}],"tokens":["--lq-color-accent","--lq-color-bg-primary","--lq-color-fill-primary-dark","--lq-color-fill-primary-light","--lq-color-label-primary-dark","--lq-color-label-primary-light","--lq-color-label-secondary-dark","--lq-color-label-secondary-light","--lq-color-separator","--lq-color-system-blue-default-dark","--lq-color-system-blue-default-light","--lq-duration-","--lq-easing-","--lq-font-family-sans","--lq-font-size-body","--lq-glass-","--lq-hit-target-touch","--lq-lens-","--lq-letter-spacing-body","--lq-line-height-body","--lq-radius-18","--lq-radius-md","--lq-space-","--lq-text-editor-pad","--lq-text-editor-rows-max","--lq-text-editor-rows-min","--lq-text-field-rows-"],"keyboard":[{"key":"Keyboard","action":"Keyboard: the native provides all standard text-editing keys: character entry, ←/→/↑/↓, Home/End, selection with Shift, Cmd/Ctrl+A/C/V/X, Backspace/Delete. Enter inserts a newline (the long-form editor default — it does not submit an enclosing form, unlike a single-line field). Tab moves focus out of the editor (a does not trap Tab). No custom key handling is added by the component."}],"variants":[],"variantPages":[]}