# 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)
- [Keyboard Clearance](#keyboard-clearance)
- [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<String>? onChanged,
  InputType type = InputType.text,
  String? className,
  String? placeholderClassName,
  String? placeholder,
  bool enabled = true,
  bool readOnly = false,
  bool autofocus = false,
  TextInputAction? textInputAction,
  Iterable<String>? autofillHints = const <String>[],
  ValueChanged<String>? onSubmitted,
  VoidCallback? onEditingComplete,
  VoidCallback? onTap,
  TapRegionCallback? onTapOutside,
  int? maxLines,
  int minLines = 1,
  FocusNode? focusNode,
  TextEditingController? controller,
  Set<String>? states,
  List<TextInputFormatter>? inputFormatters,
  String decimalSeparator = '.',
  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<String>?` | `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), with `decimalSeparator` as its one decimal mark; 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`). When `null`, resolves to `.done` on a single-line field and `.newline` on a multiline one. |
| `autofillHints` | `Iterable<String>?` | `const []` | Hints for a password manager or the platform autofill service (e.g., `[AutofillHints.email]`). The empty default keeps platform autofill on with no hint; `null` opts the field out. Fields of one form go inside an `AutofillGroup`, which the host provides; `WInput` never creates one. |
| `onSubmitted` | `ValueChanged<String>?` | `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<String>?` | `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<TextInputFormatter>?` | `null` | Formatters applied as the user types (digit-only, masks, etc.) |
| `decimalSeparator` | `String` | `'.'` | The one decimal mark a `number` field accepts. Pass the locale's mark (`','` in Turkish or German) so `2,5` is not typed as `25`; the text stays as typed, so convert it to `.` before it goes on the wire. One character, never a digit, `+` or `-` (debug asserts). Ignored when `inputFormatters` is set. |
| `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

A single-line field centres its text and placeholder vertically in whatever height `min-h-*` or `h-*` gives the box, as a CSS input does; a multiline field keeps its text at the top.

### 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...',
)
```

## Keyboard Clearance

A focused `WInput` lifts the WHOLE field above the software keyboard, not just the line the caret is on.

Flutter reveals the caret rect with `scrollPadding` around it, which is the same box as the field while the field is one line tall. On a multiline field it is not: the caret sits on the first line, so line one clears the keyboard and every line below it stays under it. `WInput` widens `scrollPadding` by however much of the field sits below the caret, so Flutter's own caret scroll carries the rest of the field with it, and it recomputes as the caret moves down through the text.

The keyboard is not the only thing in the way. A [WKeyboardActions](https://anilcancakir.com/wind_kit/docs/v1.11/widgets/w-keyboard-actions.md) toolbar is drawn on top of the keyboard and the engine reports nothing about it, so the region a field has to clear is the keyboard plus the bar. `WInput` reads the bar's published height and adds it, which means a field inside a `WKeyboardActions` needs no setup at all.

```dart
WInput(
  type: InputType.multiline,
  minLines: 3,
  maxLines: 5,
  className: 'w-full p-4 border rounded-xl',
  placeholder: 'Write an update...',
)
```

> [!NOTE]
> Nothing here is a prop. The clearance is computed from the field's own geometry, so a field that sits inside your own scroll view behaves the same as one that does not.

## 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(),
)
```

> **The Return key closes the keyboard by default.** With no `textInputAction`, a single-line field resolves to `TextInputAction.done` and a multiline one to `.newline`. Passing `.next` is a deliberate opt-in, because Flutter implements it as `focusNode.nextFocus()`: the next FOCUSABLE widget, not the next text field. On a form whose traversal order runs through buttons, colour swatches and switches, the same Return key does two different things on one screen, dismissing the keyboard where the next focusable is a button and jumping past several fields where it happens to be editable. Reach for `.next` only where you know the following focusable IS the next input, which is a fact only the form itself has. For a toolbar that advances through a known field order instead, see [WKeyboardActions](https://anilcancakir.com/wind_kit/docs/v1.11/widgets/w-keyboard-actions.md).

## 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 `<input>`. 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](https://anilcancakir.com/wind_kit/docs/v1.11/widgets/w-form-input.md) - Form-integrated input with labels and validation.
- [WButton](https://anilcancakir.com/wind_kit/docs/v1.11/widgets/w-button.md) - Interactive button component.
- [WText](https://anilcancakir.com/wind_kit/docs/v1.11/widgets/w-text.md) - Typography component.
