mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
feat: Expose FocusNode of FocusTraversalGroup (#171979)
Expose the focus node of `FocusTraversalGroup` so customers can listen to focus changes of its descendants. Fixes https://github.com/flutter/flutter/issues/171516 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
This commit is contained in:
parent
94265c52b3
commit
753dbac882
@ -2038,6 +2038,7 @@ class FocusTraversalGroup extends StatefulWidget {
|
||||
FocusTraversalPolicy? policy,
|
||||
this.descendantsAreFocusable = true,
|
||||
this.descendantsAreTraversable = true,
|
||||
this.onFocusNodeCreated,
|
||||
required this.child,
|
||||
}) : policy = policy ?? ReadingOrderTraversalPolicy();
|
||||
|
||||
@ -2069,6 +2070,9 @@ class FocusTraversalGroup extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
/// Called when the [FocusNode] of this widget is created.
|
||||
final void Function(FocusNode)? onFocusNodeCreated;
|
||||
|
||||
/// Returns the [FocusTraversalPolicy] that applies to the nearest ancestor of
|
||||
/// the given [FocusNode].
|
||||
///
|
||||
@ -2206,6 +2210,12 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
|
||||
policy: widget.policy,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.onFocusNodeCreated?.call(focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
|
||||
@ -3786,6 +3786,78 @@ void main() {
|
||||
expect(childScope.hasFocus, isTrue);
|
||||
expect(nodeA.hasFocus, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('GIVEN onFocusNodeCreated is not null '
|
||||
'THEN it is called when the FocusTraversalGroup is built', (WidgetTester tester) async {
|
||||
FocusNode? node;
|
||||
await tester.pumpWidget(
|
||||
FocusTraversalGroup(
|
||||
onFocusNodeCreated: (FocusNode createdNode) => node = createdNode,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(node, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'GIVEN a FocusScope with no focusable descendants '
|
||||
'WHEN the user presses TAB to navigate focus '
|
||||
'THEN focus should skip the scope and land on the next focusable widget without requiring multiple TAB presses',
|
||||
(WidgetTester tester) async {
|
||||
final FocusNode enabledButton1Node = FocusNode();
|
||||
addTearDown(enabledButton1Node.dispose);
|
||||
|
||||
final FocusNode enabledButton2Node = FocusNode();
|
||||
addTearDown(enabledButton2Node.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
MaterialButton(
|
||||
focusNode: enabledButton1Node,
|
||||
onPressed: () {}, // enabled
|
||||
child: const Text('Enabled Button 1'),
|
||||
),
|
||||
FocusTraversalGroup(
|
||||
child: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
MaterialButton(
|
||||
onPressed: null, // disabled
|
||||
child: Text('Disabled Button 1'),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
MaterialButton(
|
||||
onPressed: null, // disabled
|
||||
child: Text('Disabled Button 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MaterialButton(
|
||||
focusNode: enabledButton2Node,
|
||||
onPressed: () {}, // enabled
|
||||
child: const Text('Enabled Button 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
expect(enabledButton1Node.hasPrimaryFocus, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
expect(enabledButton2Node.hasPrimaryFocus, isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class TestRoute extends PageRouteBuilder<void> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user