From 510ecaa4e76cbe740bad20071040cb7cd15e7170 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Fri, 1 Sep 2023 11:08:21 +0200 Subject: [PATCH] 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. --- .../flutter/lib/src/material/ink_well.dart | 29 +++++++++++++-- .../flutter/test/material/ink_well_test.dart | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 8814a847ad..fe0fa9aca2 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:collection'; import 'package:flutter/foundation.dart'; @@ -809,8 +810,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> bool _hovering = false; final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{}; late final Map> _actionMap = >{ - ActivateIntent: CallbackAction(onInvoke: simulateTap), - ButtonActivateIntent: CallbackAction(onInvoke: simulateTap), + ActivateIntent: CallbackAction(onInvoke: activateOnIntent), + ButtonActivateIntent: CallbackAction(onInvoke: activateOnIntent), }; MaterialStatesController? internalStatesController; @@ -818,6 +819,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>(); + static const Duration _activationDuration = Duration(milliseconds: 100); + Timer? _activationTimer; + @override void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) { final bool lastAnyPressed = _anyChildInkResponsePressed; @@ -833,6 +837,25 @@ class _InkResponseState extends State<_InkResponseStateWidget> } 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]) { _startNewSplash(context: context); handleTap(); @@ -917,6 +940,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange); statesController.removeListener(handleStatesControllerChange); internalStatesController?.dispose(); + _activationTimer?.cancel(); + _activationTimer = null; super.dispose(); } diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index 8f3903ad9f..0fe244a3c4 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -2253,4 +2253,39 @@ testWidgetsWithLeakTracking('InkResponse radius can be updated', (WidgetTester t expect(log, equals(['tap'])); 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 { + 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(); + }); }