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

SelectableText defers to EditableText for wantKeepAlive, plus improved docs.
This commit is contained in:
Justin McCandless 2021-12-03 13:33:55 -08:00 committed by GitHub
parent 80f732bc4e
commit e9553cd5df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 5 deletions

View file

@ -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.
///

View file

@ -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<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
class _SelectableTextState extends State<SelectableText> implements TextSelectionGestureDetectorBuilderDelegate {
EditableTextState? get _editableText => editableTextKey.currentState;
late _TextSpanEditingController _controller;
@ -579,12 +581,8 @@ class _SelectableTextState extends State<SelectableText> 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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 <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);
});
}