Skip to content

LayrzTextInput

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 10 revisions

LayrzTextInput

A Material-free, single-line text input field with optional label and hint text, prefix/suffix slots, error display, help affordance, and keyboard shortcut badging.

Metadata
Mirrors: ThemedTextInput (layrz_theme)
Phase: M2 (Core primitives)
Domain: Inputs
Primitive: EditableText
Status: Shipped in 0.0.9.


Overview

LayrzTextInput is the foundational text input widget and the base of the entire M3+ input family. Every other Layrz*Input (e.g., LayrzNumberInput, LayrzSelectInput) composes LayrzTextInput internally rather than reimplementing the field chrome. Picker-style inputs (date, time, color, emoji, etc.) render as a read-only LayrzTextInput that opens their selection surface on tap.

Design Principles

  • Filled visual style only — single-line, consistent with Material 3 conventions, with a filled background and border
  • Label or hint is mandatory — at least one of labelText or hintText must be non-null; both together are valid; neither alone is valid (debug assertion enforced)
  • Label is static — displayed above the field, never animated, never repositioned
  • Hint text is display-only — shown inside the empty field when there is no label, disappears when the field has text
  • Optional label icon — an icon rendered beside the label text via RichText, inheriting label styling
  • Prefix and suffix are mutually exclusive per slot — at most one of prefixIcon, prefix, or prefixText per slot; the same rule applies to the suffix trio
  • Errors are caller-owned — no built-in validator callback; callers provide a List<String> of error messages joined with ", " into a single line
  • Disabled and read-only are distinct — disabled blocks all taps and shows muted text; read-only fires onTap (used by pickers) and displays a lock icon in the suffix
  • Selection handles and toolbar are deferred — text selection and keyboard selection work, but touch drag handles, selection toolbar, and magnifier are DESIGN-74 scope

Interaction States

The six interaction states of the field, their visual treatment, and border rendering:

State Fill Border (always 1.5px) Text
Rest sf1 transparent fg1, hint fg3
Hover sf1 transparent fg1
Focus sf1 colors.primary fg1
Error colors.danger.shade50 colors.danger fg1
Disabled sf1 transparent fg4
Read-only sf1 transparent fg1 + lock icon

State precedence: disabled > read-only > error > pressed > hover/focused > default.

Key differences:

  • Focus preserves the rest fill (surface2) and only changes the border to primary
  • Read-only is the rest state plus a lock icon in the suffix (transparent border, full-contrast text)
  • Disabled darkens text to fg4 and uses transparent border to signal modal state
  • All borders are solid with constant width (1.5px) and radius (r2, 8 logical pixels); transparency renders invisible borders where needed
  • Error state uses danger-coloured fill and border
  • Geometry is byte-identical across all states; content height is fixed via internal token-based calculation

API Structure

Core Constructor (Simplified)

The constructor requires at least one of labelText or hintText:

class LayrzTextInput extends StatefulWidget {
  /// The label text displayed above the input field (optional if hintText is provided).
  ///
  /// When provided, rendered in `label` typography, `fg2` colour, static above the field.
  /// At least one of [labelText] or [hintText] must be non-null.
  final String? labelText;

  /// Hint text displayed inside the empty field when there is no label (optional if labelText is provided).
  ///
  /// Rendered in the field's text style but with `fg3` (muted) colour.
  /// Disappears when the field has text.
  /// At least one of [labelText] or [hintText] must be non-null.
  final String? hintText;

  // ... rest of parameters
}

Assertion: labelText != null || hintText != null. Both can be non-null; neither alone is an error.

Key Parameters

  • labelText (String?, optional) — label displayed above the field. At least one of this or hintText must be non-null.
  • hintText (String?, optional) — hint inside the empty field. At least one of this or labelText must be non-null.
  • isRequired (bool, default false) — when true, a red * appears next to the label text
  • prefixIcon, prefix, prefixText (mutually exclusive) — prefix slot content
  • suffixIcon, suffix, suffixText (mutually exclusive) — suffix slot content
  • errors (List, default []) — error messages, joined with ", " into a single line below the field
  • hideDetails (bool, default false) — when true, hides error text (field error state remains)
  • disabled (bool, default false) — blocks all input and taps, darkens text to fg4, uses transparent border
  • readOnly (bool, default false) — blocks editing but fires onTap; displays lock icon in suffix
  • onChanged, onSubmit, onFocusChanged, onTap — interaction callbacks
  • controller, focusNode — text and focus management (caller-owned if supplied)
  • keyboardType, textInputAction, inputFormatters, maxLength, autofocus, textCapitalization, autocorrect, enableSuggestions — standard EditableText passthroughs
  • shortcut (Set?, optional) — keyboard shortcut badge (display-only, hidden on mobile)
  • actions (Set?, optional) — text selection toolbar actions (copy, cut, paste, select all). When null, all four built-in actions are offered. Pass const {} to suppress the toolbar entirely. The set is further intersected with field state: an obscured field never offers copy or cut; a read-only field never offers cut or paste. Custom actions dedupe only by identity (so const sets are impossible, see LayrzSelectableAction).
  • padding (EdgeInsets?, optional) — custom padding; when null, defaults to pd2 (8 logical pixels on all sides). When provided, overrides this default entirely.
  • helpTitleText, helpContentText (optional) — help affordance tooltip

Key Invariants

  • Label or hint is mandatory — at least one of labelText or hintText must be non-null (debug assertion)
  • Slot exclusivity — at most one of three forms per slot; assertion enforced in debug mode
  • Read-only lock icon — always appears in the suffix when readOnly: true, coexisting with caller-supplied suffixes
  • Error text is single-line on wide widths — multiple errors are joined with ", " (comma-space), not rendered as a bulleted list
  • Errors move to tooltip on compact widths — below the sm breakpoint (viewport width < 960px), validation errors leave the inline slot and display in a tooltip anchored to the error icon, positioned left, with one error per line
  • Error icon coexists — the error ! icon is independent and always appears if errors is non-empty
  • Shortcut is display-only — keyboard shortcuts badge is rendered but does not bind key events
  • Single-line onlymaxLines is always 1; multiline textarea is separate component (LayrzTextAreaInput)
  • Geometry is byte-identical — border width (1.5px), content height, and radius (r2, 8 logical pixels) are identical in all six states; only colour and transparency vary (D15)
  • Fixed content height — independent of icon or slot presence; icons fit inside the height, caller-supplied widgets are constrained to it
  • Text and icon sizing are token-based — text uses body typography (16px, w400); icons are sized at body.fontSize + sp1 (20px), unless an IconTheme provides a size
  • Default padding is uniform — padding defaults to pd2 (8 logical pixels on all four sides); custom padding overrides this via the padding parameter
  • Selection overlay is invisible — caret, selection via keyboard, and Ctrl+A/Ctrl+C work; touch handles and toolbar are DESIGN-74 scope

Text Selection and Gestures

The field supports touch and keyboard text selection via the following gestures:

  • Single tap — places the caret at the tapped position
  • Double tap — selects the word under the pointer
  • Triple tap — selects the entire field content
  • Long press — selects the word under the pointer; on touch, displays a magnifier above the pointer; dragging after long-press extends the selection
  • Drag during long-press — extends the selection from the initial long-press position to the drag endpoint
  • Keyboard selection — Shift+arrow keys extend the caret, Ctrl+A selects all, Ctrl+C/Ctrl+X/Ctrl+V perform copy/cut/paste

On platforms with a selection toolbar (iOS, macOS, Android), tapping a selected text area opens the toolbar with the available actions (see actions parameter above). The toolbar is dismissed by tapping outside the selection, or by deselecting via keyboard or tap.


Responsive Error Rendering

Wide Widths (md and above, ≥ 960px)

Validation errors are displayed inline beneath the field:

  • All error messages joined with ", " into a single line
  • Character counter (if maxLength is set) appears to the right of the error line

Compact Widths (sm and below, < 960px)

Validation errors appear in a tooltip anchored to the error icon:

  • The inline error slot beneath the field is hidden, giving the character counter full width (no truncation)
  • Tapping the error icon opens a tooltip positioned to the left of the icon
  • Each error appears on its own line within the tooltip (errors joined with newlines, not comma-space)
  • The tooltip uses LayrzTooltipTrigger.tap, so tapping the icon toggles the tooltip open/closed

Threshold: The swap occurs at the sm breakpoint (width < 960px). This includes:

  • Phone-width browser windows
  • Most tablets in portrait orientation
  • Narrow desktop windows

This is a transparent, internal optimization. Consumers do not opt in; the field automatically adapts based on available viewport width.


Dependencies

  • M1 Theme System (LayrzTheme, LayrzThemeData, LayrzTokens)
  • M2 Tooltips (LayrzTooltip) — for help affordances
  • layrz_icons (^1.1.1) — for error and lock icons
  • Flutter 3.47+EditableText and widget state API

Usage Examples

Basic with Label

LayrzTextInput(
  labelText: 'Email',
  hintText: 'user@example.com',
  keyboardType: TextInputType.emailAddress,
  onChanged: (value) => setState(() => email = value),
)

Search with Hint Only (No Label)

LayrzTextInput(
  hintText: 'Search...',
  prefixIcon: MdiIcons.magnify,
  onChanged: (value) => setState(() => query = value),
)

Required Field

LayrzTextInput(
  labelText: 'Username',
  isRequired: true,
  errors: username.isEmpty ? ['Username is required'] : [],
  onChanged: (value) => setState(() => username = value),
)

Read-Only (Picker Style)

LayrzTextInput(
  labelText: 'Date',
  readOnly: true,
  controller: TextEditingController(text: selectedDate?.toString() ?? ''),
  onTap: () => _showDatePicker(),  // Opens date picker
)

Disabled Field

LayrzTextInput(
  labelText: 'Locked Field',
  disabled: true,
  controller: TextEditingController(text: 'This field is disabled'),
)

With Error Messages

LayrzTextInput(
  labelText: 'Password',
  isRequired: true,
  obscureText: true,
  errors: _validatePassword(password),  // Returns ['too short', 'no capitals'] → joins as 'too short, no capitals'
  onChanged: (value) => setState(() => password = value),
)

Architectural Role

LayrzTextInput is the foundational input component. All concrete input types compose it, ensuring visual and behavioral consistency across the entire input family.


Last updated: 2026-08-18
Related documents: Input Contract, Milestone 2, Design Tokens, Decisions (D32, D33, D34), Architecture

Clone this wiki locally