Unverified Commit 6e10719d authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

FocusableActionDetector widget (#44867)

This adds a FocusableActionDetector, a widget which combines the functionality of Actions, Shortcuts, MouseRegion and a Focus widget to create a detector that defines actions and key bindings, and will notify that the focus or hover highlights should be shown or not. This widget can be used to give a control the required detection modes for focus and hover handling on desktop and web platforms.

I replaced a bunch of similar code in many of our widgets with this widget, and found that pretty much any control that wants to be focusable wants all of these features as well: focus highlights, hover highlights, and actions to activate it.

Also eliminated an extra _hasFocus variable in FocusState that wasn't being used.
parent cce445e2
......@@ -159,7 +159,6 @@ class Checkbox extends StatefulWidget {
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
bool _showHighlight = false;
@override
void initState() {
......@@ -168,8 +167,6 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_updateHighlightMode(FocusManager.instance.highlightMode);
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
}
void _actionHandler(FocusNode node, Intent intent){
......@@ -197,28 +194,20 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
);
}
void _updateHighlightMode(FocusHighlightMode mode) {
switch (FocusManager.instance.highlightMode) {
case FocusHighlightMode.touch:
_showHighlight = false;
break;
case FocusHighlightMode.traditional:
_showHighlight = true;
break;
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
setState(() { _updateHighlightMode(mode); });
}
bool hovering = false;
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
......@@ -233,35 +222,30 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
break;
}
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return MouseRegion(
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: enabled,
debugLabel: '${describeIdentity(widget)}(${widget.value})',
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
value: widget.value,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor,
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
hovering: enabled && _showHighlight && hovering,
);
},
),
),
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
value: widget.value,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor,
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: _focused,
hovering: _hovering,
);
},
),
);
}
......
......@@ -187,7 +187,6 @@ class Radio<T> extends StatefulWidget {
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
bool _showHighlight = false;
@override
void initState() {
......@@ -196,8 +195,6 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_updateHighlightMode(FocusManager.instance.highlightMode);
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
}
void _actionHandler(FocusNode node, Intent intent){
......@@ -215,28 +212,20 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
);
}
void _updateHighlightMode(FocusHighlightMode mode) {
switch (FocusManager.instance.highlightMode) {
case FocusHighlightMode.touch:
_showHighlight = false;
break;
case FocusHighlightMode.traditional:
_showHighlight = true;
break;
bool _focused = false;
void _handleHighlightChanged(bool focused) {
if (_focused != focused) {
setState(() { _focused = focused; });
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (_hovering != hovering) {
setState(() { _hovering = hovering; });
}
setState(() { _updateHighlightMode(mode); });
}
bool hovering = false;
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
Color _getInactiveColor(ThemeData themeData) {
return enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
}
......@@ -260,33 +249,28 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
break;
}
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return MouseRegion(
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: enabled,
debugLabel: '${describeIdentity(widget)}(${widget.value})',
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: enabled ? _handleChanged : null,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
hovering: enabled && _showHighlight && hovering,
);
},
),
),
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
enabled: enabled,
onShowFocusHighlight: _handleHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: enabled ? _handleChanged : null,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: _focused,
hovering: _hovering,
);
},
),
);
}
......
......@@ -214,7 +214,6 @@ class Switch extends StatefulWidget {
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
Map<LocalKey, ActionFactory> _actionMap;
bool _showHighlight = false;
@override
void initState() {
......@@ -223,8 +222,6 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_updateHighlightMode(FocusManager.instance.highlightMode);
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
}
void _actionHandler(FocusNode node, Intent intent){
......@@ -242,22 +239,18 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
);
}
void _updateHighlightMode(FocusHighlightMode mode) {
switch (FocusManager.instance.highlightMode) {
case FocusHighlightMode.touch:
_showHighlight = false;
break;
case FocusHighlightMode.traditional:
_showHighlight = true;
break;
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
setState(() { _updateHighlightMode(mode); });
}
Size getSwitchSize(ThemeData theme) {
......@@ -275,10 +268,6 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
bool hovering = false;
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
Widget buildMaterialSwitch(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
......@@ -300,40 +289,34 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
}
return MouseRegion(
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: enabled,
debugLabel: '${describeIdentity(widget)}({$widget.value})',
child: Builder(
builder: (BuildContext context) {
final bool hasFocus = Focus.of(context).hasFocus;
return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior,
value: widget.value,
activeColor: activeThumbColor,
inactiveColor: inactiveThumbColor,
hoverColor: hoverColor,
focusColor: focusColor,
activeThumbImage: widget.activeThumbImage,
inactiveThumbImage: widget.inactiveThumbImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: enabled && _showHighlight && hasFocus,
hovering: enabled && _showHighlight && hovering,
vsync: this,
);
},
),
),
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior,
value: widget.value,
activeColor: activeThumbColor,
inactiveColor: inactiveThumbColor,
hoverColor: hoverColor,
focusColor: focusColor,
activeThumbImage: widget.activeThumbImage,
inactiveThumbImage: widget.inactiveThumbImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: _focused,
hovering: _hovering,
vsync: this,
);
},
),
);
}
......
......@@ -474,7 +474,9 @@ abstract class RenderToggleable extends RenderConstrainedBox {
final double reactionRadius = hasFocus || hovering
? kRadialReactionRadius
: _kRadialReactionRadiusTween.evaluate(_reaction);
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
if (reactionRadius > 0.0) {
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
}
}
}
......
......@@ -325,7 +325,6 @@ class Focus extends StatefulWidget {
class _FocusState extends State<Focus> {
FocusNode _internalNode;
FocusNode get focusNode => widget.focusNode ?? _internalNode;
bool _hasFocus;
bool _hasPrimaryFocus;
bool _canRequestFocus;
bool _didAutofocus = false;
......@@ -347,7 +346,6 @@ class _FocusState extends State<Focus> {
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
_hasFocus = focusNode.hasFocus;
_canRequestFocus = focusNode.canRequestFocus;
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
......@@ -425,22 +423,19 @@ class _FocusState extends State<Focus> {
}
void _handleFocusChanged() {
if (_hasFocus != focusNode.hasFocus) {
setState(() {
_hasFocus = focusNode.hasFocus;
});
if (widget.onFocusChange != null) {
widget.onFocusChange(focusNode.hasFocus);
}
final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
final bool canRequestFocus = focusNode.canRequestFocus;
if (widget.onFocusChange != null) {
widget.onFocusChange(focusNode.hasFocus);
}
if (_hasPrimaryFocus != focusNode.hasPrimaryFocus) {
if (_hasPrimaryFocus != hasPrimaryFocus) {
setState(() {
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
_hasPrimaryFocus = hasPrimaryFocus;
});
}
if (_canRequestFocus != focusNode.canRequestFocus) {
if (_canRequestFocus != canRequestFocus) {
setState(() {
_canRequestFocus = focusNode.canRequestFocus;
_canRequestFocus = canRequestFocus;
});
}
}
......
......@@ -254,11 +254,13 @@ class Shortcuts extends StatefulWidget {
/// [shortcuts] change materially.
final ShortcutManager manager;
/// The map of shortcuts that the [manager] will be given to manage.
/// {@template flutter.widgets.shortcuts.shortcuts}
/// The map of shortcuts that the [ShortcutManager] will be given to manage.
///
/// For performance reasons, it is recommended that a pre-built map is passed
/// in here (e.g. a final variable from your widget class) instead of defining
/// it inline in the build function.
/// {@endtemplate}
final Map<LogicalKeySet, Intent> shortcuts;
/// The child widget for this [Shortcuts] widget.
......
......@@ -68,7 +68,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isEnabled: true,
......@@ -83,7 +83,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
......@@ -99,10 +99,9 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isFocusable: true,
));
await tester.pumpWidget(const Material(
......@@ -112,7 +111,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
......@@ -134,7 +133,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
label: 'foo',
textDirection: TextDirection.ltr,
hasCheckedState: true,
......
......@@ -180,12 +180,11 @@ void main() {
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
id: 2,
flags: <SemanticsFlag>[
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isFocusable,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
),
],
......@@ -202,12 +201,12 @@ void main() {
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
id: 2,
flags: <SemanticsFlag>[
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isChecked,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
),
],
......@@ -396,6 +395,7 @@ void main() {
}
await tester.pumpWidget(buildApp());
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
......@@ -415,6 +415,7 @@ void main() {
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp());
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
......@@ -429,6 +430,7 @@ void main() {
// Check when the radio is selected, but disabled.
groupValue = 0;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -38,7 +40,7 @@ class TestDispatcher1 extends TestDispatcher {
}
void main() {
test('$Action passes parameters on when invoked.', () {
test('Action passes parameters on when invoked.', () {
bool invoked = false;
FocusNode passedNode;
final TestAction action = TestAction(onInvoke: (FocusNode node, Intent invocation) {
......@@ -52,7 +54,7 @@ void main() {
expect(invoked, isTrue);
});
group(ActionDispatcher, () {
test('$ActionDispatcher invokes actions when asked.', () {
test('ActionDispatcher invokes actions when asked.', () {
bool invoked = false;
FocusNode passedNode;
const ActionDispatcher dispatcher = ActionDispatcher();
......@@ -94,7 +96,7 @@ void main() {
setUp(clear);
testWidgets('$Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
FocusNode passedNode;
......@@ -124,7 +126,7 @@ void main() {
expect(result, isTrue);
expect(invoked, isTrue);
});
testWidgets('$Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const Intent intent = Intent(TestAction.key);
......@@ -159,7 +161,7 @@ void main() {
expect(invoked, isTrue);
expect(invokedIntent, equals(intent));
});
testWidgets('$Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const Intent intent = Intent(TestAction.key);
......@@ -200,7 +202,7 @@ void main() {
expect(invokedAction, equals(testAction));
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
});
testWidgets("$Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const Intent intent = Intent(TestAction.key);
......@@ -240,7 +242,7 @@ void main() {
expect(invokedAction, equals(testAction));
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
});
testWidgets('$Actions widget can be found with of', (WidgetTester tester) async {
testWidgets('Actions widget can be found with of', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
......@@ -259,9 +261,77 @@ void main() {
);
expect(dispatcher, equals(testDispatcher));
});
testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const Intent intent = Intent(TestAction.key);
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
final Action testAction = TestAction(
onInvoke: (FocusNode node, Intent invocation) {
invoked = true;
},
);
bool hovering = false;
bool focusing = false;
Future<void> buildTest(bool enabled) async {
await tester.pumpWidget(
Center(
child: Actions(
dispatcher: TestDispatcher1(postInvoke: collect),
actions: const <LocalKey, ActionFactory>{},
child: FocusableActionDetector(
enabled: enabled,
focusNode: focusNode,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): intent,
},
actions: <LocalKey, ActionFactory>{
TestAction.key: () => testAction,
},
onShowHoverHighlight: (bool value) => hovering = value,
onShowFocusHighlight: (bool value) => focusing = value,
child: Container(width: 100, height: 100, key: containerKey),
),
),
),
);
return tester.pump();
}
await buildTest(true);
focusNode.requestFocus();
await tester.pump();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(hovering, isTrue);
expect(focusing, isTrue);
expect(invoked, isTrue);
invoked = false;
await buildTest(false);
expect(hovering, isFalse);
expect(focusing, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(invoked, isFalse);
await buildTest(true);
expect(focusing, isFalse);
expect(hovering, isTrue);
await buildTest(false);
expect(focusing, isFalse);
expect(hovering, isFalse);
await gesture.moveTo(Offset.zero);
await buildTest(true);
expect(hovering, isFalse);
expect(focusing, isFalse);
});
});
group('Diagnostics', () {
testWidgets('default $Intent debugFillProperties', (WidgetTester tester) async {
testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const Intent(ValueKey<String>('foo')).debugFillProperties(builder);
......@@ -275,7 +345,7 @@ void main() {
expect(description, equals(<String>['key: [<\'foo\'>]']));
});
testWidgets('$CallbackAction debugFillProperties', (WidgetTester tester) async {
testWidgets('CallbackAction debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
CallbackAction(
......@@ -292,7 +362,7 @@ void main() {
expect(description, equals(<String>['intentKey: [<\'foo\'>]']));
});
testWidgets('default $Actions debugFillProperties', (WidgetTester tester) async {
testWidgets('default Actions debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
Actions(
......@@ -311,7 +381,7 @@ void main() {
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
expect(description[1], equals('actions: {}'));
});
testWidgets('$Actions implements debugFillProperties', (WidgetTester tester) async {
testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
Actions(
......
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