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
dbbef15a
Unverified
Commit
dbbef15a
authored
Oct 25, 2022
by
Greg Spencer
Committed by
GitHub
Oct 25, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add Focus.parentNode to allow controlling the shape of the Focus tree. (#113655)
parent
0fe29f58
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
153 additions
and
65 deletions
+153
-65
focus_scope.dart
packages/flutter/lib/src/widgets/focus_scope.dart
+20
-1
focus_scope_test.dart
packages/flutter/test/widgets/focus_scope_test.dart
+133
-64
No files found.
packages/flutter/lib/src/widgets/focus_scope.dart
View file @
dbbef15a
...
...
@@ -117,6 +117,7 @@ class Focus extends StatefulWidget {
super
.
key
,
required
this
.
child
,
this
.
focusNode
,
this
.
parentNode
,
this
.
autofocus
=
false
,
this
.
onFocusChange
,
FocusOnKeyEventCallback
?
onKeyEvent
,
...
...
@@ -144,6 +145,7 @@ class Focus extends StatefulWidget {
Key
?
key
,
required
Widget
child
,
required
FocusNode
focusNode
,
FocusNode
?
parentNode
,
bool
autofocus
,
ValueChanged
<
bool
>?
onFocusChange
,
bool
includeSemantics
,
...
...
@@ -153,6 +155,22 @@ class Focus extends StatefulWidget {
// when then widget is updated.
bool
get
_usingExternalFocus
=>
false
;
/// The optional parent node to use when reparenting the [focusNode] for this
/// [Focus] widget.
///
/// If [parentNode] is null, then [Focus.maybeOf] is used to find the parent
/// in the widget tree, which is typically what is desired, since it is easier
/// to reason about the focus tree if it mirrors the shape of the widget tree.
///
/// Set this property if the focus tree needs to have a different shape than
/// the widget tree. This is typically in cases where a dialog is in an
/// [Overlay] (or another part of the widget tree), and focus should
/// behave as if the widgets in the overlay are descendants of the given
/// [parentNode] for purposes of focus.
///
/// Defaults to null.
final
FocusNode
?
parentNode
;
/// The child widget of this [Focus].
///
/// {@macro flutter.widgets.ProxyWidget.child}
...
...
@@ -467,6 +485,7 @@ class _FocusWithExternalFocusNode extends Focus {
super
.
key
,
required
super
.
child
,
required
FocusNode
super
.
focusNode
,
super
.
parentNode
,
super
.
autofocus
,
super
.
onFocusChange
,
super
.
includeSemantics
,
...
...
@@ -656,7 +675,7 @@ class _FocusState extends State<Focus> {
@override
Widget
build
(
BuildContext
context
)
{
_focusAttachment
!.
reparent
();
_focusAttachment
!.
reparent
(
parent:
widget
.
parentNode
);
Widget
child
=
widget
.
child
;
if
(
widget
.
includeSemantics
)
{
child
=
Semantics
(
...
...
packages/flutter/test/widgets/focus_scope_test.dart
View file @
dbbef15a
...
...
@@ -9,70 +9,6 @@ import 'package:flutter_test/flutter_test.dart';
import
'semantics_tester.dart'
;
class
TestFocus
extends
StatefulWidget
{
const
TestFocus
({
super
.
key
,
this
.
debugLabel
,
this
.
name
=
'a'
,
this
.
autofocus
=
false
,
});
final
String
?
debugLabel
;
final
String
name
;
final
bool
autofocus
;
@override
TestFocusState
createState
()
=>
TestFocusState
();
}
class
TestFocusState
extends
State
<
TestFocus
>
{
late
FocusNode
focusNode
;
late
String
_label
;
bool
built
=
false
;
@override
void
dispose
()
{
focusNode
.
removeListener
(
_updateLabel
);
focusNode
.
dispose
();
super
.
dispose
();
}
String
get
label
=>
focusNode
.
hasFocus
?
'
${widget.name.toUpperCase()}
FOCUSED'
:
widget
.
name
.
toLowerCase
();
@override
void
initState
()
{
super
.
initState
();
focusNode
=
FocusNode
(
debugLabel:
widget
.
debugLabel
);
_label
=
label
;
focusNode
.
addListener
(
_updateLabel
);
}
void
_updateLabel
()
{
setState
(()
{
_label
=
label
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
built
=
true
;
return
GestureDetector
(
onTap:
()
{
FocusScope
.
of
(
context
).
requestFocus
(
focusNode
);
},
child:
Focus
(
autofocus:
widget
.
autofocus
,
focusNode:
focusNode
,
debugLabel:
widget
.
debugLabel
,
child:
Text
(
_label
,
textDirection:
TextDirection
.
ltr
,
),
),
);
}
}
void
main
(
)
{
group
(
'FocusScope'
,
()
{
testWidgets
(
'Can focus'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -530,6 +466,72 @@ void main() {
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
});
testWidgets
(
'Setting parentNode determines focus tree hierarchy.'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
topNode
=
FocusNode
(
debugLabel:
'Top'
);
final
FocusNode
parentNode
=
FocusNode
(
debugLabel:
'Parent'
);
final
FocusNode
childNode
=
FocusNode
(
debugLabel:
'Child'
);
final
FocusNode
insertedNode
=
FocusNode
(
debugLabel:
'Inserted'
);
await
tester
.
pumpWidget
(
FocusScope
(
child:
Focus
.
withExternalFocusNode
(
focusNode:
topNode
,
child:
Column
(
children:
<
Widget
>[
Focus
.
withExternalFocusNode
(
focusNode:
parentNode
,
child:
const
SizedBox
(),
),
Focus
.
withExternalFocusNode
(
focusNode:
childNode
,
parentNode:
parentNode
,
autofocus:
true
,
child:
const
SizedBox
(),
)
],
),
),
),
);
await
tester
.
pump
();
expect
(
childNode
.
hasPrimaryFocus
,
isTrue
);
expect
(
parentNode
.
hasFocus
,
isTrue
);
expect
(
topNode
.
hasFocus
,
isTrue
);
// Check that inserting a Focus in between doesn't reparent the child.
await
tester
.
pumpWidget
(
FocusScope
(
child:
Focus
.
withExternalFocusNode
(
focusNode:
topNode
,
child:
Column
(
children:
<
Widget
>[
Focus
.
withExternalFocusNode
(
focusNode:
parentNode
,
child:
const
SizedBox
(),
),
Focus
.
withExternalFocusNode
(
focusNode:
insertedNode
,
child:
Focus
.
withExternalFocusNode
(
focusNode:
childNode
,
parentNode:
parentNode
,
autofocus:
true
,
child:
const
SizedBox
(),
),
)
],
),
),
),
);
await
tester
.
pump
();
expect
(
childNode
.
hasPrimaryFocus
,
isTrue
);
expect
(
parentNode
.
hasFocus
,
isTrue
);
expect
(
topNode
.
hasFocus
,
isTrue
);
expect
(
insertedNode
.
hasFocus
,
isFalse
);
});
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing focused widget doesn't move focus to next widget within FocusScope"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
...
...
@@ -2015,3 +2017,70 @@ void main() {
});
});
}
class
TestFocus
extends
StatefulWidget
{
const
TestFocus
({
super
.
key
,
this
.
debugLabel
,
this
.
name
=
'a'
,
this
.
autofocus
=
false
,
this
.
parentNode
,
});
final
String
?
debugLabel
;
final
String
name
;
final
bool
autofocus
;
final
FocusNode
?
parentNode
;
@override
TestFocusState
createState
()
=>
TestFocusState
();
}
class
TestFocusState
extends
State
<
TestFocus
>
{
late
FocusNode
focusNode
;
late
String
_label
;
bool
built
=
false
;
@override
void
dispose
()
{
focusNode
.
removeListener
(
_updateLabel
);
focusNode
.
dispose
();
super
.
dispose
();
}
String
get
label
=>
focusNode
.
hasFocus
?
'
${widget.name.toUpperCase()}
FOCUSED'
:
widget
.
name
.
toLowerCase
();
@override
void
initState
()
{
super
.
initState
();
focusNode
=
FocusNode
(
debugLabel:
widget
.
debugLabel
);
_label
=
label
;
focusNode
.
addListener
(
_updateLabel
);
}
void
_updateLabel
()
{
setState
(()
{
_label
=
label
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
built
=
true
;
return
GestureDetector
(
onTap:
()
{
FocusScope
.
of
(
context
).
requestFocus
(
focusNode
);
},
child:
Focus
(
autofocus:
widget
.
autofocus
,
focusNode:
focusNode
,
parentNode:
widget
.
parentNode
,
debugLabel:
widget
.
debugLabel
,
child:
Text
(
_label
,
textDirection:
TextDirection
.
ltr
,
),
),
);
}
}
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