keyboard_actions_bar 2.0.0
keyboard_actions_bar: ^2.0.0 copied to clipboard
A fully customizable keyboard toolbar for Flutter with zero FocusNode boilerplate, Material 3 theming, tap-outside dismiss, and 6 built-in actions.
import 'package:flutter/material.dart';
import 'package:keyboard_actions_bar/keyboard_actions_bar.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'keyboard_actions_bar',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system, // follows the phone's light/dark setting
home: const _HomePage(),
);
}
// ── Home ──────────────────────────────────────────────────────────────────────
class _HomePage extends StatefulWidget {
const _HomePage();
@override
State<_HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<_HomePage> {
final _done = TextEditingController();
final _nav1 = TextEditingController();
final _nav2 = TextEditingController();
final _nav3 = TextEditingController();
final _clear = TextEditingController();
final _insertAt = TextEditingController();
final _customLbl = TextEditingController();
final _customIcon = TextEditingController();
final _customBldr = TextEditingController();
final _counter = TextEditingController();
final _chips = TextEditingController();
final _enabledDemo = TextEditingController();
final _disabledDemo = TextEditingController();
final _precedence = TextEditingController();
final _autoName = TextEditingController();
final _autoEmail = TextEditingController();
@override
void dispose() {
for (final c in [
_done,
_nav1,
_nav2,
_nav3,
_clear,
_insertAt,
_customLbl,
_customIcon,
_customBldr,
_counter,
_chips,
_enabledDemo,
_disabledDemo,
_precedence,
_autoName,
_autoEmail,
]) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('keyboard_actions_bar'),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
body: KeyboardActionsBar(
config: KeyboardConfig.allPlatforms,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// ── 1. done() ─────────────────────────────────────────────
_Sep('1. KeyboardAction.done()'),
KeyboardField(
actions: [KeyboardAction.done()],
child: _tf(_done, 'done — closes the keyboard'),
),
// ── 2. next() / prev() ────────────────────────────────────
_Sep('2. KeyboardAction.next() / prev()'),
KeyboardField(
actions: [KeyboardAction.next(), KeyboardAction.done()],
child: _tf(_nav1, 'nav 1 — next only'),
),
const SizedBox(height: 10),
KeyboardField(
actions: [
KeyboardAction.prev(),
KeyboardAction.next(),
KeyboardAction.done(),
],
child: _tf(_nav2, 'nav 2 — prev + next'),
),
const SizedBox(height: 10),
KeyboardField(
actions: [KeyboardAction.prev(), KeyboardAction.done()],
child: _tf(_nav3, 'nav 3 — prev only'),
),
// ── 3. clear(controller) ──────────────────────────────────
_Sep('3. KeyboardAction.clear(controller)'),
KeyboardField(
actions: [
KeyboardAction.clear(_clear),
KeyboardAction.done(),
],
child: _tf(_clear, 'clear — tap ⌫ to wipe field'),
),
// ── 4. insertAt(controller) ───────────────────────────────
_Sep('4. KeyboardAction.insertAt(controller)'),
KeyboardField(
actions: [
KeyboardAction.insertAt(_insertAt),
KeyboardAction.insertAt(_insertAt,
char: '.com', label: '.com'),
KeyboardAction.done(),
],
child: _tf(_insertAt, 'insertAt — "@" or ".com"',
type: TextInputType.emailAddress),
),
// ── 5. custom(label:) ─────────────────────────────────────
_Sep('5. KeyboardAction.custom(label:)'),
KeyboardField(
actions: [
KeyboardAction.custom(
label: '#tag',
onTap: () {
_customLbl.text += '#tag ';
_customLbl.selection = TextSelection.collapsed(
offset: _customLbl.text.length);
},
),
KeyboardAction.done(),
],
child: _tf(_customLbl, 'custom label — tap "#tag"'),
),
// ── 6. custom(icon:) ──────────────────────────────────────
_Sep('6. KeyboardAction.custom(icon:)'),
KeyboardField(
actions: [
KeyboardAction.custom(
icon: Icons.emoji_emotions_outlined,
onTap: () {
_customIcon.text += '😊';
_customIcon.selection = TextSelection.collapsed(
offset: _customIcon.text.length);
},
),
KeyboardAction.custom(
icon: Icons.send_outlined,
onTap: () {
FocusScope.of(context).unfocus();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sent: "${_customIcon.text}"')),
);
},
),
KeyboardAction.done(),
],
child: _tf(_customIcon, 'custom icon — 😊 emoji / ✈ send'),
),
// ── 7. custom(builder:) ───────────────────────────────────
_Sep('7. KeyboardAction.custom(builder:)'),
KeyboardField(
actions: [
KeyboardAction.custom(
onTap: () {},
builder: (ctx, node) => GestureDetector(
onTap: () {
node.unfocus();
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content:
Text('Submitted: "${_customBldr.text}"')),
);
},
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(ctx).colorScheme.primary,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Submit',
style: TextStyle(
color: Theme.of(ctx).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
child: _tf(_customBldr, 'custom builder — full widget button'),
),
// ── 8. live character counter ─────────────────────────────
_Sep('8. footer — live character counter'),
KeyboardField(
actions: [KeyboardAction.done()],
footer: ValueListenableBuilder<TextEditingValue>(
valueListenable: _counter,
builder: (ctx, val, _) {
final n = val.text.length;
return Container(
width: double.infinity,
color: Theme.of(ctx)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: .5),
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 6),
child: Text(
'$n / 80',
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 12,
color: n > 60
? Theme.of(ctx).colorScheme.error
: Theme.of(ctx).colorScheme.onSurface,
),
),
);
},
),
child: TextField(
controller: _counter,
maxLength: 80,
maxLines: 2,
decoration: const InputDecoration(
labelText: 'counter (80 chars max)',
border: OutlineInputBorder(),
alignLabelWithHint: true,
counterText: '',
),
),
),
// ── 9. suggestion chips ───────────────────────────────────
_Sep('9. footer — suggestion chips'),
KeyboardField(
actions: [KeyboardAction.done()],
footer: Builder(
builder: (ctx) => Container(
color: Theme.of(ctx)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: .5),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
'flutter',
'dart',
'mobile',
'ios',
'android'
]
.map((t) => Padding(
padding: const EdgeInsets.only(right: 6),
child: ActionChip(
label: Text('#$t'),
visualDensity: VisualDensity.compact,
onPressed: () {
final s = _chips.text;
final v = s.isEmpty ? '#$t' : '$s #$t';
_chips
..text = v
..selection = TextSelection.collapsed(
offset: v.length);
},
),
))
.toList(),
),
),
),
),
child: _tf(_chips, 'chips — tap to append'),
),
// ── 10. enabled: false ────────────────────────────────────
_Sep('10. KeyboardField(enabled: false)'),
KeyboardField(
actions: [KeyboardAction.done()],
child: _tf(_enabledDemo, 'enabled — normal toolbar'),
),
const SizedBox(height: 10),
KeyboardField(
enabled: false,
child: _tf(_disabledDemo, 'enabled: false — no toolbar shown'),
),
// ── 11. Done label precedence ─────────────────────────────
_Sep('11. per-field label overrides config default'),
KeyboardField(
actions: [KeyboardAction.done(label: 'Save')],
child: _tf(_precedence,
'this field\'s Done button says "Save" instead'),
),
// ── 12. Auto-discovery — no KeyboardField wrapper ─────────
_Sep('12. auto-discovery — bare TextField, no wrapper'),
_tf(_autoName, 'auto — name (bare TextField)'),
const SizedBox(height: 10),
_tf(_autoEmail, 'auto — email (bare TextField, last field)',
type: TextInputType.emailAddress),
const SizedBox(height: 48),
],
),
),
);
}
// ── Helpers ───────────────────────────────────────────────────────────────────
TextField _tf(
TextEditingController ctrl,
String label, {
TextInputType? type,
}) =>
TextField(
controller: ctrl,
keyboardType: type,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
);
class _Sep extends StatelessWidget {
final String label;
const _Sep(this.label);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(top: 28, bottom: 8),
child: Row(children: [
Expanded(
child:
Divider(color: Theme.of(context).colorScheme.outlineVariant)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
Expanded(
child:
Divider(color: Theme.of(context).colorScheme.outlineVariant)),
]),
);
}