Unverified Commit d3bdbed4 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[EditableText] preserve selection/composition range on unfocus (#86796)

parent 497eb13d
......@@ -884,6 +884,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
_controller!.dispose();
_controller = null;
}
if (widget.focusNode != oldWidget.focusNode) {
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
}
_effectiveFocusNode.canRequestFocus = widget.enabled ?? true;
}
......@@ -915,6 +920,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
@override
void dispose() {
_effectiveFocusNode.removeListener(_handleFocusChanged);
_focusNode?.dispose();
_controller?.dispose();
super.dispose();
......@@ -926,6 +932,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
_editableText.requestKeyboard();
}
void _handleFocusChanged() {
setState(() {
// Rebuild the widget on focus change to show/hide the text selection
// highlight.
});
}
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
......@@ -1202,7 +1215,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
// Only show the selection highlight when the text field is focused.
selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null,
selectionControls: widget.selectionEnabled
? textSelectionControls : null,
onChanged: widget.onChanged,
......
......@@ -985,6 +985,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
_createLocalController();
}
_effectiveFocusNode.canRequestFocus = _isEnabled;
_effectiveFocusNode.addListener(_handleFocusChanged);
}
bool get _canRequestFocus {
......@@ -1013,7 +1014,14 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
_controller!.dispose();
_controller = null;
}
if (widget.focusNode != oldWidget.focusNode) {
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
}
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
if(_effectiveController.selection.isCollapsed) {
_showSelectionHandles = !widget.readOnly;
......@@ -1048,6 +1056,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
@override
void dispose() {
_effectiveFocusNode.removeListener(_handleFocusChanged);
_focusNode?.dispose();
_controller?.dispose();
super.dispose();
......@@ -1083,6 +1092,13 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
return false;
}
void _handleFocusChanged() {
setState(() {
// Rebuild the widget on focus change to show/hide the text selection
// highlight.
});
}
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
......@@ -1239,7 +1255,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
// Only show the selection highlight when the text field is focused.
selectionColor: focusNode.hasFocus ? selectionColor : null,
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
......
......@@ -2454,9 +2454,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
} else {
WidgetsBinding.instance!.removeObserver(this);
// Clear the selection and composition state if this widget lost focus.
_value = TextEditingValue(text: _value.text);
_currentPromptRectRange = null;
setState(() { _currentPromptRectRange = null; });
}
updateKeepAlive();
}
......@@ -2754,7 +2752,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return widget.controller.buildTextSpan(
context: context,
style: widget.style,
withComposing: !widget.readOnly,
withComposing: !widget.readOnly && _hasFocus,
);
}
}
......
......@@ -1868,6 +1868,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
_primaryFocus = _markedForFocus;
_markedForFocus = null;
}
assert(_markedForFocus == null);
if (previousFocus != _primaryFocus) {
assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus'));
if (previousFocus != null) {
......
......@@ -120,6 +120,9 @@ void main() {
await tester.idle();
expect(tester.testTextInput.isVisible, isTrue);
// Prevent the gesture recognizer from recognizing the next tap as a
// double-tap.
await tester.pump(const Duration(seconds: 1));
tester.testTextInput.hide();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
......
......@@ -3948,37 +3948,6 @@ void main() {
feedback.dispose();
});
testWidgets('Text field drops selection when losing focus', (WidgetTester tester) async {
final Key key1 = UniqueKey();
final TextEditingController controller1 = TextEditingController();
final Key key2 = UniqueKey();
await tester.pumpWidget(
overlay(
child: Column(
children: <Widget>[
TextField(
key: key1,
controller: controller1,
),
TextField(key: key2),
],
),
),
);
await tester.tap(find.byKey(key1));
await tester.enterText(find.byKey(key1), 'abcd');
await tester.pump();
controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 3);
await tester.pump();
expect(controller1.selection, isNot(equals(TextRange.empty)));
await tester.tap(find.byKey(key2));
await tester.pump();
expect(controller1.selection, equals(TextRange.empty));
});
testWidgets('Selection is consistent with text length', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......@@ -5509,7 +5478,7 @@ void main() {
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
......
......@@ -54,23 +54,29 @@ void main() {
});
testWidgets('Empty textSelectionTheme will use defaults', (WidgetTester tester) async {
const Color defaultCursorColor = Color(0x002196f3);
const Color defaultCursorColor = Color(0xff2196f3);
const Color defaultSelectionColor = Color(0x662196f3);
const Color defaultSelectionHandleColor = Color(0xff2196f3);
EditableText.debugDeterministicCursor = true;
addTearDown(() {
EditableText.debugDeterministicCursor = false;
});
// Test TextField's cursor & selection color.
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(),
child: TextField(autofocus: true),
),
),
);
await tester.pump();
await tester.pumpAndSettle();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor, defaultCursorColor);
expect(Color(renderEditable.selectionColor!.value), defaultSelectionColor);
expect(renderEditable.selectionColor?.value, defaultSelectionColor.value);
// Test the selection handle color.
await tester.pumpWidget(
......@@ -104,19 +110,25 @@ void main() {
textSelectionTheme: textSelectionTheme,
);
EditableText.debugDeterministicCursor = true;
addTearDown(() {
EditableText.debugDeterministicCursor = false;
});
// Test TextField's cursor & selection color.
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: const Material(
child: TextField(),
child: TextField(autofocus: true),
),
),
);
await tester.pumpAndSettle();
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor, textSelectionTheme.cursorColor!.withAlpha(0));
expect(renderEditable.cursorColor, textSelectionTheme.cursorColor);
expect(renderEditable.selectionColor, textSelectionTheme.selectionColor);
// Test the selection handle color.
......@@ -157,6 +169,10 @@ void main() {
selectionHandleColor: Color(0x00ffeedd),
);
EditableText.debugDeterministicCursor = true;
addTearDown(() {
EditableText.debugDeterministicCursor = false;
});
// Test TextField's cursor & selection color.
await tester.pumpWidget(
MaterialApp(
......@@ -164,15 +180,15 @@ void main() {
home: const Material(
child: TextSelectionTheme(
data: widgetTextSelectionTheme,
child: TextField(),
child: TextField(autofocus: true),
),
),
),
);
await tester.pumpAndSettle();
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorColor, widgetTextSelectionTheme.cursorColor!.withAlpha(0));
expect(renderEditable.cursorColor, widgetTextSelectionTheme.cursorColor);
expect(renderEditable.selectionColor, widgetTextSelectionTheme.selectionColor);
// Test the selection handle color.
......
......@@ -516,6 +516,45 @@ void main() {
expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline'));
});
testWidgets('selection persists when unfocused', (WidgetTester tester) async {
const TextEditingValue value = TextEditingValue(
text: 'test test',
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 5, extentOffset: 7),
);
controller.value = value;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
expect(controller.value, value);
expect(focusNode.hasFocus, isFalse);
focusNode.requestFocus();
await tester.pump();
expect(controller.value, value);
expect(focusNode.hasFocus, isTrue);
focusNode.unfocus();
await tester.pump();
expect(controller.value, value);
expect(focusNode.hasFocus, isFalse);
});
testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
......@@ -3906,6 +3945,8 @@ void main() {
));
assert(focusNode.hasFocus);
// Autofocus has a one frame delay.
await tester.pump();
final RenderEditable renderEditable = findRenderEditable(tester);
// The actual text span is split into 3 parts with the middle part underlined.
......@@ -3915,6 +3956,8 @@ void main() {
expect(textSpan.style!.decoration, TextDecoration.underline);
focusNode.unfocus();
// Drain microtasks.
await tester.idle();
await tester.pump();
expect((renderEditable.text! as TextSpan).children, isNull);
......
......@@ -1388,42 +1388,7 @@ void main() {
expect(topLeft.dx, equals(399.0));
});
testWidgets('Selectable text drops selection when losing focus', (WidgetTester tester) async {
final Key key1 = UniqueKey();
final Key key2 = UniqueKey();
await tester.pumpWidget(
overlay(
child: Column(
children: <Widget>[
SelectableText(
'text 1',
key: key1,
),
SelectableText(
'text 2',
key: key2,
),
],
),
),
);
await tester.tap(find.byKey(key1));
await tester.pump();
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 3);
await tester.pump();
expect(controller.selection, isNot(equals(TextRange.empty)));
await tester.tap(find.byKey(key2));
await tester.pump();
expect(controller.selection, equals(TextRange.empty));
});
testWidgets('Selectable text is skipped during focus traversal',
(WidgetTester tester) async {
testWidgets('Selectable text is skipped during focus traversal', (WidgetTester tester) async {
final FocusNode firstFieldFocus = FocusNode();
final FocusNode lastFieldFocus = FocusNode();
......@@ -2028,7 +1993,7 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -5);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
}, variant: KeySimulatorTransitModeVariant.all());
......
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