Unverified Commit 510ecaa4 authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Fix MaterialState.pressed is missing when pressing button with keyboard (#133558)

## Description

This PR fixes changes how `InkWell` reacts to keyboard activation. 

**Before**: the activation started a splash and immediately terminated it which did not let time for widgets that resolve material state properties to react (visually it also mean the splash does not have time to expand).

**After**: the activation starts and terminates after a delay (I arbitrary choose 200ms for the moment).

## Related Issue

Fixes https://github.com/flutter/flutter/issues/132377.

## Tests

Adds one test.
parent f0b682bc
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -809,8 +810,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> ...@@ -809,8 +810,8 @@ class _InkResponseState extends State<_InkResponseStateWidget>
bool _hovering = false; bool _hovering = false;
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{}; final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{ late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: simulateTap), ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: activateOnIntent),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: simulateTap), ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: activateOnIntent),
}; };
MaterialStatesController? internalStatesController; MaterialStatesController? internalStatesController;
...@@ -818,6 +819,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> ...@@ -818,6 +819,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>(); final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>();
static const Duration _activationDuration = Duration(milliseconds: 100);
Timer? _activationTimer;
@override @override
void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) { void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) {
final bool lastAnyPressed = _anyChildInkResponsePressed; final bool lastAnyPressed = _anyChildInkResponsePressed;
...@@ -833,6 +837,25 @@ class _InkResponseState extends State<_InkResponseStateWidget> ...@@ -833,6 +837,25 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty; bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
void activateOnIntent(Intent? intent) {
_activationTimer?.cancel();
_activationTimer = null;
_startNewSplash(context: context);
_currentSplash?.confirm();
_currentSplash = null;
if (widget.onTap != null) {
if (widget.enableFeedback) {
Feedback.forTap(context);
}
widget.onTap?.call();
}
// Delay the call to `updateHighlight` to simulate a pressed delay
// and give MaterialStatesController listeners a chance to react.
_activationTimer = Timer(_activationDuration, () {
updateHighlight(_HighlightType.pressed, value: false);
});
}
void simulateTap([Intent? intent]) { void simulateTap([Intent? intent]) {
_startNewSplash(context: context); _startNewSplash(context: context);
handleTap(); handleTap();
...@@ -917,6 +940,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> ...@@ -917,6 +940,8 @@ class _InkResponseState extends State<_InkResponseStateWidget>
FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange); FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange);
statesController.removeListener(handleStatesControllerChange); statesController.removeListener(handleStatesControllerChange);
internalStatesController?.dispose(); internalStatesController?.dispose();
_activationTimer?.cancel();
_activationTimer = null;
super.dispose(); super.dispose();
} }
......
...@@ -2253,4 +2253,39 @@ testWidgetsWithLeakTracking('InkResponse radius can be updated', (WidgetTester t ...@@ -2253,4 +2253,39 @@ testWidgetsWithLeakTracking('InkResponse radius can be updated', (WidgetTester t
expect(log, equals(<String>['tap'])); expect(log, equals(<String>['tap']));
log.clear(); log.clear();
}); });
testWidgetsWithLeakTracking('InkWell activation action does not end immediately', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/132377.
final MaterialStatesController controller = MaterialStatesController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Shortcuts(
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.enter): ButtonActivateIntent(),
},
child: Material(
child: Center(
child: InkWell(
autofocus: true,
onTap: () {},
statesController: controller,
),
),
),
),
));
// Invoke the InkWell activation action.
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
// The InkWell is in pressed state.
await tester.pump(const Duration(milliseconds: 99));
expect(controller.value.contains(MaterialState.pressed), isTrue);
await tester.pumpAndSettle();
expect(controller.value.contains(MaterialState.pressed), isFalse);
controller.dispose();
});
} }
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