Skip to content

Buttons that announce themselves but cannot be pressed #487

Description

@maximsan

Twenty-seven widgets announce themselves as buttons that a screen reader cannot press.

What is wrong

The app has one idiom for "a tappable thing that should read as a single announcement":

Semantics(
  button: true,
  label: someLabel,
  excludeSemantics: true,
  child:InkWell(onTap: …),
)

excludeSemantics: true drops the descendants' semantics — which is the point, so the label is read once instead of the label and then each line inside it. But it drops the InkWell's tap action along with the text. What is left is a node flagged isButton carrying no SemanticsAction.tap.

VoiceOver and TalkBack announce it as a button. Activating it does nothing.

Measured, not assumed

Against the shared BorderedTapRow, with a real onTap:

ACTIONS: 0
FLAGS: 8        // isButton, and nothing else

The fix

Pass the handler to the Semantics as well as to the InkWellonTap: onTap beside button: true. The tap keeps working for sighted users either way; this is what gives the node an action to carry.

Where excludeSemantics is itself conditional, the handler goes on only when the exclusion is active. Beside a live InkWell it makes the annotation a node boundary, splitting the row in two and putting the button flag on the node that does not carry the label.

Where

This listed nine files when it was written. The guard below found twenty-seven nodes, and three of the nine were the wrong file:

  • the drill rows are ReplayRow, not practice_drills_widget.dart
  • the match tile is match_tile.dart, not match_board_view.dart
  • path_module_section.dart has no button — its excluded node is a label rewrite, which is correct

lib/core/widgets/bordered_tap_row.dart is the shared one, so fixing it covers every caller. The rest are in PR #620.

term_of_day_banner.dart was already fixed (#96, PR #484) — it was the tenth, and shipping a new one knowingly broken was not an option. Its test shows the shape the assertion takes.

Acceptance

  • Every Semantics(button: true, excludeSemantics: true) node in lib/ carries a tap action — test/unit/semantics_button_tap_guard_test.dart reads every source under lib/ and fails listing any that does not
  • A test asserts it for the shared BorderedTapRow, so the idiom cannot regress to the flag alone — test/widget/core/widgets/bordered_tap_row_test.dart performs the announced action and checks the handler runs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions