Skip to content

LayrzComboBoxInput

Kenny Mochizuki Escalona edited this page Aug 27, 2026 · 6 revisions

LayrzComboBoxInput

Autocomplete text field with searchable dropdown options.

Specification Status: LayrzComboBoxInput has shipped and is implemented at lib/src/inputs/src/combobox/combobox_input.dart (+ combobox_surface.dart). The section below marked Shipped behavior describes the actual, current implementation.

Everything below that, starting at "Metadata", is the original pre-implementation design sketch carried over from the layrz_theme migration plan. It predates the actual implementation and does not match the shipped API in several places (for example: the shipped overlay mechanism is LayrzAnchoredPanel, not RawAutocomplete; there is no position/LComboboxPosition parameter — desktop always covers the field via coverAnchor: true; maxOptionsToDisplay/maxChoicesToDisplay do not exist — see the "Dropdown height" section below). Reconciling the rest of this legacy sketch with the shipped widget is out of scope for this unit and is left for a dedicated documentation pass.

Shipped behavior: one field, reparented into the panel

LayrzComboBoxInput is rebuilt on LayrzAnchoredPanel (desktop, >= 960px) and covers its anchor exactly the way LayrzSelectInput does (coverAnchor: true, widthPolicy: matchAnchor) — the "elevated field" illusion (DESIGN-145): the opened panel appears exactly on top of the closed field, same position and width, rather than beside it. Below 960px it opens a bottom sheet instead. The former hand-rolled RawMenuAnchor overlay, its own layout delegate, and its own background/shadow painting are gone.

The panel's first row IS the live text input — not a second search field. The closed field and the panel's first row share the exact same TextEditingController and FocusNode instances; when the panel opens, that one live LayrzEditableField element is reparented from the closed anchor's slot into the panel's first row within the same frame, rather than being unmounted and rebuilt. Typing continues into the panel with no character loss and no caret jump. The panel row draws no border of its own — the border that reads as the field's own comes from LayrzAnchoredPanel's border parameter (LayrzAnchoredPanelBorder), painted around the panel's outer decorated box, colored primary (or danger when errors is non-empty), matching LayrzSelectInput.

There is no "Use "…"" confirmation row (BREAKING). An earlier version of the open panel rendered a confirmation row above the suggestions that had to be tapped (or reached via arrow-down) to commit typed text as the value. It is gone: the typed text is already the value, reported live through onChanged as the user types, and the options below are suggestions, not a required choice. allowFreeForm keeps its existing meaning otherwise. Keyboard navigation shifted accordingly — arrow-down now lands on the first suggestion instead of on the removed confirmation row.

Label and error text render outside the anchor. Both used to render inside the widget handed to LayrzAnchoredPanel as its anchor, which grew the anchor's rect upward by the label's own height — so the panel opened covering the label instead of the field beneath it. They now compose around the anchor instead (mirroring LayrzSelectInput._appendExtras), so the panel aligns to the field. The error footer is not gated on labelText being non-null: a field with errors and no label still renders its error text.

Metadata

Property Value
Mirror ThemedTextInput with enableCombobox: true
Phase M3 Inputs
Domain Inputs
SDK Primitive RawAutocomplete (SDK: autocomplete.dart) + EditableText with custom TextSelectionControls

Conformance Status

Architectural question: LayrzComboBoxInput differs fundamentally from picker-style inputs and even from plain LayrzTextInput. It composes an EDITABLE text field (not read-only like pickers) combined with a dropdown overlay showing filtered options. This is distinct from:

  • LayrzTextInput: Static text editing, no autocomplete
  • LayrzSelectInput: Read-only field opening a modal with single selection
  • LayrzTextAreaInput: Multiline editable text, no autocomplete

Should LayrzComboBoxInput:

  • Inherit the Input Contract's standard API (label, prefix, suffix, help, etc.)?
  • Or is it a separate implementation pattern?

Value Type and Interaction

  • Value type: String — the user-entered or selected text
  • Interaction pattern: An editable text field with a dropdown overlay showing options that match the user's input
  • Selection options: Options from a provided list, filtered by the user's typed text
  • Confirmation: User can either:
    • Click an option to select it (auto-fills the field)
    • Continue typing and press Enter to submit the manual entry
    • Blur the field to finalize (behavior configurable)

Deltas from the Input Contract

Combobox-Specific Parameters

// Design sketch — illustrative only
class LayrzComboBoxInput extends StatefulWidget {
  /// The current text content (user-typed or selected).
  final String? value;

  /// Callback invoked when the field's text actually changes — typing, selecting
  /// an option that differs from the current text, or an external value update.
  /// Does NOT re-fire when a commit does not change the text (e.g. re-selecting
  /// the option already shown). See "Selection callbacks" below.
  final void Function(String)? onChanged;

  /// Callback invoked on every commit — selecting an option or pressing Enter —
  /// unconditionally, including a same-value re-selection. See "Selection
  /// callbacks" below.
  final void Function(String)? onSubmit;

  /// List of autocomplete options.
  /// These strings are matched against the user's input for filtering.
  final List<String> options;

  /// Whether to enable autocomplete filtering.
  /// If false, all options are always displayed.
  final bool enableAutocomplete;

  /// Text displayed when the filtered options list is empty.
  final String emptyOptionsText;

  /// Position of the dropdown: above or below the field.
  final LComboboxPosition position;

  /// Placeholder text shown when the field is empty.
  final String? placeholder;

  /// Keyboard type (text, URL, email, etc.).
  final TextInputType keyboardType;

  /// Text input action (e.g., TextInputAction.done, .next).
  final TextInputAction textInputAction;

  const LayrzComboBoxInput({
    required this.options,
    this.value,
    this.onChanged,
    this.onSubmit,
    this.enableAutocomplete = true,
    this.emptyOptionsText = 'No options',
    this.position = .below,
    this.placeholder,
    this.keyboardType = .text,
    this.textInputAction = .done,
    // ... shared contract parameters (label, prefix, suffix, help, etc.)
  });
}

enum LComboboxPosition {
  /// Dropdown appears below the text field.
  below,

  /// Dropdown appears above the text field.
  above,
}

Dropdown height: fixed, not caller-configurable

There is no maxOptionsToDisplay (or any other height-related) parameter. The desktop overlay's option list is capped at a fixed 300 logical pixels and scrolls past that — the same rule applied to LayrzSelectInput's and LayrzDurationInput's overlays. A caller that needs the panel taller or shorter than 300px cannot ask for it; this is a deliberate capability trade against a parameter that previously produced an incorrect panel height for any value other than its own default.

Selection callbacks: onChanged tracks text, onSubmit tracks commits

onChanged and onSubmit answer two different questions and can disagree on a single selection:

  • onChanged — "did the field's text change?" Fires once when it does (typing, selecting a different option, an external value push). Stays silent when a commit does not change the text, including re-selecting the option already shown.
  • onSubmit — "did the user just commit a value?" Fires on every commit, unconditionally, same value or not.

A caller that needs to react to "the user made a selection" — even a repeated one — should use onSubmit, not onChanged.

Input Chrome

Inherits some Input Contract chrome, but with differences:

// Design sketch — shared with LayrzTextInput (editable variant):
class LayrzComboBoxInput {
  /// Label displayed above the field.
  final String? labelText;

  /// Custom label widget (mutually exclusive with labelText).
  final Widget? label;

  /// Placeholder/hint text (shown when empty).
  final String? placeholder;

  /// Icon in the prefix slot.
  final IconData? prefixIcon;

  /// Custom widget in the prefix slot (mutually exclusive with prefixIcon).
  final Widget? prefixWidget;

  /// Callback when prefix is tapped.
  final VoidCallback? onPrefixTap;

  /// Icon in the suffix slot (often a dropdown arrow).
  final IconData? suffixIcon;

  /// Custom widget in the suffix slot (mutually exclusive with suffixIcon).
  final Widget? suffixWidget;

  /// Callback when suffix is tapped.
  final VoidCallback? onSuffixTap;

  /// Help title for tooltip.
  final String? helpTitleText;

  /// Help content for tooltip.
  final String? helpContentText;

  /// Selects the padding ramp (see [Input-Contract](Input-Contract)).
  ///
  /// Default: `pd2`/10px. `dense: true`: `pd1`/6px. Identical on every
  /// viewport. Both the closed field and the open panel row read the same
  /// chrome-owned inset. No public `padding` override (D66).
  final bool dense;

  /// Error messages to display below the field.
  final List<String> errors;

  /// Whether to display errors and help text.
  final bool hideDetails;

  /// Focus node for managing focus.
  final FocusNode? focusNode;

  /// Text controller for external value management.
  final TextEditingController? controller;

  /// Whether the field is disabled.
  final bool disabled;

  /// Character input formatters.
  final List<TextInputFormatter> inputFormatters;
}

Reference: Current layrz_theme API

ThemedTextInput with enableCombobox: true (source: lib/src/inputs/src/general/text_input.dart):

Parameter Type Notes
value String? Current text content
onChanged void Function(String)? Text change callback
onSubmitted VoidCallback? Submit callback
controller TextEditingController? Text controller
choices List<String> Autocomplete option list
enableCombobox bool Enable combobox mode (default: false)
maxChoicesToDisplay int Max options shown (default: 5)
emptyChoicesText String Empty list message (default: "No choices")
position ThemedComboboxPosition Dropdown position: above or below (default: below)
labelText String? Label text
label Widget? Label widget
placeholder String? Placeholder text
prefixIcon IconData? Icon in prefix slot
prefixWidget Widget? Custom prefix widget
onPrefixTap VoidCallback? Prefix tap handler
suffixIcon IconData? Icon in suffix slot
suffixWidget Widget? Custom suffix widget
onSuffixTap VoidCallback? Suffix tap handler
errors List<String> Error messages
hideDetails bool Hide errors/help text
padding EdgeInsets? Field padding
disabled bool Disable the field
focusNode FocusNode? Focus node
keyboardType TextInputType Keyboard type (default: text)
textInputAction TextInputAction? Enter key behavior
inputFormatters List<TextInputFormatter> Input formatters
autofillHints List<String> Autofill hints
autocorrect bool Spell check (default: true)
enableSuggestions bool Suggestions (default: true)
textStyle TextStyle? Custom text style

ThemedComboboxPosition enum:

  • below: Dropdown below the field
  • above: Dropdown above the field

Dependencies and Blockers

  • RawAutocomplete (SDK) — the core autocomplete overlay mechanism
  • EditableText + custom TextSelectionControls — CRITICAL BLOCKER. Same as LayrzTextInput and LayrzTextAreaInput. Requires:
    • Text selection (tap and drag)
    • Copy/paste (long-press or keyboard)
    • Cut operations
    • Desktop selection toolbar
    • Mobile selection handles with magnifier

Implementation Notes from layrz_theme

  • Overlay focus behavior: The RawAutocomplete overlay is excluded from ambient focus traversal, so Tab navigation skips it.
  • Option filtering: Options are filtered case-insensitively by matching the start of each option against the user's typed text.
  • Dropdown positioning: The dropdown is smart-positioned based on available screen space. If position is below but there's no room, it moves above.
  • Option selection: Clicking an option fills the field with the option text.
  • Manual entry: User can type any text, not just options. Pressing Enter or Tab submits the manual entry.
  • Blur behavior: When the field loses focus, the dropdown closes (behavior may be configurable).

Open Questions

  1. Separate component or factory?: Should LayrzComboBoxInput be:

    • A distinct widget class (recommended for clarity)?
    • A factory constructor of LayrzTextInput (e.g., LayrzTextInput.combobox())?
    • Or a parameter-driven variant (e.g., LayrzTextInput(enableCombobox: true))?
  2. Option matching and filtering: Does option matching:

    • Match from the start of each option?
    • Match anywhere in the option?
    • Use a custom filter function?
    • Case-sensitive or case-insensitive?
  3. Option rendering: Are options:

    • Plain text strings?
    • Can they include icons, colors, or custom widgets?
    • Is there a callback to format option display?
  4. Value type flexibility: Currently, options and value are both String. Should there be a generic LayrzComboBoxInput<T> variant where:

    • Options are of type T
    • A toString() or custom formatter converts T to display string?
  5. Free-form entry: Should the field allow any text the user types, or only text from the options list?

  6. Dropdown open/close control: Can the caller programmatically open/close the dropdown, or is it automatic?

  7. Keyboard navigation in dropdown: Are arrow keys, Enter, and Escape supported to navigate and select options?

  8. Accessibility: Should the dropdown be labeled for screen readers? Should the combobox role be applied?

  9. Material-free TextSelectionControls: Same CRITICAL BLOCKER as for LayrzTextInput. What is the implementation plan?

  10. Distinguishing from LayrzSelectInput: When should a user choose LayrzComboBoxInput (editable, free-form) vs. LayrzSelectInput (read-only, restricted to options)? Should documentation clarify the use cases?


Last updated: 2026-08-26 (v0.0.13 — documented the shipped implementation: LayrzAnchoredPanel with coverAnchor: true, the panel's first row as the same live field reparented from the closed anchor, removal of the Use "…" confirmation row (BREAKING), and label/error text hoisted outside the anchor; ff72bec on 2026-08-25 previously documented the removal of maxOptionsToDisplay and the fixed 300px overlay height, which stands unchanged; the remaining pre-implementation sections are unchanged and still need a full documentation pass)
Related documents: Input Contract, Flutter 347 Audit, Component Catalog, Design Tokens

Clone this wiki locally