Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
d3bdbed4
Unverified
Commit
d3bdbed4
authored
Aug 07, 2021
by
LongCatIsLooong
Committed by
GitHub
Aug 07, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[EditableText] preserve selection/composition range on unfocus (#86796)
parent
497eb13d
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
110 additions
and
84 deletions
+110
-84
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+15
-1
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+18
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+2
-4
focus_manager.dart
packages/flutter/lib/src/widgets/focus_manager.dart
+1
-0
text_field_focus_test.dart
packages/flutter/test/material/text_field_focus_test.dart
+3
-0
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+1
-32
text_selection_theme_test.dart
...ages/flutter/test/material/text_selection_theme_test.dart
+25
-9
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+43
-0
selectable_text_test.dart
packages/flutter/test/widgets/selectable_text_test.dart
+2
-37
No files found.
packages/flutter/lib/src/cupertino/text_field.dart
View file @
d3bdbed4
...
...
@@ -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
,
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
d3bdbed4
...
...
@@ -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
,
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
d3bdbed4
...
...
@@ -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
,
);
}
}
...
...
packages/flutter/lib/src/widgets/focus_manager.dart
View file @
d3bdbed4
...
...
@@ -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
)
{
...
...
packages/flutter/test/material/text_field_focus_test.dart
View file @
d3bdbed4
...
...
@@ -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
));
...
...
packages/flutter/test/material/text_field_test.dart
View file @
d3bdbed4
...
...
@@ -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.
...
...
packages/flutter/test/material/text_selection_theme_test.dart
View file @
d3bdbed4
...
...
@@ -54,23 +54,29 @@ void main() {
});
testWidgets
(
'Empty textSelectionTheme will use defaults'
,
(
WidgetTester
tester
)
async
{
const
Color
defaultCursorColor
=
Color
(
0x
00
2196f3
);
const
Color
defaultCursorColor
=
Color
(
0x
ff
2196f3
);
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
.
pump
AndSettle
();
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.
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
d3bdbed4
...
...
@@ -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
);
...
...
packages/flutter/test/widgets/selectable_text_test.dart
View file @
d3bdbed4
...
...
@@ -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
());
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment