diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 3dd20a0d27..e797bc9ed1 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -177,6 +177,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe /// rounded rectangle border around the text field. If you set the [decoration] /// property to null, the decoration will be removed entirely. /// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// /// Remember to call [TextEditingController.dispose] when it is no longer /// needed. This will ensure we discard any resources used by the object. /// diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart index c8ad95b26e..bf09e043ce 100644 --- a/packages/flutter/lib/src/material/selectable_text.dart +++ b/packages/flutter/lib/src/material/selectable_text.dart @@ -123,6 +123,8 @@ class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestur /// behavior is useful, for example, to make the text bold while using the /// default font family and size. /// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// /// {@tool snippet} /// /// ```dart @@ -451,7 +453,7 @@ class SelectableText extends StatefulWidget { } } -class _SelectableTextState extends State with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { +class _SelectableTextState extends State implements TextSelectionGestureDetectorBuilderDelegate { EditableTextState? get _editableText => editableTextKey.currentState; late _TextSpanEditingController _controller; @@ -579,12 +581,8 @@ class _SelectableTextState extends State with AutomaticKeepAlive return false; } - @override - bool get wantKeepAlive => true; - @override Widget build(BuildContext context) { - super.build(context); // See AutomaticKeepAliveClientMixin. // TODO(garyq): Assert to block WidgetSpans from being used here are removed, // but we still do not yet have nice handling of things like carets, clipboard, // and other features. We should add proper support. Currently, caret handling diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index fc167cfd11..0257ea3286 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -168,6 +168,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete /// To integrate the [TextField] into a [Form] with other [FormField] widgets, /// consider using [TextFormField]. /// +/// {@template flutter.material.textfield.wantKeepAlive} +/// When the widget has focus, it will prevent itself from disposing via its +/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in +/// order to avoid losing the selection. Removing the focus will allow it to be +/// disposed. +/// {@endtemplate} +/// /// Remember to call [TextEditingController.dispose] of the [TextEditingController] /// when it is no longer needed. This will ensure we discard any resources used /// by the object. diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 3b1c810814..d6fef5c037 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -31,6 +31,8 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType; /// If a [controller] is not specified, [initialValue] can be used to give /// the automatically generated controller an initial value. /// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// /// Remember to call [TextEditingController.dispose] of the [TextEditingController] /// when it is no longer needed. This will ensure we discard any resources used /// by the object. diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index ae31e5db4b..3a0ac65ce4 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -344,6 +344,10 @@ class ToolbarOptions { /// [onSubmitted] can be used to manually move focus to another input widget /// when a user finishes with the currently focused input widget. /// +/// When the widget has focus, it will prevent itself from disposing via +/// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the +/// selection. Removing the focus will allow it to be disposed. +/// /// Rather than using this widget directly, consider using [TextField], which /// is a full-featured, material-design text input field with placeholder text, /// labels, and [Form] integration. diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 6473cf5026..e9f98b5431 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -4869,4 +4869,91 @@ void main() { matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'), ); }); + + testWidgets('keeps alive when has focus', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: DefaultTabController( + length: 2, + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverToBoxAdapter( + child: Container( + height: 200, + color: Colors.black12, + child: const Center(child: Text('Sliver 1')), + ), + ), + const SliverToBoxAdapter( + child: Center( + child: TabBar( + labelColor: Colors.black, + tabs: [ + Tab(text: 'Sliver Tab 1'), + Tab(text: 'Sliver Tab 2'), + ], + ), + ) + ), + ]; + }, + body: const TabBarView( + children: [ + Padding( + padding: EdgeInsets.only(top: 100.0), + child: Text('Regular Text'), + ), + Padding( + padding: EdgeInsets.only(top: 100.0), + child: SelectableText('Selectable Text'), + ), + ], + ), + ), + ), + ), + ), + ); + + // Without any selection, the offscreen widget is disposed and can't be + // found, for both Text and SelectableText. + expect(find.text('Regular Text', skipOffstage: false), findsOneWidget); + expect(find.byType(SelectableText, skipOffstage: false), findsNothing); + + await tester.tap(find.text('Sliver Tab 2')); + await tester.pumpAndSettle(); + expect(find.text('Regular Text', skipOffstage: false), findsNothing); + expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget); + + await tester.tap(find.text('Sliver Tab 1')); + await tester.pumpAndSettle(); + expect(find.text('Regular Text', skipOffstage: false), findsOneWidget); + expect(find.byType(SelectableText, skipOffstage: false), findsNothing); + + // Switch back to tab 2 and select some text in SelectableText. + await tester.tap(find.text('Sliver Tab 2')); + await tester.pumpAndSettle(); + expect(find.text('Regular Text', skipOffstage: false), findsNothing); + expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget); + + final EditableText editableText = tester.widget(find.byType(EditableText)); + expect(editableText.controller.selection.isValid, isFalse); + await tester.tapAt(textOffsetToPosition(tester, 4)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, 4)); + await tester.pumpAndSettle(); + expect(editableText.controller.selection.isValid, isTrue); + expect(editableText.controller.selection.baseOffset, 0); + expect(editableText.controller.selection.extentOffset, 'Selectable'.length); + + // Switch back to tab 1. The SelectableText remains because it is preserving + // its selection. + await tester.tap(find.text('Sliver Tab 1')); + await tester.pumpAndSettle(); + expect(find.text('Regular Text', skipOffstage: false), findsOneWidget); + expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget); + }); }