Skip to content

Commit a9c2f8b

Browse files
authored
Add onFocusChange property for ListTile widget (#111498)
1 parent 50f101a commit a9c2f8b

11 files changed

+182
-0
lines changed

packages/flutter/lib/src/material/checkbox_list_tile.dart

+5
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class CheckboxListTile extends StatelessWidget {
176176
this.side,
177177
this.visualDensity,
178178
this.focusNode,
179+
this.onFocusChange,
179180
this.enableFeedback,
180181
}) : assert(tristate != null),
181182
assert(tristate || value != null),
@@ -320,6 +321,9 @@ class CheckboxListTile extends StatelessWidget {
320321
/// {@macro flutter.widgets.Focus.focusNode}
321322
final FocusNode? focusNode;
322323

324+
/// {@macro flutter.material.inkwell.onFocusChange}
325+
final ValueChanged<bool>? onFocusChange;
326+
323327
/// {@macro flutter.material.ListTile.enableFeedback}
324328
///
325329
/// See also:
@@ -401,6 +405,7 @@ class CheckboxListTile extends StatelessWidget {
401405
tileColor: tileColor,
402406
visualDensity: visualDensity,
403407
focusNode: focusNode,
408+
onFocusChange: onFocusChange,
404409
enableFeedback: enableFeedback,
405410
),
406411
);

packages/flutter/lib/src/material/ink_well.dart

+2
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,12 @@ class InkResponse extends StatelessWidget {
559559
/// duplication of information.
560560
final bool excludeFromSemantics;
561561

562+
/// {@template flutter.material.inkwell.onFocusChange}
562563
/// Handler called when the focus changes.
563564
///
564565
/// Called with true if this widget's node gains focus, and false if it loses
565566
/// focus.
567+
/// {@endtemplate}
566568
final ValueChanged<bool>? onFocusChange;
567569

568570
/// {@macro flutter.widgets.Focus.autofocus}

packages/flutter/lib/src/material/list_tile.dart

+5
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class ListTile extends StatelessWidget {
282282
this.enabled = true,
283283
this.onTap,
284284
this.onLongPress,
285+
this.onFocusChange,
285286
this.mouseCursor,
286287
this.selected = false,
287288
this.focusColor,
@@ -456,6 +457,9 @@ class ListTile extends StatelessWidget {
456457
/// Inoperative if [enabled] is false.
457458
final GestureLongPressCallback? onLongPress;
458459

460+
/// {@macro flutter.material.inkwell.onFocusChange}
461+
final ValueChanged<bool>? onFocusChange;
462+
459463
/// {@template flutter.material.ListTile.mouseCursor}
460464
/// The cursor for a mouse pointer when it enters or is hovering over the
461465
/// widget.
@@ -738,6 +742,7 @@ class ListTile extends StatelessWidget {
738742
customBorder: shape ?? tileTheme.shape,
739743
onTap: enabled ? onTap : null,
740744
onLongPress: enabled ? onLongPress : null,
745+
onFocusChange: onFocusChange,
741746
mouseCursor: effectiveMouseCursor,
742747
canRequestFocus: enabled,
743748
focusNode: focusNode,

packages/flutter/lib/src/material/radio_list_tile.dart

+5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class RadioListTile<T> extends StatelessWidget {
171171
this.selectedTileColor,
172172
this.visualDensity,
173173
this.focusNode,
174+
this.onFocusChange,
174175
this.enableFeedback,
175176
}) : assert(toggleable != null),
176177
assert(isThreeLine != null),
@@ -320,6 +321,9 @@ class RadioListTile<T> extends StatelessWidget {
320321
/// {@macro flutter.widgets.Focus.focusNode}
321322
final FocusNode? focusNode;
322323

324+
/// {@macro flutter.material.inkwell.onFocusChange}
325+
final ValueChanged<bool>? onFocusChange;
326+
323327
/// {@macro flutter.material.ListTile.enableFeedback}
324328
///
325329
/// See also:
@@ -385,6 +389,7 @@ class RadioListTile<T> extends StatelessWidget {
385389
contentPadding: contentPadding,
386390
visualDensity: visualDensity,
387391
focusNode: focusNode,
392+
onFocusChange: onFocusChange,
388393
enableFeedback: enableFeedback,
389394
),
390395
);

packages/flutter/lib/src/material/switch.dart

+10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class Switch extends StatelessWidget {
114114
this.overlayColor,
115115
this.splashRadius,
116116
this.focusNode,
117+
this.onFocusChange,
117118
this.autofocus = false,
118119
}) : _switchType = _SwitchType.material,
119120
assert(dragStartBehavior != null),
@@ -158,6 +159,7 @@ class Switch extends StatelessWidget {
158159
this.overlayColor,
159160
this.splashRadius,
160161
this.focusNode,
162+
this.onFocusChange,
161163
this.autofocus = false,
162164
}) : assert(autofocus != null),
163165
assert(activeThumbImage != null || onActiveThumbImageError == null),
@@ -455,6 +457,9 @@ class Switch extends StatelessWidget {
455457
/// {@macro flutter.widgets.Focus.focusNode}
456458
final FocusNode? focusNode;
457459

460+
/// {@macro flutter.material.inkwell.onFocusChange}
461+
final ValueChanged<bool>? onFocusChange;
462+
458463
/// {@macro flutter.widgets.Focus.autofocus}
459464
final bool autofocus;
460465

@@ -478,6 +483,7 @@ class Switch extends StatelessWidget {
478483
final Size size = _getSwitchSize(context);
479484
return Focus(
480485
focusNode: focusNode,
486+
onFocusChange: onFocusChange,
481487
autofocus: autofocus,
482488
child: Container(
483489
width: size.width, // Same size as the Material switch.
@@ -518,6 +524,7 @@ class Switch extends StatelessWidget {
518524
overlayColor: overlayColor,
519525
splashRadius: splashRadius,
520526
focusNode: focusNode,
527+
onFocusChange: onFocusChange,
521528
autofocus: autofocus,
522529
);
523530
}
@@ -577,6 +584,7 @@ class _MaterialSwitch extends StatefulWidget {
577584
this.overlayColor,
578585
this.splashRadius,
579586
this.focusNode,
587+
this.onFocusChange,
580588
this.autofocus = false,
581589
}) : assert(dragStartBehavior != null),
582590
assert(activeThumbImage != null || onActiveThumbImageError == null),
@@ -603,6 +611,7 @@ class _MaterialSwitch extends StatefulWidget {
603611
final MaterialStateProperty<Color?>? overlayColor;
604612
final double? splashRadius;
605613
final FocusNode? focusNode;
614+
final Function(bool)? onFocusChange;
606615
final bool autofocus;
607616
final Size size;
608617

@@ -822,6 +831,7 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
822831
child: buildToggleable(
823832
mouseCursor: effectiveMouseCursor,
824833
focusNode: widget.focusNode,
834+
onFocusChange: widget.onFocusChange,
825835
autofocus: widget.autofocus,
826836
size: widget.size,
827837
painter: _painter

packages/flutter/lib/src/material/switch_list_tile.dart

+8
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class SwitchListTile extends StatelessWidget {
176176
this.selectedTileColor,
177177
this.visualDensity,
178178
this.focusNode,
179+
this.onFocusChange,
179180
this.enableFeedback,
180181
this.hoverColor,
181182
}) : _switchListTileType = _SwitchListTileType.material,
@@ -221,6 +222,7 @@ class SwitchListTile extends StatelessWidget {
221222
this.selectedTileColor,
222223
this.visualDensity,
223224
this.focusNode,
225+
this.onFocusChange,
224226
this.enableFeedback,
225227
this.hoverColor,
226228
}) : _switchListTileType = _SwitchListTileType.adaptive,
@@ -368,6 +370,9 @@ class SwitchListTile extends StatelessWidget {
368370
/// {@macro flutter.widgets.Focus.focusNode}
369371
final FocusNode? focusNode;
370372

373+
/// {@macro flutter.material.inkwell.onFocusChange}
374+
final ValueChanged<bool>? onFocusChange;
375+
371376
/// {@macro flutter.material.ListTile.enableFeedback}
372377
///
373378
/// See also:
@@ -394,6 +399,7 @@ class SwitchListTile extends StatelessWidget {
394399
inactiveTrackColor: inactiveTrackColor,
395400
inactiveThumbColor: inactiveThumbColor,
396401
autofocus: autofocus,
402+
onFocusChange: onFocusChange,
397403
);
398404
break;
399405

@@ -409,6 +415,7 @@ class SwitchListTile extends StatelessWidget {
409415
inactiveTrackColor: inactiveTrackColor,
410416
inactiveThumbColor: inactiveThumbColor,
411417
autofocus: autofocus,
418+
onFocusChange: onFocusChange,
412419
);
413420
}
414421

@@ -452,6 +459,7 @@ class SwitchListTile extends StatelessWidget {
452459
tileColor: tileColor,
453460
visualDensity: visualDensity,
454461
focusNode: focusNode,
462+
onFocusChange: onFocusChange,
455463
enableFeedback: enableFeedback,
456464
hoverColor: hoverColor,
457465
),

packages/flutter/lib/src/material/toggleable.dart

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
305305
/// build method - potentially after wrapping it in other widgets.
306306
Widget buildToggleable({
307307
FocusNode? focusNode,
308+
Function(bool)? onFocusChange,
308309
bool autofocus = false,
309310
required MaterialStateProperty<MouseCursor> mouseCursor,
310311
required Size size,
@@ -314,6 +315,7 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
314315
actions: _actionMap,
315316
focusNode: focusNode,
316317
autofocus: autofocus,
318+
onFocusChange: onFocusChange,
317319
enabled: isInteractive,
318320
onShowFocusHighlight: _handleFocusHighlightChanged,
319321
onShowHoverHighlight: _handleHoverChanged,

packages/flutter/test/material/checkbox_list_tile_test.dart

+29
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,35 @@ void main() {
431431
expect(tileNode.hasPrimaryFocus, isTrue);
432432
});
433433

434+
testWidgets('CheckboxListTile onFocusChange callback', (WidgetTester tester) async {
435+
final FocusNode node = FocusNode(debugLabel: 'CheckboxListTile onFocusChange');
436+
bool gotFocus = false;
437+
await tester.pumpWidget(
438+
MaterialApp(
439+
home: Material(
440+
child: CheckboxListTile(
441+
value: true,
442+
focusNode: node,
443+
onFocusChange: (bool focused) {
444+
gotFocus = focused;
445+
},
446+
onChanged: (bool? value) {},
447+
),
448+
),
449+
),
450+
);
451+
452+
node.requestFocus();
453+
await tester.pump();
454+
expect(gotFocus, isTrue);
455+
expect(node.hasFocus, isTrue);
456+
457+
node.unfocus();
458+
await tester.pump();
459+
expect(gotFocus, isFalse);
460+
expect(node.hasFocus, isFalse);
461+
});
462+
434463
testWidgets('CheckboxListTile can be disabled', (WidgetTester tester) async {
435464
bool? value = false;
436465
bool enabled = true;

packages/flutter/test/material/list_tile_test.dart

+28
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,34 @@ void main() {
14761476
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
14771477
});
14781478

1479+
testWidgets('ListTile onFocusChange callback', (WidgetTester tester) async {
1480+
final FocusNode node = FocusNode(debugLabel: 'ListTile Focus');
1481+
bool gotFocus = false;
1482+
await tester.pumpWidget(
1483+
MaterialApp(
1484+
home: Material(
1485+
child: ListTile(
1486+
focusNode: node,
1487+
onFocusChange: (bool focused) {
1488+
gotFocus = focused;
1489+
},
1490+
onTap: () {},
1491+
),
1492+
),
1493+
),
1494+
);
1495+
1496+
node.requestFocus();
1497+
await tester.pump();
1498+
expect(gotFocus, isTrue);
1499+
expect(node.hasFocus, isTrue);
1500+
1501+
node.unfocus();
1502+
await tester.pump();
1503+
expect(gotFocus, isFalse);
1504+
expect(node.hasFocus, isFalse);
1505+
});
1506+
14791507
testWidgets('ListTile respects tileColor & selectedTileColor', (WidgetTester tester) async {
14801508
bool isSelected = false;
14811509
final Color tileColor = Colors.green.shade500;

packages/flutter/test/material/radio_list_tile_test.dart

+30
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,36 @@ void main() {
793793
expect(tileNode.hasPrimaryFocus, isTrue);
794794
});
795795

796+
testWidgets('RadioListTile onFocusChange callback', (WidgetTester tester) async {
797+
final FocusNode node = FocusNode(debugLabel: 'RadioListTile onFocusChange');
798+
bool gotFocus = false;
799+
await tester.pumpWidget(
800+
MaterialApp(
801+
home: Material(
802+
child: RadioListTile<bool>(
803+
value: true,
804+
focusNode: node,
805+
onFocusChange: (bool focused) {
806+
gotFocus = focused;
807+
},
808+
onChanged: (bool? value) {},
809+
groupValue: true,
810+
),
811+
),
812+
),
813+
);
814+
815+
node.requestFocus();
816+
await tester.pump();
817+
expect(gotFocus, isTrue);
818+
expect(node.hasFocus, isTrue);
819+
820+
node.unfocus();
821+
await tester.pump();
822+
expect(gotFocus, isFalse);
823+
expect(node.hasFocus, isFalse);
824+
});
825+
796826
group('feedback', () {
797827
late FeedbackTester feedback;
798828

packages/flutter/test/material/switch_list_tile_test.dart

+58
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,64 @@ void main() {
480480
expect(tileNode.hasPrimaryFocus, isTrue);
481481
});
482482

483+
testWidgets('SwitchListTile onFocusChange callback', (WidgetTester tester) async {
484+
final FocusNode node = FocusNode(debugLabel: 'SwitchListTile onFocusChange');
485+
bool gotFocus = false;
486+
await tester.pumpWidget(
487+
MaterialApp(
488+
home: Material(
489+
child: SwitchListTile(
490+
value: true,
491+
focusNode: node,
492+
onFocusChange: (bool focused) {
493+
gotFocus = focused;
494+
},
495+
onChanged: (bool value) {},
496+
),
497+
),
498+
),
499+
);
500+
501+
node.requestFocus();
502+
await tester.pump();
503+
expect(gotFocus, isTrue);
504+
expect(node.hasFocus, isTrue);
505+
506+
node.unfocus();
507+
await tester.pump();
508+
expect(gotFocus, isFalse);
509+
expect(node.hasFocus, isFalse);
510+
});
511+
512+
testWidgets('SwitchListTile.adaptive onFocusChange Callback', (WidgetTester tester) async {
513+
final FocusNode node = FocusNode(debugLabel: 'SwitchListTile.adaptive onFocusChange');
514+
bool gotFocus = false;
515+
await tester.pumpWidget(
516+
MaterialApp(
517+
home: Material(
518+
child: SwitchListTile.adaptive(
519+
value: true,
520+
focusNode: node,
521+
onFocusChange: (bool focused) {
522+
gotFocus = focused;
523+
},
524+
onChanged: (bool value) {},
525+
),
526+
),
527+
),
528+
);
529+
530+
node.requestFocus();
531+
await tester.pump();
532+
expect(gotFocus, isTrue);
533+
expect(node.hasFocus, isTrue);
534+
535+
node.unfocus();
536+
await tester.pump();
537+
expect(gotFocus, isFalse);
538+
expect(node.hasFocus, isFalse);
539+
});
540+
483541
group('feedback', () {
484542
late FeedbackTester feedback;
485543

0 commit comments

Comments
 (0)