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