Skip to content

Commit e2876c9

Browse files
Merge pull request #1453 from flutter-form-builder-ecosystem/feature/1297-improve-focus
feat: #1297 Improve focus behavior
2 parents 3ef4125 + 125f1a7 commit e2876c9

35 files changed

+1508
-868
lines changed

example/lib/minimal_code_example.dart

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_form_builder/flutter_form_builder.dart';
3+
import 'package:flutter_localizations/flutter_localizations.dart';
4+
import 'package:form_builder_validators/form_builder_validators.dart';
5+
6+
void main() => runApp(const MyApp());
7+
8+
class MyApp extends StatelessWidget {
9+
const MyApp({super.key});
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
return MaterialApp(
14+
title: 'Flutter FormBuilder Example',
15+
debugShowCheckedModeBanner: false,
16+
localizationsDelegates: const [
17+
FormBuilderLocalizations.delegate,
18+
...GlobalMaterialLocalizations.delegates,
19+
GlobalWidgetsLocalizations.delegate,
20+
],
21+
supportedLocales: FormBuilderLocalizations.supportedLocales,
22+
home: const _ExamplePage(),
23+
);
24+
}
25+
}
26+
27+
class _ExamplePage extends StatefulWidget {
28+
const _ExamplePage();
29+
30+
@override
31+
State<_ExamplePage> createState() => _ExamplePageState();
32+
}
33+
34+
class _ExamplePageState extends State<_ExamplePage> {
35+
final _formKey = GlobalKey<FormBuilderState>();
36+
37+
@override
38+
Widget build(BuildContext context) {
39+
return Scaffold(
40+
appBar: AppBar(title: const Text('Minimal code example')),
41+
body: Padding(
42+
padding: const EdgeInsets.all(16),
43+
child: FormBuilder(
44+
key: _formKey,
45+
child: Column(
46+
children: [
47+
FormBuilderFilterChip<String>(
48+
decoration: const InputDecoration(
49+
labelText: 'The language of my people',
50+
enabled: false,
51+
),
52+
name: 'languages_filter',
53+
selectedColor: Colors.red,
54+
options: const [
55+
FormBuilderChipOption(
56+
value: 'Dart',
57+
avatar: CircleAvatar(child: Text('D')),
58+
),
59+
FormBuilderChipOption(
60+
value: 'Kotlin',
61+
avatar: CircleAvatar(child: Text('K')),
62+
),
63+
FormBuilderChipOption(
64+
value: 'Java',
65+
avatar: CircleAvatar(child: Text('J')),
66+
),
67+
FormBuilderChipOption(
68+
value: 'Swift',
69+
avatar: CircleAvatar(child: Text('S')),
70+
),
71+
FormBuilderChipOption(
72+
value: 'Objective-C',
73+
avatar: CircleAvatar(child: Text('O')),
74+
),
75+
],
76+
validator: FormBuilderValidators.compose([
77+
FormBuilderValidators.minLength(1),
78+
FormBuilderValidators.maxLength(3),
79+
]),
80+
),
81+
const SizedBox(height: 10),
82+
ElevatedButton(
83+
onPressed: () {
84+
_formKey.currentState?.saveAndValidate();
85+
debugPrint(_formKey.currentState?.value.toString());
86+
},
87+
child: const Text('Print'),
88+
)
89+
],
90+
),
91+
),
92+
),
93+
);
94+
}
95+
}

lib/src/fields/form_builder_checkbox.dart

+25-1
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ class FormBuilderCheckbox extends FormBuilderFieldDecoration<bool> {
125125

126126
return InputDecorator(
127127
decoration: state.decoration,
128+
isFocused: state.effectiveFocusNode.hasFocus,
128129
child: CheckboxListTile(
129130
dense: true,
130131
isThreeLine: false,
132+
focusNode: state.effectiveFocusNode,
131133
title: title,
132134
subtitle: subtitle,
133135
value: tristate ? state.value : (state.value ?? false),
@@ -158,4 +160,26 @@ class FormBuilderCheckbox extends FormBuilderFieldDecoration<bool> {
158160
}
159161

160162
class _FormBuilderCheckboxState
161-
extends FormBuilderFieldDecorationState<FormBuilderCheckbox, bool> {}
163+
extends FormBuilderFieldDecorationState<FormBuilderCheckbox, bool> {
164+
void handleFocusChange() {
165+
setState(() {});
166+
}
167+
168+
@override
169+
void initState() {
170+
super.initState();
171+
effectiveFocusNode.addListener(handleFocusChange);
172+
}
173+
174+
@override
175+
void dispose() {
176+
effectiveFocusNode.removeListener(handleFocusChange);
177+
super.dispose();
178+
}
179+
180+
@override
181+
void didChange(bool? value) {
182+
focus();
183+
super.didChange(value);
184+
}
185+
}

lib/src/fields/form_builder_checkbox_group.dart

+37-30
Original file line numberDiff line numberDiff line change
@@ -69,36 +69,43 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
6969
builder: (FormFieldState<List<T>?> field) {
7070
final state = field as _FormBuilderCheckboxGroupState<T>;
7171

72-
return InputDecorator(
73-
decoration: state.decoration,
74-
child: GroupedCheckbox<T>(
75-
orientation: orientation,
76-
value: state.value,
77-
options: options,
78-
onChanged: (val) {
79-
field.didChange(val);
80-
},
81-
disabled: state.enabled
82-
? disabled
83-
: options.map((e) => e.value).toList(),
84-
activeColor: activeColor,
85-
visualDensity: visualDensity,
86-
focusColor: focusColor,
87-
checkColor: checkColor,
88-
materialTapTargetSize: materialTapTargetSize,
89-
hoverColor: hoverColor,
90-
tristate: tristate,
91-
wrapAlignment: wrapAlignment,
92-
wrapCrossAxisAlignment: wrapCrossAxisAlignment,
93-
wrapDirection: wrapDirection,
94-
wrapRunAlignment: wrapRunAlignment,
95-
wrapRunSpacing: wrapRunSpacing,
96-
wrapSpacing: wrapSpacing,
97-
wrapTextDirection: wrapTextDirection,
98-
wrapVerticalDirection: wrapVerticalDirection,
99-
separator: separator,
100-
controlAffinity: controlAffinity,
101-
itemDecoration: itemDecoration,
72+
return Focus(
73+
focusNode: state.effectiveFocusNode,
74+
skipTraversal: true,
75+
canRequestFocus: state.enabled,
76+
debugLabel: 'FormBuilderCheckboxGroup-$name',
77+
child: InputDecorator(
78+
decoration: state.decoration,
79+
isFocused: state.effectiveFocusNode.hasFocus,
80+
child: GroupedCheckbox<T>(
81+
orientation: orientation,
82+
value: state.value,
83+
options: options,
84+
onChanged: (val) {
85+
field.didChange(val);
86+
},
87+
disabled: state.enabled
88+
? disabled
89+
: options.map((e) => e.value).toList(),
90+
activeColor: activeColor,
91+
visualDensity: visualDensity,
92+
focusColor: focusColor,
93+
checkColor: checkColor,
94+
materialTapTargetSize: materialTapTargetSize,
95+
hoverColor: hoverColor,
96+
tristate: tristate,
97+
wrapAlignment: wrapAlignment,
98+
wrapCrossAxisAlignment: wrapCrossAxisAlignment,
99+
wrapDirection: wrapDirection,
100+
wrapRunAlignment: wrapRunAlignment,
101+
wrapRunSpacing: wrapRunSpacing,
102+
wrapSpacing: wrapSpacing,
103+
wrapTextDirection: wrapTextDirection,
104+
wrapVerticalDirection: wrapVerticalDirection,
105+
separator: separator,
106+
controlAffinity: controlAffinity,
107+
itemDecoration: itemDecoration,
108+
),
102109
),
103110
);
104111
},

lib/src/fields/form_builder_choice_chips.dart

+81-51
Original file line numberDiff line numberDiff line change
@@ -395,56 +395,63 @@ class FormBuilderChoiceChip<T> extends FormBuilderFieldDecoration<T> {
395395
}) : super(builder: (FormFieldState<T?> field) {
396396
final state = field as _FormBuilderChoiceChipState<T>;
397397

398-
return InputDecorator(
399-
decoration: state.decoration,
400-
child: Wrap(
401-
direction: direction,
402-
alignment: alignment,
403-
crossAxisAlignment: crossAxisAlignment,
404-
runAlignment: runAlignment,
405-
runSpacing: runSpacing,
406-
spacing: spacing,
407-
textDirection: textDirection,
408-
verticalDirection: verticalDirection,
409-
children: <Widget>[
410-
for (FormBuilderChipOption<T> option in options)
411-
ChoiceChip(
412-
label: option,
413-
side: side,
414-
shape: shape,
415-
selected: field.value == option.value,
416-
onSelected: state.enabled
417-
? (selected) {
418-
final choice = selected ? option.value : null;
419-
state.didChange(choice);
420-
}
421-
: null,
422-
avatar: option.avatar,
423-
selectedColor: selectedColor,
424-
disabledColor: disabledColor,
425-
backgroundColor: backgroundColor,
426-
shadowColor: shadowColor,
427-
selectedShadowColor: selectedShadowColor,
428-
elevation: elevation,
429-
pressElevation: pressElevation,
430-
materialTapTargetSize: materialTapTargetSize,
431-
labelStyle: labelStyle,
432-
labelPadding: labelPadding,
433-
padding: padding,
434-
visualDensity: visualDensity,
435-
avatarBorder: avatarBorder,
436-
showCheckmark: showCheckmark,
437-
surfaceTintColor: surfaceTintColor,
438-
clipBehavior: clipBehavior,
439-
checkmarkColor: checkmarkColor,
440-
autofocus: autofocus,
441-
avatarBoxConstraints: avatarBoxConstraints,
442-
chipAnimationStyle: chipAnimationStyle,
443-
color: color,
444-
iconTheme: iconTheme,
445-
tooltip: tooltip,
446-
),
447-
],
398+
return Focus(
399+
focusNode: state.effectiveFocusNode,
400+
skipTraversal: true,
401+
canRequestFocus: state.enabled,
402+
debugLabel: 'FormBuilderChoiceChip-$name',
403+
child: InputDecorator(
404+
decoration: state.decoration,
405+
isFocused: state.effectiveFocusNode.hasFocus,
406+
child: Wrap(
407+
direction: direction,
408+
alignment: alignment,
409+
crossAxisAlignment: crossAxisAlignment,
410+
runAlignment: runAlignment,
411+
runSpacing: runSpacing,
412+
spacing: spacing,
413+
textDirection: textDirection,
414+
verticalDirection: verticalDirection,
415+
children: <Widget>[
416+
for (FormBuilderChipOption<T> option in options)
417+
ChoiceChip(
418+
label: option,
419+
side: side,
420+
shape: shape,
421+
selected: field.value == option.value,
422+
onSelected: state.enabled
423+
? (selected) {
424+
final choice = selected ? option.value : null;
425+
state.didChange(choice);
426+
}
427+
: null,
428+
avatar: option.avatar,
429+
selectedColor: selectedColor,
430+
disabledColor: disabledColor,
431+
backgroundColor: backgroundColor,
432+
shadowColor: shadowColor,
433+
selectedShadowColor: selectedShadowColor,
434+
elevation: elevation,
435+
pressElevation: pressElevation,
436+
materialTapTargetSize: materialTapTargetSize,
437+
labelStyle: labelStyle,
438+
labelPadding: labelPadding,
439+
padding: padding,
440+
visualDensity: visualDensity,
441+
avatarBorder: avatarBorder,
442+
showCheckmark: showCheckmark,
443+
surfaceTintColor: surfaceTintColor,
444+
clipBehavior: clipBehavior,
445+
checkmarkColor: checkmarkColor,
446+
autofocus: autofocus,
447+
avatarBoxConstraints: avatarBoxConstraints,
448+
chipAnimationStyle: chipAnimationStyle,
449+
color: color,
450+
iconTheme: iconTheme,
451+
tooltip: tooltip,
452+
),
453+
],
454+
),
448455
),
449456
);
450457
});
@@ -455,4 +462,27 @@ class FormBuilderChoiceChip<T> extends FormBuilderFieldDecoration<T> {
455462
}
456463

457464
class _FormBuilderChoiceChipState<T>
458-
extends FormBuilderFieldDecorationState<FormBuilderChoiceChip<T>, T> {}
465+
extends FormBuilderFieldDecorationState<FormBuilderChoiceChip<T>, T> {
466+
void handleFocusChange() {
467+
setState(() {});
468+
}
469+
470+
@override
471+
void initState() {
472+
super.initState();
473+
effectiveFocusNode.addListener(handleFocusChange);
474+
}
475+
476+
@override
477+
void dispose() {
478+
effectiveFocusNode.removeListener(handleFocusChange);
479+
super.dispose();
480+
}
481+
482+
@override
483+
void didChange(T? value) {
484+
focus();
485+
// effectiveFocusNode.requestFocus();
486+
super.didChange(value);
487+
}
488+
}

0 commit comments

Comments
 (0)