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
f5e4d2b4
Unverified
Commit
f5e4d2b4
authored
Jul 29, 2022
by
Greg Spencer
Committed by
GitHub
Jul 29, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Replace FocusTrap with TapRegionSurface (#107262)
parent
347de992
Changes
21
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1808 additions
and
591 deletions
+1808
-591
density.dart
dev/manual_tests/lib/density.dart
+5
-7
text_field_tap_region.0.dart
...s/api/lib/widgets/tap_region/text_field_tap_region.0.dart
+250
-0
text_field_tap_region.0_test.dart
...test/widgets/tap_region/text_field_tap_region.0_test.dart
+100
-0
bottom_tab_bar.dart
packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
+20
-16
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+20
-12
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+25
-6
app.dart
packages/flutter/lib/src/widgets/app.dart
+5
-2
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+171
-88
routes.dart
packages/flutter/lib/src/widgets/routes.dart
+33
-218
tap_region.dart
packages/flutter/lib/src/widgets/tap_region.dart
+562
-0
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+24
-17
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+140
-0
debug_test.dart
packages/flutter/test/material/debug_test.dart
+1
-1
input_decorator_test.dart
packages/flutter/test/material/input_decorator_test.dart
+3
-3
text_field_focus_test.dart
packages/flutter/test/material/text_field_focus_test.dart
+17
-14
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+220
-0
editable_text_shortcuts_test.dart
...es/flutter/test/widgets/editable_text_shortcuts_test.dart
+1
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+22
-4
routes_test.dart
packages/flutter/test/widgets/routes_test.dart
+0
-203
tap_region_test.dart
packages/flutter/test/widgets/tap_region_test.dart
+188
-0
No files found.
dev/manual_tests/lib/density.dart
View file @
f5e4d2b4
...
...
@@ -631,7 +631,6 @@ class _MyHomePageState extends State<MyHomePage> {
data:
Theme
.
of
(
context
).
copyWith
(
visualDensity:
_model
.
density
),
child:
Directionality
(
textDirection:
_model
.
rtl
?
TextDirection
.
rtl
:
TextDirection
.
ltr
,
child:
Scrollbar
(
child:
MediaQuery
(
data:
MediaQuery
.
of
(
context
).
copyWith
(
textScaleFactor:
_model
.
size
),
child:
SizedBox
.
expand
(
...
...
@@ -645,7 +644,6 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
);
}
}
examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart
0 → 100644
View file @
f5e4d2b4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Flutter code sample for [TextFieldTapRegion].
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
void
main
(
)
=>
runApp
(
const
TapRegionApp
());
class
TapRegionApp
extends
StatelessWidget
{
const
TapRegionApp
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
(
home:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'TextFieldTapRegion Example'
)),
body:
const
TextFieldTapRegionExample
(),
),
);
}
}
class
TextFieldTapRegionExample
extends
StatefulWidget
{
const
TextFieldTapRegionExample
({
super
.
key
});
@override
State
<
TextFieldTapRegionExample
>
createState
()
=>
_TextFieldTapRegionExampleState
();
}
class
_TextFieldTapRegionExampleState
extends
State
<
TextFieldTapRegionExample
>
{
int
value
=
0
;
@override
Widget
build
(
BuildContext
context
)
{
return
ListView
(
children:
<
Widget
>[
Center
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
20.0
),
child:
SizedBox
(
width:
150
,
height:
80
,
child:
IntegerSpinnerField
(
value:
value
,
autofocus:
true
,
onChanged:
(
int
newValue
)
{
if
(
value
==
newValue
)
{
// Avoid unnecessary redraws.
return
;
}
setState
(()
{
// Update the value and redraw.
value
=
newValue
;
});
},
),
),
),
),
],
);
}
}
/// An integer example of the generic [SpinnerField] that validates input and
/// increments by a delta.
class
IntegerSpinnerField
extends
StatelessWidget
{
const
IntegerSpinnerField
({
super
.
key
,
required
this
.
value
,
this
.
autofocus
=
false
,
this
.
delta
=
1
,
this
.
onChanged
,
});
final
int
value
;
final
bool
autofocus
;
final
int
delta
;
final
ValueChanged
<
int
>?
onChanged
;
@override
Widget
build
(
BuildContext
context
)
{
return
SpinnerField
<
int
>(
value:
value
,
onChanged:
onChanged
,
autofocus:
autofocus
,
fromString:
(
String
stringValue
)
=>
int
.
tryParse
(
stringValue
)
??
value
,
increment:
(
int
i
)
=>
i
+
delta
,
decrement:
(
int
i
)
=>
i
-
delta
,
// Add a text formatter that only allows integer values and a leading
// minus sign.
inputFormatters:
<
TextInputFormatter
>[
TextInputFormatter
.
withFunction
(
(
TextEditingValue
oldValue
,
TextEditingValue
newValue
)
{
String
newString
;
if
(
newValue
.
text
.
startsWith
(
'-'
))
{
newString
=
'-
${newValue.text.replaceAll(RegExp(r'\D'), '')}
'
;
}
else
{
newString
=
newValue
.
text
.
replaceAll
(
RegExp
(
r'\D'
),
''
);
}
return
newValue
.
copyWith
(
text:
newString
,
selection:
newValue
.
selection
.
copyWith
(
baseOffset:
newValue
.
selection
.
baseOffset
.
clamp
(
0
,
newString
.
length
),
extentOffset:
newValue
.
selection
.
extentOffset
.
clamp
(
0
,
newString
.
length
),
),
);
},
)
],
);
}
}
/// A generic "spinner" field example which adds extra buttons next to a
/// [TextField] to increment and decrement the value.
///
/// This widget uses [TextFieldTapRegion] to indicate that tapping on the
/// spinner buttons should not cause the text field to lose focus.
class
SpinnerField
<
T
>
extends
StatefulWidget
{
SpinnerField
({
super
.
key
,
required
this
.
value
,
required
this
.
fromString
,
this
.
autofocus
=
false
,
String
Function
(
T
value
)?
asString
,
this
.
increment
,
this
.
decrement
,
this
.
onChanged
,
this
.
inputFormatters
=
const
<
TextInputFormatter
>[],
})
:
asString
=
asString
??
((
T
value
)
=>
value
.
toString
());
final
T
value
;
final
T
Function
(
T
value
)?
increment
;
final
T
Function
(
T
value
)?
decrement
;
final
String
Function
(
T
value
)
asString
;
final
T
Function
(
String
value
)
fromString
;
final
ValueChanged
<
T
>?
onChanged
;
final
List
<
TextInputFormatter
>
inputFormatters
;
final
bool
autofocus
;
@override
State
<
SpinnerField
<
T
>>
createState
()
=>
_SpinnerFieldState
<
T
>();
}
class
_SpinnerFieldState
<
T
>
extends
State
<
SpinnerField
<
T
>>
{
TextEditingController
controller
=
TextEditingController
();
@override
void
initState
()
{
super
.
initState
();
_updateText
(
widget
.
asString
(
widget
.
value
));
}
@override
void
dispose
()
{
controller
.
dispose
();
super
.
dispose
();
}
@override
void
didUpdateWidget
(
covariant
SpinnerField
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
oldWidget
.
asString
!=
widget
.
asString
||
oldWidget
.
value
!=
widget
.
value
)
{
final
String
newText
=
widget
.
asString
(
widget
.
value
);
_updateText
(
newText
);
}
}
void
_updateText
(
String
text
,
{
bool
collapsed
=
true
})
{
if
(
text
!=
controller
.
text
)
{
controller
.
value
=
TextEditingValue
(
text:
text
,
selection:
collapsed
?
TextSelection
.
collapsed
(
offset:
text
.
length
)
:
TextSelection
(
baseOffset:
0
,
extentOffset:
text
.
length
),
);
}
}
void
_spin
(
T
Function
(
T
value
)?
spinFunction
)
{
if
(
spinFunction
==
null
)
{
return
;
}
final
T
newValue
=
spinFunction
(
widget
.
value
);
widget
.
onChanged
?.
call
(
newValue
);
_updateText
(
widget
.
asString
(
newValue
),
collapsed:
false
);
}
void
_increment
()
{
_spin
(
widget
.
increment
);
}
void
_decrement
()
{
_spin
(
widget
.
decrement
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
CallbackShortcuts
(
bindings:
<
ShortcutActivator
,
VoidCallback
>{
const
SingleActivator
(
LogicalKeyboardKey
.
arrowUp
):
_increment
,
const
SingleActivator
(
LogicalKeyboardKey
.
arrowDown
):
_decrement
,
},
child:
Row
(
children:
<
Widget
>[
Expanded
(
child:
TextField
(
autofocus:
widget
.
autofocus
,
inputFormatters:
widget
.
inputFormatters
,
decoration:
const
InputDecoration
(
border:
OutlineInputBorder
(),
),
onChanged:
(
String
value
)
=>
widget
.
onChanged
?.
call
(
widget
.
fromString
(
value
)),
controller:
controller
,
textAlign:
TextAlign
.
center
,
),
),
const
SizedBox
(
width:
12
),
// Without this TextFieldTapRegion, tapping on the buttons below would
// increment the value, but it would cause the text field to be
// unfocused, since tapping outside of a text field should unfocus it
// on non-mobile platforms.
TextFieldTapRegion
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
<
Widget
>[
Expanded
(
child:
OutlinedButton
(
onPressed:
_increment
,
child:
const
Icon
(
Icons
.
add
),
),
),
Expanded
(
child:
OutlinedButton
(
onPressed:
_decrement
,
child:
const
Icon
(
Icons
.
remove
),
),
),
],
),
)
],
),
);
}
}
examples/api/test/widgets/tap_region/text_field_tap_region.0_test.dart
0 → 100644
View file @
f5e4d2b4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter_api_samples/widgets/tap_region/text_field_tap_region.0.dart'
as
example
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'shows a text field with a zero count, and the spinner buttons'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
TapRegionApp
(),
);
expect
(
find
.
byType
(
TextField
),
findsOneWidget
);
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'0'
));
expect
(
find
.
byIcon
(
Icons
.
add
),
findsOneWidget
);
expect
(
find
.
byIcon
(
Icons
.
remove
),
findsOneWidget
);
});
testWidgets
(
'tapping increment/decrement works'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
TapRegionApp
(),
);
await
tester
.
pump
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'0'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
1
)),
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
add
));
await
tester
.
pumpAndSettle
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'1'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
1
)),
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove
));
await
tester
.
pumpAndSettle
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'-1'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
2
)),
);
});
testWidgets
(
'entering text and then incrementing/decrementing works'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
TapRegionApp
(),
);
await
tester
.
pump
();
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
add
));
await
tester
.
pumpAndSettle
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'1'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
1
)),
);
await
tester
.
enterText
(
find
.
byType
(
TextField
),
'123'
);
await
tester
.
pumpAndSettle
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'123'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
3
)),
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove
));
await
tester
.
pumpAndSettle
();
expect
(
getFieldValue
(
tester
).
text
,
equals
(
'121'
));
expect
(
getFieldValue
(
tester
).
selection
,
equals
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
3
)),
);
final
FocusNode
textFieldFocusNode
=
Focus
.
of
(
tester
.
element
(
find
.
byWidgetPredicate
((
Widget
widget
)
{
return
widget
.
runtimeType
.
toString
()
==
'_Editable'
;
}),
),
);
expect
(
textFieldFocusNode
.
hasPrimaryFocus
,
isTrue
);
});
}
TextEditingValue
getFieldValue
(
WidgetTester
tester
)
{
return
(
tester
.
widget
(
find
.
byType
(
TextField
))
as
TextField
).
controller
!.
value
;
}
packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
View file @
f5e4d2b4
...
...
@@ -230,6 +230,9 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
_wrapActiveItem
(
context
,
Expanded
(
// Make tab items part of the EditableText tap region so that
// switching tabs doesn't unfocus text fields.
child:
TextFieldTapRegion
(
child:
Semantics
(
selected:
active
,
hint:
localizations
.
tabSemanticsLabel
(
...
...
@@ -252,6 +255,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
),
),
),
),
active:
active
,
),
);
...
...
packages/flutter/lib/src/cupertino/text_field.dart
View file @
f5e4d2b4
...
...
@@ -251,6 +251,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
onChanged
,
this
.
onEditingComplete
,
this
.
onSubmitted
,
this
.
onTapOutside
,
this
.
inputFormatters
,
this
.
enabled
,
this
.
cursorWidth
=
2.0
,
...
...
@@ -411,6 +412,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
onChanged
,
this
.
onEditingComplete
,
this
.
onSubmitted
,
this
.
onTapOutside
,
this
.
inputFormatters
,
this
.
enabled
,
this
.
cursorWidth
=
2.0
,
...
...
@@ -692,6 +694,9 @@ class CupertinoTextField extends StatefulWidget {
/// the user is done editing.
final
ValueChanged
<
String
>?
onSubmitted
;
/// {@macro flutter.widgets.editableText.onTapOutside}
final
TapRegionCallback
?
onTapOutside
;
/// {@macro flutter.widgets.editableText.inputFormatters}
final
List
<
TextInputFormatter
>?
inputFormatters
;
...
...
@@ -1277,6 +1282,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
onSelectionChanged:
_handleSelectionChanged
,
onEditingComplete:
widget
.
onEditingComplete
,
onSubmitted:
widget
.
onSubmitted
,
onTapOutside:
widget
.
onTapOutside
,
inputFormatters:
formatters
,
rendererIgnoresPointer:
true
,
cursorWidth:
widget
.
cursorWidth
,
...
...
@@ -1315,6 +1321,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
_requestKeyboard
();
},
onDidGainAccessibilityFocus:
handleDidGainAccessibilityFocus
,
child:
TextFieldTapRegion
(
child:
IgnorePointer
(
ignoring:
!
enabled
,
child:
Container
(
...
...
@@ -1331,6 +1338,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
),
),
),
),
);
}
}
packages/flutter/lib/src/material/text_field.dart
View file @
f5e4d2b4
...
...
@@ -320,6 +320,7 @@ class TextField extends StatefulWidget {
bool
?
enableInteractiveSelection
,
this
.
selectionControls
,
this
.
onTap
,
this
.
onTapOutside
,
this
.
mouseCursor
,
this
.
buildCounter
,
this
.
scrollController
,
...
...
@@ -675,6 +676,24 @@ class TextField extends StatefulWidget {
/// {@endtemplate}
final
GestureTapCallback
?
onTap
;
/// {@macro flutter.widgets.editableText.onTapOutside}
///
/// {@tool dartpad}
/// This example shows how to use a `TextFieldTapRegion` to wrap a set of
/// "spinner" buttons that increment and decrement a value in the [TextField]
/// without causing the text field to lose keyboard focus.
///
/// This example includes a generic `SpinnerField<T>` class that you can copy
/// into your own project and customize.
///
/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TapRegion] for how the region group is determined.
final
TapRegionCallback
?
onTapOutside
;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
...
...
@@ -1267,6 +1286,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
onSubmitted:
widget
.
onSubmitted
,
onAppPrivateCommand:
widget
.
onAppPrivateCommand
,
onSelectionHandleTapped:
_handleSelectionHandleTapped
,
onTapOutside:
widget
.
onTapOutside
,
inputFormatters:
formatters
,
rendererIgnoresPointer:
true
,
mouseCursor:
MouseCursor
.
defer
,
// TextField will handle the cursor
...
...
@@ -1334,12 +1354,11 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
semanticsMaxValueLength
=
null
;
}
return
FocusTrapArea
(
focusNode:
focusNode
,
child:
MouseRegion
(
return
MouseRegion
(
cursor:
effectiveMouseCursor
,
onEnter:
(
PointerEnterEvent
event
)
=>
_handleHover
(
true
),
onExit:
(
PointerExitEvent
event
)
=>
_handleHover
(
false
),
child:
TextFieldTapRegion
(
child:
IgnorePointer
(
ignoring:
!
_isEnabled
,
child:
AnimatedBuilder
(
...
...
packages/flutter/lib/src/widgets/app.dart
View file @
f5e4d2b4
...
...
@@ -26,6 +26,7 @@ import 'scrollable.dart';
import
'semantics_debugger.dart'
;
import
'shared_app_data.dart'
;
import
'shortcuts.dart'
;
import
'tap_region.dart'
;
import
'text.dart'
;
import
'title.dart'
;
import
'widget_inspector.dart'
;
...
...
@@ -1740,6 +1741,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
actions:
widget
.
actions
??
WidgetsApp
.
defaultActions
,
child:
FocusTraversalGroup
(
policy:
ReadingOrderTraversalPolicy
(),
child:
TapRegionSurface
(
child:
ShortcutRegistrar
(
child:
child
,
),
...
...
@@ -1748,6 +1750,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
),
),
),
),
);
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
f5e4d2b4
...
...
@@ -32,6 +32,7 @@ import 'scroll_controller.dart';
import
'scroll_physics.dart'
;
import
'scrollable.dart'
;
import
'shortcuts.dart'
;
import
'tap_region.dart'
;
import
'text.dart'
;
import
'text_editing_intents.dart'
;
import
'text_selection.dart'
;
...
...
@@ -608,6 +609,7 @@ class EditableText extends StatefulWidget {
this
.
onAppPrivateCommand
,
this
.
onSelectionChanged
,
this
.
onSelectionHandleTapped
,
this
.
onTapOutside
,
List
<
TextInputFormatter
>?
inputFormatters
,
this
.
mouseCursor
,
this
.
rendererIgnoresPointer
=
false
,
...
...
@@ -1213,6 +1215,46 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
final
VoidCallback
?
onSelectionHandleTapped
;
/// {@template flutter.widgets.editableText.onTapOutside}
/// Called for each tap that occurs outside of the[TextFieldTapRegion] group
/// when the text field is focused.
///
/// If this is null, [FocusNode.unfocus] will be called on the [focusNode] for
/// this text field when a [PointerDownEvent] is received on another part of
/// the UI. However, it will not unfocus as a result of mobile application
/// touch events (which does not include mouse clicks), to conform with the
/// platform conventions. To change this behavior, a callback may be set here
/// that operates differently from the default.
///
/// When adding additional controls to a text field (for example, a spinner, a
/// button that copies the selected text, or modifies formatting), it is
/// helpful if tapping on that control doesn't unfocus the text field. In
/// order for an external widget to be considered as part of the text field
/// for the purposes of tapping "outside" of the field, wrap the control in a
/// [TextFieldTapRegion].
///
/// The [PointerDownEvent] passed to the function is the event that caused the
/// notification. It is possible that the event may occur outside of the
/// immediate bounding box defined by the text field, although it will be
/// within the bounding box of a [TextFieldTapRegion] member.
/// {@endtemplate}
///
/// {@tool dartpad}
/// This example shows how to use a `TextFieldTapRegion` to wrap a set of
/// "spinner" buttons that increment and decrement a value in the [TextField]
/// without causing the text field to lose keyboard focus.
///
/// This example includes a generic `SpinnerField<T>` class that you can copy
/// into your own project and customize.
///
/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TapRegion] for how the region group is determined.
final
TapRegionCallback
?
onTapOutside
;
/// {@template flutter.widgets.editableText.inputFormatters}
/// Optional input validation and formatting overrides.
///
...
...
@@ -3421,6 +3463,43 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return
Actions
.
invoke
(
context
,
intent
);
}
/// The default behavior used if [onTapOutside] is null.
///
/// The `event` argument is the [PointerDownEvent] that caused the notification.
void
_defaultOnTapOutside
(
PointerDownEvent
event
)
{
/// The focus dropping behavior is only present on desktop platforms
/// and mobile browsers.
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
android
:
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
fuchsia
:
// On mobile platforms, we don't unfocus on touch events unless they're
// in the web browser, but we do unfocus for all other kinds of events.
switch
(
event
.
kind
)
{
case
ui
.
PointerDeviceKind
.
touch
:
if
(
kIsWeb
)
{
widget
.
focusNode
.
unfocus
();
}
break
;
case
ui
.
PointerDeviceKind
.
mouse
:
case
ui
.
PointerDeviceKind
.
stylus
:
case
ui
.
PointerDeviceKind
.
invertedStylus
:
case
ui
.
PointerDeviceKind
.
unknown
:
widget
.
focusNode
.
unfocus
();
break
;
case
ui
.
PointerDeviceKind
.
trackpad
:
throw
UnimplementedError
(
'Unexpected pointer down event for trackpad'
);
}
break
;
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
widget
.
focusNode
.
unfocus
();
break
;
}
}
late
final
Map
<
Type
,
Action
<
Intent
>>
_actions
=
<
Type
,
Action
<
Intent
>>{
DoNothingAndStopPropagationTextIntent:
DoNothingAction
(
consumesKey:
false
),
ReplaceTextIntent:
_replaceTextAction
,
...
...
@@ -3458,7 +3537,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
super
.
build
(
context
);
// See AutomaticKeepAliveClientMixin.
final
TextSelectionControls
?
controls
=
widget
.
selectionControls
;
return
MouseRegion
(
return
TextFieldTapRegion
(
onTapOutside:
widget
.
onTapOutside
??
_defaultOnTapOutside
,
debugLabel:
kReleaseMode
?
null
:
'EditableText'
,
child:
MouseRegion
(
cursor:
widget
.
mouseCursor
??
SystemMouseCursors
.
text
,
child:
Actions
(
actions:
_actions
,
...
...
@@ -3470,7 +3552,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
child:
Focus
(
focusNode:
widget
.
focusNode
,
includeSemantics:
false
,
debugLabel
:
'EditableText'
,
debugLabel:
kReleaseMode
?
null
:
'EditableText'
,
child:
Scrollable
(
excludeFromSemantics:
true
,
axisDirection:
_isMultiline
?
AxisDirection
.
down
:
AxisDirection
.
right
,
...
...
@@ -3552,6 +3634,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
),
),
),
),
);
}
...
...
packages/flutter/lib/src/widgets/routes.dart
View file @
f5e4d2b4
...
...
@@ -6,7 +6,6 @@ import 'dart:async';
import
'dart:ui'
as
ui
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/services.dart'
;
...
...
@@ -886,8 +885,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
controller:
primaryScrollController
,
child:
FocusScope
(
node:
focusScopeNode
,
// immutable
child:
FocusTrap
(
focusScopeNode:
focusScopeNode
,
child:
RepaintBoundary
(
child:
AnimatedBuilder
(
animation:
_listenable
,
// immutable
...
...
@@ -930,7 +927,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
),
),
),
),
);
},
),
...
...
@@ -2147,184 +2143,3 @@ typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<doubl
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
typedef
RouteTransitionsBuilder
=
Widget
Function
(
BuildContext
context
,
Animation
<
double
>
animation
,
Animation
<
double
>
secondaryAnimation
,
Widget
child
);
/// The [FocusTrap] widget removes focus when a mouse primary pointer makes contact with another
/// region of the screen.
///
/// When a primary pointer makes contact with the screen, this widget determines if that pointer
/// contacted an existing focused widget. If not, this asks the [FocusScopeNode] to reset the
/// focus state. This allows [TextField]s and other focusable widgets to give up their focus
/// state, without creating a gesture detector that competes with others on screen.
///
/// In cases where focus is conceptually larger than the focused render object, a [FocusTrapArea]
/// can be used to expand the focus area to include all render objects below that. This is used by
/// the [TextField] widgets to prevent a loss of focus when interacting with decorations on the
/// text area.
///
/// See also:
///
/// * [FocusTrapArea], the widget that allows expanding the conceptual focus area.
class
FocusTrap
extends
SingleChildRenderObjectWidget
{
/// Create a new [FocusTrap] widget scoped to the provided [focusScopeNode].
const
FocusTrap
({
required
this
.
focusScopeNode
,
required
Widget
super
.
child
,
super
.
key
,
});
/// The [focusScopeNode] that this focus trap widget operates on.
final
FocusScopeNode
focusScopeNode
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
_RenderFocusTrap
(
focusScopeNode
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
RenderObject
renderObject
)
{
if
(
renderObject
is
_RenderFocusTrap
)
{
renderObject
.
focusScopeNode
=
focusScopeNode
;
}
}
}
/// Declares a widget subtree which is part of the provided [focusNode]'s focus area
/// without attaching focus to that region.
///
/// This is used by text field widgets which decorate a smaller editable text area.
/// This area is conceptually part of the editable text, but not attached to the
/// focus context. The [FocusTrapArea] is used to inform the framework of this
/// relationship, so that primary pointer contact inside of this region but above
/// the editable text focus will not trigger loss of focus.
///
/// See also:
///
/// * [FocusTrap], the widget which removes focus based on primary pointer interactions.
class
FocusTrapArea
extends
SingleChildRenderObjectWidget
{
/// Create a new [FocusTrapArea] that expands the area of the provided [focusNode].
const
FocusTrapArea
({
required
this
.
focusNode
,
super
.
key
,
super
.
child
});
/// The [FocusNode] that the focus trap area will expand to.
final
FocusNode
focusNode
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
_RenderFocusTrapArea
(
focusNode
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
RenderObject
renderObject
)
{
if
(
renderObject
is
_RenderFocusTrapArea
)
{
renderObject
.
focusNode
=
focusNode
;
}
}
}
class
_RenderFocusTrapArea
extends
RenderProxyBox
{
_RenderFocusTrapArea
(
this
.
focusNode
);
FocusNode
focusNode
;
}
class
_RenderFocusTrap
extends
RenderProxyBoxWithHitTestBehavior
{
_RenderFocusTrap
(
this
.
_focusScopeNode
);
Rect
?
currentFocusRect
;
Expando
<
BoxHitTestResult
>
cachedResults
=
Expando
<
BoxHitTestResult
>();
FocusScopeNode
_focusScopeNode
;
FocusNode
?
_previousFocus
;
FocusScopeNode
get
focusScopeNode
=>
_focusScopeNode
;
set
focusScopeNode
(
FocusScopeNode
value
)
{
if
(
focusScopeNode
==
value
)
{
return
;
}
_focusScopeNode
=
value
;
}
@override
bool
hitTest
(
BoxHitTestResult
result
,
{
required
Offset
position
})
{
bool
hitTarget
=
false
;
if
(
size
.
contains
(
position
))
{
hitTarget
=
hitTestChildren
(
result
,
position:
position
)
||
hitTestSelf
(
position
);
if
(
hitTarget
)
{
final
BoxHitTestEntry
entry
=
BoxHitTestEntry
(
this
,
position
);
cachedResults
[
entry
]
=
result
;
result
.
add
(
entry
);
}
}
return
hitTarget
;
}
/// The focus dropping behavior is only present on desktop platforms
/// and mobile browsers.
bool
get
_shouldIgnoreEvents
{
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
android
:
case
TargetPlatform
.
iOS
:
return
!
kIsWeb
;
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
case
TargetPlatform
.
fuchsia
:
return
false
;
}
}
void
_checkForUnfocus
()
{
if
(
_previousFocus
==
null
)
{
return
;
}
// Only continue to unfocus if the previous focus matches the current focus.
// If the focus has changed in the meantime, it was probably intentional.
if
(
FocusManager
.
instance
.
primaryFocus
==
_previousFocus
)
{
_previousFocus
!.
unfocus
();
}
_previousFocus
=
null
;
}
@override
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
assert
(
debugHandleEvent
(
event
,
entry
));
if
(
event
is
!
PointerDownEvent
||
event
.
buttons
!=
kPrimaryButton
||
event
.
kind
!=
PointerDeviceKind
.
mouse
||
_shouldIgnoreEvents
||
_focusScopeNode
.
focusedChild
==
null
)
{
return
;
}
final
BoxHitTestResult
?
result
=
cachedResults
[
entry
];
final
FocusNode
?
focusNode
=
_focusScopeNode
.
focusedChild
;
if
(
focusNode
==
null
||
result
==
null
)
{
return
;
}
final
RenderObject
?
renderObject
=
focusNode
.
context
?.
findRenderObject
();
if
(
renderObject
==
null
)
{
return
;
}
bool
hitCurrentFocus
=
false
;
for
(
final
HitTestEntry
entry
in
result
.
path
)
{
final
HitTestTarget
target
=
entry
.
target
;
if
(
target
==
renderObject
)
{
hitCurrentFocus
=
true
;
break
;
}
if
(
target
is
_RenderFocusTrapArea
&&
target
.
focusNode
==
focusNode
)
{
hitCurrentFocus
=
true
;
break
;
}
}
if
(!
hitCurrentFocus
)
{
_previousFocus
=
focusNode
;
// Check post-frame to see that the focus hasn't changed before
// unfocusing. This also allows a button tap to capture the previously
// active focus before FocusTrap tries to unfocus it, and avoids a bounce
// through the scope's focus node in between.
SchedulerBinding
.
instance
.
scheduleTask
<
void
>(
_checkForUnfocus
,
Priority
.
touch
);
}
}
}
packages/flutter/lib/src/widgets/tap_region.dart
0 → 100644
View file @
f5e4d2b4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/rendering.dart'
;
import
'editable_text.dart'
;
import
'framework.dart'
;
// Enable if you want verbose logging about tap region changes.
const
bool
_kDebugTapRegion
=
false
;
bool
_tapRegionDebug
(
String
message
,
[
Iterable
<
String
>?
details
])
{
if
(
_kDebugTapRegion
)
{
debugPrint
(
'TAP REGION:
$message
'
);
if
(
details
!=
null
&&
details
.
isNotEmpty
)
{
for
(
final
String
detail
in
details
)
{
debugPrint
(
'
$detail
'
);
}
}
}
// Return true so that it can be easily used inside of an assert.
return
true
;
}
/// The type of callback that [TapRegion.onTapOutside] and
/// [TapRegion.onTapInside] take.
///
/// The event is the pointer event that caused the callback to be called.
typedef
TapRegionCallback
=
void
Function
(
PointerDownEvent
event
);
/// An interface for registering and unregistering a [RenderTapRegion]
/// (typically created with a [TapRegion] widget) with a
/// [RenderTapRegionSurface] (typically created with a [TapRegionSurface]
/// widget).
abstract
class
TapRegionRegistry
{
/// Register the given [RenderTapRegion] with the registry.
void
registerTapRegion
(
RenderTapRegion
region
);
/// Unregister the given [RenderTapRegion] with the registry.
void
unregisterTapRegion
(
RenderTapRegion
region
);
/// Allows finding of the nearest [TapRegionRegistry], such as a
/// [RenderTapRegionSurface].
///
/// Will throw if a [TapRegionRegistry] isn't found.
static
TapRegionRegistry
of
(
BuildContext
context
)
{
final
TapRegionRegistry
?
registry
=
maybeOf
(
context
);
assert
(()
{
if
(
registry
==
null
)
{
throw
FlutterError
(
'TapRegionRegistry.of() was called with a context that does not contain a TapRegionSurface widget.
\n
'
'No TapRegionSurface widget ancestor could be found starting from the context that was passed to '
'TapRegionRegistry.of().
\n
'
'The context used was:
\n
'
'
$context
'
,
);
}
return
true
;
}());
return
registry
!;
}
/// Allows finding of the nearest [TapRegionRegistry], such as a
/// [RenderTapRegionSurface].
static
TapRegionRegistry
?
maybeOf
(
BuildContext
context
)
{
return
context
.
findAncestorRenderObjectOfType
<
RenderTapRegionSurface
>();
}
}
/// A widget that provides notification of a tap inside or outside of a set of
/// registered regions, without participating in the [gesture
/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system.
///
/// The regions are defined by adding [TapRegion] widgets to the widget tree
/// around the regions of interest, and they will register with this
/// `TapRegionSurface`. Each of the tap regions can optionally belong to a group
/// by assigning a [TapRegion.groupId], where all the regions with the same
/// groupId act as if they were all one region.
///
/// When a tap outside of a registered region or region group is detected, its
/// [TapRegion.onTapOutside] callback is called. If the tap is outside one
/// member of a group, but inside another, no notification is made.
///
/// When a tap inside of a registered region or region group is detected, its
/// [TapRegion.onTapInside] callback is called. If the tap is inside one member
/// of a group, all members are notified.
///
/// The `TapRegionSurface` should be defined at the highest level needed to
/// encompass the entire area where taps should be monitored. This is typically
/// around the entire app. If the entire app isn't covered, then taps outside of
/// the `TapRegionSurface` will be ignored and no [TapRegion.onTapOutside] calls
/// wil be made for those events. The [WidgetsApp], [MaterialApp] and
/// [CupertinoApp] automatically include a `TapRegionSurface` around their
/// entire app.
///
/// [TapRegionSurface] does not participate in the [gesture
/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system, so if multiple [TapRegionSurface]s are active at the same time, they
/// will all fire, and so will any other gestures recognized by a
/// [GestureDetector] or other pointer event handlers.
///
/// [TapRegion]s register only with the nearest ancestor `TapRegionSurface`.
///
/// See also:
///
/// * [RenderTapRegionSurface], the render object that is inserted into the
/// render tree by this widget.
/// * <https://flutter.dev/gestures/#gesture-disambiguation> for more
/// information about the gesture system and how it disambiguates inputs.
class
TapRegionSurface
extends
SingleChildRenderObjectWidget
{
/// Creates a const [RenderTapRegionSurface].
///
/// The [child] attribute is required.
const
TapRegionSurface
({
super
.
key
,
required
Widget
super
.
child
,
});
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
RenderTapRegionSurface
();
}
@override
void
updateRenderObject
(
BuildContext
context
,
RenderProxyBoxWithHitTestBehavior
renderObject
,
)
{}
}
/// A render object that provides notification of a tap inside or outside of a
/// set of registered regions, without participating in the [gesture
/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system.
///
/// The regions are defined by adding [RenderTapRegion] render objects in the
/// render tree around the regions of interest, and they will register with this
/// `RenderTapRegionSurface`. Each of the tap regions can optionally belong to a
/// group by assigning a [RenderTapRegion.groupId], where all the regions with
/// the same groupId act as if they were all one region.
///
/// When a tap outside of a registered region or region group is detected, its
/// [TapRegion.onTapOutside] callback is called. If the tap is outside one
/// member of a group, but inside another, no notification is made.
///
/// When a tap inside of a registered region or region group is detected, its
/// [TapRegion.onTapInside] callback is called. If the tap is inside one member
/// of a group, all members are notified.
///
/// The `RenderTapRegionSurface` should be defined at the highest level needed
/// to encompass the entire area where taps should be monitored. This is
/// typically around the entire app. If the entire app isn't covered, then taps
/// outside of the `RenderTapRegionSurface` will be ignored and no
/// [RenderTapRegion.onTapOutside] calls wil be made for those events. The
/// [WidgetsApp], [MaterialApp] and [CupertinoApp] automatically include a
/// `RenderTapRegionSurface` around the entire app.
///
/// `RenderTapRegionSurface` does not participate in the [gesture
/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system, so if multiple `RenderTapRegionSurface`s are active at the same
/// time, they will all fire, and so will any other gestures recognized by a
/// [GestureDetector] or other pointer event handlers.
///
/// [RenderTapRegion]s register only with the nearest ancestor
/// `RenderTapRegionSurface`.
///
/// See also:
///
/// * [TapRegionSurface], a widget that inserts a `RenderTapRegionSurface` into
/// the render tree.
/// * [TapRegionRegistry.of], which can find the nearest ancestor
/// [RenderTapRegionSurface], which is a [TapRegionRegistry].
class
RenderTapRegionSurface
extends
RenderProxyBoxWithHitTestBehavior
with
TapRegionRegistry
{
final
Expando
<
BoxHitTestResult
>
_cachedResults
=
Expando
<
BoxHitTestResult
>();
final
Set
<
RenderTapRegion
>
_registeredRegions
=
<
RenderTapRegion
>{};
final
Map
<
Object
?,
Set
<
RenderTapRegion
>>
_groupIdToRegions
=
<
Object
?,
Set
<
RenderTapRegion
>>{};
@override
void
registerTapRegion
(
RenderTapRegion
region
)
{
assert
(
_tapRegionDebug
(
'Region
$region
registered.'
));
assert
(!
_registeredRegions
.
contains
(
region
));
_registeredRegions
.
add
(
region
);
if
(
region
.
groupId
!=
null
)
{
_groupIdToRegions
[
region
.
groupId
]
??=
<
RenderTapRegion
>{};
_groupIdToRegions
[
region
.
groupId
]!.
add
(
region
);
}
}
@override
void
unregisterTapRegion
(
RenderTapRegion
region
)
{
assert
(
_tapRegionDebug
(
'Region
$region
unregistered.'
));
assert
(
_registeredRegions
.
contains
(
region
));
_registeredRegions
.
remove
(
region
);
if
(
region
.
groupId
!=
null
)
{
assert
(
_groupIdToRegions
.
containsKey
(
region
.
groupId
));
_groupIdToRegions
[
region
.
groupId
]!.
remove
(
region
);
if
(
_groupIdToRegions
[
region
.
groupId
]!.
isEmpty
)
{
_groupIdToRegions
.
remove
(
region
.
groupId
);
}
}
}
@override
bool
hitTest
(
BoxHitTestResult
result
,
{
required
Offset
position
})
{
if
(!
size
.
contains
(
position
))
{
return
false
;
}
final
bool
hitTarget
=
hitTestChildren
(
result
,
position:
position
)
||
hitTestSelf
(
position
);
if
(
hitTarget
)
{
final
BoxHitTestEntry
entry
=
BoxHitTestEntry
(
this
,
position
);
_cachedResults
[
entry
]
=
result
;
result
.
add
(
entry
);
}
return
hitTarget
;
}
@override
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
assert
(
debugHandleEvent
(
event
,
entry
));
assert
(()
{
for
(
final
RenderTapRegion
region
in
_registeredRegions
)
{
if
(!
region
.
enabled
)
{
return
false
;
}
}
return
true
;
}(),
'A RenderTapRegion was registered when it was disabled.'
);
if
(
event
is
!
PointerDownEvent
||
event
.
buttons
!=
kPrimaryButton
)
{
return
;
}
if
(
_registeredRegions
.
isEmpty
)
{
assert
(
_tapRegionDebug
(
'Ignored tap event because no regions are registered.'
));
return
;
}
final
BoxHitTestResult
?
result
=
_cachedResults
[
entry
];
if
(
result
==
null
)
{
assert
(
_tapRegionDebug
(
'Ignored tap event because no surface descendants were hit.'
));
return
;
}
// A child was hit, so we need to call onTapOutside for those regions or
// groups of regions that were not hit.
final
Set
<
RenderTapRegion
>
hitRegions
=
_getRegionsHit
(
_registeredRegions
,
result
.
path
).
cast
<
RenderTapRegion
>().
toSet
();
final
Set
<
RenderTapRegion
>
insideRegions
=
<
RenderTapRegion
>{};
assert
(
_tapRegionDebug
(
'Tap event hit
${hitRegions.length}
descendants.'
));
for
(
final
RenderTapRegion
region
in
hitRegions
)
{
if
(
region
.
groupId
==
null
)
{
insideRegions
.
add
(
region
);
continue
;
}
// Add all grouped regions to the insideRegions so that groups act as a
// single region.
insideRegions
.
addAll
(
_groupIdToRegions
[
region
.
groupId
]!);
}
// If they're not inside, then they're outside.
final
Set
<
RenderTapRegion
>
outsideRegions
=
_registeredRegions
.
difference
(
insideRegions
);
for
(
final
RenderTapRegion
region
in
outsideRegions
)
{
assert
(
_tapRegionDebug
(
'Calling onTapOutside for
$region
'
));
region
.
onTapOutside
?.
call
(
event
);
}
for
(
final
RenderTapRegion
region
in
insideRegions
)
{
assert
(
_tapRegionDebug
(
'Calling onTapInside for
$region
'
));
region
.
onTapInside
?.
call
(
event
);
}
}
// Returns the registered regions that are in the hit path.
Iterable
<
HitTestTarget
>
_getRegionsHit
(
Set
<
RenderTapRegion
>
detectors
,
Iterable
<
HitTestEntry
>
hitTestPath
)
{
final
Set
<
HitTestTarget
>
hitRegions
=
<
HitTestTarget
>{};
for
(
final
HitTestEntry
<
HitTestTarget
>
entry
in
hitTestPath
)
{
final
HitTestTarget
target
=
entry
.
target
;
if
(
_registeredRegions
.
contains
(
target
))
{
hitRegions
.
add
(
target
);
}
}
return
hitRegions
;
}
}
/// A widget that defines a region that can detect taps inside or outside of
/// itself and any group of regions it belongs to, without participating in the
/// [gesture disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system.
///
/// This widget indicates to the nearest ancestor [TapRegionSurface] that the
/// region occupied by its child will participate in the tap detection for that
/// surface.
///
/// If this region belongs to a group (by virtue of its [groupId]), all the
/// regions in the group will act as one.
///
/// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing.
class
TapRegion
extends
SingleChildRenderObjectWidget
{
/// Creates a const [TapRegion].
///
/// The [child] argument is required.
const
TapRegion
({
super
.
key
,
required
super
.
child
,
this
.
enabled
=
true
,
this
.
onTapOutside
,
this
.
onTapInside
,
this
.
groupId
,
String
?
debugLabel
,
})
:
debugLabel
=
kReleaseMode
?
null
:
debugLabel
;
/// Whether or not this [TapRegion] is enabled as part of the composite region.
final
bool
enabled
;
/// A callback to be invoked when a tap is detected outside of this
/// [TapRegion] and any other region with the same [groupId], if any.
///
/// The [PointerDownEvent] passed to the function is the event that caused the
/// notification. If this region is part of a group (i.e. [groupId] is set),
/// then it's possible that the event may be outside of this immediate region,
/// although it will be within the region of one of the group members.
final
TapRegionCallback
?
onTapOutside
;
/// A callback to be invoked when a tap is detected inside of this
/// [TapRegion], or any other tap region with the same [groupId], if any.
///
/// The [PointerDownEvent] passed to the function is the event that caused the
/// notification. If this region is part of a group (i.e. [groupId] is set),
/// then it's possible that the event may be outside of this immediate region,
/// although it will be within the region of one of the group members.
final
TapRegionCallback
?
onTapInside
;
/// An optional group ID that groups [TapRegion]s together so that they
/// operate as one region. If any member of a group is hit by a particular
/// tap, then the [onTapOutside] will not be called for any members of the
/// group. If any member of the group is hit, then all members will have their
/// [onTapInside] called.
///
/// If the group id is null, then only this region is hit tested.
final
Object
?
groupId
;
/// An optional debug label to help with debugging in debug mode.
///
/// Will be null in release mode.
final
String
?
debugLabel
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
RenderTapRegion
(
registry:
TapRegionRegistry
.
maybeOf
(
context
),
enabled:
enabled
,
onTapOutside:
onTapOutside
,
onTapInside:
onTapInside
,
groupId:
groupId
,
debugLabel:
debugLabel
,
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
covariant
RenderTapRegion
renderObject
)
{
renderObject
.
registry
=
TapRegionRegistry
.
maybeOf
(
context
);
renderObject
.
enabled
=
enabled
;
renderObject
.
groupId
=
groupId
;
renderObject
.
onTapOutside
=
onTapOutside
;
renderObject
.
onTapInside
=
onTapInside
;
if
(
kReleaseMode
)
{
renderObject
.
debugLabel
=
debugLabel
;
}
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
Object
?>(
'debugLabel'
,
debugLabel
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Object
?>(
'groupId'
,
groupId
,
defaultValue:
null
));
properties
.
add
(
FlagProperty
(
'enabled'
,
value:
enabled
,
ifFalse:
'DISABLED'
,
defaultValue:
true
));
}
}
/// A render object that defines a region that can detect taps inside or outside
/// of itself and any group of regions it belongs to, without participating in
/// the [gesture
/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
/// system.
///
/// This render object indicates to the nearest ancestor [TapRegionSurface] that
/// the region occupied by its child will participate in the tap detection for
/// that surface.
///
/// If this region belongs to a group (by virtue of its [groupId]), all the
/// regions in the group will act as one.
///
/// If there is no [RenderTapRegionSurface] ancestor in the render tree,
/// `RenderTapRegion` will do nothing.
///
/// See also:
///
/// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render
/// tree.
class
RenderTapRegion
extends
RenderProxyBox
with
Diagnosticable
{
/// Creates a [RenderTapRegion].
RenderTapRegion
({
TapRegionRegistry
?
registry
,
bool
enabled
=
true
,
this
.
onTapOutside
,
this
.
onTapInside
,
Object
?
groupId
,
String
?
debugLabel
,
})
:
_registry
=
registry
,
_enabled
=
enabled
,
_groupId
=
groupId
,
debugLabel
=
kReleaseMode
?
null
:
debugLabel
;
bool
_isRegistered
=
false
;
/// A callback to be invoked when a tap is detected outside of this
/// [RenderTapRegion] and any other region with the same [groupId], if any.
///
/// The [PointerDownEvent] passed to the function is the event that caused the
/// notification. If this region is part of a group (i.e. [groupId] is set),
/// then it's possible that the event may be outside of this immediate region,
/// although it will be within the region of one of the group members.
TapRegionCallback
?
onTapOutside
;
/// A callback to be invoked when a tap is detected inside of this
/// [RenderTapRegion], or any other tap region with the same [groupId], if any.
///
/// The [PointerDownEvent] passed to the function is the event that caused the
/// notification. If this region is part of a group (i.e. [groupId] is set),
/// then it's possible that the event may be outside of this immediate region,
/// although it will be within the region of one of the group members.
TapRegionCallback
?
onTapInside
;
/// A label used in debug builds. Will be null in release builds.
String
?
debugLabel
;
/// Whether or not this region should participate in the composite region.
bool
get
enabled
=>
_enabled
;
bool
_enabled
;
set
enabled
(
bool
value
)
{
if
(
_enabled
!=
value
)
{
_enabled
=
value
;
markNeedsLayout
();
}
}
/// An optional group ID that groups [RenderTapRegion]s together so that they
/// operate as one region. If any member of a group is hit by a particular
/// tap, then the [onTapOutside] will not be called for any members of the
/// group. If any member of the group is hit, then all members will have their
/// [onTapInside] called.
///
/// If the group id is null, then only this region is hit tested.
Object
?
get
groupId
=>
_groupId
;
Object
?
_groupId
;
set
groupId
(
Object
?
value
)
{
if
(
_groupId
!=
value
)
{
_groupId
=
value
;
markNeedsLayout
();
}
}
/// The registry that this [RenderTapRegion] should register with.
///
/// If the `registry` is null, then this region will not be registered
/// anywhere, and will not do any tap detection.
///
/// A [RenderTapRegionSurface] is a [TapRegionRegistry].
TapRegionRegistry
?
get
registry
=>
_registry
;
TapRegionRegistry
?
_registry
;
set
registry
(
TapRegionRegistry
?
value
)
{
if
(
_registry
!=
value
)
{
if
(
_isRegistered
)
{
_registry
!.
unregisterTapRegion
(
this
);
_isRegistered
=
false
;
}
_registry
=
value
;
markNeedsLayout
();
}
}
@override
void
layout
(
Constraints
constraints
,
{
bool
parentUsesSize
=
false
})
{
super
.
layout
(
constraints
,
parentUsesSize:
parentUsesSize
);
if
(
_registry
==
null
)
{
return
;
}
if
(
_isRegistered
)
{
_registry
!.
unregisterTapRegion
(
this
);
}
final
bool
shouldBeRegistered
=
_enabled
&&
_registry
!=
null
;
if
(
shouldBeRegistered
)
{
_registry
!.
registerTapRegion
(
this
);
}
_isRegistered
=
shouldBeRegistered
;
}
@override
void
dispose
()
{
if
(
_isRegistered
)
{
_registry
!.
unregisterTapRegion
(
this
);
}
super
.
dispose
();
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
Object
?>(
'debugLabel'
,
debugLabel
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Object
?>(
'groupId'
,
groupId
,
defaultValue:
null
));
properties
.
add
(
FlagProperty
(
'enabled'
,
value:
enabled
,
ifFalse:
'DISABLED'
,
defaultValue:
true
));
}
}
/// A [TapRegion] that adds its children to the tap region group for widgets
/// based on the [EditableText] text editing widget, such as [TextField] and
/// [CupertinoTextField].
///
/// Widgets that are wrapped with a `TextFieldTapRegion` are considered to be
/// part of a text field for purposes of unfocus behavior. So, when the user
/// taps on them, the currently focused text field won't be unfocused by
/// default. This allows controls like spinners, copy buttons, and formatting
/// buttons to be associated with a text field without causing the text field to
/// lose focus when they are interacted with.
///
/// {@tool dartpad}
/// This example shows how to use a `TextFieldTapRegion` to wrap a set of
/// "spinner" buttons that increment and decrement a value in the text field
/// without causing the text field to lose keyboard focus.
///
/// This example includes a generic `SpinnerField<T>` class that you can copy/paste
/// into your own project and customize.
///
/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TapRegion], the widget that this widget uses to add widgets to the group
/// of text fields.
class
TextFieldTapRegion
extends
TapRegion
{
/// Creates a const [TextFieldTapRegion].
///
/// The [child] field is required.
const
TextFieldTapRegion
({
super
.
key
,
required
super
.
child
,
super
.
enabled
,
super
.
onTapOutside
,
super
.
onTapInside
,
super
.
debugLabel
,
})
:
super
(
groupId:
EditableText
);
}
packages/flutter/lib/src/widgets/text_selection.dart
View file @
f5e4d2b4
...
...
@@ -20,6 +20,7 @@ import 'editable_text.dart';
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'overlay.dart'
;
import
'tap_region.dart'
;
import
'ticker_provider.dart'
;
import
'transitions.dart'
;
...
...
@@ -958,8 +959,10 @@ class SelectionOverlay {
dragStartBehavior:
dragStartBehavior
,
);
}
return
ExcludeSemantics
(
return
TextFieldTapRegion
(
child:
ExcludeSemantics
(
child:
handle
,
),
);
}
...
...
@@ -983,8 +986,10 @@ class SelectionOverlay {
dragStartBehavior:
dragStartBehavior
,
);
}
return
ExcludeSemantics
(
return
TextFieldTapRegion
(
child:
ExcludeSemantics
(
child:
handle
,
),
);
}
...
...
@@ -1015,7 +1020,8 @@ class SelectionOverlay {
selectionEndpoints
.
first
.
point
.
dy
-
lineHeightAtStart
,
);
return
Directionality
(
return
TextFieldTapRegion
(
child:
Directionality
(
textDirection:
Directionality
.
of
(
this
.
context
),
child:
_SelectionToolbarOverlay
(
preferredLineHeight:
lineHeightAtStart
,
...
...
@@ -1029,6 +1035,7 @@ class SelectionOverlay {
selectionDelegate:
selectionDelegate
,
clipboardStatus:
clipboardStatus
,
),
),
);
}
}
...
...
packages/flutter/lib/widgets.dart
View file @
f5e4d2b4
...
...
@@ -128,6 +128,7 @@ export 'src/widgets/slotted_render_object_widget.dart';
export
'src/widgets/spacer.dart'
;
export
'src/widgets/status_transitions.dart'
;
export
'src/widgets/table.dart'
;
export
'src/widgets/tap_region.dart'
;
export
'src/widgets/text.dart'
;
export
'src/widgets/text_editing_intents.dart'
;
export
'src/widgets/text_selection.dart'
;
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
f5e4d2b4
...
...
@@ -5960,4 +5960,144 @@ void main() {
expect
(
controller
.
selection
.
extentOffset
,
5
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
});
group
(
'TapRegion integration'
,
()
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
CupertinoTextField
(
autofocus:
true
,
focusNode:
focusNode
,
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
// Tap outside the border.
await
tester
.
tapAt
(
const
Offset
(
10
,
10
));
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
},
variant:
TargetPlatformVariant
.
desktop
());
testWidgets
(
"Tapping outside doesn't lose focus on mobile"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
CupertinoTextField
(
autofocus:
true
,
focusNode:
focusNode
,
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
// Tap just outside the border, but not inside the EditableText.
await
tester
.
tapAt
(
const
Offset
(
10
,
10
));
await
tester
.
pump
();
// Focus is lost on mobile browsers, but not mobile apps.
expect
(
focusNode
.
hasPrimaryFocus
,
kIsWeb
?
isFalse
:
isTrue
);
},
variant:
TargetPlatformVariant
.
mobile
());
testWidgets
(
"tapping on toolbar doesn't lose focus"
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
;
final
EditableTextState
state
;
controller
=
TextEditingController
(
text:
'A B C'
);
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
autofocus:
true
,
focusNode:
focusNode
,
controller:
controller
,
),
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
// Select the first 2 words.
state
.
renderEditable
.
selectPositionAt
(
from:
textOffsetToPosition
(
tester
,
0
),
to:
textOffsetToPosition
(
tester
,
2
),
cause:
SelectionChangedCause
.
tap
,
);
final
Offset
midSelection
=
textOffsetToPosition
(
tester
,
2
);
// Right click the selection.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
midSelection
,
kind:
PointerDeviceKind
.
mouse
,
buttons:
kSecondaryMouseButton
,
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Copy'
),
findsOneWidget
);
// Copy the first word.
await
tester
.
tap
(
find
.
text
(
'Copy'
));
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
(),
skip:
kIsWeb
,
// [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
);
testWidgets
(
"Tapping on border doesn't lose focus"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
CupertinoTextField
(
autofocus:
true
,
focusNode:
focusNode
,
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
final
Rect
borderBox
=
tester
.
getRect
(
find
.
byType
(
CupertinoTextField
));
// Tap just inside the border, but not inside the EditableText.
await
tester
.
tapAt
(
borderBox
.
topLeft
+
const
Offset
(
1
,
1
));
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
());
});
}
packages/flutter/test/material/debug_test.dart
View file @
f5e4d2b4
...
...
@@ -134,7 +134,6 @@ void main() {
' _FadeUpwardsPageTransition
\n
'
' AnimatedBuilder
\n
'
' RepaintBoundary
\n
'
' FocusTrap
\n
'
' _FocusMarker
\n
'
' Semantics
\n
'
' FocusScope
\n
'
...
...
@@ -192,6 +191,7 @@ void main() {
' Focus
\n
'
' Shortcuts
\n
'
' ShortcutRegistrar
\n
'
' TapRegionSurface
\n
'
' _FocusMarker
\n
'
' Focus
\n
'
' _FocusTraversalGroupMarker
\n
'
...
...
packages/flutter/test/material/input_decorator_test.dart
View file @
f5e4d2b4
...
...
@@ -5417,7 +5417,7 @@ void main() {
final
double
floatedLabelWidth
=
getLabelRect
(
tester
).
width
;
expect
(
floatedLabelWidth
>
labelWidth
,
isTrue
);
expect
(
floatedLabelWidth
,
greaterThan
(
labelWidth
)
);
final
Widget
target
=
getLabeledInputDecorator
(
FloatingLabelBehavior
.
auto
);
await
tester
.
pumpWidget
(
target
);
...
...
@@ -5430,8 +5430,8 @@ void main() {
// Default animation duration is 200 millisecond.
await
tester
.
pumpFrames
(
target
,
const
Duration
(
milliseconds:
100
));
expect
(
getLabelRect
(
tester
).
width
>
labelWidth
,
isTrue
);
expect
(
getLabelRect
(
tester
).
width
<
floatedLabelWidth
,
isTrue
);
expect
(
getLabelRect
(
tester
).
width
,
greaterThan
(
labelWidth
)
);
expect
(
getLabelRect
(
tester
).
width
,
lessThanOrEqualTo
(
floatedLabelWidth
)
);
await
tester
.
pumpAndSettle
();
...
...
packages/flutter/test/material/text_field_focus_test.dart
View file @
f5e4d2b4
...
...
@@ -186,7 +186,7 @@ void main() {
expect
(
find
.
byType
(
TextField
),
findsOneWidget
);
expect
(
tester
.
testTextInput
.
isVisible
,
isTrue
);
await
tester
.
drag
(
find
.
byType
(
ListView
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
drag
(
find
.
byType
(
TextField
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
pump
();
expect
(
find
.
byType
(
TextField
,
skipOffstage:
false
),
findsOneWidget
);
expect
(
tester
.
testTextInput
.
isVisible
,
isTrue
);
...
...
@@ -225,7 +225,7 @@ void main() {
FocusScope
.
of
(
tester
.
element
(
find
.
byType
(
TextField
))).
requestFocus
(
focusNode
);
await
tester
.
pump
();
expect
(
find
.
byType
(
TextField
),
findsOneWidget
);
await
tester
.
drag
(
find
.
byType
(
ListView
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
drag
(
find
.
byType
(
TextField
),
const
Offset
(
0.0
,
-
1000.0
));
await
tester
.
pump
();
expect
(
find
.
byType
(
TextField
,
skipOffstage:
false
),
findsOneWidget
);
await
tester
.
pumpWidget
(
makeTest
(
'test'
));
...
...
@@ -490,8 +490,8 @@ void main() {
},
variant:
TargetPlatformVariant
.
desktop
());
testWidgets
(
'A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop after tab navigation'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNodeA
=
FocusNode
();
final
FocusNode
focusNodeB
=
FocusNode
();
final
FocusNode
focusNodeA
=
FocusNode
(
debugLabel:
'A'
);
final
FocusNode
focusNodeB
=
FocusNode
(
debugLabel:
'B'
);
final
Key
key
=
UniqueKey
();
await
tester
.
pumpWidget
(
...
...
@@ -518,30 +518,33 @@ void main() {
);
// Tab over to the 3rd text field.
for
(
int
i
=
0
;
i
<
3
;
i
+=
1
)
{
await
tester
.
sendKeyDownEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
sendKeyUpEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
tab
);
await
tester
.
pump
();
}
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
PointerDeviceKind
.
mouse
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
expect
(
focusNodeA
.
hasFocus
,
true
);
expect
(
focusNodeB
.
hasFocus
,
false
);
// Click on the container to not hit either text field.
final
TestGesture
down2
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byKey
(
key
)),
kind:
PointerDeviceKind
.
mouse
);
await
click
(
find
.
byKey
(
key
)
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
await
down2
.
up
();
await
down2
.
removePointer
();
expect
(
focusNodeA
.
hasFocus
,
false
);
expect
(
focusNodeB
.
hasFocus
,
false
);
// Second text field can still gain focus.
final
TestGesture
down3
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
TextField
).
last
),
kind:
PointerDeviceKind
.
mouse
);
await
click
(
find
.
byType
(
TextField
).
last
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
await
down3
.
up
();
await
down3
.
removePointer
();
expect
(
focusNodeA
.
hasFocus
,
false
);
expect
(
focusNodeB
.
hasFocus
,
true
);
...
...
packages/flutter/test/material/text_field_test.dart
View file @
f5e4d2b4
...
...
@@ -11785,4 +11785,224 @@ void main() {
expect
(
controller
.
selection
.
extentOffset
,
5
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
});
group
(
'TapRegion integration'
,
()
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
Opacity
(
opacity:
0.5
,
child:
TextField
(
autofocus:
true
,
focusNode:
focusNode
,
decoration:
const
InputDecoration
(
hintText:
'Placeholder'
,
border:
OutlineInputBorder
(),
),
),
),
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
await
tester
.
tapAt
(
const
Offset
(
10
,
10
));
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
},
variant:
TargetPlatformVariant
.
desktop
());
testWidgets
(
"Tapping outside doesn't lose focus on mobile"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
Opacity
(
opacity:
0.5
,
child:
TextField
(
autofocus:
true
,
focusNode:
focusNode
,
decoration:
const
InputDecoration
(
hintText:
'Placeholder'
,
border:
OutlineInputBorder
(),
),
),
),
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
await
tester
.
tapAt
(
const
Offset
(
10
,
10
));
await
tester
.
pump
();
// Focus is lost on mobile browsers, but not mobile apps.
expect
(
focusNode
.
hasPrimaryFocus
,
kIsWeb
?
isFalse
:
isTrue
);
},
variant:
TargetPlatformVariant
.
mobile
());
testWidgets
(
"Tapping on toolbar doesn't lose focus"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
final
TextEditingController
controller
=
TextEditingController
(
text:
'A B C'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
Opacity
(
opacity:
0.5
,
child:
TextField
(
controller:
controller
,
focusNode:
focusNode
,
decoration:
const
InputDecoration
(
hintText:
'Placeholder'
),
),
),
),
),
),
),
);
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
state
.
renderEditable
.
selectWordsInRange
(
from:
Offset
.
zero
,
cause:
SelectionChangedCause
.
tap
);
final
Offset
aPosition
=
textOffsetToPosition
(
tester
,
1
);
// Right clicking shows the menu.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
aPosition
,
kind:
PointerDeviceKind
.
mouse
,
buttons:
kSecondaryMouseButton
,
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
// Sanity check that the toolbar widget exists.
expect
(
find
.
text
(
'Copy'
),
findsOneWidget
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
// Now tap on it to see if we lose focus.
await
tester
.
tap
(
find
.
text
(
'Copy'
));
await
tester
.
pumpAndSettle
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
(),
skip:
isBrowser
,
// [intended] On the web, the toolbar isn't rendered by Flutter.
);
testWidgets
(
"Tapping on input decorator doesn't lose focus"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
SizedBox
(
width:
100
,
height:
100
,
child:
Opacity
(
opacity:
0.5
,
child:
TextField
(
autofocus:
true
,
focusNode:
focusNode
,
decoration:
const
InputDecoration
(
hintText:
'Placeholder'
,
border:
OutlineInputBorder
(),
),
),
),
),
),
),
),
);
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
final
Rect
decorationBox
=
tester
.
getRect
(
find
.
byType
(
TextField
));
// Tap just inside the decoration, but not inside the EditableText.
await
tester
.
tapAt
(
decorationBox
.
topLeft
+
const
Offset
(
1
,
1
));
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
());
// PointerDownEvents can't be trackpad events, apparently, so we skip that one.
for
(
final
PointerDeviceKind
pointerDeviceKind
in
PointerDeviceKind
.
values
.
toSet
()..
remove
(
PointerDeviceKind
.
trackpad
))
{
testWidgets
(
'Default TextField handling of onTapOutside follows platform conventions for
${pointerDeviceKind.name}
'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Column
(
children:
<
Widget
>[
const
Text
(
'Outside'
),
TextField
(
autofocus:
true
,
focusNode:
focusNode
,
),
],
),
),
),
);
await
tester
.
pump
();
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
pointerDeviceKind
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
await
click
(
find
.
text
(
'Outside'
));
switch
(
pointerDeviceKind
)
{
case
PointerDeviceKind
.
touch
:
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
expect
(
focusNode
.
hasPrimaryFocus
,
equals
(!
kIsWeb
));
break
;
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
break
;
}
break
;
case
PointerDeviceKind
.
mouse
:
case
PointerDeviceKind
.
stylus
:
case
PointerDeviceKind
.
invertedStylus
:
case
PointerDeviceKind
.
trackpad
:
case
PointerDeviceKind
.
unknown
:
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
break
;
}
},
variant:
TargetPlatformVariant
.
all
());
}
});
}
packages/flutter/test/widgets/editable_text_shortcuts_test.dart
View file @
f5e4d2b4
...
...
@@ -1475,6 +1475,7 @@ void main() {
offset:
2
,
);
await
tester
.
pumpWidget
(
buildEditableText
());
await
tester
.
pump
();
// Wait for autofocus to take effect.
await
sendKeyCombination
(
tester
,
const
SingleActivator
(
LogicalKeyboardKey
.
arrowDown
));
await
tester
.
pump
();
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
f5e4d2b4
...
...
@@ -12595,13 +12595,22 @@ class MockTextFormatter extends TextInputFormatter {
class
MockTextSelectionControls
extends
Fake
implements
TextSelectionControls
{
@override
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
ClipboardStatusNotifier
?
clipboardStatus
,
Offset
?
lastSecondaryTapDownPosition
)
{
return
Container
();
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
ClipboardStatusNotifier
?
clipboardStatus
,
Offset
?
lastSecondaryTapDownPosition
,
)
{
return
const
SizedBox
();
}
@override
Widget
buildHandle
(
BuildContext
context
,
TextSelectionHandleType
type
,
double
textLineHeight
,
[
VoidCallback
?
onTap
])
{
return
Container
();
return
const
SizedBox
();
}
@override
...
...
@@ -12671,7 +12680,16 @@ class _CustomTextSelectionControls extends TextSelectionControls {
final
VoidCallback
?
onCut
;
@override
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
ClipboardStatusNotifier
?
clipboardStatus
,
Offset
?
lastSecondaryTapDownPosition
)
{
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
ClipboardStatusNotifier
?
clipboardStatus
,
Offset
?
lastSecondaryTapDownPosition
,
)
{
final
Offset
selectionMidpoint
=
position
;
final
TextSelectionPoint
startTextSelectionPoint
=
endpoints
[
0
];
final
TextSelectionPoint
endTextSelectionPoint
=
endpoints
.
length
>
1
...
...
packages/flutter/test/widgets/routes_test.dart
View file @
f5e4d2b4
...
...
@@ -5,7 +5,6 @@
import
'dart:collection'
;
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -1976,143 +1975,6 @@ void main() {
await
tester
.
restoreFrom
(
restorationData
);
expect
(
find
.
byType
(
AlertDialog
),
findsOneWidget
);
},
skip:
isBrowser
);
// https://github.com/flutter/flutter/issues/33615
testWidgets
(
'FocusTrap moves focus to given focus scope when triggered'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
focusScope
=
FocusScopeNode
();
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test'
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
FocusScope
(
node:
focusScope
,
child:
FocusTrap
(
focusScopeNode:
focusScope
,
child:
Column
(
children:
<
Widget
>[
const
Text
(
'Other Widget'
),
FocusTrapTestWidget
(
'Focusable'
,
focusNode:
focusNode
,
onTap:
()
{
focusNode
.
requestFocus
();
}),
],
),
),
),
),
);
await
tester
.
pump
();
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
PointerDeviceKind
.
mouse
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
expect
(
focusScope
.
hasFocus
,
isFalse
);
expect
(
focusNode
.
hasFocus
,
isFalse
);
await
click
(
find
.
text
(
'Focusable'
));
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
focusScope
.
hasFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
await
click
(
find
.
text
(
'Other Widget'
));
// Have to wait out the double click timer.
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
android
:
if
(
kIsWeb
)
{
// Web is a desktop platform.
expect
(
focusScope
.
hasPrimaryFocus
,
isTrue
);
expect
(
focusNode
.
hasFocus
,
isFalse
);
}
else
{
expect
(
focusScope
.
hasFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
}
break
;
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
expect
(
focusScope
.
hasPrimaryFocus
,
isTrue
);
expect
(
focusNode
.
hasFocus
,
isFalse
);
break
;
}
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
"FocusTrap doesn't unfocus if focus was set to something else before the frame ends"
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
focusScope
=
FocusScopeNode
();
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test'
);
final
FocusNode
otherFocusNode
=
FocusNode
(
debugLabel:
'Other'
);
FocusNode
?
previousFocus
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
FocusScope
(
node:
focusScope
,
child:
FocusTrap
(
focusScopeNode:
focusScope
,
child:
Column
(
children:
<
Widget
>[
FocusTrapTestWidget
(
'Other Widget'
,
focusNode:
otherFocusNode
,
onTap:
()
{
previousFocus
=
FocusManager
.
instance
.
primaryFocus
;
otherFocusNode
.
requestFocus
();
},
),
FocusTrapTestWidget
(
'Focusable'
,
focusNode:
focusNode
,
onTap:
()
{
focusNode
.
requestFocus
();
},
),
],
),
),
),
),
);
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
PointerDeviceKind
.
mouse
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
await
tester
.
pump
();
expect
(
focusScope
.
hasFocus
,
isFalse
);
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
await
click
(
find
.
text
(
'Focusable'
));
expect
(
focusScope
.
hasFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
await
click
(
find
.
text
(
'Other Widget'
));
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// The previous focus as collected by the "Other Widget" should be the
// previous focus, not be unfocused to the scope, since the primary focus
// was set by something other than the FocusTrap (the "Other Widget") during
// the frame.
expect
(
previousFocus
,
equals
(
focusNode
));
expect
(
focusScope
.
hasFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
expect
(
otherFocusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
());
}
double
_getOpacity
(
GlobalKey
key
,
WidgetTester
tester
)
{
...
...
@@ -2327,68 +2189,3 @@ class _RestorableDialogTestWidget extends StatelessWidget {
);
}
}
class
FocusTrapTestWidget
extends
StatefulWidget
{
const
FocusTrapTestWidget
(
this
.
label
,
{
super
.
key
,
required
this
.
focusNode
,
this
.
onTap
,
this
.
autofocus
=
false
,
});
final
String
label
;
final
FocusNode
focusNode
;
final
VoidCallback
?
onTap
;
final
bool
autofocus
;
@override
State
<
FocusTrapTestWidget
>
createState
()
=>
_FocusTrapTestWidgetState
();
}
class
_FocusTrapTestWidgetState
extends
State
<
FocusTrapTestWidget
>
{
Color
color
=
Colors
.
white
;
@override
void
initState
()
{
super
.
initState
();
widget
.
focusNode
.
addListener
(
_handleFocusChange
);
}
void
_handleFocusChange
()
{
if
(
widget
.
focusNode
.
hasPrimaryFocus
)
{
setState
(()
{
color
=
Colors
.
grey
.
shade500
;
});
}
else
{
setState
(()
{
color
=
Colors
.
white
;
});
}
}
@override
void
dispose
()
{
widget
.
focusNode
.
removeListener
(
_handleFocusChange
);
widget
.
focusNode
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Focus
(
autofocus:
widget
.
autofocus
,
focusNode:
widget
.
focusNode
,
child:
GestureDetector
(
onTap:
()
{
widget
.
onTap
?.
call
();
},
child:
Container
(
color:
color
,
alignment:
Alignment
.
center
,
child:
Text
(
widget
.
label
,
style:
const
TextStyle
(
color:
Colors
.
black
)),
),
),
);
}
}
packages/flutter/test/widgets/tap_region_test.dart
0 → 100644
View file @
f5e4d2b4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'TapRegionSurface detects outside taps'
,
(
WidgetTester
tester
)
async
{
final
Set
<
String
>
clickedOutside
=
<
String
>{};
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
children:
<
Widget
>[
const
Text
(
'Outside Surface'
),
TapRegionSurface
(
child:
Row
(
children:
<
Widget
>[
const
Text
(
'Outside'
),
TapRegion
(
onTapOutside:
(
PointerEvent
event
)
{
clickedOutside
.
add
(
'No Group'
);
},
child:
const
Text
(
'No Group'
),
),
TapRegion
(
groupId:
1
,
onTapOutside:
(
PointerEvent
event
)
{
clickedOutside
.
add
(
'Group 1 A'
);
},
child:
const
Text
(
'Group 1 A'
),
),
TapRegion
(
groupId:
1
,
onTapOutside:
(
PointerEvent
event
)
{
clickedOutside
.
add
(
'Group 1 B'
);
},
child:
const
Text
(
'Group 1 B'
),
),
],
),
),
],
),
),
);
await
tester
.
pump
();
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
PointerDeviceKind
.
mouse
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
expect
(
clickedOutside
,
isEmpty
);
await
click
(
find
.
text
(
'No Group'
));
expect
(
clickedOutside
,
unorderedEquals
(<
String
>{
'Group 1 A'
,
'Group 1 B'
,
}));
clickedOutside
.
clear
();
await
click
(
find
.
text
(
'Group 1 A'
));
expect
(
clickedOutside
,
equals
(<
String
>{
'No Group'
,
}));
clickedOutside
.
clear
();
await
click
(
find
.
text
(
'Group 1 B'
));
expect
(
clickedOutside
,
equals
(<
String
>{
'No Group'
,
}));
clickedOutside
.
clear
();
await
click
(
find
.
text
(
'Outside'
));
expect
(
clickedOutside
,
unorderedEquals
(<
String
>{
'No Group'
,
'Group 1 A'
,
'Group 1 B'
,
}));
clickedOutside
.
clear
();
await
click
(
find
.
text
(
'Outside Surface'
));
expect
(
clickedOutside
,
isEmpty
);
});
testWidgets
(
'TapRegionSurface detects inside taps'
,
(
WidgetTester
tester
)
async
{
final
Set
<
String
>
clickedInside
=
<
String
>{};
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
children:
<
Widget
>[
const
Text
(
'Outside Surface'
),
TapRegionSurface
(
child:
Row
(
children:
<
Widget
>[
const
Text
(
'Outside'
),
TapRegion
(
onTapInside:
(
PointerEvent
event
)
{
clickedInside
.
add
(
'No Group'
);
},
child:
const
Text
(
'No Group'
),
),
TapRegion
(
groupId:
1
,
onTapInside:
(
PointerEvent
event
)
{
clickedInside
.
add
(
'Group 1 A'
);
},
child:
const
Text
(
'Group 1 A'
),
),
TapRegion
(
groupId:
1
,
onTapInside:
(
PointerEvent
event
)
{
clickedInside
.
add
(
'Group 1 B'
);
},
child:
const
Text
(
'Group 1 B'
),
),
],
),
),
],
),
),
);
await
tester
.
pump
();
Future
<
void
>
click
(
Finder
finder
)
async
{
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
finder
),
kind:
PointerDeviceKind
.
mouse
,
);
await
gesture
.
up
();
await
gesture
.
removePointer
();
}
expect
(
clickedInside
,
isEmpty
);
await
click
(
find
.
text
(
'No Group'
));
expect
(
clickedInside
,
unorderedEquals
(<
String
>{
'No Group'
,
}));
clickedInside
.
clear
();
await
click
(
find
.
text
(
'Group 1 A'
));
expect
(
clickedInside
,
equals
(<
String
>{
'Group 1 A'
,
'Group 1 B'
,
}));
clickedInside
.
clear
();
await
click
(
find
.
text
(
'Group 1 B'
));
expect
(
clickedInside
,
equals
(<
String
>{
'Group 1 A'
,
'Group 1 B'
,
}));
clickedInside
.
clear
();
await
click
(
find
.
text
(
'Outside'
));
expect
(
clickedInside
,
isEmpty
);
clickedInside
.
clear
();
await
click
(
find
.
text
(
'Outside Surface'
));
expect
(
clickedInside
,
isEmpty
);
});
}
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