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
0f4e1db7
Unverified
Commit
0f4e1db7
authored
Aug 04, 2021
by
LongCatIsLooong
Committed by
GitHub
Aug 04, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[Focus] defer autofocus resolution to `_applyFocusChange` (#85562)
parent
8847015a
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
196 additions
and
32 deletions
+196
-32
focus_manager.dart
packages/flutter/lib/src/widgets/focus_manager.dart
+52
-31
focus_scope.dart
packages/flutter/lib/src/widgets/focus_scope.dart
+2
-1
focus_manager_test.dart
packages/flutter/test/widgets/focus_manager_test.dart
+142
-0
No files found.
packages/flutter/lib/src/widgets/focus_manager.dart
View file @
0f4e1db7
...
...
@@ -96,6 +96,36 @@ typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent
/// was handled.
typedef
FocusOnKeyEventCallback
=
KeyEventResult
Function
(
FocusNode
node
,
KeyEvent
event
);
// Represents a pending autofocus request.
@immutable
class
_Autofocus
{
const
_Autofocus
({
required
this
.
scope
,
required
this
.
autofocusNode
});
final
FocusScopeNode
scope
;
final
FocusNode
autofocusNode
;
// Applies the autofocus request, if the node is still attached to the
// original scope and the scope has no focused child.
//
// The widget tree is responsible for calling reparent/detach on attached
// nodes to keep their parent/manager information up-to-date, so here we can
// safely check if the scope/node involved in each autofocus request is
// still attached, and discard the ones are no longer attached to the
// original manager.
void
applyIfValid
(
FocusManager
manager
)
{
final
bool
shouldApply
=
(
scope
.
parent
!=
null
||
identical
(
scope
,
manager
.
rootScope
))
&&
identical
(
scope
.
_manager
,
manager
)
&&
scope
.
focusedChild
==
null
&&
autofocusNode
.
ancestors
.
contains
(
scope
);
if
(
shouldApply
)
{
assert
(
_focusDebug
(
'Applying autofocus:
$autofocusNode
'
));
autofocusNode
.
_doRequestFocus
(
findFirstFocus:
true
);
}
else
{
assert
(
_focusDebug
(
'Autofocus request discarded for node:
$autofocusNode
.'
));
}
}
}
/// An attachment point for a [FocusNode].
///
/// Using a [FocusAttachment] is rarely needed, unless you are building
...
...
@@ -1353,14 +1383,16 @@ class FocusScopeNode extends FocusNode {
/// The node is notified that it has received the primary focus in a
/// microtask, so notification may lag the request by up to one frame.
void
autofocus
(
FocusNode
node
)
{
assert
(
_focusDebug
(
'Node autofocusing:
$node
'
));
if
(
focusedChild
==
null
)
{
// Attach the node to the tree first, so in _applyFocusChange if the node
// is detached we don't add it back to the tree.
if
(
node
.
_parent
==
null
)
{
_reparent
(
node
);
}
assert
(
node
.
ancestors
.
contains
(
this
),
'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.'
);
node
.
_doRequestFocus
(
findFirstFocus:
true
);
}
assert
(
_manager
!=
null
);
assert
(
_focusDebug
(
'Autofocus scheduled for
$node
: scope
$this
'
));
_manager
?.
_pendingAutofocuses
.
add
(
_Autofocus
(
scope:
this
,
autofocusNode:
node
));
_manager
?.
_markNeedsUpdate
();
}
@override
...
...
@@ -1368,13 +1400,14 @@ class FocusScopeNode extends FocusNode {
assert
(
findFirstFocus
!=
null
);
// It is possible that a previously focused child is no longer focusable.
while
(
focusedChild
!=
null
&&
!
focusedChild
!.
canRequestFocus
)
while
(
this
.
focusedChild
!=
null
&&
!
this
.
focusedChild
!.
canRequestFocus
)
_focusedChildren
.
removeLast
();
final
FocusNode
?
focusedChild
=
this
.
focusedChild
;
// If findFirstFocus is false, then the request is to make this scope the
// focus instead of looking for the ultimate first focus for this scope and
// its descendants.
if
(!
findFirstFocus
)
{
if
(!
findFirstFocus
||
focusedChild
==
null
)
{
if
(
canRequestFocus
)
{
_setAsFocusedChildForScope
();
_markNextFocus
(
this
);
...
...
@@ -1382,28 +1415,7 @@ class FocusScopeNode extends FocusNode {
return
;
}
// Start with the primary focus as the focused child of this scope, if there
// is one. Otherwise start with this node itself.
FocusNode
primaryFocus
=
focusedChild
??
this
;
// Keep going down through scopes until the ultimately focusable item is
// found, a scope doesn't have a focusedChild, or a non-scope is
// encountered.
while
(
primaryFocus
is
FocusScopeNode
&&
primaryFocus
.
focusedChild
!=
null
)
{
primaryFocus
=
primaryFocus
.
focusedChild
!;
}
if
(
identical
(
primaryFocus
,
this
))
{
// We didn't find a FocusNode at the leaf, so we're focusing the scope, if
// allowed.
if
(
primaryFocus
.
canRequestFocus
)
{
_setAsFocusedChildForScope
();
_markNextFocus
(
this
);
}
}
else
{
// We found a FocusScopeNode at the leaf, so ask it to focus itself
// instead of this scope. That will cause this scope to return true from
// hasFocus, but false from hasPrimaryFocus.
primaryFocus
.
_doRequestFocus
(
findFirstFocus:
findFirstFocus
);
}
focusedChild
.
_doRequestFocus
(
findFirstFocus:
true
);
}
@override
...
...
@@ -1811,6 +1823,9 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
}
}
// The list of autofocus requests made since the last _appyFocusChange call.
final
List
<
_Autofocus
>
_pendingAutofocuses
=
<
_Autofocus
>[];
// True indicates that there is an update pending.
bool
_haveScheduledUpdate
=
false
;
...
...
@@ -1828,6 +1843,12 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
void
_applyFocusChange
()
{
_haveScheduledUpdate
=
false
;
final
FocusNode
?
previousFocus
=
_primaryFocus
;
for
(
final
_Autofocus
autofocus
in
_pendingAutofocuses
)
{
autofocus
.
applyIfValid
(
this
);
}
_pendingAutofocuses
.
clear
();
if
(
_primaryFocus
==
null
&&
_markedForFocus
==
null
)
{
// If we don't have any current focus, and nobody has asked to focus yet,
// then revert to the root scope.
...
...
packages/flutter/lib/src/widgets/focus_scope.dart
View file @
0f4e1db7
...
...
@@ -676,7 +676,8 @@ class _FocusState extends State<Focus> {
focusNode
.
descendantsAreFocusable
=
widget
.
descendantsAreFocusable
;
}
else
{
_focusAttachment
!.
detach
();
focusNode
.
removeListener
(
_handleFocusChanged
);
oldWidget
.
focusNode
?.
removeListener
(
_handleFocusChanged
);
_internalNode
?.
removeListener
(
_handleAutofocus
);
_initNode
();
}
...
...
packages/flutter/test/widgets/focus_manager_test.dart
View file @
0f4e1db7
...
...
@@ -1199,6 +1199,148 @@ void main() {
});
});
group
(
'Autofocus'
,
()
{
testWidgets
(
'works when the previous focused node is detached'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
node1
=
FocusNode
();
final
FocusNode
node2
=
FocusNode
();
await
tester
.
pumpWidget
(
FocusScope
(
child:
Focus
(
autofocus:
true
,
focusNode:
node1
,
child:
const
Placeholder
()),
),
);
await
tester
.
pump
();
expect
(
node1
.
hasPrimaryFocus
,
isTrue
);
await
tester
.
pumpWidget
(
FocusScope
(
child:
SizedBox
(
child:
Focus
(
autofocus:
true
,
focusNode:
node2
,
child:
const
Placeholder
()),
),
),
);
await
tester
.
pump
();
expect
(
node2
.
hasPrimaryFocus
,
isTrue
);
});
testWidgets
(
'node detached before autofocus is applied'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
scopeNode
=
FocusScopeNode
();
final
FocusNode
node1
=
FocusNode
();
await
tester
.
pumpWidget
(
FocusScope
(
node:
scopeNode
,
child:
Focus
(
autofocus:
true
,
focusNode:
node1
,
child:
const
Placeholder
(),
),
),
);
await
tester
.
pumpWidget
(
FocusScope
(
node:
scopeNode
,
child:
const
Focus
(
child:
Placeholder
()),
),
);
await
tester
.
pump
();
expect
(
node1
.
hasPrimaryFocus
,
isFalse
);
expect
(
scopeNode
.
hasPrimaryFocus
,
isTrue
);
});
testWidgets
(
'autofocus the first candidate'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
node1
=
FocusNode
();
final
FocusNode
node2
=
FocusNode
();
final
FocusNode
node3
=
FocusNode
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
children:
<
Focus
>[
Focus
(
autofocus:
true
,
focusNode:
node1
,
child:
const
SizedBox
(),
),
Focus
(
autofocus:
true
,
focusNode:
node2
,
child:
const
SizedBox
(),
),
Focus
(
autofocus:
true
,
focusNode:
node3
,
child:
const
SizedBox
(),
),
],
),
),
);
expect
(
node1
.
hasPrimaryFocus
,
isTrue
);
});
testWidgets
(
'Autofocus works with global key reparenting'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
node
=
FocusNode
();
final
FocusScopeNode
scope1
=
FocusScopeNode
(
debugLabel:
'scope1'
);
final
FocusScopeNode
scope2
=
FocusScopeNode
(
debugLabel:
'scope2'
);
final
GlobalKey
key
=
GlobalKey
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
children:
<
Focus
>[
FocusScope
(
node:
scope1
,
child:
Focus
(
key:
key
,
focusNode:
node
,
child:
const
SizedBox
(),
),
),
FocusScope
(
node:
scope2
,
child:
const
SizedBox
()),
],
),
),
);
// _applyFocusChange will be called before persistentCallbacks,
// guaranteeing the focus changes are applied before the BuildContext
// `node` attaches to gets reparented.
scope1
.
autofocus
(
node
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
children:
<
Focus
>[
FocusScope
(
node:
scope1
,
child:
const
SizedBox
()),
FocusScope
(
node:
scope2
,
child:
Focus
(
key:
key
,
focusNode:
node
,
child:
const
SizedBox
(),
),
),
],
),
),
);
expect
(
node
.
hasPrimaryFocus
,
isTrue
);
expect
(
scope2
.
hasFocus
,
isTrue
);
});
});
testWidgets
(
"Doesn't lose focused child when reparenting if the nearestScope doesn't change."
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
parent1
=
FocusScopeNode
(
debugLabel:
'parent1'
);
...
...
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