Skip to content

Chip delete button tap target does not meet platform recommended tap target size #94098

Closed

Description

@NickAb

Reopening #42455 Chip delete button tap target is too small.

Steps to Reproduce

  1. Add androidTapTargetGuideline test case to packages/flutter/test/material/chip_test.dart. See code below.
      testWidgets('delete button tap target complies with minimum tap target guideline', (WidgetTester tester) async {
        final SemanticsHandle handle = tester.ensureSemantics();
        final UniqueKey deleteKey = UniqueKey();
        await tester.pumpWidget(
          _wrapForChip(
            child: Column(
              children: <Widget>[
                Chip(
                  label: const Text('Label'),
                  deleteIcon: Icon(Icons.delete, key: deleteKey),
                  onDeleted: () { },
                ),
              ],
            ),
          ),
        );
    
        await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
    
        handle.dispose();
      });
  2. Execute cd packages/flutter and flutter test flutter/test/material/chip_test.dart

Expected results:

  • All tests pass

Actual results:

  • delete button tap target complies with minimum tap target guideline [E] fails with
    Expected: Tappable objects should be at least Size(48.0, 48.0)
    Actual: <Instance of 'WidgetTester'>
    Which: SemanticsNode#5(Rect.fromLTRB(90.0, 12.0, 114.0, 36.0), actions: [tap], flags: [isButton,
    isFocusable], label: "Delete", textDirection: ltr): expected tap target size of at least Size(48.0,
    48.0), but found Size(24.0, 24.0)
            See also: https://support.google.com/accessibility/android/answer/7101858?hl=en
    
Flutter Doctor
[✓] Flutter (Channel unknown, 2.6.0-12.0.pre.805, on Debian GNU/Linux rodete 5.10.46-5rodete1-amd64, locale en_US.utf8)
    • Flutter version 2.6.0-12.0.pre.805 at /usr/local/google/home/abalov/flutter
    • Upstream repository unknown
    • Framework revision aee9ade8e3 (60 minutes ago), 2021-11-23 04:03:03 -0500
    • Engine revision f4cb61cefc
    • Dart version 2.16.0 (build 2.16.0-36.0.dev)
    • DevTools version 2.8.0

Activity

  1. added
    p: material_uimaterial_ui package in flutter/packages
    frameworkflutter/packages/flutter repository. See also f: labels.
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    on Nov 23, 2021
  2. self-assigned this
    on Nov 23, 2021
  3. NickAb commented on Dec 23, 2021

    @NickAb
    ContributorAuthor

    Any updates?

  4. removed their assignment
    on May 2, 2023
  5. benjaminwg2017 commented on Apr 23, 2025

    @benjaminwg2017

    This impacts our app (chalk) as well, see internal b/411597987

  6. flutter-triage-bot commented on Apr 23, 2025

    @flutter-triage-bot

    This issue is missing a priority label. Please set a priority label when adding the triaged-design label.

  7. 8 remaining items

  8. added
    P2Important issues not at the top of the work list
    and removed
    P1High-priority issues at the top of the work list
    on Apr 30, 2025
  9. Piinks commented on Apr 30, 2025

    @Piinks
    Contributor

    Marked P2 since we were able to provide a solution to unblock folks for now.

  10. bleroux commented on May 2, 2025

    @bleroux
    Contributor

    @Piinks Interesting. For the moment, I'm not sure this issue and #123528 are directly related.

    The test from this issues's description does not specify a target platform so it runs on Android (and I can confirm the proposed test is still failing on master).
    On Android visual density defaults to VisualDensity.standard and ThemeData.materialTapTargetSize defaults to MaterialTapTargetSize.padded.

    See

    switch (platform) {
    case TargetPlatform.android:
    case TargetPlatform.fuchsia:
    case TargetPlatform.iOS:
    materialTapTargetSize ??= MaterialTapTargetSize.padded;
    case TargetPlatform.linux:
    case TargetPlatform.macOS:
    case TargetPlatform.windows:
    materialTapTargetSize ??= MaterialTapTargetSize.shrinkWrap;
    }

    So setting those values does not change the result of the given test.

    What I noticed when running your code sample from #94098 (comment) on Android (with or without the workaround)

    • the hit area of the button is 48x48
    Enregistrement.de.l.ecran.2025-05-02.a.14.32.07.mov
    • when activating TalkBack the semantics rect is way smaller and hard to hit.
    Enregistrement.de.l.ecran.2025-05-02.a.14.33.03.mov

    So it seems to be a problem related to the Semantics object surrounding the delete icon.
    I will study Chip implementation and share my findings. First thing I noticed is that Chip does not rely on a Material button for the delete button:

    Widget? _buildDeleteIcon(
    BuildContext context,
    ThemeData theme,
    ChipThemeData chipTheme,
    ChipThemeData chipDefaults,
    ) {
    if (!hasDeleteButton) {
    return null;
    }
    final IconThemeData iconTheme =
    widget.iconTheme ??
    chipTheme.iconTheme ??
    theme.chipTheme.iconTheme ??
    _ChipDefaultsM3(context, widget.isEnabled).iconTheme!;
    final Color? effectiveDeleteIconColor =
    widget.deleteIconColor ??
    chipTheme.deleteIconColor ??
    theme.chipTheme.deleteIconColor ??
    widget.iconTheme?.color ??
    chipTheme.iconTheme?.color ??
    chipDefaults.deleteIconColor;
    final double effectiveIconSize =
    widget.iconTheme?.size ??
    chipTheme.iconTheme?.size ??
    theme.chipTheme.iconTheme?.size ??
    _ChipDefaultsM3(context, widget.isEnabled).iconTheme!.size!;
    return Semantics(
    container: true,
    button: true,
    child: _wrapWithTooltip(
    tooltip:
    widget.deleteButtonTooltipMessage ??
    MaterialLocalizations.of(context).deleteButtonTooltip,
    enabled: widget.isEnabled && widget.onDeleted != null,
    child: InkWell(
    // Radius should be slightly less than the full size of the chip.
    radius: (_kChipHeight + (widget.padding?.vertical ?? 0.0)) * .45,
    // Keeps the splash from being constrained to the icon alone.
    splashFactory: _UnconstrainedInkSplashFactory(Theme.of(context).splashFactory),
    customBorder: const CircleBorder(),
    onTap: widget.isEnabled ? widget.onDeleted : null,
    child: IconTheme(
    data: iconTheme.copyWith(color: effectiveDeleteIconColor, size: effectiveIconSize),
    child: widget.deleteIcon,
    ),
    ),
    ),
    );
    }

  11. bleroux commented on May 2, 2025

    @bleroux
    Contributor

    chip.dart implements a custom hit testing strategy which explains why tapping outside of the Icon works.

    The code below also contains interesting comments to explain the reasonning:

    bool _hitIsOnDeleteIcon({
    required EdgeInsetsGeometry padding,
    required EdgeInsetsGeometry labelPadding,
    required Offset tapPosition,
    required Size chipSize,
    required Size deleteButtonSize,
    required TextDirection textDirection,
    }) {
    // The chipSize includes the padding, so we need to deflate the size and adjust the
    // tap position to account for the padding.
    final EdgeInsets resolvedPadding = padding.resolve(textDirection);
    final Size deflatedSize = resolvedPadding.deflateSize(chipSize);
    final Offset adjustedPosition = tapPosition - Offset(resolvedPadding.left, resolvedPadding.top);
    // The delete button hit area should be at least the width of the delete
    // button and right label padding, but, if there's room, up to 24 pixels
    // from the center of the delete icon (corresponding to part of a 48x48 square
    // that Material would prefer for touch targets), but no more than approximately
    // half of the overall size of the chip when the chip is small.
    //
    // This isn't affected by materialTapTargetSize because it only applies to the
    // width of the tappable region within the chip, not outside of the chip,
    // which is handled elsewhere. Also because delete buttons aren't specified to
    // be used on touch devices, only desktop devices.
    // Max out at not quite half, so that tests that tap on the center of a small
    // chip will still hit the chip, not the delete button.
    final double accessibleDeleteButtonWidth = math.min(
    deflatedSize.width * 0.499,
    math.min(
    labelPadding.resolve(textDirection).right + deleteButtonSize.width,
    24.0 + deleteButtonSize.width / 2.0,
    ),
    );
    return switch (textDirection) {
    TextDirection.ltr => adjustedPosition.dx >= deflatedSize.width - accessibleDeleteButtonWidth,
    TextDirection.rtl => adjustedPosition.dx <= accessibleDeleteButtonWidth,
    };
    }

    In conclusion, there is some logic to expand the hit area of the delete button, but it won't change the Semantics rect (which is the icon rect).
    I will experiment a possible solution for this.

  12. self-assigned this
    on May 4, 2025
  13. moved this from To do to PR submitted in Nevercodeon May 6, 2025
  14. moved this from PR submitted to Done (PR merged) in Nevercodeon May 16, 2025
  15. added
    r: fixedIssue is closed as already fixed in a newer version
    on May 17, 2025
  16. github-actions commented on May 31, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  17. locked as resolved and limited conversation to collaborators on May 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)customer: chalk (g3)frameworkflutter/packages/flutter repository. See also f: labels.p: material_uimaterial_ui package in flutter/packagesr: fixedIssue is closed as already fixed in a newer versionteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages teamworkaround availableThere is a workaround available to overcome the issue

Type

No type

Projects

  • Status
    Done (PR merged)

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions