这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@Renzo-Olivares
Copy link
Contributor

@Renzo-Olivares Renzo-Olivares commented Mar 20, 2025

Currently when using a CustomScrollView, screen readers cannot list or move focus to elements that are outside the current Viewport and cache extent because we do not create semantic nodes for these elements.

This change introduces SliverEnsureSemantics which ensures its sliver child is included in the semantics tree, whether or not it is currently visible on the screen or within the cache extent. This way screen readers are aware the elements are there and can navigate to them / create accessibility traversal menus with this information.

  • Under the hood a new flag has been added to RenderSliver called ensureSemantics. RenderViewportBase uses this in its visitChildrenForSemantics to ensure a sliver is visited when creating the semantics tree. Previously a sliver was not visited if it was not visible or within the cache extent. RenderViewportBase also uses this in describeSemanticsClip and describeApproximatePaintClip to ensure a sliver child that wants to "ensure semantics" is not clipped out if it is not currently visible in the viewport or outside the cache extent.
  • RenderSliverMultiBoxAdaptor.semanticBounds now leverages its first child as an anchor for assistive technologies to be able to reach it if the Sliver is a child of SliverEnsureSemantics. If not it will still be dropped from the semantics tree.
  • RenderProxySliver now considers child overrides of semanticBounds.

On the engine side we move from using a joystick method to scroll with SemanticsAction.scrollUp and SemanticsAction.scrollDown to using SemanticsAction.scrollToOffset completely letting the browser drive the scrolling with its current dom scroll position "scrollTop" or "scrollLeft". This is possible by calculating the total quantity of content under the scrollable and sizing the scroll element based on that.

Code sample
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Flutter code sample for [SliverEnsureSemantics].

void main() => runApp(const SliverEnsureSemanticsExampleApp());

class SliverEnsureSemanticsExampleApp extends StatelessWidget {
  const SliverEnsureSemanticsExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: SliverEnsureSemanticsExample());
  }
}

class SliverEnsureSemanticsExample extends StatefulWidget {
  const SliverEnsureSemanticsExample({super.key});

  @override
  State<SliverEnsureSemanticsExample> createState() =>
      _SliverEnsureSemanticsExampleState();
}

class _SliverEnsureSemanticsExampleState
    extends State<SliverEnsureSemanticsExample> {
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.colorScheme.inversePrimary,
        title: const Text('SliverEnsureSemantics Demo'),
      ),
      body: Center(
        child: CustomScrollView(
          semanticChildCount: 106,
          slivers: <Widget>[
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 0,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Steps to reproduce',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          const Text('Issue description'),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Expected Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Actual Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Code Sample',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Screenshots',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Logs',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverFixedExtentList(
              itemExtent: 44.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text('Item $index'),
                    ),
                  );
                },
                childCount: 50,
                semanticIndexOffset: 1,
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 51,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 1'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 52,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 2'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 53,
                  child: Semantics(link: true, child: const Text('Link #1')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 54,
                  child: OverflowBar(
                    children: <Widget>[
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 1'),
                      ),
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 2'),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 55,
                  child: Semantics(link: true, child: const Text('Link #2')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverSemanticsList(
                sliver: SliverFixedExtentList(
                  itemExtent: 44.0,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Semantics(
                        role: SemanticsRole.listItem,
                        child: Card(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Text('Second List Item $index'),
                          ),
                        ),
                      );
                    },
                    childCount: 50,
                    semanticIndexOffset: 56,
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 107,
                  child: Semantics(link: true, child: const Text('Link #3')),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// A sliver that assigns the role of SemanticsRole.list to its sliver child.
class SliverSemanticsList extends SingleChildRenderObjectWidget {
  const SliverSemanticsList({super.key, required Widget sliver})
    : super(child: sliver);

  @override
  RenderSliverSemanticsList createRenderObject(BuildContext context) =>
      RenderSliverSemanticsList();
}

class RenderSliverSemanticsList extends RenderProxySliver {
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.role = SemanticsRole.list;
  }
}

Fixes: #160217

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: scrolling Viewports, list views, slivers, etc. platform-web Web applications specifically labels Mar 20, 2025
@Renzo-Olivares Renzo-Olivares force-pushed the a11y-screenreader-scrolling-fix branch from 109391c to 665bc40 Compare March 20, 2025 21:16
@Renzo-Olivares
Copy link
Contributor Author

The framework portion of this is ready for an initial review, i'm still making sure the web engine part of this is correct.

@Renzo-Olivares Renzo-Olivares force-pushed the a11y-screenreader-scrolling-fix branch 5 times, most recently from b93cf6c to 1e38a79 Compare March 27, 2025 00:43
@github-actions github-actions bot added the a: tests "flutter test", flutter_test, or one of our tests label Mar 27, 2025
@Renzo-Olivares Renzo-Olivares marked this pull request as ready for review March 27, 2025 03:09
@Renzo-Olivares Renzo-Olivares marked this pull request as draft March 27, 2025 03:10
@Renzo-Olivares Renzo-Olivares force-pushed the a11y-screenreader-scrolling-fix branch from 855e9cf to 81f50e7 Compare March 27, 2025 08:01
/// to ensure a sliver is included in the semantics tree regardless of its geometry.
bool get ensureSemantics => _ensureSemantics;
bool _ensureSemantics = false;
set ensureSemantics(bool value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this setter used any where? it is also a bit weird to implement setter for a property that is going to be override by subclass

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this setter.

(RenderSliver sliver) => sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0,
(RenderSliver sliver) =>
sliver.geometry!.visible ||
sliver.geometry!.cacheExtent > 0.0 ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like there as long as the sliver has cacheExtent they will also be preserved. can the SliverEnsureSemantics sets a cacheExtent in itself to avoid adding ensureSemantics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that may interfere with some of the logic in layoutChildSequence which uses a sliver's cacheExtent to determine the remaining cache extent.

if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}

Maybe with a small amount of items and setting cacheExtent to a very small number like 0.1 or less this does not have much impact, but I can see if someone has many this may become an issue. It seems strange that these invisible items would affect the remaining cache extent. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think relying on the slivers cacheExtent wouldn't work for the describeApproximatePaintClip and describeSemanticsClip case either since we wouldn't be able to differentiate between an item that is invisible but does not want to be clipped vs an item that is invisible but should be clipped, maybe if we use a magic number but not sure how stable that would be (would a normal sliver ever reach this magic number).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, if it has other implication then we better off implement a new api for this

RenderSliverToBoxAdapter({super.child});

@override
Rect get semanticBounds {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting I originally thought this was needed to avoid the node from being dropped by shouldDrop or sendSemanticsUpdate invisible checks, but that doesn't seem to be the case when I removed it now. The rect in the semantics tree is also accurate without this so I will remove it.

@github-actions github-actions bot removed the a: tests "flutter test", flutter_test, or one of our tests label Mar 27, 2025
@Renzo-Olivares Renzo-Olivares force-pushed the a11y-screenreader-scrolling-fix branch 2 times, most recently from 142f8b3 to c0fa504 Compare April 1, 2025 19:22
@Renzo-Olivares Renzo-Olivares marked this pull request as ready for review April 1, 2025 23:22
@Renzo-Olivares Renzo-Olivares requested a review from chunhtai April 1, 2025 23:42
@Renzo-Olivares Renzo-Olivares force-pushed the a11y-screenreader-scrolling-fix branch 3 times, most recently from 5fa67ff to 4ae2692 Compare April 3, 2025 17:18
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the API surface looks good to me, but i left some question about the geometry

void initState() {
// Scrolling is controlled by setting overflow-y/overflow-x to 'scroll`. The
// default overflow = "visible" needs to be unset.
semanticsObject.element.style.overflow = '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tangential to this pr but should the semantics scrollable be a role instead of a behavior? right now it seems hard to annotate other role like list with a scrollable. cc @yjbanov

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate? I imagine using a behavior would make it easier to assign a role. Scrollability seems to be an orthogonal concept to list. You can have scrollable and non-scrollable lists, and you can have scrollables that are not lists (although most often they are a list). For example, you can have scrollable tables, lists, and menus. But you can also have non-scrollable variants of those. To express that you can set the role independently of using the scrollable behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, right now SemanticScrollable is a SemanticRole. I'd argue that we should change that.

(RenderSliver sliver) => sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0,
(RenderSliver sliver) =>
sliver.geometry!.visible ||
sliver.geometry!.cacheExtent > 0.0 ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, if it has other implication then we better off implement a new api for this

}

/// Ensures its sliver child is included in the semantics tree.
class RenderSliverEnsureSemantics extends RenderProxySliver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider moving this to widget layer and keep this private

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been a while since i looked at the sliver code. Can we assume this child will some how gets laid out even if it is offscreen? my understand is that to get the correct gemoetry (to calculate paint extend) this has to be laid out by the parent. If so, can you point me to the code that this happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't necessarily assume that. For the case of my examples where a single item is wrapped with a SliverToBoxAdapter, this works because SliverToBoxAdapter always lays its child out.

The code is mainly here

double layoutChildSequence({
required RenderSliver? child,
required double scrollOffset,
required double overlap,
required double layoutOffset,
required double remainingPaintExtent,
required double mainAxisExtent,
required double crossAxisExtent,
required GrowthDirection growthDirection,
required RenderSliver? Function(RenderSliver child) advance,
required double remainingCacheExtent,
required double cacheOrigin,
}) {
assert(scrollOffset.isFinite);
assert(scrollOffset >= 0.0);
final double initialLayoutOffset = layoutOffset;
final ScrollDirection adjustedUserScrollDirection = applyGrowthDirectionToScrollDirection(
offset.userScrollDirection,
growthDirection,
);
double maxPaintOffset = layoutOffset + overlap;
double precedingScrollExtent = 0.0;
while (child != null) {
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
// If the scrollOffset is too small we adjust the paddedOrigin because it
// doesn't make sense to ask a sliver for content before its scroll
// offset.
final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
assert(sliverScrollOffset >= correctedCacheOrigin.abs());
assert(correctedCacheOrigin <= 0.0);
assert(sliverScrollOffset >= 0.0);
assert(cacheExtentCorrection <= 0.0);
child.layout(
SliverConstraints(
axisDirection: axisDirection,
growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection,
scrollOffset: sliverScrollOffset,
precedingScrollExtent: precedingScrollExtent,
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(
0.0,
remainingPaintExtent - layoutOffset + initialLayoutOffset,
),
crossAxisExtent: crossAxisExtent,
crossAxisDirection: crossAxisDirection,
viewportMainAxisExtent: mainAxisExtent,
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
cacheOrigin: correctedCacheOrigin,
),
parentUsesSize: true,
);
final SliverGeometry childLayoutGeometry = child.geometry!;
assert(childLayoutGeometry.debugAssertIsValid());
// If there is a correction to apply, we'll have to start over.
if (childLayoutGeometry.scrollOffsetCorrection != null) {
return childLayoutGeometry.scrollOffsetCorrection!;
}
// We use the child's paint origin in our coordinate system as the
// layoutOffset we store in the child's parent data.
final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
// `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
// because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
// 'scrollOffset` to roughly position these invisible slivers in the right order.
if (childLayoutGeometry.visible || scrollOffset > 0) {
updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
} else {
updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
}
maxPaintOffset = math.max(
effectiveLayoutOffset + childLayoutGeometry.paintExtent,
maxPaintOffset,
);
scrollOffset -= childLayoutGeometry.scrollExtent;
precedingScrollExtent += childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}
updateOutOfBandData(growthDirection, childLayoutGeometry);
// move on to the next child
child = advance(child);
}
// we made it without a correction, whee!
return 0.0;
}

From my understanding RenderViewportBase will call layout on all its children, but its up to the child to actually lay itself out. Lazily created slivers like RenderSliverList will not layout all their children, so this approach actually doesn't work for that case, even if wrapped with an SliverEnsureSemantics because the geometry will have an empty rect. I wonder if we can leverage that RenderSliverList always lays out its first child in order to calculate the total scroll extent, and use that as the semantic bounds while the lazy sliver is off screen.

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops how I explained it above is how it actually works by default right now. Since the RenderSliverList always builds its first child those are its current bounds, if you wrap a SliverList with SliverEnsureSemantics, this ensures that RenderViewportBase still visits its in visitChildrenForSemantics and ensures that it doesn't get clipped out in the describeSemanticsClip or describeApproximateClipRect.

So for example

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(const MyApp());
  SemanticsBinding.instance.ensureSemantics();
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: CustomScrollView(
          semanticChildCount: 52,
          slivers: <Widget>[
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 0,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Steps to reproduce',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Text('Issue description'),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Expected Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Actual Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Code Sample',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Screenshots',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Logs',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text('Item $index'),
                    ),
                  );
                },
                childCount: 50,
                semanticIndexOffset: 1,
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 52,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(header: true, child: Text('Footer 1')),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 53,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(header: true, child: Text('Footer 2')),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 54,
                  child: Semantics(link: true, child: Text('Link #1')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 55,
                  child: OverflowBar(
                    children: <Widget>[
                      TextButton(onPressed: () {}, child: Text('Button 1')),
                      TextButton(onPressed: () {}, child: Text('Button 2')),
                    ],
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 56,
                  child: Semantics(link: true, child: Text('Link #2')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    return Semantics(
                      role: SemanticsRole.listItem,
                      child: Card(
                        child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text('Second List Item $index'),
                        ),
                      ),
                    );
                  },
                  childCount: 50,
                  semanticIndexOffset: 56,
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 107,
                  child: Semantics(link: true, child: Text('Link #3')),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

this works because the second list still lays out its first child, so the browser has this information and can jump to it. (With the caveat that this is not possible yet without hardcoding the role in RenderSliverList to SemanticsRole.list, but I am working on this functionality in a follow up PR).

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disregard my last comment, I had some test code that made this work while trying this, currently RenderSliverList reports empty bounds even though it lays out its first child. I still think we can leverage the first child to use as an initial anchor for semantics. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline, concluded it is reasonable to use the first child bounds as the semantic bounds while the sliver is off screen.

/// to ensure a sliver is included in the semantics tree regardless of its geometry.
///
/// [RenderSliverEnsureSemantics] overrides this value to `true` to ensure
/// its sliver child is included in the semantics tree.
Copy link
Contributor

@chunhtai chunhtai Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhere in the doc should explain the following

  1. the difference between this and off screen rendersliver that was laid out due to cache extent
  2. A typical example why someone may want to override this getter to return true.
  3. The implication when return true. ie. element may be built and probably(?) laid out

/// Whether this sliver should be included in the semantics tree.
///
/// This value is used by [RenderViewportBase.visitChildrenForSemantics]
/// to ensure a sliver is included in the semantics tree regardless of its geometry.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regardless of its geometry

does this mean the geometry may not be accurate?

I was assuming the geometry must be accurate, at least the scroll extent. Otherwise, how would the engine calculate the amount it needs to scroll to review this item?

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the geometry may not be accurate for example for lazy slivers, but for lazy slivers like SliverList, we always lay out at least the first child so we can estimate the total scroll extent. The first childs rect should be enough information to move the list into view.

edit: Disregard this comment, I was running some test code when I tried this that made this work. Essentially EnsureSliverSemantics will ensure semantics at the RenderViewportbase level, but it is still up to a sliver to provide valid semanticBounds. In my testing RenderSliverToBoxAdapter does this since it always lays out its child, but RenderSliverList does not so even if wrapped with EnsureSliverSemantics it will not be included because the underlying sliver does not provide valid semantic bounds (it probably should since it always lays out its first child).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For lazy slivers like SliverList, this PR now leverages the fact that the first child is always laid out as an anchor for semantics to bring the list into view.

@github-actions github-actions bot added the d: api docs Issues with https://api.flutter.dev/ label Apr 4, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 12, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 12, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 13, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Apr 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 14, 2025
romanejaquez pushed a commit to romanejaquez/flutter that referenced this pull request Aug 14, 2025
Currently when using a `CustomScrollView`, screen readers cannot list or
move focus to elements that are outside the current Viewport and cache
extent because we do not create semantic nodes for these elements.

This change introduces `SliverEnsureSemantics` which ensures its sliver
child is included in the semantics tree, whether or not it is currently
visible on the screen or within the cache extent. This way screen
readers are aware the elements are there and can navigate to them /
create accessibility traversal menus with this information.
* Under the hood a new flag has been added to `RenderSliver` called
`ensureSemantics`. `RenderViewportBase` uses this in its
`visitChildrenForSemantics` to ensure a sliver is visited when creating
the semantics tree. Previously a sliver was not visited if it was not
visible or within the cache extent. `RenderViewportBase` also uses this
in `describeSemanticsClip` and `describeApproximatePaintClip` to ensure
a sliver child that wants to "ensure semantics" is not clipped out if it
is not currently visible in the viewport or outside the cache extent.
* `RenderSliverMultiBoxAdaptor.semanticBounds` now leverages its first
child as an anchor for assistive technologies to be able to reach it if
the Sliver is a child of `SliverEnsureSemantics`. If not it will still
be dropped from the semantics tree.
* `RenderProxySliver` now considers child overrides of `semanticBounds`.

On the engine side we move from using a joystick method to scroll with
`SemanticsAction.scrollUp` and `SemanticsAction.scrollDown` to using
`SemanticsAction.scrollToOffset` completely letting the browser drive
the scrolling with its current dom scroll position "scrollTop" or
"scrollLeft". This is possible by calculating the total quantity of
content under the scrollable and sizing the scroll element based on
that.

<details open><summary>Code sample</summary>

```dart
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Flutter code sample for [SliverEnsureSemantics].

void main() => runApp(const SliverEnsureSemanticsExampleApp());

class SliverEnsureSemanticsExampleApp extends StatelessWidget {
  const SliverEnsureSemanticsExampleApp({super.key});

  @OverRide
  Widget build(BuildContext context) {
    return const MaterialApp(home: SliverEnsureSemanticsExample());
  }
}

class SliverEnsureSemanticsExample extends StatefulWidget {
  const SliverEnsureSemanticsExample({super.key});

  @OverRide
  State<SliverEnsureSemanticsExample> createState() =>
      _SliverEnsureSemanticsExampleState();
}

class _SliverEnsureSemanticsExampleState
    extends State<SliverEnsureSemanticsExample> {
  @OverRide
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.colorScheme.inversePrimary,
        title: const Text('SliverEnsureSemantics Demo'),
      ),
      body: Center(
        child: CustomScrollView(
          semanticChildCount: 106,
          slivers: <Widget>[
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 0,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Steps to reproduce',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          const Text('Issue description'),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Expected Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Actual Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Code Sample',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Screenshots',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Logs',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverFixedExtentList(
              itemExtent: 44.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text('Item $index'),
                    ),
                  );
                },
                childCount: 50,
                semanticIndexOffset: 1,
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 51,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 1'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 52,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 2'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 53,
                  child: Semantics(link: true, child: const Text('Link flutter#1')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 54,
                  child: OverflowBar(
                    children: <Widget>[
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 1'),
                      ),
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 2'),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 55,
                  child: Semantics(link: true, child: const Text('Link flutter#2')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverSemanticsList(
                sliver: SliverFixedExtentList(
                  itemExtent: 44.0,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Semantics(
                        role: SemanticsRole.listItem,
                        child: Card(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Text('Second List Item $index'),
                          ),
                        ),
                      );
                    },
                    childCount: 50,
                    semanticIndexOffset: 56,
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 107,
                  child: Semantics(link: true, child: const Text('Link flutter#3')),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// A sliver that assigns the role of SemanticsRole.list to its sliver child.
class SliverSemanticsList extends SingleChildRenderObjectWidget {
  const SliverSemanticsList({super.key, required Widget sliver})
    : super(child: sliver);

  @OverRide
  RenderSliverSemanticsList createRenderObject(BuildContext context) =>
      RenderSliverSemanticsList();
}

class RenderSliverSemanticsList extends RenderProxySliver {
  @OverRide
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.role = SemanticsRole.list;
  }
}
```
</details>

Fixes: flutter#160217

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Renzo Olivares <roliv@google.com>
romanejaquez pushed a commit to romanejaquez/flutter that referenced this pull request Aug 14, 2025
<!-- start_original_pr_link -->
Reverts: flutter#165589
<!-- end_original_pr_link -->
<!-- start_initiating_author -->
Initiated by: Renzo-Olivares
<!-- end_initiating_author -->
<!-- start_revert_reason -->
Reason for reverting: breaking internal tests
<!-- end_revert_reason -->
<!-- start_original_pr_author -->
Original PR Author: Renzo-Olivares
<!-- end_original_pr_author -->

<!-- start_reviewers -->
Reviewed By: {Piinks}
<!-- end_reviewers -->

<!-- start_revert_body -->
This change reverts the following previous change:
Currently when using a `CustomScrollView`, screen readers cannot list or
move focus to elements that are outside the current Viewport and cache
extent because we do not create semantic nodes for these elements.

This change introduces `SliverEnsureSemantics` which ensures its sliver
child is included in the semantics tree, whether or not it is currently
visible on the screen or within the cache extent. This way screen
readers are aware the elements are there and can navigate to them /
create accessibility traversal menus with this information.
* Under the hood a new flag has been added to `RenderSliver` called
`ensureSemantics`. `RenderViewportBase` uses this in its
`visitChildrenForSemantics` to ensure a sliver is visited when creating
the semantics tree. Previously a sliver was not visited if it was not
visible or within the cache extent. `RenderViewportBase` also uses this
in `describeSemanticsClip` and `describeApproximatePaintClip` to ensure
a sliver child that wants to "ensure semantics" is not clipped out if it
is not currently visible in the viewport or outside the cache extent.
* `RenderSliverMultiBoxAdaptor.semanticBounds` now leverages its first
child as an anchor for assistive technologies to be able to reach it if
the Sliver is a child of `SliverEnsureSemantics`. If not it will still
be dropped from the semantics tree.
* `RenderProxySliver` now considers child overrides of `semanticBounds`.

On the engine side we move from using a joystick method to scroll with
`SemanticsAction.scrollUp` and `SemanticsAction.scrollDown` to using
`SemanticsAction.scrollToOffset` completely letting the browser drive
the scrolling with its current dom scroll position "scrollTop" or
"scrollLeft". This is possible by calculating the total quantity of
content under the scrollable and sizing the scroll element based on
that.

<details open><summary>Code sample</summary>

```dart
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Flutter code sample for [SliverEnsureSemantics].

void main() => runApp(const SliverEnsureSemanticsExampleApp());

class SliverEnsureSemanticsExampleApp extends StatelessWidget {
  const SliverEnsureSemanticsExampleApp({super.key});

  @OverRide
  Widget build(BuildContext context) {
    return const MaterialApp(home: SliverEnsureSemanticsExample());
  }
}

class SliverEnsureSemanticsExample extends StatefulWidget {
  const SliverEnsureSemanticsExample({super.key});

  @OverRide
  State<SliverEnsureSemanticsExample> createState() =>
      _SliverEnsureSemanticsExampleState();
}

class _SliverEnsureSemanticsExampleState
    extends State<SliverEnsureSemanticsExample> {
  @OverRide
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.colorScheme.inversePrimary,
        title: const Text('SliverEnsureSemantics Demo'),
      ),
      body: Center(
        child: CustomScrollView(
          semanticChildCount: 106,
          slivers: <Widget>[
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 0,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Steps to reproduce',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          const Text('Issue description'),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Expected Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Actual Results',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Code Sample',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Screenshots',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                          Semantics(
                            header: true,
                            headingLevel: 3,
                            child: Text(
                              'Logs',
                              style: theme.textTheme.headlineSmall,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverFixedExtentList(
              itemExtent: 44.0,
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text('Item $index'),
                    ),
                  );
                },
                childCount: 50,
                semanticIndexOffset: 1,
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 51,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 1'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 52,
                  child: Card(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Semantics(
                        header: true,
                        child: const Text('Footer 2'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 53,
                  child: Semantics(link: true, child: const Text('Link flutter#1')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 54,
                  child: OverflowBar(
                    children: <Widget>[
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 1'),
                      ),
                      TextButton(
                        onPressed: () {},
                        child: const Text('Button 2'),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 55,
                  child: Semantics(link: true, child: const Text('Link flutter#2')),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverSemanticsList(
                sliver: SliverFixedExtentList(
                  itemExtent: 44.0,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Semantics(
                        role: SemanticsRole.listItem,
                        child: Card(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Text('Second List Item $index'),
                          ),
                        ),
                      );
                    },
                    childCount: 50,
                    semanticIndexOffset: 56,
                  ),
                ),
              ),
            ),
            SliverEnsureSemantics(
              sliver: SliverToBoxAdapter(
                child: IndexedSemantics(
                  index: 107,
                  child: Semantics(link: true, child: const Text('Link flutter#3')),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// A sliver that assigns the role of SemanticsRole.list to its sliver child.
class SliverSemanticsList extends SingleChildRenderObjectWidget {
  const SliverSemanticsList({super.key, required Widget sliver})
    : super(child: sliver);

  @OverRide
  RenderSliverSemanticsList createRenderObject(BuildContext context) =>
      RenderSliverSemanticsList();
}

class RenderSliverSemanticsList extends RenderProxySliver {
  @OverRide
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.role = SemanticsRole.list;
  }
}
```
</details>

Fixes: flutter#160217

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
<!-- end_revert_body -->

Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com>
romanejaquez pushed a commit to romanejaquez/flutter that referenced this pull request Aug 14, 2025
This reverts commit 2fc716d, and updates the cross-axis size of the
`_scrollOverflowElement` to be 1px (non-zero), so it is taken into
account by the scrollable elements scrollHeight.

Fixes flutter#160217

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Renzo Olivares <roliv@google.com>
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 15, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos engine flutter/engine related. See also e: labels. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. platform-web Web applications specifically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Screen readers cannot list or move focus to elements outside of the current viewport

4 participants