Unverified Commit e9553cd5 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

SelectableText keep alive only when it has selection (#94493)

SelectableText defers to EditableText for wantKeepAlive, plus improved docs.
parent 80f732bc
...@@ -177,6 +177,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe ...@@ -177,6 +177,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
/// rounded rectangle border around the text field. If you set the [decoration] /// rounded rectangle border around the text field. If you set the [decoration]
/// property to null, the decoration will be removed entirely. /// property to null, the decoration will be removed entirely.
/// ///
/// {@macro flutter.material.textfield.wantKeepAlive}
///
/// Remember to call [TextEditingController.dispose] when it is no longer /// Remember to call [TextEditingController.dispose] when it is no longer
/// needed. This will ensure we discard any resources used by the object. /// needed. This will ensure we discard any resources used by the object.
/// ///
......
...@@ -123,6 +123,8 @@ class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestur ...@@ -123,6 +123,8 @@ class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestur
/// behavior is useful, for example, to make the text bold while using the /// behavior is useful, for example, to make the text bold while using the
/// default font family and size. /// default font family and size.
/// ///
/// {@macro flutter.material.textfield.wantKeepAlive}
///
/// {@tool snippet} /// {@tool snippet}
/// ///
/// ```dart /// ```dart
...@@ -451,7 +453,7 @@ class SelectableText extends StatefulWidget { ...@@ -451,7 +453,7 @@ class SelectableText extends StatefulWidget {
} }
} }
class _SelectableTextState extends State<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { class _SelectableTextState extends State<SelectableText> implements TextSelectionGestureDetectorBuilderDelegate {
EditableTextState? get _editableText => editableTextKey.currentState; EditableTextState? get _editableText => editableTextKey.currentState;
late _TextSpanEditingController _controller; late _TextSpanEditingController _controller;
...@@ -579,12 +581,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive ...@@ -579,12 +581,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
return false; return false;
} }
@override
bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
// TODO(garyq): Assert to block WidgetSpans from being used here are removed, // 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, // 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 // and other features. We should add proper support. Currently, caret handling
......
...@@ -168,6 +168,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -168,6 +168,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
/// To integrate the [TextField] into a [Form] with other [FormField] widgets, /// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField]. /// 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] /// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used /// when it is no longer needed. This will ensure we discard any resources used
/// by the object. /// by the object.
......
...@@ -31,6 +31,8 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType; ...@@ -31,6 +31,8 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;
/// If a [controller] is not specified, [initialValue] can be used to give /// If a [controller] is not specified, [initialValue] can be used to give
/// the automatically generated controller an initial value. /// the automatically generated controller an initial value.
/// ///
/// {@macro flutter.material.textfield.wantKeepAlive}
///
/// Remember to call [TextEditingController.dispose] of the [TextEditingController] /// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used /// when it is no longer needed. This will ensure we discard any resources used
/// by the object. /// by the object.
......
...@@ -344,6 +344,10 @@ class ToolbarOptions { ...@@ -344,6 +344,10 @@ class ToolbarOptions {
/// [onSubmitted] can be used to manually move focus to another input widget /// [onSubmitted] can be used to manually move focus to another input widget
/// when a user finishes with the currently focused 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 /// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text, /// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration. /// labels, and [Form] integration.
......
...@@ -4869,4 +4869,91 @@ void main() { ...@@ -4869,4 +4869,91 @@ void main() {
matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'), 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 <Widget>[
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>[
Tab(text: 'Sliver Tab 1'),
Tab(text: 'Sliver Tab 2'),
],
),
)
),
];
},
body: const TabBarView(
children: <Widget>[
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);
});
} }
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