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.
This commit is contained in:
Bruno Leroux 2023-09-01 11:08:21 +02:00 committed by GitHub
parent f0b682bc05
commit 510ecaa4e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 2 deletions

View File

@ -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>
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>
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>
} }
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>
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();
} }

View File

@ -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();
});
} }