Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/flutter/lib/src/material/button_style_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,16 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
),
);

// When the button is disabled, absorb taps to prevent them from

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.

Nit: this is not pointer absorption, this is gesture arena competition. So technically we are not absorbing taps (the way AbsorbPointer does), we are claiming taps so that ancestor tap recognizers do not win.

// 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?

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.

That is what I thought as well...

I wrote it in the PR description:

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.

onTap: widget.enabled ? null : () {},

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.

do we need to worry about ancestor onLongPress, onDoubleTap etc?

behavior: widget.enabled ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
excludeFromSemantics: true,
child: result,
);
Comment on lines +577 to +582

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! 👍🏽


if (widget.tooltip != null) {
result = Tooltip(message: widget.tooltip, child: result);
}
Expand Down
27 changes: 27 additions & 0 deletions packages/flutter/test/material/elevated_button_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2692,4 +2692,31 @@ void main() {
// The button should still be focused.
expect(getButtonFocusNode().hasFocus, true);
});

// Regression test for https://github.com/flutter/flutter/issues/138981
testWidgets('Disabled ElevatedButton does not let taps pass through to parent GestureDetector', (
WidgetTester tester,
) async {
var parentTapCount = 0;

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: GestureDetector(
onTap: () {
parentTapCount++;
},
child: const Center(child: ElevatedButton(onPressed: null, child: Text('Disabled'))),
),
),
),
);

// Tap the disabled button.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();

// The parent GestureDetector should NOT have received the tap.
expect(parentTapCount, 0);
});
}
Loading