Skip to content

Commit e5706c3

Browse files
blerouxwalley892
authored andcommitted
Fix EditableText _justResumed is not accurate (flutter#177658)
## Description This PR tweaks the selection logic added in flutter#157399. Before this PR (and since flutter#157399) when the app resumed while a TextField was selected the selection of the TextField is maintained. This is the right behavior for the currently focused TextField. But when there are several TextFields, after the app resumed and the user move the focus to a another TextField, the behavior should be to select all the content of the newly focused TextField. To achieve this the `_justResumed`flag added in flutter#157399 should be reset as soon as the focus move as it is needed only for the current focused TextField to restore its selection just after the app resumed. ## Related Issue Fixes [Pressing tab does select all content when app is resumed for TextFields which were not focused](flutter#177650) ## Tests - Adds 1 test
1 parent 492d252 commit e5706c3

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3214,10 +3214,23 @@ class EditableTextState extends State<EditableText>
32143214
widget.focusNode.addListener(_handleFocusChanged);
32153215
_cursorVisibilityNotifier.value = widget.showCursor;
32163216
_spellCheckConfiguration = _inferSpellCheckConfiguration(widget.spellCheckConfiguration);
3217-
_appLifecycleListener = AppLifecycleListener(onResume: () => _justResumed = true);
3217+
_appLifecycleListener = AppLifecycleListener(onResume: _onResume);
32183218
_initProcessTextActions();
32193219
}
32203220

3221+
void _onResume() {
3222+
_justResumed = true;
3223+
// To prevent adding multiple listeners, remove any existing one first.
3224+
FocusManager.instance.removeListener(_resetJustResumed);
3225+
// Reset _justResumed as soon as there is a focus change.
3226+
FocusManager.instance.addListener(_resetJustResumed);
3227+
}
3228+
3229+
void _resetJustResumed() {
3230+
_justResumed = false;
3231+
FocusManager.instance.removeListener(_resetJustResumed);
3232+
}
3233+
32213234
/// Query the engine to initialize the list of text processing actions to show
32223235
/// in the text selection toolbar.
32233236
Future<void> _initProcessTextActions() async {
@@ -3436,6 +3449,7 @@ class EditableTextState extends State<EditableText>
34363449
_cursorVisibilityNotifier.dispose();
34373450
_appLifecycleListener.dispose();
34383451
FocusManager.instance.removeListener(_unflagInternalFocus);
3452+
FocusManager.instance.removeListener(_resetJustResumed);
34393453
_disposeScrollNotificationObserver();
34403454
super.dispose();
34413455
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');

packages/flutter/test/widgets/editable_text_test.dart

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16306,6 +16306,15 @@ void main() {
1630616306
});
1630716307

1630816308
group('selection behavior when receiving focus', () {
16309+
Future<void> setAppLifecycleState(AppLifecycleState state) async {
16310+
final ByteData? message = const StringCodec().encodeMessage(state.toString());
16311+
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
16312+
'flutter/lifecycle',
16313+
message,
16314+
(_) {},
16315+
);
16316+
}
16317+
1630916318
testWidgets('tabbing between fields', (WidgetTester tester) async {
1631016319
final bool isDesktop =
1631116320
debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
@@ -16590,12 +16599,6 @@ void main() {
1659016599

1659116600
// Regression test for https://github.com/flutter/flutter/issues/156078.
1659216601
testWidgets('when having focus regained after the app resumed', (WidgetTester tester) async {
16593-
Future<void> setAppLifeCycleState(AppLifecycleState state) async {
16594-
final ByteData? message = const StringCodec().encodeMessage(state.toString());
16595-
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
16596-
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
16597-
}
16598-
1659916602
final TextEditingController controller = TextEditingController(text: 'Flutter!');
1660016603
addTearDown(controller.dispose);
1660116604
final FocusNode focusNode = FocusNode();
@@ -16620,12 +16623,82 @@ void main() {
1662016623
expect(focusNode.hasFocus, true);
1662116624
expect(controller.selection, collapsedAtEnd('Flutter!').selection);
1662216625

16623-
await setAppLifeCycleState(AppLifecycleState.inactive);
16624-
await setAppLifeCycleState(AppLifecycleState.resumed);
16626+
await setAppLifecycleState(AppLifecycleState.inactive);
16627+
await setAppLifecycleState(AppLifecycleState.resumed);
1662516628

1662616629
expect(focusNode.hasFocus, true);
1662716630
expect(controller.selection, collapsedAtEnd('Flutter!').selection);
1662816631
}, variant: TargetPlatformVariant.all());
16632+
16633+
testWidgets(
16634+
'moving focus after the app resumed should select all the content on desktop',
16635+
(WidgetTester tester) async {
16636+
final TextEditingController controller1 = TextEditingController.fromValue(
16637+
collapsedAtEnd('Flutter!'),
16638+
);
16639+
addTearDown(controller1.dispose);
16640+
final TextEditingController controller2 = TextEditingController.fromValue(
16641+
collapsedAtEnd('Dart!'),
16642+
);
16643+
addTearDown(controller2.dispose);
16644+
final FocusNode focusNode1 = FocusNode();
16645+
addTearDown(focusNode1.dispose);
16646+
final FocusNode focusNode2 = FocusNode();
16647+
addTearDown(focusNode2.dispose);
16648+
16649+
await tester.pumpWidget(
16650+
MaterialApp(
16651+
home: Center(
16652+
child: Column(
16653+
children: <Widget>[
16654+
EditableText(
16655+
key: ValueKey<String>(controller1.text),
16656+
controller: controller1,
16657+
focusNode: focusNode1,
16658+
autofocus: true,
16659+
style: Typography.material2018().black.titleMedium!,
16660+
cursorColor: Colors.blue,
16661+
backgroundCursorColor: Colors.grey,
16662+
),
16663+
EditableText(
16664+
key: ValueKey<String>(controller2.text),
16665+
controller: controller2,
16666+
focusNode: focusNode2,
16667+
style: Typography.material2018().black.titleMedium!,
16668+
cursorColor: Colors.blue,
16669+
backgroundCursorColor: Colors.grey,
16670+
),
16671+
],
16672+
),
16673+
),
16674+
),
16675+
);
16676+
16677+
expect(focusNode1.hasFocus, true);
16678+
expect(focusNode2.hasFocus, false);
16679+
expect(controller1.selection, collapsedAtEnd('Flutter!').selection);
16680+
expect(controller2.selection, collapsedAtEnd('Dart!').selection);
16681+
16682+
// Pause and resume the application.
16683+
await setAppLifecycleState(AppLifecycleState.inactive);
16684+
await setAppLifecycleState(AppLifecycleState.resumed);
16685+
16686+
// Change focus to the second EditableText.
16687+
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
16688+
await tester.pumpAndSettle();
16689+
16690+
expect(focusNode1.hasFocus, false);
16691+
expect(focusNode2.hasFocus, true);
16692+
expect(controller1.selection, collapsedAtEnd('Flutter!').selection);
16693+
16694+
// The text of the second EditableText should be entirely selected.
16695+
expect(
16696+
controller2.selection,
16697+
TextSelection(baseOffset: 0, extentOffset: controller2.text.length),
16698+
);
16699+
},
16700+
variant: TargetPlatformVariant.desktop(),
16701+
);
1662916702
});
1663016703

1663116704
testWidgets('EditableText respects MediaQuery.boldText', (WidgetTester tester) async {

0 commit comments

Comments
 (0)