# WInput
A utility-first form input that combines React-style controlled state management with Tailwind-like styling. It renders Material-free (`EditableText` core inside a `Container`) and works under any theming ancestor: Material, Cupertino, a custom app, or a bare `WidgetsApp`. No Material ancestor required (fixes #102).
- [Basic Usage](#basic-usage)
- [Constructor](#constructor)
- [Props](#props)
- [Layout Modes](#layout-modes)
- [Event Handling](#event-handling)
- [State Variants](#state-variants)
- [Accessibility](#accessibility)
- [Styling Examples](#styling-examples)
- [All Supported Classes](#all-supported-classes)
- [Customizing Theme](#customizing-theme)
- [Related Documentation](#related-documentation)
```dart
WInput(
value: _email,
onChanged: (value) => setState(() => _email = value),
type: InputType.email,
placeholder: 'Enter your email',
className: 'p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500',
placeholderClassName: 'text-gray-400 italic',
)
```
## Basic Usage
Pass `value` and `onChanged` for controlled (React-style) binding. Omit both to let the field manage its own state internally. `WInput` works under any ancestor widget tree. No `MaterialApp` required.
```dart
WInput(
placeholder: 'Type something...',
className: 'w-full p-2 border rounded',
onChanged: (val) => print(val),
)
```
## Constructor
```dart
const WInput({
Key? key,
String? value,
ValueChanged? onChanged,
InputType type = InputType.text,
String? className,
String? placeholderClassName,
String? placeholder,
bool enabled = true,
bool readOnly = false,
bool autofocus = false,
TextInputAction? textInputAction,
ValueChanged? onSubmitted,
VoidCallback? onEditingComplete,
VoidCallback? onTap,
TapRegionCallback? onTapOutside,
int? maxLines,
int minLines = 1,
FocusNode? focusNode,
TextEditingController? controller,
Set? states,
List? inputFormatters,
TextCapitalization textCapitalization = TextCapitalization.none,
bool autocorrect = true,
bool enableSuggestions = true,
Widget? prefix,
Widget? suffix,
String? semanticLabel,
})
```
## Props
| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `className` | `String?` | `null` | Wind utility classes for the input field |
| `placeholderClassName` | `String?` | `null` | Utility classes for styling the hint text |
| `value` | `String?` | `null` | The controlled value of the input |
| `onChanged` | `ValueChanged?` | `null` | Callback when text changes |
| `type` | `InputType` | `InputType.text` | Input keyboard and behavior (`text`, `password`, `email`, `number`, `multiline`). `number` restricts input to a signed decimal on every platform (web included); pass `inputFormatters` to override. |
| `placeholder` | `String?` | `null` | Hint text shown when input is empty |
| `enabled` | `bool` | `true` | Whether the input is interactive |
| `readOnly` | `bool` | `false` | Whether the input is read-only |
| `autofocus` | `bool` | `false` | Autofocus on mount |
| `textInputAction` | `TextInputAction?` | `null` | Keyboard action (e.g., `.done`, `.next`) |
| `onSubmitted` | `ValueChanged?` | `null` | Callback when action button is pressed |
| `onTapOutside` | `TapRegionCallback?` | `null` | Callback when tapping outside (useful for blur) |
| `maxLines` | `int?` | `null` | Max lines for multiline input |
| `minLines` | `int` | `1` | Min lines for multiline input |
| `prefix` | `Widget?` | `null` | Widget displayed before text |
| `suffix` | `Widget?` | `null` | Widget displayed after text |
| `controller` | `TextEditingController?` | `null` | Optional external controller |
| `states` | `Set?` | `null` | Custom states for dynamic styling |
| `onEditingComplete` | `VoidCallback?` | `null` | Callback fired when the user finishes editing (loses focus, presses done, etc.) |
| `onTap` | `VoidCallback?` | `null` | Callback fired when the field is tapped |
| `focusNode` | `FocusNode?` | `null` | External focus controller for programmatic focus management |
| `inputFormatters` | `List?` | `null` | Formatters applied as the user types (digit-only, masks, etc.) |
| `textCapitalization` | `TextCapitalization` | `TextCapitalization.none` | Auto-capitalize behavior (`none` / `sentences` / `words` / `characters`) |
| `autocorrect` | `bool` | `true` | Enable OS autocorrect suggestions |
| `enableSuggestions` | `bool` | `true` | Enable OS suggestion bar (Android) |
| `semanticLabel` | `String?` | `null` | Accessible name for the single textbox semantics node. Falls back to `placeholder` when `null`. Used by screen readers and `getByLabel`-style locators. |
## Layout Modes
### Multiline Input
For text areas, set the `type` to `InputType.multiline`. You can control the height using `minLines` or standard sizing classes.
```dart
WInput(
type: InputType.multiline,
minLines: 3,
maxLines: 5,
className: 'w-full p-4 border rounded-xl bg-gray-50',
placeholder: 'Enter your message...',
)
```
## Event Handling
`WInput` provides standard callbacks for user interaction. Use `onSubmitted` for handling "Enter" or keyboard action buttons.
```dart
WInput(
className: 'border p-2',
textInputAction: TextInputAction.search,
onSubmitted: (value) {
_performSearch(value);
},
onTapOutside: (_) => FocusScope.of(context).unfocus(),
)
```
## State Variants
Wind automatically manages `focus:`, `disabled:` (when `enabled: false`), and `readonly:` (when `readOnly: true`) states. You can also trigger custom states like `error:` by passing them to the `states` property.
```dart
WInput(
states: _hasError ? {'error'} : {},
className: 'border border-gray-300 focus:border-blue-500 error:border-red-500',
)
```
> [!NOTE]
> `focus:` styles (like `ring-2`) are applied automatically when the field gains focus.
## Accessibility
`WInput` emits exactly **one** typeable textbox semantics node. Its accessible name is `semanticLabel` when provided, falling back to `placeholder`. This single-node shape ensures label-by-text locators (e.g., Playwright `getByLabel`, `dusk` label lookup) resolve cleanly with no duplicate accessible names.
`InputType.password` fields report `isObscured: true` in the semantics tree so screen readers announce the field as a password input.
When the field is `enabled: false`, it is non-editable and reports `isReadOnly` in the semantics tree.
> [!NOTE]
> `WInput` supports native text selection (drag-select, double-tap word, long-press) with Cupertino-style selection handles on all platforms. This keeps `WInput` cupertino-only (no `package:flutter/material.dart` import) and consistent with the rest of Wind's own look. Interactive selection requires an `Overlay` ancestor (`MaterialApp` / `CupertinoApp` / `WidgetsApp` all provide one). Under a bare root with no `Overlay` (unusual in practice), typing and focus still work, but all interactive selection (drag-select, double-tap word, long-press, the drag handles, and the toolbar) is suppressed rather than throwing.
> [!NOTE]
> **Web:** Flutter renders to a canvas (CanvasKit or Skwasm) and does not use a native browser ``. Mouse drag-select, double-click word, and keyboard copy (Ctrl+C / Cmd+C) all work through Flutter's own selection layer. Right-click opens the browser's native context menu by default (not WInput's selection toolbar). Apps that want WInput's selection toolbar on right-click can call `BrowserContextMenu.disableContextMenu()` once at startup; this is an app-level opt-in and not WInput behavior. No magnifier is shown on web.
> [!WARNING]
> Passing both `value` and `controller` at the same time throws an `AssertionError` in debug mode. Use either `value` + `onChanged` (controlled) or `controller` (external), not both.
## Styling Examples
### Search Input with Prefix
You can use the `prefix` prop to add icons while maintaining the utility-first styling of the input.
```dart
WInput(
prefix: Icon(Icons.search, color: Colors.gray),
className: 'pl-10 p-2 bg-gray-100 rounded-full border-transparent focus:bg-white focus:border-blue-500',
placeholder: 'Search...',
)
```
### Password Field
Use `InputType.password` to obscure text. You can use the `suffix` prop to add a visibility toggle.
```dart
WInput(
type: _obscure ? InputType.password : InputType.text,
suffix: IconButton(
icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscure = !_obscure),
),
className: 'p-3 border rounded',
)
```
## All Supported Classes
| Category | Classes |
|:---------|:--------|
| Layout | `w-{size}`, `h-{size}`, `flex-1`, `w-full` |
| Spacing | `p-{n}`, `px-{n}`, `py-{n}`, `m-{n}` |
| Typography | `text-{size}`, `font-{weight}`, `italic`, `uppercase` |
| Colors | `bg-{color}`, `text-{color}` |
| Borders | `border`, `border-{n}`, `rounded-{size}`, `border-{color}` |
| Effects | `shadow-{size}`, `opacity-{n}`, `ring-{n}`, `ring-offset-{n}` |
## Customizing Theme
You can override the default input appearance through `WindThemeData`. This is useful for setting global border colors or padding defaults.
```dart
WindThemeData(
colors: {
'input-border': Colors.blueGrey,
},
// WInput respects global spacing units for p-x classes
baseSpacingUnit: 4.0,
)
```
## Related Documentation
- [WFormInput](./w-form-input.md) - Form-integrated input with labels and validation.
- [WButton](./w-button.md) - Interactive button component.
- [WText](./w-text.md) - Typography component.