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_column — IconButton(icon: Icon(Icons.close))
undo_button — IconButton(icon: Icon(Icons.undo))
clear_button — IconButton(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.
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 notooltip:and noSemanticslabel, so there is nothing for a screen reader to announce even in principle:close_controls_column—IconButton(icon: Icon(Icons.close))undo_button—IconButton(icon: Icon(Icons.undo))clear_button—IconButton(icon: Icon(Icons.delete))_ColorSelectionIconButtonswatches —Icon(isActive ? Icons.lens : Icons.panorama_fish_eye), where the colour carries both which swatch it is and whether it is selectedThe 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.yellowmeasures 1.14:1 againstsurfaceContainerLowin a light scheme).navigate_buttonanddraw_buttonare fine in this respect — they have realTextchildren.Adding
tooltip:to the fourIconButtons and aSemantics(label: …)(ortooltip) to the swatches would fix this part, andtooltipdoubles 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_buttonanddraw_button, which do have text.What I checked before reporting:
ScaleAndFadewraps the controls inOpacity(maxOpacity - progress * (maxOpacity - minOpacity)), andRenderOpacitydrops an alpha-0 child from the semantics tree — which would be correct behaviour, since faded-out controls are not actionable anyway. Measured at rest, theOpacityabovedraw_buttonis 1.0 anddraw_buttonoccupies a real 48×88 rect, so the controls are fully visible and still absent from the tree.SemanticsOwner.rootSemanticsNodefinds 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):
Why this matters / context
feedbackBuildermakes the sheet fully replaceable, which is great — I used it to give our sheet aSafeArea, a width cap and a labelled field. ButControlsColumnis constructed insideFeedbackWidget.buildwith 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 acontrolsBuilder-style hook, would solve it.Happy to open a PR for part 1 if that's welcome.