Skip to content

Controls column is inaccessible to screen readers (no labels; and no semantics nodes at all in my measurement) #390

Description

@GranatenUdo

Summary

The controls column is not usable with a screen reader. There are two separable problems; the first is plain from the source, the second I can reproduce but not explain.

Tested with feedback: 3.2.0, Flutter 3.44.8.

1. Most of the controls have no accessible name (source-evident)

In lib/src/controls_column.dart, seven of the controls are icon-only with no tooltip: and no Semantics label, so there is nothing for a screen reader to announce even in principle:

  • close_controls_columnIconButton(icon: Icon(Icons.close))
  • undo_buttonIconButton(icon: Icon(Icons.undo))
  • clear_buttonIconButton(icon: Icon(Icons.delete))
  • the four _ColorSelectionIconButton swatches — Icon(isActive ? Icons.lens : Icons.panorama_fish_eye), where the colour carries both which swatch it is and whether it is selected

The swatches are the sharpest case: colour alone conveys the meaning, so a non-visual user gets nothing, and a low-vision user gets nothing either if the swatch colour happens to be low-contrast against the controls Card (with the package defaults, Colors.yellow measures 1.14:1 against surfaceContainerLow in a light scheme).

navigate_button and draw_button are fine in this respect — they have real Text children.

Adding tooltip: to the four IconButtons and a Semantics(label: …) (or tooltip) to the swatches would fix this part, and tooltip doubles as a visible affordance on desktop/web.

2. In my measurement, the controls column emits no semantics nodes at all

With semantics enabled and the overlay open, the live semantics tree contains only the nodes from the bottom sheet. Nothing from the controls column appears — including navigate_button and draw_button, which do have text.

What I checked before reporting:

  • Not the fade. ScaleAndFade wraps the controls in Opacity(maxOpacity - progress * (maxOpacity - minOpacity)), and RenderOpacity drops an alpha-0 child from the semantics tree — which would be correct behaviour, since faded-out controls are not actionable anyway. Measured at rest, the Opacity above draw_button is 1.0 and draw_button occupies a real 48×88 rect, so the controls are fully visible and still absent from the tree.
  • Not a broken traversal on my side. The same walk over SemanticsOwner.rootSemanticsNode finds 14 labelled nodes on an ordinary screen in the same app, and the widgets themselves are present (find.byKey(ValueKey('draw_button')) matches).

I did not determine the cause, so I'm reporting the observation rather than guessing at it. Repro sketch (widget test):

final handle = tester.ensureSemantics();
// ... pump an app wrapped in BetterFeedback, then open the overlay ...
final root = tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!;
// walk root.visitChildren and collect nodes with a label or a tap action
// -> only the bottom sheet's nodes are present
handle.dispose();

Why this matters / context

feedbackBuilder makes the sheet fully replaceable, which is great — I used it to give our sheet a SafeArea, a width cap and a labelled field. But ControlsColumn is constructed inside FeedbackWidget.build with no equivalent hook, so there is no way for an app to fix any of the above without vendoring the overlay. Either the labels landing upstream, or a controlsBuilder-style hook, would solve it.

Happy to open a PR for part 1 if that's welcome.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions