// A label handed down to whatever control sits in a labelled row. // // Most controls in this app are not labelled at the call site — they sit in a // PropertyRow (or a settings row) that already draws the name beside them, so // a sighted user reads "Show grid" and then the box. A screen reader walking // the tree gets the Text and the checkbox as two unrelated things, and the // checkbox itself says nothing. // // The obvious fix — merging the whole row into one semantics node — falls over // on rows holding more than one control (a Size row is two fields in a button // group; merged, it becomes one unreadable blob). So instead the row publishes // its label down the subtree and each control picks it up as a FALLBACK for // its own semanticLabel. Explicit always wins. import "package:flutter/widgets.dart"; class PropertyLabelScope extends InheritedWidget { const PropertyLabelScope({ super.key, required this.label, required super.child, }); final String label; /// nearest enclosing row label, or null if this control isn't in one. static String? maybeOf(BuildContext context) { return context .dependOnInheritedWidgetOfExactType() ?.label; } @override bool updateShouldNotify(PropertyLabelScope old) => label != old.label; } /// Resolves the label a control should announce: its own if it was given one, /// otherwise the row it lives in. Returns null when neither exists, which is /// the case a control should be given an explicit label to fix. String? resolveSemanticLabel(BuildContext context, String? explicit) { if (explicit != null) return explicit; return PropertyLabelScope.maybeOf(context); } // Marks the control slot of a labelled row, so the controls inside it can stop // asking the call site what they should look like. // // A property section is a docked surface: its rows sit ON a panel, not on the // page. An outline field in there reads as floating - it draws its own border // and background against a background that already has one. Secondary is the // pairing that reads as docked, and it is the ONLY correct answer inside a // row, which makes `variant:` at those call sites a parameter whose every // value but one is a bug. // // So the row publishes the fact, and TextField/Select resolve against it. This // is the same trick PropertyLabelScope plays one widget up - the row knows // something the control cant see, and hands it down rather than making every // call site repeat it. Unlike the label, though, this one is NOT a fallback: // an explicit `variant:` inside a row loses. Thats the point. class PropertySlotScope extends InheritedWidget { const PropertySlotScope({super.key, required super.child}); /// true when this control sits in a property row's control slot. static bool of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType() != null; } @override bool updateShouldNotify(PropertySlotScope old) => false; }