Unverified Commit d94c18d2 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add external focus node constructor to Focus widget (#90843)

I've added a Focus.withExternalFocusNode constructor to the Focus widget (and the FocusScope widget) that makes it explicit that the widget's attributes won't affect the settings of the given focus node.

This is to help address #83023, which is a snag in the API that people run into occasionally.

This should help make it explicit when you want the widget attributes to take precedence, and when you don't.
parent c48c428e
...@@ -14,9 +14,10 @@ import 'inherited_notifier.dart'; ...@@ -14,9 +14,10 @@ import 'inherited_notifier.dart';
/// ///
/// When the focus is gained or lost, [onFocusChange] is called. /// When the focus is gained or lost, [onFocusChange] is called.
/// ///
/// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for /// For keyboard events, [onKey] and [onKeyEvent] are called if
/// this widget's [focusNode], unless a focused descendant's [onKey] callback /// [FocusNode.hasFocus] is true for this widget's [focusNode], unless a focused
/// returns [KeyEventResult.handled] when called. /// descendant's [onKey] or [onKeyEvent] callback returned
/// [KeyEventResult.handled] when called.
/// ///
/// This widget does not provide any visual indication that the focus has /// This widget does not provide any visual indication that the focus has
/// changed. Any desired visual changes should be made when [onFocusChange] is /// changed. Any desired visual changes should be made when [onFocusChange] is
...@@ -27,9 +28,9 @@ import 'inherited_notifier.dart'; ...@@ -27,9 +28,9 @@ import 'inherited_notifier.dart';
/// changes, use the [Focus.of] and [FocusScope.of] static methods. /// changes, use the [Focus.of] and [FocusScope.of] static methods.
/// ///
/// To access the focused state of the nearest [Focus] widget, use /// To access the focused state of the nearest [Focus] widget, use
/// [FocusNode.hasFocus] from a build method, which also establishes a relationship /// [FocusNode.hasFocus] from a build method, which also establishes a
/// between the calling widget and the [Focus] widget that will rebuild the /// relationship between the calling widget and the [Focus] widget that will
/// calling widget when the focus changes. /// rebuild the calling widget when the focus changes.
/// ///
/// Managing a [FocusNode] means managing its lifecycle, listening for changes /// Managing a [FocusNode] means managing its lifecycle, listening for changes
/// in focus, and re-parenting it when needed to keep the focus hierarchy in /// in focus, and re-parenting it when needed to keep the focus hierarchy in
...@@ -38,6 +39,18 @@ import 'inherited_notifier.dart'; ...@@ -38,6 +39,18 @@ import 'inherited_notifier.dart';
/// management entails if you are not using a [Focus] widget and you need to do /// management entails if you are not using a [Focus] widget and you need to do
/// it yourself. /// it yourself.
/// ///
/// If the [Focus] default constructor is used, then this widget will manage any
/// given [focusNode] by overwriting the appropriate values of the [focusNode]
/// with the values of [FocusNode.onKey], [FocusNode.onKeyEvent],
/// [FocusNode.skipTraversal], [FocusNode.canRequestFocus], and
/// [FocusNode.descendantsAreFocusable] whenever the [Focus] widget is updated.
///
/// If the [Focus.withExternalFocusNode] is used instead, then the values
/// returned by [onKey], [onKeyEvent], [skipTraversal], [canRequestFocus], and
/// [descendantsAreFocusable] will be the values in the external focus node, and
/// the external focus node's values will not be overwritten when the widget is
/// updated.
///
/// To collect a sub-tree of nodes into an exclusive group that restricts focus /// To collect a sub-tree of nodes into an exclusive group that restricts focus
/// traversal to the group, use a [FocusScope]. To collect a sub-tree of nodes /// traversal to the group, use a [FocusScope]. To collect a sub-tree of nodes
/// into a group that has a specific order to its traversal but allows the /// into a group that has a specific order to its traversal but allows the
...@@ -106,38 +119,84 @@ class Focus extends StatefulWidget { ...@@ -106,38 +119,84 @@ class Focus extends StatefulWidget {
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.onFocusChange, this.onFocusChange,
this.onKey, FocusOnKeyEventCallback? onKeyEvent,
this.onKeyEvent, FocusOnKeyCallback? onKey,
this.debugLabel, bool? canRequestFocus,
this.canRequestFocus, bool? skipTraversal,
this.descendantsAreFocusable = true, bool? descendantsAreFocusable,
this.skipTraversal,
this.includeSemantics = true, this.includeSemantics = true,
}) : assert(child != null), String? debugLabel,
}) : _onKeyEvent = onKeyEvent,
_onKey = onKey,
_canRequestFocus = canRequestFocus,
_skipTraversal = skipTraversal,
_descendantsAreFocusable = descendantsAreFocusable,
_debugLabel = debugLabel,
assert(child != null),
assert(autofocus != null), assert(autofocus != null),
assert(descendantsAreFocusable != null),
assert(includeSemantics != null), assert(includeSemantics != null),
super(key: key); super(key: key);
/// A debug label for this widget. /// Creates a Focus widget that uses the given [focusNode] as the source of
/// /// truth for attributes on the node, rather than the attributes of this widget.
/// Not used for anything except to be printed in the diagnostic output from const factory Focus.withExternalFocusNode({
/// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided, Key? key,
/// since that node can have its own [FocusNode.debugLabel]. required Widget child,
/// required FocusNode focusNode,
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To bool autofocus,
/// print it to the console call [debugDumpFocusTree]. ValueChanged<bool>? onFocusChange,
/// bool includeSemantics,
/// Defaults to null. }) = _FocusWithExternalFocusNode;
final String? debugLabel;
// Indicates whether the widget's focusNode attributes should have priority
// when then widget is updated.
bool get _usingExternalFocus => false;
/// The child widget of this [Focus]. /// The child widget of this [Focus].
/// ///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
final Widget child; final Widget child;
/// Handler for keys pressed when this object or one of its children has /// {@template flutter.widgets.Focus.focusNode}
/// An optional focus node to use as the focus node for this widget.
///
/// If one is not supplied, then one will be automatically allocated, owned,
/// and managed by this widget. The widget will be focusable even if a
/// [focusNode] is not supplied. If supplied, the given `focusNode` will be
/// _hosted_ by this widget, but not owned. See [FocusNode] for more
/// information on what being hosted and/or owned implies.
///
/// Supplying a focus node is sometimes useful if an ancestor to this widget
/// wants to control when this widget has the focus. The owner will be
/// responsible for calling [FocusNode.dispose] on the focus node when it is
/// done with it, but this widget will attach/detach and reparent the node
/// when needed.
/// {@endtemplate}
///
/// A non-null [focusNode] must be supplied if using the
/// [Focus.withExternalFocusNode] constructor is used.
final FocusNode? focusNode;
/// {@template flutter.widgets.Focus.autofocus}
/// True if this widget will be selected as the initial focus when no other
/// node in its scope is currently focused.
///
/// Ideally, there is only one widget with autofocus set in each [FocusScope].
/// If there is more than one widget with autofocus set, then the first one
/// added to the tree will get focus.
///
/// Must not be null. Defaults to false.
/// {@endtemplate}
final bool autofocus;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus. /// focus.
final ValueChanged<bool>? onFocusChange;
/// A handler for keys that are pressed when this object or one of its
/// children has focus.
/// ///
/// Key events are first given to the [FocusNode] that has primary focus, and /// Key events are first given to the [FocusNode] that has primary focus, and
/// if its [onKeyEvent] method returns [KeyEventResult.ignored], then they are /// if its [onKeyEvent] method returns [KeyEventResult.ignored], then they are
...@@ -149,10 +208,11 @@ class Focus extends StatefulWidget { ...@@ -149,10 +208,11 @@ class Focus extends StatefulWidget {
/// keyboards in general. For text input, consider [TextField], /// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these /// [EditableText], or [CupertinoTextField] instead, which do support these
/// things. /// things.
final FocusOnKeyEventCallback? onKeyEvent; FocusOnKeyEventCallback? get onKeyEvent => _onKeyEvent ?? focusNode?.onKeyEvent;
final FocusOnKeyEventCallback? _onKeyEvent;
/// Handler for keys pressed when this object or one of its children has /// A handler for keys that are pressed when this object or one of its
/// focus. /// children has focus.
/// ///
/// This is a legacy API based on [RawKeyEvent] and will be deprecated in the /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
/// future. Prefer [onKeyEvent] instead. /// future. Prefer [onKeyEvent] instead.
...@@ -167,67 +227,8 @@ class Focus extends StatefulWidget { ...@@ -167,67 +227,8 @@ class Focus extends StatefulWidget {
/// keyboards in general. For text input, consider [TextField], /// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these /// [EditableText], or [CupertinoTextField] instead, which do support these
/// things. /// things.
final FocusOnKeyCallback? onKey; FocusOnKeyCallback? get onKey => _onKey ?? focusNode?.onKey;
final FocusOnKeyCallback? _onKey;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus.
final ValueChanged<bool>? onFocusChange;
/// {@template flutter.widgets.Focus.autofocus}
/// True if this widget will be selected as the initial focus when no other
/// node in its scope is currently focused.
///
/// Ideally, there is only one widget with autofocus set in each [FocusScope].
/// If there is more than one widget with autofocus set, then the first one
/// added to the tree will get focus.
///
/// Must not be null. Defaults to false.
/// {@endtemplate}
final bool autofocus;
/// {@template flutter.widgets.Focus.focusNode}
/// An optional focus node to use as the focus node for this widget.
///
/// If one is not supplied, then one will be automatically allocated, owned,
/// and managed by this widget. The widget will be focusable even if a
/// [focusNode] is not supplied. If supplied, the given `focusNode` will be
/// _hosted_ by this widget, but not owned. See [FocusNode] for more
/// information on what being hosted and/or owned implies.
///
/// Supplying a focus node is sometimes useful if an ancestor to this widget
/// wants to control when this widget has the focus. The owner will be
/// responsible for calling [FocusNode.dispose] on the focus node when it is
/// done with it, but this widget will attach/detach and reparent the node
/// when needed.
/// {@endtemplate}
final FocusNode? focusNode;
/// Sets the [FocusNode.skipTraversal] flag on the focus node so that it won't
/// be visited by the [FocusTraversalPolicy].
///
/// This is sometimes useful if a [Focus] widget should receive key events as
/// part of the focus chain, but shouldn't be accessible via focus traversal.
///
/// This is different from [FocusNode.canRequestFocus] because it only implies
/// that the widget can't be reached via traversal, not that it can't be
/// focused. It may still be focused explicitly.
final bool? skipTraversal;
/// {@template flutter.widgets.Focus.includeSemantics}
/// Include semantics information in this widget.
///
/// If true, this widget will include a [Semantics] node that indicates the
/// [SemanticsProperties.focusable] and [SemanticsProperties.focused]
/// properties.
///
/// It is not typical to set this to false, as that can affect the semantics
/// information available to accessibility systems.
///
/// Must not be null, defaults to true.
/// {@endtemplate}
final bool includeSemantics;
/// {@template flutter.widgets.Focus.canRequestFocus} /// {@template flutter.widgets.Focus.canRequestFocus}
/// If true, this widget may request the primary focus. /// If true, this widget may request the primary focus.
...@@ -250,7 +251,20 @@ class Focus extends StatefulWidget { ...@@ -250,7 +251,20 @@ class Focus extends StatefulWidget {
/// * [FocusTraversalPolicy], a class that can be extended to describe a /// * [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy. /// traversal policy.
/// {@endtemplate} /// {@endtemplate}
final bool? canRequestFocus; bool get canRequestFocus => _canRequestFocus ?? focusNode?.canRequestFocus ?? true;
final bool? _canRequestFocus;
/// Sets the [FocusNode.skipTraversal] flag on the focus node so that it won't
/// be visited by the [FocusTraversalPolicy].
///
/// This is sometimes useful if a [Focus] widget should receive key events as
/// part of the focus chain, but shouldn't be accessible via focus traversal.
///
/// This is different from [FocusNode.canRequestFocus] because it only implies
/// that the widget can't be reached via traversal, not that it can't be
/// focused. It may still be focused explicitly.
bool get skipTraversal => _skipTraversal ?? focusNode?.skipTraversal ?? false;
final bool? _skipTraversal;
/// {@template flutter.widgets.Focus.descendantsAreFocusable} /// {@template flutter.widgets.Focus.descendantsAreFocusable}
/// If false, will make this widget's descendants unfocusable. /// If false, will make this widget's descendants unfocusable.
...@@ -274,7 +288,34 @@ class Focus extends StatefulWidget { ...@@ -274,7 +288,34 @@ class Focus extends StatefulWidget {
/// `descendantsAreFocusable` parameter to conditionally block focus for a /// `descendantsAreFocusable` parameter to conditionally block focus for a
/// subtree. /// subtree.
/// {@endtemplate} /// {@endtemplate}
final bool descendantsAreFocusable; bool get descendantsAreFocusable => _descendantsAreFocusable ?? focusNode?.descendantsAreFocusable ?? true;
final bool? _descendantsAreFocusable;
/// {@template flutter.widgets.Focus.includeSemantics}
/// Include semantics information in this widget.
///
/// If true, this widget will include a [Semantics] node that indicates the
/// [SemanticsProperties.focusable] and [SemanticsProperties.focused]
/// properties.
///
/// It is not typical to set this to false, as that can affect the semantics
/// information available to accessibility systems.
///
/// Must not be null, defaults to true.
/// {@endtemplate}
final bool includeSemantics;
/// A debug label for this widget.
///
/// Not used for anything except to be printed in the diagnostic output from
/// [toString] or [toStringDeep].
///
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To
/// print it to the console call [debugDumpFocusTree].
///
/// Defaults to null.
String? get debugLabel => _debugLabel ?? focusNode?.debugLabel;
final String? _debugLabel;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the /// Returns the [focusNode] of the [Focus] that most tightly encloses the
/// given [BuildContext]. /// given [BuildContext].
...@@ -386,12 +427,47 @@ class Focus extends StatefulWidget { ...@@ -386,12 +427,47 @@ class Focus extends StatefulWidget {
State<Focus> createState() => _FocusState(); State<Focus> createState() => _FocusState();
} }
// Implements the behavior differences when the Focus.withExternalFocusNode
// constructor is used.
class _FocusWithExternalFocusNode extends Focus {
const _FocusWithExternalFocusNode({
Key? key,
required Widget child,
required FocusNode focusNode,
bool autofocus = false,
ValueChanged<bool>? onFocusChange,
bool includeSemantics = true,
}) : super(
key: key,
child: child,
focusNode: focusNode,
autofocus: autofocus,
onFocusChange: onFocusChange,
includeSemantics: includeSemantics,
);
@override
bool get _usingExternalFocus => true;
@override
FocusOnKeyEventCallback? get onKeyEvent => focusNode!.onKeyEvent;
@override
FocusOnKeyCallback? get onKey => focusNode!.onKey;
@override
bool get canRequestFocus => focusNode!.canRequestFocus;
@override
bool get skipTraversal => focusNode!.skipTraversal;
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
String? get debugLabel => focusNode!.debugLabel;
}
class _FocusState extends State<Focus> { class _FocusState extends State<Focus> {
FocusNode? _internalNode; FocusNode? _internalNode;
FocusNode get focusNode => widget.focusNode ?? _internalNode!; FocusNode get focusNode => widget.focusNode ?? _internalNode!;
bool? _hasPrimaryFocus; late bool _hadPrimaryFocus;
bool? _canRequestFocus; late bool _couldRequestFocus;
bool? _descendantsAreFocusable; late bool _descendantsWereFocusable;
bool _didAutofocus = false; bool _didAutofocus = false;
FocusAttachment? _focusAttachment; FocusAttachment? _focusAttachment;
...@@ -410,14 +486,14 @@ class _FocusState extends State<Focus> { ...@@ -410,14 +486,14 @@ class _FocusState extends State<Focus> {
} }
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable; focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
if (widget.skipTraversal != null) { if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal!; focusNode.skipTraversal = widget.skipTraversal;
} }
if (widget.canRequestFocus != null) { if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus!; focusNode.canRequestFocus = widget.canRequestFocus;
} }
_canRequestFocus = focusNode.canRequestFocus; _couldRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable; _descendantsWereFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus; _hadPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey); _focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
// Add listener even if the _internalNode existed before, since it should // Add listener even if the _internalNode existed before, since it should
...@@ -429,9 +505,9 @@ class _FocusState extends State<Focus> { ...@@ -429,9 +505,9 @@ class _FocusState extends State<Focus> {
FocusNode _createNode() { FocusNode _createNode() {
return FocusNode( return FocusNode(
debugLabel: widget.debugLabel, debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true, canRequestFocus: widget.canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable, descendantsAreFocusable: widget.descendantsAreFocusable,
skipTraversal: widget.skipTraversal ?? false, skipTraversal: widget.skipTraversal,
); );
} }
...@@ -479,29 +555,34 @@ class _FocusState extends State<Focus> { ...@@ -479,29 +555,34 @@ class _FocusState extends State<Focus> {
void didUpdateWidget(Focus oldWidget) { void didUpdateWidget(Focus oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
assert(() { assert(() {
// Only update the debug label in debug builds, and only if we own the // Only update the debug label in debug builds.
// node. if (oldWidget.focusNode == widget.focusNode &&
if (oldWidget.debugLabel != widget.debugLabel && _internalNode != null) { !widget._usingExternalFocus &&
_internalNode!.debugLabel = widget.debugLabel; oldWidget.debugLabel != widget.debugLabel) {
focusNode.debugLabel = widget.debugLabel;
} }
return true; return true;
}()); }());
if (oldWidget.focusNode == widget.focusNode) { if (oldWidget.focusNode == widget.focusNode) {
if (widget.onKey != focusNode.onKey) { if (!widget._usingExternalFocus) {
focusNode.onKey = widget.onKey; if (widget.onKey != focusNode.onKey) {
} focusNode.onKey = widget.onKey;
if (widget.skipTraversal != null) { }
focusNode.skipTraversal = widget.skipTraversal!; if (widget.onKeyEvent != focusNode.onKeyEvent) {
focusNode.onKeyEvent = widget.onKeyEvent;
}
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal;
}
if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus;
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
} }
if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus!;
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
} else { } else {
_focusAttachment!.detach(); _focusAttachment!.detach();
oldWidget.focusNode?.removeListener(_handleFocusChanged); oldWidget.focusNode?.removeListener(_handleFocusChanged);
_internalNode?.removeListener(_handleAutofocus);
_initNode(); _initNode();
} }
...@@ -515,19 +596,21 @@ class _FocusState extends State<Focus> { ...@@ -515,19 +596,21 @@ class _FocusState extends State<Focus> {
final bool canRequestFocus = focusNode.canRequestFocus; final bool canRequestFocus = focusNode.canRequestFocus;
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable; final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
widget.onFocusChange?.call(focusNode.hasFocus); widget.onFocusChange?.call(focusNode.hasFocus);
if (_hasPrimaryFocus != hasPrimaryFocus) { // Check the cached states that matter here, and call setState if they have
// changed.
if (_hadPrimaryFocus != hasPrimaryFocus) {
setState(() { setState(() {
_hasPrimaryFocus = hasPrimaryFocus; _hadPrimaryFocus = hasPrimaryFocus;
}); });
} }
if (_canRequestFocus != canRequestFocus) { if (_couldRequestFocus != canRequestFocus) {
setState(() { setState(() {
_canRequestFocus = canRequestFocus; _couldRequestFocus = canRequestFocus;
}); });
} }
if (_descendantsAreFocusable != descendantsAreFocusable) { if (_descendantsWereFocusable != descendantsAreFocusable) {
setState(() { setState(() {
_descendantsAreFocusable = descendantsAreFocusable; _descendantsWereFocusable = descendantsAreFocusable;
}); });
} }
} }
...@@ -538,8 +621,8 @@ class _FocusState extends State<Focus> { ...@@ -538,8 +621,8 @@ class _FocusState extends State<Focus> {
Widget child = widget.child; Widget child = widget.child;
if (widget.includeSemantics) { if (widget.includeSemantics) {
child = Semantics( child = Semantics(
focusable: _canRequestFocus, focusable: _couldRequestFocus,
focused: _hasPrimaryFocus, focused: _hadPrimaryFocus,
child: widget.child, child: widget.child,
); );
} }
...@@ -642,6 +725,17 @@ class FocusScope extends Focus { ...@@ -642,6 +725,17 @@ class FocusScope extends Focus {
debugLabel: debugLabel, debugLabel: debugLabel,
); );
/// Creates a FocusScope widget that uses the given [focusScopeNode] as the
/// source of truth for attributes on the node, rather than the attributes of
/// this widget.
const factory FocusScope.withExternalFocusNode({
Key? key,
required Widget child,
required FocusScopeNode focusScopeNode,
bool autofocus,
ValueChanged<bool>? onFocusChange,
}) = _FocusScopeWithExternalFocusNode;
/// Returns the [FocusScopeNode] of the [FocusScope] that most tightly /// Returns the [FocusScopeNode] of the [FocusScope] that most tightly
/// encloses the given [context]. /// encloses the given [context].
/// ///
...@@ -659,13 +753,47 @@ class FocusScope extends Focus { ...@@ -659,13 +753,47 @@ class FocusScope extends Focus {
State<Focus> createState() => _FocusScopeState(); State<Focus> createState() => _FocusScopeState();
} }
// Implements the behavior differences when the FocusScope.withExternalFocusNode
// constructor is used.
class _FocusScopeWithExternalFocusNode extends FocusScope {
const _FocusScopeWithExternalFocusNode({
Key? key,
required Widget child,
required FocusScopeNode focusScopeNode,
bool autofocus = false,
ValueChanged<bool>? onFocusChange,
}) : super(
key: key,
child: child,
node: focusScopeNode,
autofocus: autofocus,
onFocusChange: onFocusChange,
);
@override
bool get _usingExternalFocus => true;
@override
FocusOnKeyEventCallback? get onKeyEvent => focusNode!.onKeyEvent;
@override
FocusOnKeyCallback? get onKey => focusNode!.onKey;
@override
bool get canRequestFocus => focusNode!.canRequestFocus;
@override
bool get skipTraversal => focusNode!.skipTraversal;
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
String? get debugLabel => focusNode!.debugLabel;
}
class _FocusScopeState extends _FocusState { class _FocusScopeState extends _FocusState {
@override @override
FocusScopeNode _createNode() { FocusScopeNode _createNode() {
return FocusScopeNode( return FocusScopeNode(
debugLabel: widget.debugLabel, debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true, canRequestFocus: widget.canRequestFocus,
skipTraversal: widget.skipTraversal ?? false, skipTraversal: widget.skipTraversal,
); );
} }
......
...@@ -1079,6 +1079,69 @@ void main() { ...@@ -1079,6 +1079,69 @@ void main() {
expect(focusNodeB.hasPrimaryFocus, isFalse); expect(focusNodeB.hasPrimaryFocus, isFalse);
expect(focusNodeA.hasPrimaryFocus, isTrue); expect(focusNodeA.hasPrimaryFocus, isTrue);
}); });
testWidgets("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusScopeNode focusScopeNode = FocusScopeNode();
bool? keyEventHandled;
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
keyEventHandled = true;
return KeyEventResult.handled;
}
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
keyEventHandled = true;
return KeyEventResult.handled;
}
KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored;
focusScopeNode.onKey = ignoreCallback;
focusScopeNode.onKeyEvent = ignoreEventCallback;
focusScopeNode.descendantsAreFocusable = false;
focusScopeNode.skipTraversal = false;
focusScopeNode.canRequestFocus = true;
FocusScope focusScopeWidget = FocusScope.withExternalFocusNode(
focusScopeNode: focusScopeNode,
child: Container(key: key1),
);
await tester.pumpWidget(focusScopeWidget);
expect(focusScopeNode.onKey, equals(ignoreCallback));
expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isFalse);
expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
FocusScope.of(key1.currentContext!).requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isNull);
focusScopeNode.onKey = handleCallback;
focusScopeNode.onKeyEvent = handleEventCallback;
focusScopeNode.descendantsAreFocusable = true;
focusScopeWidget = FocusScope.withExternalFocusNode(
focusScopeNode: focusScopeNode,
child: Container(key: key1),
);
await tester.pumpWidget(focusScopeWidget);
expect(focusScopeNode.onKey, equals(handleCallback));
expect(focusScopeNode.onKeyEvent, equals(handleEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isTrue);
expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
}); });
group('Focus', () { group('Focus', () {
...@@ -1598,29 +1661,164 @@ void main() { ...@@ -1598,29 +1661,164 @@ void main() {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
bool? keyEventHandled; bool? keyEventHandled;
await tester.pumpWidget( // ignore: prefer_function_declarations_over_variables
Focus( final FocusOnKeyCallback handleCallback = (FocusNode node, RawKeyEvent event) {
onKey: (_, __) => KeyEventResult.ignored, // This one does nothing. keyEventHandled = true;
focusNode: focusNode, return KeyEventResult.handled;
child: Container(key: key1), };
), // ignore: prefer_function_declarations_over_variables
final FocusOnKeyCallback ignoreCallback = (FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
Focus focusWidget = Focus(
onKey: ignoreCallback, // This one does nothing.
focusNode: focusNode,
skipTraversal: true,
canRequestFocus: true,
child: Container(key: key1),
); );
focusNode.onKeyEvent = null;
await tester.pumpWidget(focusWidget);
expect(focusNode.onKey, equals(ignoreCallback));
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
Focus.of(key1.currentContext!).requestFocus(); Focus.of(key1.currentContext!).requestFocus();
await tester.pump(); await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isNull); expect(keyEventHandled, isNull);
await tester.pumpWidget( focusWidget = Focus(
Focus( onKey: handleCallback,
onKey: (FocusNode node, RawKeyEvent event) { // The updated handler handles the key. focusNode: focusNode,
keyEventHandled = true; skipTraversal: true,
return KeyEventResult.handled; canRequestFocus: true,
}, child: Container(key: key1),
focusNode: focusNode, );
child: Container(key: key1), await tester.pumpWidget(focusWidget);
), expect(focusNode.onKey, equals(handleCallback));
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
bool? keyEventHandled;
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyEventCallback handleEventCallback = (FocusNode node, KeyEvent event) {
keyEventHandled = true;
return KeyEventResult.handled;
};
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyEventCallback ignoreEventCallback = (FocusNode node, KeyEvent event) => KeyEventResult.ignored;
Focus focusWidget = Focus(
onKeyEvent: ignoreEventCallback, // This one does nothing.
focusNode: focusNode,
skipTraversal: true,
canRequestFocus: true,
child: Container(key: key1),
);
focusNode.onKeyEvent = null;
await tester.pumpWidget(focusWidget);
expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
Focus.of(key1.currentContext!).requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isNull);
focusWidget = Focus(
onKeyEvent: handleEventCallback,
focusNode: focusNode,
skipTraversal: true,
canRequestFocus: true,
child: Container(key: key1),
);
await tester.pumpWidget(focusWidget);
expect(focusNode.onKeyEvent, equals(handleEventCallback));
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
bool? keyEventHandled;
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyCallback handleCallback = (FocusNode node, RawKeyEvent event) {
keyEventHandled = true;
return KeyEventResult.handled;
};
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyEventCallback handleEventCallback = (FocusNode node, KeyEvent event) {
keyEventHandled = true;
return KeyEventResult.handled;
};
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyCallback ignoreCallback = (FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
// ignore: prefer_function_declarations_over_variables
final FocusOnKeyEventCallback ignoreEventCallback = (FocusNode node, KeyEvent event) => KeyEventResult.ignored;
focusNode.onKey = ignoreCallback;
focusNode.onKeyEvent = ignoreEventCallback;
focusNode.descendantsAreFocusable = false;
focusNode.skipTraversal = false;
focusNode.canRequestFocus = true;
Focus focusWidget = Focus.withExternalFocusNode(
focusNode: focusNode,
child: Container(key: key1),
);
await tester.pumpWidget(focusWidget);
expect(focusNode.onKey, equals(ignoreCallback));
expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusNode.descendantsAreFocusable, isFalse);
expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
Focus.of(key1.currentContext!).requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isNull);
focusNode.onKey = handleCallback;
focusNode.onKeyEvent = handleEventCallback;
focusNode.descendantsAreFocusable = true;
focusWidget = Focus.withExternalFocusNode(
focusNode: focusNode,
child: Container(key: key1),
); );
await tester.pumpWidget(focusWidget);
expect(focusNode.onKey, equals(handleCallback));
expect(focusNode.onKeyEvent, equals(handleEventCallback));
expect(focusNode.descendantsAreFocusable, isTrue);
expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue); expect(keyEventHandled, isTrue);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment