Adjustments to FocusHighlightMode handling (#162417)
Fixes https://github.com/flutter/flutter/issues/158527 Adjustments: * Using the mouse/trackpad does no longer change `FocusHighlightMode`s (this matches observed behavior on Android) * Changing focus via a11y on the web forces `FocusHighlightModes.touch`, which hides the visual input focus indication from non-Textfields. The reason here is in order to give something input focus on the web it also has to have a11y focus, which is indicated separately.
This commit is contained in:
parent
58b7ee137e
commit
1e035cc693
@ -75,6 +75,23 @@ mixin SemanticsBinding on BindingBase {
|
||||
_semanticsEnabled.removeListener(listener);
|
||||
}
|
||||
|
||||
final ObserverList<ValueSetter<ui.SemanticsActionEvent>> _semanticsActionListeners =
|
||||
ObserverList<ValueSetter<ui.SemanticsActionEvent>>();
|
||||
|
||||
/// Adds a listener that is called for every [SemanticsActionEvent] received.
|
||||
///
|
||||
/// The listeners are called before [performSemanticsAction] is invoked.
|
||||
///
|
||||
/// To remove the listener, call [removeSemanticsActionListener].
|
||||
void addSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
|
||||
_semanticsActionListeners.add(listener);
|
||||
}
|
||||
|
||||
/// Removes a listener previously added with [addSemanticsActionListener].
|
||||
void removeSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
|
||||
_semanticsActionListeners.remove(listener);
|
||||
}
|
||||
|
||||
/// The number of clients registered to listen for semantics.
|
||||
///
|
||||
/// The number is increased whenever [ensureSemantics] is called and decreased
|
||||
@ -125,6 +142,15 @@ mixin SemanticsBinding on BindingBase {
|
||||
arguments is ByteData
|
||||
? action.copyWith(arguments: const StandardMessageCodec().decodeMessage(arguments))
|
||||
: action;
|
||||
// Listeners may get added/removed while the iteration is in progress. Since the list cannot
|
||||
// be modified while iterating, we are creating a local copy for the iteration.
|
||||
final List<ValueSetter<ui.SemanticsActionEvent>> localListeners = _semanticsActionListeners
|
||||
.toList(growable: false);
|
||||
for (final ValueSetter<ui.SemanticsActionEvent> listener in localListeners) {
|
||||
if (_semanticsActionListeners.contains(listener)) {
|
||||
listener(decodedAction);
|
||||
}
|
||||
}
|
||||
performSemanticsAction(decodedAction);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'binding.dart';
|
||||
@ -2064,9 +2065,9 @@ class _HighlightModeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// If set, indicates if the last interaction detected was touch or not. If
|
||||
// null, no interactions have occurred yet.
|
||||
bool? _lastInteractionWasTouch;
|
||||
// If null, no interactions have occurred yet and the default highlight mode for the current
|
||||
// platform applies.
|
||||
bool? _lastInteractionRequiresTraditionalHighlights;
|
||||
|
||||
FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
|
||||
FocusHighlightMode? _highlightMode;
|
||||
@ -2122,6 +2123,7 @@ class _HighlightModeManager {
|
||||
// HardwareKeyboard.
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = handleKeyMessage;
|
||||
GestureBinding.instance.pointerRouter.addGlobalRoute(handlePointerEvent);
|
||||
SemanticsBinding.instance.addSemanticsActionListener(handleSemanticsAction);
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@ -2132,6 +2134,7 @@ class _HighlightModeManager {
|
||||
if (ServicesBinding.instance.keyEventManager.keyMessageHandler == handleKeyMessage) {
|
||||
GestureBinding.instance.pointerRouter.removeGlobalRoute(handlePointerEvent);
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||
SemanticsBinding.instance.removeSemanticsActionListener(handleSemanticsAction);
|
||||
}
|
||||
_listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
|
||||
}
|
||||
@ -2175,29 +2178,28 @@ class _HighlightModeManager {
|
||||
}
|
||||
|
||||
void handlePointerEvent(PointerEvent event) {
|
||||
final FocusHighlightMode expectedMode;
|
||||
switch (event.kind) {
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.stylus:
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
_lastInteractionWasTouch = true;
|
||||
expectedMode = FocusHighlightMode.touch;
|
||||
if (_lastInteractionRequiresTraditionalHighlights != true) {
|
||||
_lastInteractionRequiresTraditionalHighlights = true;
|
||||
updateMode();
|
||||
}
|
||||
case PointerDeviceKind.mouse:
|
||||
case PointerDeviceKind.trackpad:
|
||||
case PointerDeviceKind.unknown:
|
||||
_lastInteractionWasTouch = false;
|
||||
expectedMode = FocusHighlightMode.traditional;
|
||||
}
|
||||
if (expectedMode != highlightMode) {
|
||||
updateMode();
|
||||
}
|
||||
}
|
||||
|
||||
bool handleKeyMessage(KeyMessage message) {
|
||||
// ignore: use_if_null_to_convert_nulls_to_bools
|
||||
if (_lastInteractionRequiresTraditionalHighlights != false) {
|
||||
// Update highlightMode first, since things responding to the keys might
|
||||
// look at the highlight mode, and it should be accurate.
|
||||
_lastInteractionWasTouch = false;
|
||||
_lastInteractionRequiresTraditionalHighlights = false;
|
||||
updateMode();
|
||||
}
|
||||
|
||||
assert(_focusDebug(() => 'Received key event $message'));
|
||||
if (FocusManager.instance.primaryFocus == null) {
|
||||
@ -2290,20 +2292,29 @@ class _HighlightModeManager {
|
||||
return handled;
|
||||
}
|
||||
|
||||
void handleSemanticsAction(SemanticsActionEvent semanticsActionEvent) {
|
||||
if (kIsWeb &&
|
||||
semanticsActionEvent.type == SemanticsAction.focus &&
|
||||
_lastInteractionRequiresTraditionalHighlights != true) {
|
||||
_lastInteractionRequiresTraditionalHighlights = true;
|
||||
updateMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Update function to be called whenever the state relating to highlightMode
|
||||
// changes.
|
||||
void updateMode() {
|
||||
final FocusHighlightMode newMode;
|
||||
switch (strategy) {
|
||||
case FocusHighlightStrategy.automatic:
|
||||
if (_lastInteractionWasTouch == null) {
|
||||
if (_lastInteractionRequiresTraditionalHighlights == 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!) {
|
||||
if (_lastInteractionRequiresTraditionalHighlights!) {
|
||||
newMode = FocusHighlightMode.touch;
|
||||
} else {
|
||||
newMode = FocusHighlightMode.traditional;
|
||||
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/feedback_tester.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
@ -3317,6 +3318,62 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(onLongPressed, false);
|
||||
});
|
||||
|
||||
testWidgets('does not draw focus color when focused by semantics on the web', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/158527.
|
||||
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
const Color focusColor = Colors.orange;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: IconButton(
|
||||
focusColor: focusColor,
|
||||
focusNode: focusNode,
|
||||
icon: const Icon(Icons.headphones),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Make sure we are in "traditional mode" where the button could potentially draw focus highlights.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
|
||||
|
||||
expect(focusNode.hasFocus, isFalse);
|
||||
|
||||
// Focus on it with semantics.
|
||||
tester.platformDispatcher.onSemanticsActionEvent!(
|
||||
SemanticsActionEvent(
|
||||
type: SemanticsAction.focus,
|
||||
viewId: tester.view.viewId,
|
||||
nodeId: tester.semantics.find(find.byIcon(Icons.headphones)).id,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Make sure no focus highlight was drawn.
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) {
|
||||
return object.runtimeType.toString() == '_RenderInkFeatures';
|
||||
});
|
||||
expect(focusNode.hasFocus, isTrue);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
|
||||
expect(inkFeatures, isNot(paints..rect(color: focusColor)));
|
||||
|
||||
// Check that focus highlight is drawn in traditional mode.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasFocus, isTrue);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
|
||||
expect(inkFeatures, paints..rect(color: focusColor));
|
||||
}, skip: !isBrowser); // [intended] tests web-specific behavior.
|
||||
}
|
||||
|
||||
Widget buildAllVariants({
|
||||
|
@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
const double _defaultBorderWidth = 1.0;
|
||||
@ -692,6 +693,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// focusColor
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) {
|
||||
@ -748,6 +750,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// focusColor
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) {
|
||||
@ -811,6 +814,7 @@ void main() {
|
||||
await hoverGesture.moveTo(Offset.zero);
|
||||
|
||||
// focusColor
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) {
|
||||
|
@ -482,6 +482,7 @@ void main() {
|
||||
await hoverGesture.moveTo(Offset.zero);
|
||||
|
||||
// focusColor
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) {
|
||||
|
@ -1519,19 +1519,19 @@ void main() {
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await gesture.up();
|
||||
expect(callCount, equals(3));
|
||||
expect(lastMode, FocusHighlightMode.traditional);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
|
||||
expect(callCount, equals(2));
|
||||
expect(lastMode, FocusHighlightMode.touch);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
|
||||
await tester.tap(find.byType(Container), warnIfMissed: false);
|
||||
expect(callCount, equals(4));
|
||||
expect(callCount, equals(2));
|
||||
expect(lastMode, FocusHighlightMode.touch);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
expect(callCount, equals(5));
|
||||
expect(callCount, equals(3));
|
||||
expect(lastMode, FocusHighlightMode.traditional);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
|
||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
|
||||
expect(callCount, equals(6));
|
||||
expect(callCount, equals(4));
|
||||
expect(lastMode, FocusHighlightMode.touch);
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user