diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 2872ea4657..96cdea718c 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -1367,35 +1367,6 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { /// the [WidgetsBinding] instance. static FocusManager get instance => WidgetsBinding.instance.focusManager; - bool get _lastInteractionWasTouch { - // Assume that if we're on one of these mobile platforms, or if there's no - // mouse connected, that the initial interaction will be touch-based, and - // that it's traditional mouse and keyboard on all others. - // - // This only affects the initial value: the ongoing value is updated to a - // known correct value as soon as any pointer events are received. - if (_lastInteractionWasTouchValue == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - _lastInteractionWasTouchValue = !WidgetsBinding.instance.mouseTracker.mouseIsConnected; - break; - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - _lastInteractionWasTouchValue = false; - break; - } - } - return _lastInteractionWasTouchValue; - } - bool _lastInteractionWasTouchValue; - set _lastInteractionWasTouch(bool value) { - _lastInteractionWasTouchValue = value; - } - - /// Sets the strategy by which [highlightMode] is determined. /// /// If set to [FocusHighlightStrategy.automatic], then the highlight mode will @@ -1427,6 +1398,30 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { _updateHighlightMode(); } + static FocusHighlightMode get _defaultModeForPlatform { + // Assume that if we're on one of the mobile platforms, and there's no mouse + // connected, that the initial interaction will be touch-based, and that + // it's traditional mouse and keyboard on all other platforms. + // + // This only affects the initial value: the ongoing value is updated to a + // known correct value as soon as any pointer/keyboard events are received. + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) { + return FocusHighlightMode.traditional; + } + return FocusHighlightMode.touch; + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + return FocusHighlightMode.traditional; + } + assert(false, 'Unhandled target platform $defaultTargetPlatform'); + return null; + } + /// Indicates the current interaction mode for focus highlights. /// /// The value returned depends upon the [highlightStrategy] used, and possibly @@ -1438,8 +1433,14 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { /// /// If [highlightMode] returns [FocusHighlightMode.traditional], then widgets should /// draw their focus highlight whenever they are focused. - FocusHighlightMode get highlightMode => _highlightMode; - FocusHighlightMode _highlightMode = FocusHighlightMode.touch; + // Don't want to set _highlightMode here, since it's possible for the target + // platform to change (especially in tests). + FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform; + FocusHighlightMode _highlightMode; + + // If set, indicates if the last interaction detected was touch or not. + // If null, no interactions have occurred yet. + bool _lastInteractionWasTouch; // Update function to be called whenever the state relating to highlightMode // changes. @@ -1447,6 +1448,13 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { FocusHighlightMode newMode; switch (highlightStrategy) { case FocusHighlightStrategy.automatic: + if (_lastInteractionWasTouch == null) { + // If we don't have any information about the last interaction yet, + // then just rely on the default value for the platform, which will be + // determined based on the target platform if _highlightMode is not + // set. + return; + } if (_lastInteractionWasTouch) { newMode = FocusHighlightMode.touch; } else { @@ -1460,8 +1468,12 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { newMode = FocusHighlightMode.traditional; break; } - if (newMode != _highlightMode) { - _highlightMode = newMode; + // We can't just compare newMode with _highlightMode here, since + // _highlightMode could be null, so we want to compare with the return value + // for the getter, since that's what clients will be looking at. + final FocusHighlightMode oldMode = highlightMode; + _highlightMode = newMode; + if (highlightMode != oldMode) { _notifyHighlightModeListeners(); } } @@ -1485,7 +1497,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { for (final ValueChanged listener in localListeners) { try { if (_listeners.contains(listener)) { - listener(_highlightMode); + listener(highlightMode); } } catch (exception, stack) { InformationCollector collector; @@ -1517,20 +1529,21 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope'); void _handlePointerEvent(PointerEvent event) { - bool currentInteractionIsTouch; + FocusHighlightMode expectedMode; switch (event.kind) { case PointerDeviceKind.touch: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: - currentInteractionIsTouch = true; + _lastInteractionWasTouch = true; + expectedMode = FocusHighlightMode.touch; break; case PointerDeviceKind.mouse: case PointerDeviceKind.unknown: - currentInteractionIsTouch = false; + _lastInteractionWasTouch = false; + expectedMode = FocusHighlightMode.traditional; break; } - if (_lastInteractionWasTouch != currentInteractionIsTouch) { - _lastInteractionWasTouch = currentInteractionIsTouch; + if (expectedMode != highlightMode) { _updateHighlightMode(); } } @@ -1538,10 +1551,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { void _handleRawKeyEvent(RawKeyEvent event) { // Update highlightMode first, since things responding to the keys might // look at the highlight mode, and it should be accurate. - if (_lastInteractionWasTouch) { - _lastInteractionWasTouch = false; - _updateHighlightMode(); - } + _lastInteractionWasTouch = false; + _updateHighlightMode(); assert(_focusDebug('Received key event ${event.logicalKey}')); // Walk the current focus from the leaf to the root, calling each one's diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart index 7a8fd61b15..824637bdf3 100644 --- a/packages/flutter/test/widgets/focus_manager_test.dart +++ b/packages/flutter/test/widgets/focus_manager_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -843,6 +844,26 @@ void main() { break; } }, variant: TargetPlatformVariant.all()); + testWidgets('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async { + expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch)); + RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); + addTearDown(gesture.removePointer); + await gesture.moveTo(Offset.zero); + expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); + }, variant: TargetPlatformVariant.mobile()); + testWidgets('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async { + expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); + RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); + addTearDown(gesture.removePointer); + await gesture.moveTo(Offset.zero); + expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); + }, variant: TargetPlatformVariant.desktop()); + testWidgets('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async { + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); + }, variant: TargetPlatformVariant.all()); testWidgets('Events change focus highlight mode.', (WidgetTester tester) async { await setupWidget(tester); int callCount = 0; diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 4cd2a38ef4..aa74d833c5 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -225,6 +225,22 @@ class TargetPlatformVariant extends TestVariant { /// the [TargetPlatform] enum. TargetPlatformVariant.all() : values = TargetPlatform.values.toSet(); + /// Creates a [TargetPlatformVariant] that includes platforms that are + /// considered desktop platforms. + TargetPlatformVariant.desktop() : values = { + TargetPlatform.linux, + TargetPlatform.macOS, + TargetPlatform.windows, + }; + + /// Creates a [TargetPlatformVariant] that includes platforms that are + /// considered mobile platforms. + TargetPlatformVariant.mobile() : values = { + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.fuchsia, + }; + /// Creates a [TargetPlatformVariant] that tests only the given value of /// [TargetPlatform]. TargetPlatformVariant.only(TargetPlatform platform) : values = {platform}; diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 2d06a491b0..ec92c4fe03 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -743,6 +743,13 @@ void main() { numberOfVariationsRun += 1; } }, variant: TargetPlatformVariant.all()); + + testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values', (WidgetTester tester) async { + final TargetPlatformVariant all = TargetPlatformVariant.all(); + final TargetPlatformVariant desktop = TargetPlatformVariant.all(); + final TargetPlatformVariant mobile = TargetPlatformVariant.all(); + expect(desktop.values.union(mobile.values), equals(all.values)); + }); }); }