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
0b0450fb
Unverified
Commit
0b0450fb
authored
Feb 07, 2023
by
Justin McCandless
Committed by
GitHub
Feb 07, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Web tab selection (#119583)
Correct selection behavior when tabbing into a field on the web.
parent
1c225675
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
280 additions
and
12 deletions
+280
-12
system_navigator.dart
packages/flutter/lib/src/services/system_navigator.dart
+0
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+34
-1
editable_text_show_on_screen_test.dart
...utter/test/widgets/editable_text_show_on_screen_test.dart
+8
-1
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+233
-4
editable_text_utils.dart
packages/flutter/test/widgets/editable_text_utils.dart
+5
-5
No files found.
packages/flutter/lib/src/services/system_navigator.dart
View file @
0b0450fb
...
...
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'system_channels.dart'
;
/// Controls specific aspects of the system navigation stack.
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
0b0450fb
...
...
@@ -2592,6 +2592,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_didAutoFocus
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
((
_
)
{
if
(
mounted
&&
renderEditable
.
hasSize
)
{
_flagInternalFocus
();
FocusScope
.
of
(
context
).
autofocus
(
widget
.
focusNode
);
}
});
...
...
@@ -2714,6 +2715,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
clipboardStatus
.
removeListener
(
_onChangedClipboardStatus
);
clipboardStatus
.
dispose
();
_cursorVisibilityNotifier
.
dispose
();
FocusManager
.
instance
.
removeListener
(
_unflagInternalFocus
);
super
.
dispose
();
assert
(
_batchEditDepth
<=
0
,
'unfinished batch edits:
$_batchEditDepth
'
);
}
...
...
@@ -3236,6 +3238,23 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
// Indicates that a call to _handleFocusChanged originated within
// EditableText, allowing it to distinguish between internal and external
// focus changes.
bool
_nextFocusChangeIsInternal
=
false
;
// Sets _nextFocusChangeIsInternal to true only until any subsequent focus
// change happens.
void
_flagInternalFocus
()
{
_nextFocusChangeIsInternal
=
true
;
FocusManager
.
instance
.
addListener
(
_unflagInternalFocus
);
}
void
_unflagInternalFocus
()
{
_nextFocusChangeIsInternal
=
false
;
FocusManager
.
instance
.
removeListener
(
_unflagInternalFocus
);
}
/// Express interest in interacting with the keyboard.
///
/// If this control is already attached to the keyboard, this function will
...
...
@@ -3247,6 +3266,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasFocus
)
{
_openInputConnection
();
}
else
{
_flagInternalFocus
();
widget
.
focusNode
.
requestFocus
();
// This eventually calls _openInputConnection also, see _handleFocusChanged.
}
}
...
...
@@ -3677,7 +3697,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(!
widget
.
readOnly
)
{
_scheduleShowCaretOnScreen
(
withAnimation:
true
);
}
if
(!
_value
.
selection
.
isValid
)
{
final
bool
shouldSelectAll
=
widget
.
selectionEnabled
&&
kIsWeb
&&
!
_isMultiline
&&
!
_nextFocusChangeIsInternal
;
if
(
shouldSelectAll
)
{
// On native web, single line <input> tags select all when receiving
// focus.
_handleSelectionChanged
(
TextSelection
(
baseOffset:
0
,
extentOffset:
_value
.
text
.
length
,
),
null
,
);
}
else
if
(!
_value
.
selection
.
isValid
)
{
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
}
...
...
@@ -3834,6 +3866,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// unfocused field that previously had a selection in the same spot.
if
(
value
==
textEditingValue
)
{
if
(!
widget
.
focusNode
.
hasFocus
)
{
_flagInternalFocus
();
widget
.
focusNode
.
requestFocus
();
_selectionOverlay
=
_createSelectionOverlay
();
}
...
...
packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
View file @
0b0450fb
...
...
@@ -553,7 +553,14 @@ void main() {
focusNode
.
requestFocus
();
await
tester
.
pumpAndSettle
();
expect
(
isCaretOnScreen
(
tester
),
!
readOnly
);
if
(
kIsWeb
)
{
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowRight
);
await
tester
.
pump
();
}
// On web, the entire field is selected, and only part of that selection
// is visible on the screen.
expect
(
isCaretOnScreen
(
tester
),
!
readOnly
&&
!
kIsWeb
);
expect
(
scrollController
.
offset
,
readOnly
?
0.0
:
greaterThan
(
0.0
));
expect
(
editableScrollController
.
offset
,
readOnly
?
0.0
:
greaterThan
(
0.0
));
});
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
0b0450fb
...
...
@@ -780,13 +780,28 @@ void main() {
focusNode
.
requestFocus
();
await
tester
.
pump
();
expect
(
controller
.
value
,
value
);
// On web, focusing a single-line input selects the entire field.
final
TextEditingValue
webValue
=
value
.
copyWith
(
selection:
TextSelection
(
baseOffset:
0
,
extentOffset:
controller
.
value
.
text
.
length
,
),
);
if
(
kIsWeb
)
{
expect
(
controller
.
value
,
webValue
);
}
else
{
expect
(
controller
.
value
,
value
);
}
expect
(
focusNode
.
hasFocus
,
isTrue
);
focusNode
.
unfocus
();
await
tester
.
pump
();
expect
(
controller
.
value
,
value
);
if
(
kIsWeb
)
{
expect
(
controller
.
value
,
webValue
);
}
else
{
expect
(
controller
.
value
,
value
);
}
expect
(
focusNode
.
hasFocus
,
isFalse
);
});
...
...
@@ -4349,7 +4364,10 @@ void main() {
],
value:
expectedValue
,
textDirection:
TextDirection
.
ltr
,
textSelection:
const
TextSelection
.
collapsed
(
offset:
24
),
// Focusing a single-line field on web selects it.
textSelection:
kIsWeb
?
const
TextSelection
(
baseOffset:
0
,
extentOffset:
24
)
:
const
TextSelection
.
collapsed
(
offset:
24
),
),
],
),
...
...
@@ -15062,7 +15080,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
),
),
);
}
}
,
),
),
);
...
...
@@ -15088,6 +15106,217 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
EditableText
.
debugDeterministicCursor
=
false
;
});
group
(
'selection behavior when receiving focus'
,
()
{
testWidgets
(
'tabbing between fields'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller1
=
TextEditingController
();
final
TextEditingController
controller2
=
TextEditingController
();
controller1
.
text
=
'Text1'
;
controller2
.
text
=
'Text2
\n
Line2'
;
final
FocusNode
focusNode1
=
FocusNode
();
final
FocusNode
focusNode2
=
FocusNode
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
EditableText
(
key:
ValueKey
<
String
>(
controller1
.
text
),
controller:
controller1
,
focusNode:
focusNode1
,
style:
Typography
.
material2018
().
black
.
titleMedium
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
),
const
SizedBox
(
height:
200.0
),
EditableText
(
key:
ValueKey
<
String
>(
controller2
.
text
),
controller:
controller2
,
focusNode:
focusNode2
,
style:
Typography
.
material2018
().
black
.
titleMedium
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
minLines:
10
,
maxLines:
20
,
),
const
SizedBox
(
height:
100.0
),
],
),
),
);
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
expect
(
controller1
.
selection
,
const
TextSelection
.
collapsed
(
offset:
-
1
),
);
expect
(
controller2
.
selection
,
const
TextSelection
.
collapsed
(
offset:
-
1
),
);
// Tab to the first field (single line).
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isTrue
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
expect
(
controller1
.
selection
,
kIsWeb
?
TextSelection
(
baseOffset:
0
,
extentOffset:
controller1
.
text
.
length
,
)
:
TextSelection
.
collapsed
(
offset:
controller1
.
text
.
length
,
),
);
// Move the cursor to another position in the first field.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
controller1
.
text
.
length
-
1
));
await
tester
.
pumpAndSettle
();
expect
(
controller1
.
selection
,
TextSelection
.
collapsed
(
offset:
controller1
.
text
.
length
-
1
,
),
);
// Tab to the second field (multiline).
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isTrue
);
expect
(
controller2
.
selection
,
TextSelection
.
collapsed
(
offset:
controller2
.
text
.
length
,
),
);
// Move the cursor to another position in the second field.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
controller2
.
text
.
length
-
1
,
index:
1
));
await
tester
.
pumpAndSettle
();
expect
(
controller2
.
selection
,
TextSelection
.
collapsed
(
offset:
controller2
.
text
.
length
-
1
,
),
);
// On web, the document root is also focusable.
if
(
kIsWeb
)
{
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
}
// Tabbing again goes back to the first field and reselects the field.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isTrue
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
expect
(
controller1
.
selection
,
kIsWeb
?
TextSelection
(
baseOffset:
0
,
extentOffset:
controller1
.
text
.
length
,
)
:
TextSelection
.
collapsed
(
offset:
controller1
.
text
.
length
-
1
,
),
);
// Tabbing to the second field again retains the moved selection.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isTrue
);
expect
(
controller2
.
selection
,
TextSelection
.
collapsed
(
offset:
controller2
.
text
.
length
-
1
,
),
);
});
testWidgets
(
'when having focus stolen between frames on web'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller1
=
TextEditingController
();
controller1
.
text
=
'Text1'
;
final
FocusNode
focusNode1
=
FocusNode
();
final
FocusNode
focusNode2
=
FocusNode
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
EditableText
(
key:
ValueKey
<
String
>(
controller1
.
text
),
controller:
controller1
,
focusNode:
focusNode1
,
style:
Typography
.
material2018
().
black
.
titleMedium
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
),
const
SizedBox
(
height:
200.0
),
Focus
(
focusNode:
focusNode2
,
child:
const
SizedBox
.
shrink
(),
),
const
SizedBox
(
height:
100.0
),
],
),
),
);
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
expect
(
controller1
.
selection
,
const
TextSelection
.
collapsed
(
offset:
-
1
),
);
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
).
first
);
// Set the text editing value in order to trigger an internal call to
// requestFocus.
state
.
userUpdateTextEditingValue
(
controller1
.
value
,
SelectionChangedCause
.
keyboard
,
);
// Focus takes a frame to update, so it hasn't changed yet.
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
// Before EditableText's listener on widget.focusNode can be called, change
// the focus again
focusNode2
.
requestFocus
();
await
tester
.
pump
();
expect
(
focusNode1
.
hasFocus
,
isFalse
);
expect
(
focusNode2
.
hasFocus
,
isTrue
);
// Focus the EditableText again, which should cause the field to be selected
// on web.
focusNode1
.
requestFocus
();
await
tester
.
pumpAndSettle
();
expect
(
focusNode1
.
hasFocus
,
isTrue
);
expect
(
focusNode2
.
hasFocus
,
isFalse
);
expect
(
controller1
.
selection
,
TextSelection
(
baseOffset:
0
,
extentOffset:
controller1
.
text
.
length
,
),
);
},
skip:
!
kIsWeb
,
// [intended]
);
});
}
class
UnsettableController
extends
TextEditingController
{
...
...
packages/flutter/test/widgets/editable_text_utils.dart
View file @
0b0450fb
...
...
@@ -10,9 +10,9 @@ import 'package:flutter_test/flutter_test.dart';
/// On web, the context menu (aka toolbar) is provided by the browser.
const
bool
isContextMenuProvidedByPlatform
=
isBrowser
;
// Returns the
first RenderEditable
.
RenderEditable
findRenderEditable
(
WidgetTester
tester
)
{
final
RenderObject
root
=
tester
.
renderObject
(
find
.
byType
(
EditableText
));
// Returns the
RenderEditable at the given index, or the first if not given
.
RenderEditable
findRenderEditable
(
WidgetTester
tester
,
{
int
index
=
0
}
)
{
final
RenderObject
root
=
tester
.
renderObject
(
find
.
byType
(
EditableText
)
.
at
(
index
)
);
expect
(
root
,
isNotNull
);
late
RenderEditable
renderEditable
;
...
...
@@ -37,8 +37,8 @@ List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBo
}).
toList
();
}
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
)
{
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
,
{
int
index
=
0
}
)
{
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
,
index:
index
);
final
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
TextSelection
.
collapsed
(
offset:
offset
),
...
...
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