WKeyboardActions
A wrapper that renders an OK button and field-navigation toolbar above the keyboard for any group of input fields, with optional platform targeting and custom close widget.
On this page
widgets/w_keyboard_actions_basic
A live demo of this example follows.
w_keyboard_actions_basic.dart
View source
Open demo
WKeyboardActions(
focusNodes: [_nameFocus, _amountFocus],
toolbarClassName: 'bg-gray-100 dark:bg-gray-800',
child: Column(
children: [
WInput(focusNode: _nameFocus, placeholder: 'Jane Doe'),
WInput(
focusNode: _amountFocus,
placeholder: '0.00',
type: InputType.number,
),
],
),
)
Basic Usage#
Create a FocusNode for each input field and pass them all in the focusNodes list. WKeyboardActions attaches listeners to every node and shows the toolbar as long as any one of them is focused.
class _MyFormState extends State<MyForm> {
final _nameFocus = FocusNode();
final _amountFocus = FocusNode();
@override
void dispose() {
_nameFocus.dispose();
_amountFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WKeyboardActions(
focusNodes: [_nameFocus, _amountFocus],
child: Column(
children: [
WInput(focusNode: _nameFocus, placeholder: 'Full Name'),
WInput(
focusNode: _amountFocus,
placeholder: '0.00',
type: InputType.number,
),
],
),
);
}
}
The toolbar is hosted in the ROOT
Overlay, not the nearest one. It positions itself in screen terms (bottom: viewInsets.bottom, so it sits on top of the keyboard), and a nestedOverlayis measured in its own box rather than the screen's. An app whose page host owns anOverlayinside a scroll view gives that overlay the height of the scrolled content, so a nearest-overlay toolbar would render hundreds of points below the viewport: present in the widget tree, absent from the screen. You need noOverlayof your own for this to work;MaterialAppandCupertinoAppboth provide the root one.
The default close button needs
material_ui'sMaterialLocalizations. Its OK label comes fromMaterialLocalizations.of(context).okButtonLabel, andwind_kitreads thematerial_uitype, so the app must provide that one:material_ui'sMaterialAppdoes. Flutter's ownMaterialApp(frompackage:flutter/material.dart) provides a different type, so under it the toolbar throws "No MaterialLocalizations found" when a field takes focus. Either migrate the root tomaterial_ui'sMaterialApp, or keep the root and addGlobalMaterialLocalizations.delegatesfrommaterial_uito itslocalizationsDelegates(checked: Flutter'sMaterialAppthen resolves thematerial_uiMaterialLocalizations). Under a bareWidgetsAppor aCupertinoApp, addGlobalMaterialLocalizations.delegatesfrommaterial_uito the app'slocalizationsDelegates(it covers every supported locale), orDefaultMaterialLocalizations.delegatewhen the app is English only: that delegate supports no other locale, so under, say, a Spanish locale the button still throws. AcloseWidgetBuilderreplaces the button and needs none of this.
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:material_ui/material_ui.dart';
import 'package:wind_kit/wind_kit.dart';
CupertinoApp(
// GlobalMaterialLocalizations.delegates covers every locale; use
// DefaultMaterialLocalizations.delegate for an English only app.
localizationsDelegates: GlobalMaterialLocalizations.delegates,
// Set the locales your app ships; the default is US English only.
supportedLocales: const [Locale('en'), Locale('es')],
home: MyForm(),
)
Constructor#
const WKeyboardActions({
Key? key,
required Widget child,
required List<FocusNode> focusNodes,
String platform = 'all',
bool nextFocus = true,
String? toolbarClassName,
Widget Function(FocusNode)? closeWidgetBuilder,
})
Props#
| Prop | Type | Default | Description |
|---|---|---|---|
child |
Widget |
Required | The form or column of inputs the toolbar is attached to. |
focusNodes |
List<FocusNode> |
Required | FocusNodes for the inputs that need keyboard actions. Order determines navigation order. |
platform |
String |
'all' |
Platform gate: 'all', 'ios', or 'android'. |
nextFocus |
bool |
true |
When true, Previous/Next arrow buttons appear in the toolbar for field navigation. |
toolbarClassName |
String? |
null |
Wind utility classes for the toolbar background. Only the background colour (bg-*) is read; other classes are ignored. |
closeWidgetBuilder |
Widget Function(FocusNode)? |
null |
Builder that replaces the default "OK" button. Receives the currently-focused FocusNode. |
Platform Targeting#
The platform prop gates the toolbar to a specific OS. The most common value is 'ios', because iOS numeric keyboards have no built-in Done button.
| Value | Toolbar visible on |
|---|---|
'all' |
Every platform, including web and desktop (default) |
'ios' |
iOS only |
'android' |
Android only |
WKeyboardActions(
platform: 'ios',
focusNodes: [_amountFocus],
child: WInput(
focusNode: _amountFocus,
placeholder: '0.00',
type: InputType.number,
),
)
Navigation Between Fields#
When nextFocus: true (the default), the toolbar shows Previous and Next arrow buttons. They move focus to the previous or next node in the focusNodes list, in the order they were provided. The Previous button is disabled at the first field; the Next button is disabled at the last.
Set nextFocus: false to show only the OK button, which is useful for single-field forms.
WKeyboardActions(
focusNodes: [_nameFocus, _emailFocus, _amountFocus],
nextFocus: true,
child: Column(
children: [
WInput(focusNode: _nameFocus, placeholder: 'Jane Doe'),
WInput(focusNode: _emailFocus, placeholder: '[email protected]'),
WInput(focusNode: _amountFocus, placeholder: '0.00'),
],
),
)
Toolbar Styling#
Pass Wind bg-* utility classes via toolbarClassName to set the toolbar background. The dark: pair is required.
WKeyboardActions(
toolbarClassName: 'bg-gray-100 dark:bg-gray-800',
focusNodes: [_focusNode],
child: myInputField,
)
When toolbarClassName is null, the toolbar uses Theme.of(context).colorScheme.surfaceContainerHighest.
Custom Close Button#
Supply closeWidgetBuilder to replace the default "OK" label with any widget. The builder receives the currently-focused FocusNode, which you can call unfocus() on.
WKeyboardActions(
focusNodes: [_focusNode],
closeWidgetBuilder: (node) => WButton(
onTap: node.unfocus,
className: 'bg-blue-600 text-white px-4 py-1.5 rounded-md text-sm font-medium',
child: const WText('Save & Close', className: 'text-white text-sm font-medium'),
),
child: myInputField,
)
The Toolbar Occludes, and Says So#
The toolbar is drawn in the root overlay at bottom: viewInsets.bottom, which puts it directly ON TOP of the keyboard. The engine reports the keyboard through viewInsets and knows nothing about the bar, so the region a focused field has to clear is the keyboard PLUS the toolbar, and anything reading viewInsets alone reserves too little. A field that cleared the keyboard came out from under it and straight under the bar.
WKeyboardActions measures its own bar and publishes the height to its subtree:
final double toolbar = WKeyboardToolbarInset.of(context);
Zero when no bar is up, which covers every platform the widget is gated off and every moment nothing is focused. WInput already consumes it, so a plain field inside a WKeyboardActions needs nothing; reach for it in a widget of your own that positions something against the keyboard.
The height is measured rather than assumed, because the row is built from IconButtons whose size comes from the ambient theme.
Styling Examples#
Minimal (single field, iOS only)#
WKeyboardActions(
platform: 'ios',
nextFocus: false,
focusNodes: [_amountFocus],
toolbarClassName: 'bg-white dark:bg-gray-900',
child: WInput(
focusNode: _amountFocus,
placeholder: '0.00',
type: InputType.number,
),
)
Multi-field form with custom toolbar colour#
WKeyboardActions(
focusNodes: [_nameFocus, _emailFocus, _amountFocus],
toolbarClassName: 'bg-blue-50 dark:bg-blue-950',
child: Column(
children: [
WInput(focusNode: _nameFocus, placeholder: 'Jane Doe'),
WInput(focusNode: _emailFocus, placeholder: '[email protected]'),
WInput(focusNode: _amountFocus, placeholder: '0.00'),
],
),
)
Custom close widget#
WKeyboardActions(
focusNodes: [_focusNode],
toolbarClassName: 'bg-emerald-50 dark:bg-emerald-900',
closeWidgetBuilder: (node) => TextButton.icon(
onPressed: node.unfocus,
icon: const Icon(Icons.check_circle_outlined),
label: const Text('Done'),
),
child: myInputField,
)