boxy icon indicating copy to clipboard operation
boxy copied to clipboard

`visitChildrenForSemantics` doesn't respect `BoxyDelegate.children` order

Open Albert221 opened this issue 2 months ago • 4 comments

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.

Image

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);
      }
    }
  }
Image
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

Albert221 avatar Oct 30 '25 17:10 Albert221