`visitChildrenForSemantics` doesn't respect `BoxyDelegate.children` order
Hey, issue pretty critical for screen reader's users. If I'm inflate'ing a widget in the delegate, the inflated widget is in the end of child handles list. For painting and hit-testing I just overriden the BoxyDelegate.children to return the BoxyChildren in the order I wanted. But the visitChildrenForSemantics doesn't respect this order, it goes using the child handles order, which is not always correct.
Below is a small reproduction example. If you take a look at the app with Accessibility Inspector, you'll see that you cannot really get the button's semantics node, because it's completely obstructed by other semantics node. But only in semantics tree, as the visitChildrenForSemantics used the original (wrong) order.
If I manually edit the RenderBoxyMixin.visitChildrenForSemantics to the below, I am able to select the button with semantic hit testing (screenshot below)
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
for (final child in childHandleMap.values.toList().reversed) {
if (!child._ignore) {
visitor(child.render);
}
}
}
Code for reproduction
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:boxy/boxy.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: BodyAndFooter(
bodyBuilder:
(context, footerHeight) => ColoredBox(
color: Colors.greenAccent,
child: ListView(
children: [
Text('text1'),
Placeholder(),
Text('text2'),
Placeholder(),
Text('text3'),
SizedBox(height: footerHeight),
],
),
),
footer: Container(
padding: const EdgeInsets.all(16),
color: Colors.red.withValues(alpha: 0.5),
child: ElevatedButton(
onPressed: () {
debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest);
},
child: Text('Footer'),
),
),
),
),
);
}
}
class BodyAndFooter extends StatelessWidget {
const BodyAndFooter({
super.key,
required this.bodyBuilder,
required this.footer,
});
final Widget Function(BuildContext context, double footerHeight) bodyBuilder;
final Widget footer;
@override
Widget build(BuildContext context) {
return CustomBoxy(
delegate: _BodyAndFooterDelegate(bodyBuilder: bodyBuilder),
children: [footer],
);
}
}
class _BodyAndFooterDelegate extends BoxyDelegate {
_BodyAndFooterDelegate({required this.bodyBuilder});
final Widget Function(BuildContext context, double footerHeight) bodyBuilder;
BoxyChild get footer => children.first;
@override
Size layout() {
final rect = Offset.zero & constraints.biggest;
footer.layout(constraints.tighten(width: rect.width));
footer.positionRect(rect, Alignment.bottomCenter);
final body = inflate(
bodyBuilder(buildContext, footer.size.height),
id: #body,
);
body.layoutRect(rect);
return rect.size;
}
@override
List<BoxyChild> get children => super.children.reversed.toList();
@override
bool shouldRelayout(_BodyAndFooterDelegate oldDelegate) =>
bodyBuilder != oldDelegate.bodyBuilder;
}
For debugging and reproduction, remember that there is some issue with iOS simulator where hot-restart doesn't really update the semantics tree properly. kill the app and run it again after making the change to semantics tree