Unverified Commit 726e5d28 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Add `FocusNode.focusabilityListenable` (#144280)

This is for https://github.com/flutter/flutter/issues/127803: a text field should unregister from the scribble scope, when it becomes unfocusable. 

When a `FocusNode` has listeners and its `_canRequestFocus` flag is set to true, it adds `+1` to `_focusabilityListeningDescendantCount` of all ancestors until it reaches the first ancestor with `descendantsAreFocusable = false`. When the a `FocusNode`'s `descendantsAreFocusable` changes, all listeners that contributed to its `_focusabilityListeningDescendantCount` will be notified.
parent 1abc5cdf
......@@ -2108,6 +2108,275 @@ void main() {
tester.binding.focusManager.removeListener(handleFocusChange);
});
group('focusability listener', () {
int focusabilityChangeCount = 0;
void focusabilityCallback() {
focusabilityChangeCount += 1;
}
setUp(() { focusabilityChangeCount = 0; });
testWidgets('canRequestFocus affects focusability of the node', (WidgetTester tester) async {
int node2CallbackCounter = 0;
void node2Callback() { node2CallbackCounter += 1; }
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
final FocusNode node2 = FocusNode(debugLabel: 'node 2')..focusabilityListenable.addListener(node2Callback);
addTearDown(node1.dispose);
addTearDown(node2.dispose);
await tester.pumpWidget(
Focus(
focusNode: node1,
child: Focus(
focusNode: node2,
child: const SizedBox(),
),
),
);
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 0);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 0);
node1.canRequestFocus = false;
expect(node1.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 0);
node1.canRequestFocus = true;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 0);
node2.canRequestFocus = false;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node2.focusabilityListenable.value, isFalse);
expect(node2CallbackCounter, 1);
node2.canRequestFocus = true;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 2);
});
testWidgets('descendantsAreFocusable affects focusability of the descendants', (WidgetTester tester) async {
int node2CallbackCounter = 0;
void node2Callback() { node2CallbackCounter += 1; }
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
final FocusNode node2 = FocusNode(debugLabel: 'node 2', descendantsAreFocusable: false)..focusabilityListenable.addListener(node2Callback);
addTearDown(node1.dispose);
addTearDown(node2.dispose);
await tester.pumpWidget(
Focus(
focusNode: node1,
child: Focus(
focusNode: node2,
child: const SizedBox(),
),
),
);
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 0);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 0);
node1.descendantsAreFocusable = false;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 0);
expect(node2.focusabilityListenable.value, isFalse);
expect(node2CallbackCounter, 1);
node1.descendantsAreFocusable = true;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 0);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 2);
node2.descendantsAreFocusable = false;
expect(node1.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 0);
expect(node2.focusabilityListenable.value, isTrue);
expect(node2CallbackCounter, 2);
});
testWidgets('Reparenting affects focusability of the node', (WidgetTester tester) async {
int node3CallbackCounter = 0;
void node3Callback() { node3CallbackCounter += 1; }
final FocusNode node1 = FocusNode(debugLabel: 'node 1');
final FocusNode node2 = FocusNode(debugLabel: 'node 2', descendantsAreFocusable: false);
final FocusNode node3 = FocusNode(debugLabel: 'node 3')..focusabilityListenable.addListener(node3Callback);
final FocusNode node4 = FocusNode(debugLabel: 'node 4')..focusabilityListenable.addListener(focusabilityCallback);
addTearDown(node1.dispose);
addTearDown(node2.dispose);
addTearDown(node3.dispose);
addTearDown(node4.dispose);
await tester.pumpWidget(
Focus(
focusNode: node1,
child: Focus(
focusNode: node2,
child: Column(
children: <Widget>[
Focus(focusNode: node3, child: Container()),
Focus(focusNode: node4, child: Container()),
],
)
),
),
);
// The listeners are notified on reparent.
expect(node4.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node3.focusabilityListenable.value, isFalse);
expect(node3CallbackCounter, 1);
// Swap node 1 and node 3.
await tester.pumpWidget(
Focus(
focusNode: node3,
child: Focus(
focusNode: node2,
child: Column(
children: <Widget>[
Focus(focusNode: node1, child: Container()),
Focus(focusNode: node4, child: Container()),
],
)
),
),
);
expect(node4.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node3.focusabilityListenable.value, isTrue);
expect(node3CallbackCounter, 2);
// Swap node 1 and node 2.
await tester.pumpWidget(
Focus(
focusNode: node3,
child: Focus(
focusNode: node1,
child: Column(
children: <Widget>[
Focus(focusNode: node2, child: Container()),
Focus(focusNode: node4, child: Container()),
],
)
),
),
);
expect(node4.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node3.focusabilityListenable.value, isTrue);
expect(node3CallbackCounter, 2);
// Swap node 2 and node 4.
await tester.pumpWidget(
Focus(
focusNode: node3,
child: Focus(
focusNode: node1,
child: Column(
children: <Widget>[
Focus(focusNode: node4, child: Container()),
Focus(focusNode: node2, child: Container()),
],
)
),
),
);
expect(node4.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node3.focusabilityListenable.value, isTrue);
expect(node3CallbackCounter, 2);
// Return to the initial state
await tester.pumpWidget(
Focus(
focusNode: node1,
child: Focus(
focusNode: node2,
child: Column(
children: <Widget>[
Focus(focusNode: node3, child: Container()),
Focus(focusNode: node4, child: Container()),
],
)
),
),
);
expect(node4.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 3);
expect(node3.focusabilityListenable.value, isFalse);
expect(node3CallbackCounter, 3);
});
testWidgets('does not get called in dispose', (WidgetTester tester) async {
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
final FocusNode node2 = FocusNode(debugLabel: 'node 2')..focusabilityListenable.addListener(focusabilityCallback);
await tester.pumpWidget(
Focus(
descendantsAreFocusable: false,
child: Column(
children: <Widget>[
Focus(focusNode: node1, child: Container()),
Focus(focusNode: node2, child: Container()),
],
),
),
);
expect(focusabilityChangeCount, 2);
await tester.pumpWidget(const SizedBox());
expect(focusabilityChangeCount, 2);
});
testWidgets('Adding removing listeners many times', (WidgetTester tester) async {
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
final FocusNode node2 = FocusNode(debugLabel: 'node 2');
for (int i = 0; i < 100; i += 1) {
node1.focusabilityListenable.removeListener(focusabilityCallback);
node1.focusabilityListenable.removeListener(focusabilityCallback);
node1.focusabilityListenable.addListener(focusabilityCallback);
node1.focusabilityListenable.removeListener(focusabilityCallback);
}
node1.focusabilityListenable.addListener(focusabilityCallback);
node1.focusabilityListenable.addListener(focusabilityCallback);
node2.focusabilityListenable.addListener(focusabilityCallback);
expect(focusabilityChangeCount, 0);
await tester.pumpWidget(
Focus(
descendantsAreFocusable: false,
child: Column(
children: <Widget>[
Focus(focusNode: node1, child: Container()),
Focus(focusNode: node2, child: Container()),
],
),
),
);
expect(focusabilityChangeCount, 3);
});
});
testWidgets('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async {
final bool oldDebugFocusChanges = debugFocusChanges;
final DebugPrintCallback oldDebugPrint = debugPrint;
......
......@@ -2141,6 +2141,169 @@ void main() {
expect(childFocusNode.canRequestFocus, isTrue);
});
});
group('focusability listener', () {
int focusabilityChangeCount = 0;
void focusabilityCallback() {
focusabilityChangeCount += 1;
}
setUp(() { focusabilityChangeCount = 0; });
testWidgets('canRequestFocus affects child focusability', (WidgetTester tester) async {
final FocusScopeNode scopeNode1 = FocusScopeNode(debugLabel: 'scope1');
final FocusScopeNode scopeNode2 = FocusScopeNode(debugLabel: 'scope2');
final FocusNode node1 = FocusNode(debugLabel: 'node 1');
final FocusNode node2 = FocusNode(debugLabel: 'node 2');
final FocusNode node3 = FocusNode(debugLabel: 'node 3');
addTearDown(scopeNode1.dispose);
addTearDown(scopeNode2.dispose);
addTearDown(node1.dispose);
addTearDown(node2.dispose);
addTearDown(node3.dispose);
await tester.pumpWidget(
FocusScope(
node: scopeNode1,
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: Container(),
),
Focus(
focusNode: node2,
child: FocusScope(
node: scopeNode2,
child: Focus(focusNode: node3, child: const SizedBox()),
),
),
],
),
),
);
node3.focusabilityListenable.addListener(focusabilityCallback);
int node1FocusabilityCallbackCount = 0;
node1.focusabilityListenable.addListener(() => node1FocusabilityCallbackCount += 1);
scopeNode1.canRequestFocus = false;
expect(node3.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node1.focusabilityListenable.value, isFalse);
expect(node1FocusabilityCallbackCount, 1);
scopeNode2.canRequestFocus = false;
expect(node3.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node1.focusabilityListenable.value, isFalse);
expect(node1FocusabilityCallbackCount, 1);
scopeNode1.canRequestFocus = true;
expect(node3.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
expect(node1.focusabilityListenable.value, isTrue);
expect(node1FocusabilityCallbackCount, 2);
scopeNode2.canRequestFocus = true;
expect(node3.focusabilityListenable.value, isTrue);
expect(focusabilityChangeCount, 2);
expect(node1.focusabilityListenable.value, isTrue);
expect(node1FocusabilityCallbackCount, 2);
});
testWidgets('onFocusabilityCallback invoked on mount, if not focusable', (WidgetTester tester) async {
final FocusScopeNode scopeNode1 = FocusScopeNode(debugLabel: 'scope1', canRequestFocus: false);
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
addTearDown(scopeNode1.dispose);
addTearDown(node1.dispose);
await tester.pumpWidget(
FocusScope(
node: scopeNode1,
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: Container(),
),
],
),
),
);
expect(node1.focusabilityListenable.value, isFalse);
expect(focusabilityChangeCount, 1);
});
testWidgets('onFocusabilityCallback is not invoked on mount, if focusable', (WidgetTester tester) async {
final FocusScopeNode scopeNode1 = FocusScopeNode(debugLabel: 'scope1');
final FocusNode node1 = FocusNode(debugLabel: 'node 1')..focusabilityListenable.addListener(focusabilityCallback);
addTearDown(scopeNode1.dispose);
addTearDown(node1.dispose);
await tester.pumpWidget(
FocusScope(
node: scopeNode1,
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: Container(),
),
],
),
),
);
expect(focusabilityChangeCount, 0);
});
testWidgets('onFocusabilityCallback on scope node', (WidgetTester tester) async {
final FocusScopeNode scopeNode1 = FocusScopeNode(debugLabel: 'scope1');
final FocusScopeNode scopeNode2 = FocusScopeNode(debugLabel: 'scope2')..focusabilityListenable.addListener(focusabilityCallback);
addTearDown(scopeNode1.dispose);
addTearDown(scopeNode2.dispose);
await tester.pumpWidget(
FocusScope(
node: scopeNode1,
child: FocusScope(node: scopeNode2, child: Container())
),
);
expect(focusabilityChangeCount, 0);
scopeNode2.canRequestFocus = false;
expect(focusabilityChangeCount, 1);
expect(scopeNode2.focusabilityListenable.value, isFalse);
scopeNode2.canRequestFocus = true;
expect(focusabilityChangeCount, 2);
expect(scopeNode2.focusabilityListenable.value, isTrue);
// scope 2 has no descendants.
scopeNode2.descendantsAreFocusable = false;
expect(focusabilityChangeCount, 2);
expect(scopeNode2.focusabilityListenable.value, isTrue);
scopeNode1.descendantsAreFocusable = false;
expect(focusabilityChangeCount, 3);
expect(scopeNode2.focusabilityListenable.value, isFalse);
scopeNode1.descendantsAreFocusable = true;
expect(focusabilityChangeCount, 4);
expect(scopeNode2.focusabilityListenable.value, isTrue);
scopeNode1.canRequestFocus = false;
expect(focusabilityChangeCount, 5);
expect(scopeNode2.focusabilityListenable.value, isFalse);
scopeNode1.canRequestFocus = true;
expect(focusabilityChangeCount, 6);
expect(scopeNode2.focusabilityListenable.value, isTrue);
});
});
}
class TestFocus extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment