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
78 changes: 77 additions & 1 deletion packages/material_ui/lib/src/ink_well.dart
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ class InkResponse extends StatelessWidget {
this.onSecondaryTapUp,
this.onSecondaryTapDown,
this.onSecondaryTapCancel,
this.onTertiaryTapUp,
this.onTertiaryTapDown,
this.onTertiaryTapCancel,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
Expand Down Expand Up @@ -410,6 +413,35 @@ class InkResponse extends StatelessWidget {
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapCallback? onSecondaryTapCancel;

/// Called when the user taps down on this part of the material with a
/// tertiary button.
///
/// A common use case for the tertiary button is opening a link in a new tab
/// or window with a middle click of a mouse.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapDownCallback? onTertiaryTapDown;

/// Called when the user releases a tertiary button tap that was started on
/// this part of the material.
///
/// See also:
///
/// * [onTertiaryTapDown], which is called when the tertiary button tap
/// starts.
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapUpCallback? onTertiaryTapUp;

/// Called when the user cancels a tertiary button tap that was started on
/// this part of the material.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapCancelCallback? onTertiaryTapCancel;

/// Called when this part of the material either becomes highlighted or stops
/// being highlighted.
///
Expand Down Expand Up @@ -674,6 +706,9 @@ class InkResponse extends StatelessWidget {
onSecondaryTapUp: onSecondaryTapUp,
onSecondaryTapDown: onSecondaryTapDown,
onSecondaryTapCancel: onSecondaryTapCancel,
onTertiaryTapUp: onTertiaryTapUp,
onTertiaryTapDown: onTertiaryTapDown,
onTertiaryTapCancel: onTertiaryTapCancel,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
mouseCursor: mouseCursor,
Expand Down Expand Up @@ -732,6 +767,9 @@ class _InkResponseStateWidget extends StatefulWidget {
this.onSecondaryTapUp,
this.onSecondaryTapDown,
this.onSecondaryTapCancel,
this.onTertiaryTapUp,
this.onTertiaryTapDown,
this.onTertiaryTapCancel,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
Expand Down Expand Up @@ -771,6 +809,9 @@ class _InkResponseStateWidget extends StatefulWidget {
final GestureTapUpCallback? onSecondaryTapUp;
final GestureTapDownCallback? onSecondaryTapDown;
final GestureTapCallback? onSecondaryTapCancel;
final GestureTapUpCallback? onTertiaryTapUp;
final GestureTapDownCallback? onTertiaryTapDown;
final GestureTapCancelCallback? onTertiaryTapCancel;
final ValueChanged<bool>? onHighlightChanged;
final ValueChanged<bool>? onHover;
final MouseCursor? mouseCursor;
Expand Down Expand Up @@ -815,6 +856,9 @@ class _InkResponseStateWidget extends StatefulWidget {
if (onSecondaryTapUp != null) 'secondary tap up',
if (onSecondaryTapDown != null) 'secondary tap down',
if (onSecondaryTapCancel != null) 'secondary tap cancel',
if (onTertiaryTapUp != null) 'tertiary tap up',
if (onTertiaryTapDown != null) 'tertiary tap down',
if (onTertiaryTapCancel != null) 'tertiary tap cancel',
];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
Expand Down Expand Up @@ -1183,6 +1227,25 @@ class _InkResponseState extends State<_InkResponseStateWidget>
widget.onSecondaryTapUp?.call(details);
}

void handleTertiaryTapDown(TapDownDetails details) {
handleAnyTapDown(details);
widget.onTertiaryTapDown?.call(details);
}

void handleTertiaryTapUp(TapUpDetails details) {
_currentSplash?.confirm();
_currentSplash = null;
updateHighlight(_HighlightType.pressed, value: false);
widget.onTertiaryTapUp?.call(details);
}

void handleTertiaryTapCancel() {
_currentSplash?.cancel();
_currentSplash = null;
widget.onTertiaryTapCancel?.call();
updateHighlight(_HighlightType.pressed, value: false);
}

void _startNewSplash({TapDownDetails? details, BuildContext? context}) {
assert(details != null || context != null);

Expand Down Expand Up @@ -1284,7 +1347,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}

bool isWidgetEnabled(_InkResponseStateWidget widget) {
return _primaryButtonEnabled(widget) || _secondaryButtonEnabled(widget);
return _primaryButtonEnabled(widget) ||
_secondaryButtonEnabled(widget) ||
_tertiaryButtonEnabled(widget);
}

bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
Expand All @@ -1302,9 +1367,14 @@ class _InkResponseState extends State<_InkResponseStateWidget>
widget.onSecondaryTapDown != null;
}

bool _tertiaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onTertiaryTapUp != null || widget.onTertiaryTapDown != null;
}

bool get enabled => isWidgetEnabled(widget);
bool get _primaryEnabled => _primaryButtonEnabled(widget);
bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
bool get _tertiaryEnabled => _tertiaryButtonEnabled(widget);

void handleMouseEnter(PointerEnterEvent event) {
_hovering = true;
Expand Down Expand Up @@ -1409,6 +1479,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
onTertiaryTapDown: _tertiaryEnabled ? handleTertiaryTapDown : null,
onTertiaryTapUp: _tertiaryEnabled ? handleTertiaryTapUp : null,
onTertiaryTapCancel: _tertiaryEnabled ? handleTertiaryTapCancel : null,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: true,
child: widget.child,
Expand Down Expand Up @@ -1524,6 +1597,9 @@ class InkWell extends InkResponse {
super.onSecondaryTapUp,
super.onSecondaryTapDown,
super.onSecondaryTapCancel,
super.onTertiaryTapUp,
super.onTertiaryTapDown,
super.onTertiaryTapCancel,
super.onHighlightChanged,
super.onHover,
super.mouseCursor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Adds `onTertiaryTapDown`, `onTertiaryTapUp` and `onTertiaryTapCancel` to `InkResponse` and `InkWell`, so a tertiary (middle) button click can be handled directly.
version: minor
96 changes: 96 additions & 0 deletions packages/material_ui/test/ink_well_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,102 @@ void main() {
},
);

// Regression test for https://github.com/flutter/flutter/issues/129058.
testWidgets('InkWell tertiary tap test', (WidgetTester tester) async {
final log = <String>[];

await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: InkWell(
onTertiaryTapDown: (TapDownDetails details) {
log.add('tertiary-tap-down');
},
onTertiaryTapUp: (TapUpDetails details) {
log.add('tertiary-tap-up');
},
onTertiaryTapCancel: () {
log.add('tertiary-tap-cancel');
},
),
),
),
),
);

await tester.tap(find.byType(InkWell), pointer: 1, buttons: kTertiaryButton);

expect(log, equals(<String>['tertiary-tap-down', 'tertiary-tap-up']));
log.clear();

final TestGesture gesture = await tester.startGesture(
tester.getCenter(find.byType(InkWell)),
pointer: 2,
buttons: kTertiaryButton,
);
await gesture.moveTo(const Offset(100, 100));
await gesture.up();

expect(log, equals(<String>['tertiary-tap-down', 'tertiary-tap-cancel']));
});

testWidgets('InkWell tertiary tap highlights only when a tertiary callback is defined', (
WidgetTester tester,
) async {
final log = <bool>[];

Widget buildFrame({required bool withTertiaryCallback}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: SizedBox(
width: 100.0,
height: 100.0,
child: InkWell(
onTap: () {},
onHighlightChanged: log.add,
onTertiaryTapUp: withTertiaryCallback ? (TapUpDetails details) {} : null,
),
),
),
),
);
}
Comment on lines +2549 to +2566

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

This test is great for verifying that highlights are shown for tertiary taps only when a callback is provided. However, it only checks for the presence of onTertiaryTapUp. The logic in _tertiaryButtonEnabled also enables tertiary tap handling if onTertiaryTapDown is provided.

To make this test more comprehensive, consider parameterizing it to check that the highlight is correctly shown when:

  1. Only onTertiaryTapUp is provided.
  2. Only onTertiaryTapDown is provided.
  3. Both are provided.
  4. Neither is provided (as is already tested).

This would provide stronger guarantees about the correctness of the enabling logic.


await tester.pumpWidget(buildFrame(withTertiaryCallback: true));

TestGesture gesture = await tester.startGesture(
tester.getRect(find.byType(InkWell)).center,
buttons: kTertiaryButton,
);
await tester.pump(const Duration(milliseconds: 200));

expect(log, equals(<bool>[true]));
log.clear();

await gesture.up();
await tester.pump(const Duration(milliseconds: 200));

expect(log, equals(<bool>[false]));
log.clear();

// Without a tertiary callback, a tertiary button press is ignored.
await tester.pumpWidget(buildFrame(withTertiaryCallback: false));

gesture = await tester.startGesture(
tester.getRect(find.byType(InkWell)).center,
buttons: kTertiaryButton,
);
await tester.pump(const Duration(milliseconds: 200));

expect(log, isEmpty);

await gesture.up();
});

testWidgets('try out hoverDuration property', (WidgetTester tester) async {
final log = <String>[];

Expand Down