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';
///
/// When the focus is gained or lost, [onFocusChange] is called.
///
/// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for
/// this widget's [focusNode], unless a focused descendant's [onKey] callback
/// returns [KeyEventResult.handled] when called.
/// For keyboard events, [onKey] and [onKeyEvent] are called if
/// [FocusNode.hasFocus] is true for this widget's [focusNode], unless a focused
/// descendant's [onKey] or [onKeyEvent] callback returned
/// [KeyEventResult.handled] when called.
///
/// This widget does not provide any visual indication that the focus has
/// changed. Any desired visual changes should be made when [onFocusChange] is
......@@ -27,9 +28,9 @@ import 'inherited_notifier.dart';
/// changes, use the [Focus.of] and [FocusScope.of] static methods.
///
/// To access the focused state of the nearest [Focus] widget, use
/// [FocusNode.hasFocus] from a build method, which also establishes a relationship
/// between the calling widget and the [Focus] widget that will rebuild the
/// calling widget when the focus changes.
/// [FocusNode.hasFocus] from a build method, which also establishes a
/// relationship between the calling widget and the [Focus] widget that will
/// rebuild the calling widget when the focus 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
......@@ -38,6 +39,18 @@ import 'inherited_notifier.dart';
/// management entails if you are not using a [Focus] widget and you need to do
/// 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
/// 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
......@@ -106,38 +119,84 @@ class Focus extends StatefulWidget {
this.focusNode,
this.autofocus = false,
this.onFocusChange,
this.onKey,
this.onKeyEvent,
this.debugLabel,
this.canRequestFocus,
this.descendantsAreFocusable = true,
this.skipTraversal,
FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey,
bool? canRequestFocus,
bool? skipTraversal,
bool? descendantsAreFocusable,
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(descendantsAreFocusable != null),
assert(includeSemantics != null),
super(key: key);
/// A debug label for this widget.
///
/// Not used for anything except to be printed in the diagnostic output from
/// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
/// since that node can have its own [FocusNode.debugLabel].
///
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To
/// print it to the console call [debugDumpFocusTree].
///
/// Defaults to null.
final String? debugLabel;
/// 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.
const factory Focus.withExternalFocusNode({
Key? key,
required Widget child,
required FocusNode focusNode,
bool autofocus,
ValueChanged<bool>? onFocusChange,
bool includeSemantics,
}) = _FocusWithExternalFocusNode;
// 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].
///
/// {@macro flutter.widgets.ProxyWidget.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.
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
/// if its [onKeyEvent] method returns [KeyEventResult.ignored], then they are
......@@ -149,10 +208,11 @@ class Focus extends StatefulWidget {
/// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these
/// 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
/// focus.
/// A handler for keys that are pressed when this object or one of its
/// children has focus.
///
/// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
/// future. Prefer [onKeyEvent] instead.
......@@ -167,67 +227,8 @@ class Focus extends StatefulWidget {
/// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these
/// things.
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;
FocusOnKeyCallback? get onKey => _onKey ?? focusNode?.onKey;
final FocusOnKeyCallback? _onKey;
/// {@template flutter.widgets.Focus.canRequestFocus}
/// If true, this widget may request the primary focus.
......@@ -250,7 +251,20 @@ class Focus extends StatefulWidget {
/// * [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy.
/// {@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}
/// If false, will make this widget's descendants unfocusable.
......@@ -274,7 +288,34 @@ class Focus extends StatefulWidget {
/// `descendantsAreFocusable` parameter to conditionally block focus for a
/// subtree.
/// {@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
/// given [BuildContext].
......@@ -386,12 +427,47 @@ class Focus extends StatefulWidget {
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> {
FocusNode? _internalNode;
FocusNode get focusNode => widget.focusNode ?? _internalNode!;
bool? _hasPrimaryFocus;
bool? _canRequestFocus;
bool? _descendantsAreFocusable;
late bool _hadPrimaryFocus;
late bool _couldRequestFocus;
late bool _descendantsWereFocusable;
bool _didAutofocus = false;
FocusAttachment? _focusAttachment;
......@@ -410,14 +486,14 @@ class _FocusState extends State<Focus> {
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal!;
focusNode.skipTraversal = widget.skipTraversal;
}
if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus!;
focusNode.canRequestFocus = widget.canRequestFocus;
}
_canRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
_couldRequestFocus = focusNode.canRequestFocus;
_descendantsWereFocusable = focusNode.descendantsAreFocusable;
_hadPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
// Add listener even if the _internalNode existed before, since it should
......@@ -429,9 +505,9 @@ class _FocusState extends State<Focus> {
FocusNode _createNode() {
return FocusNode(
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true,
canRequestFocus: widget.canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
skipTraversal: widget.skipTraversal ?? false,
skipTraversal: widget.skipTraversal,
);
}
......@@ -479,29 +555,34 @@ class _FocusState extends State<Focus> {
void didUpdateWidget(Focus oldWidget) {
super.didUpdateWidget(oldWidget);
assert(() {
// Only update the debug label in debug builds, and only if we own the
// node.
if (oldWidget.debugLabel != widget.debugLabel && _internalNode != null) {
_internalNode!.debugLabel = widget.debugLabel;
// Only update the debug label in debug builds.
if (oldWidget.focusNode == widget.focusNode &&
!widget._usingExternalFocus &&
oldWidget.debugLabel != widget.debugLabel) {
focusNode.debugLabel = widget.debugLabel;
}
return true;
}());
if (oldWidget.focusNode == widget.focusNode) {
if (!widget._usingExternalFocus) {
if (widget.onKey != focusNode.onKey) {
focusNode.onKey = widget.onKey;
}
if (widget.onKeyEvent != focusNode.onKeyEvent) {
focusNode.onKeyEvent = widget.onKeyEvent;
}
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal!;
focusNode.skipTraversal = widget.skipTraversal;
}
if (widget.canRequestFocus != null) {
focusNode.canRequestFocus = widget.canRequestFocus!;
focusNode.canRequestFocus = widget.canRequestFocus;
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
}
} else {
_focusAttachment!.detach();
oldWidget.focusNode?.removeListener(_handleFocusChanged);
_internalNode?.removeListener(_handleAutofocus);
_initNode();
}
......@@ -515,19 +596,21 @@ class _FocusState extends State<Focus> {
final bool canRequestFocus = focusNode.canRequestFocus;
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
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(() {
_hasPrimaryFocus = hasPrimaryFocus;
_hadPrimaryFocus = hasPrimaryFocus;
});
}
if (_canRequestFocus != canRequestFocus) {
if (_couldRequestFocus != canRequestFocus) {
setState(() {
_canRequestFocus = canRequestFocus;
_couldRequestFocus = canRequestFocus;
});
}
if (_descendantsAreFocusable != descendantsAreFocusable) {
if (_descendantsWereFocusable != descendantsAreFocusable) {
setState(() {
_descendantsAreFocusable = descendantsAreFocusable;
_descendantsWereFocusable = descendantsAreFocusable;
});
}
}
......@@ -538,8 +621,8 @@ class _FocusState extends State<Focus> {
Widget child = widget.child;
if (widget.includeSemantics) {
child = Semantics(
focusable: _canRequestFocus,
focused: _hasPrimaryFocus,
focusable: _couldRequestFocus,
focused: _hadPrimaryFocus,
child: widget.child,
);
}
......@@ -642,6 +725,17 @@ class FocusScope extends Focus {
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
/// encloses the given [context].
///
......@@ -659,13 +753,47 @@ class FocusScope extends Focus {
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 {
@override
FocusScopeNode _createNode() {
return FocusScopeNode(
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true,
skipTraversal: widget.skipTraversal ?? false,
canRequestFocus: widget.canRequestFocus,
skipTraversal: widget.skipTraversal,
);
}
......
......@@ -1079,6 +1079,69 @@ void main() {
expect(focusNodeB.hasPrimaryFocus, isFalse);
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', () {
......@@ -1598,29 +1661,164 @@ void main() {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
bool? keyEventHandled;
await tester.pumpWidget(
Focus(
onKey: (_, __) => KeyEventResult.ignored, // This one does nothing.
// 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 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();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isNull);
await tester.pumpWidget(
Focus(
onKey: (FocusNode node, RawKeyEvent event) { // The updated handler handles the key.
focusWidget = Focus(
onKey: handleCallback,
focusNode: focusNode,
skipTraversal: true,
canRequestFocus: true,
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);
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