Skip to content

[InkWell] fix: Prevent disabled buttons from passing taps to parent GestureDetectors#187551

Open
lucas-goldner wants to merge 3 commits into
flutter:masterfrom
lucas-goldner:fix-disabled-button-tap-passthrough
Open

[InkWell] fix: Prevent disabled buttons from passing taps to parent GestureDetectors#187551
lucas-goldner wants to merge 3 commits into
flutter:masterfrom
lucas-goldner:fix-disabled-button-tap-passthrough

Conversation

@lucas-goldner
Copy link
Copy Markdown
Contributor

@lucas-goldner lucas-goldner commented Jun 4, 2026

Description

Fixes #138981

When a disabled ButtonStyleButton (e.g. ElevatedButton, TextButton, OutlinedButton, FilledButton) with onPressed: null is placed inside a parent GestureDetector, tapping the disabled button incorrectly triggers the parent's onTap callback.

Before:

before.mov

After:

after.mov

Root Cause

The InkWell inside ButtonStyleButton uses a GestureDetector with HitTestBehavior.opaque, but when the button is disabled, all gesture callbacks are set to null. This means:

  1. The render object participates in hit testing (opaque behavior)
  2. But no TapGestureRecognizer is registered in the gesture arena
  3. The parent GestureDetector's recognizer wins the arena unopposed
  4. The parent's onTap fires — even though a disabled button was tapped

Approaches Considered

  • AbsorbPointer: Tried wrapping the InkWell with AbsorbPointer(absorbing: !widget.enabled), but this doesn't work. AbsorbPointer.hitTest returns true without adding itself to the hit test result, which still causes the parent GestureDetector to add itself via hitTestChildren and fire its callback.

  • Conditional GestureDetector wrapper (if (!widget.enabled)): This worked for tap absorption but broke the disabled and hovered ElevatedButton responds to mouse-exit test. Conditionally inserting/removing a widget changes the tree structure, causing the MouseRegion inside InkWell to lose its hover state.

  • Always-present GestureDetector wrapper (chosen approach): Wraps the InkWell in a GestureDetector that is always present but only registers a no-op onTap when the button is disabled. When enabled, onTap is null so no extra recognizer is registered. This avoids tree structure changes and correctly absorbs taps on disabled buttons by competing in and winning the gesture arena.

Fix

Added a GestureDetector wrapper in ButtonStyleButton._build() that registers a no-op onTap: () {} when the button is disabled. This TapGestureRecognizer competes in the gesture arena, wins (being closer to the tap point than the parent), and discards the event.

Pre-launch Checklist

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jun 4, 2026
@lucas-goldner lucas-goldner changed the title fix: Prevent disabled buttons from passing taps to parent GestureDetectors [InkWell] fix: Prevent disabled buttons from passing taps to parent GestureDetectors Jun 4, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request wraps the button layout in a GestureDetector to absorb taps when the button is disabled, preventing them from propagating to parent gesture detectors, and adds a regression test for this behavior. Feedback suggests using HitTestBehavior.deferToChild instead of HitTestBehavior.opaque when the button is enabled to prevent the GestureDetector from unexpectedly intercepting hit tests in transparent areas of the button.

Comment on lines +577 to +582
result = GestureDetector(
onTap: widget.enabled ? null : () {},
behavior: HitTestBehavior.opaque,
excludeFromSemantics: true,
child: result,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the button is enabled, onTap is null. Hardcoding behavior: HitTestBehavior.opaque means that even when the button is enabled, this outer GestureDetector will force the hit-test behavior to be opaque. This could potentially intercept hit tests in transparent areas of the button or its padding, affecting parent or sibling widgets in unexpected ways.

Using HitTestBehavior.deferToChild when the button is enabled ensures that the GestureDetector remains completely passive and does not alter the hit-testing behavior of the enabled button.

    result = GestureDetector(
      onTap: widget.enabled ? null : () {},
      behavior: widget.enabled ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
      excludeFromSemantics: true,
      child: result,
    );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to deferToChild! 👍🏽

@lucas-goldner
Copy link
Copy Markdown
Contributor Author

@ reviewer looking at this PR:

Hi! I noticed that the Material library in flutter/flutter has been frozen since April 7th as part of the decoupling effort. Since this PR modifies button_style_button.dart in the Material library, should I port this to the material_ui package in flutter/packages instead? Or should I keep this PR open here as-is until migration instructions are provided? Thanks!

// When the button is disabled, absorb taps to prevent them from
// propagating to parent gesture detectors.
// See https://github.com/flutter/flutter/issues/138981
result = GestureDetector(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not wrap with an AbsorbPointer instead?

WidgetTester tester,
) async {
final theme = ThemeData();
testWidgets(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these formatting changes be undone?

@victorsanni victorsanni added the CICD Run CI/CD label Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tap event goes through disabled button

2 participants