Unverified Commit 58eb0e8a authored by hangyu's avatar hangyu Committed by GitHub

Update a11y for SliverAppBar (#144437)

1. Set cacheExtent for sliverAppBar so it's not dropped from the
semantics tree.
2. Update its toolbarOpacity in a11y mode to 1.0. When scrolling in a11y
mode and the focus is back to the sliverAppBar, the content should be
visible.
fixes: https://github.com/flutter/flutter/issues/143437


## 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.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
[Data Driven Fixes]:
https://github.com/flutter/flutter/wiki/Data-driven-Fixes
parent eeff96d4
......@@ -1199,6 +1199,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
required this.forceMaterialTransparency,
required this.clipBehavior,
required this.variant,
required this.accessibleNavigation,
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
......@@ -1236,6 +1237,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final bool forceMaterialTransparency;
final Clip? clipBehavior;
final _SliverAppVariant variant;
final bool accessibleNavigation;
@override
double get minExtent => collapsedHeight;
......@@ -1263,7 +1265,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final bool isScrolledUnder = overlapsContent || forceElevated || (pinned && shrinkOffset > maxExtent - minExtent);
final bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0;
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
final double toolbarOpacity = !accessibleNavigation && (!pinned || isPinnedWithOpacityFade)
? clampDouble(visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight), 0.0, 1.0)
: 1.0;
final Widget? effectiveTitle = switch (variant) {
......@@ -1354,7 +1356,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| toolbarTextStyle != oldDelegate.toolbarTextStyle
|| titleTextStyle != oldDelegate.titleTextStyle
|| systemOverlayStyle != oldDelegate.systemOverlayStyle
|| forceMaterialTransparency != oldDelegate.forceMaterialTransparency;
|| forceMaterialTransparency != oldDelegate.forceMaterialTransparency
|| accessibleNavigation != oldDelegate.accessibleNavigation;
}
@override
......@@ -2036,6 +2039,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
variant: widget._variant,
accessibleNavigation: MediaQuery.of(context).accessibleNavigation,
),
),
);
......
......@@ -348,7 +348,14 @@ abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersist
}
final double maxExtent = this.maxExtent;
final double paintExtent = maxExtent - constraints.scrollOffset;
final double cacheExtent = calculateCacheOffset(
constraints,
from: 0.0,
to: maxExtent,
);
geometry = SliverGeometry(
cacheExtent: cacheExtent,
scrollExtent: maxExtent,
paintOrigin: math.min(constraints.overlap, 0.0),
paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
......
......@@ -27,6 +27,67 @@ void main() {
expect(render.text.style!.color!.opacity, 0.0);
});
testWidgets('a11y mode ===> 1.0 opacity', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(accessibleNavigation: true),
child: _TestWidget(
pinned: false,
floating: false,
bottom: false,
controller: controller,
),
),
);
final RenderParagraph render = tester.renderObject(find.text('Hallo Welt!!1'));
expect(render.text.style!.color!.opacity, 1.0);
controller.jumpTo(100.0);
await tester.pumpAndSettle();
expect(render.text.style!.color!.opacity, 1.0);
});
testWidgets('turn on/off a11y mode to change opacity', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
addTearDown(tester.platformDispatcher.clearAllTestValues);
addTearDown(tester.view.reset);
tester.platformDispatcher
..textScaleFactorTestValue = 123
..platformBrightnessTestValue = Brightness.dark
..accessibilityFeaturesTestValue = const FakeAccessibilityFeatures();
await tester.pumpWidget(
_TestWidget(
pinned: false,
floating: false,
bottom: false,
controller: controller,
),
);
// AccessibleNavigation is off
final RenderParagraph render = tester.renderObject(find.text('Hallo Welt!!1'));
controller.jumpTo(100.0);
await tester.pumpAndSettle();
expect(render.text.style!.color!.opacity < 1.0, true);
// Turn on accessibleNavigation
tester.platformDispatcher.accessibilityFeaturesTestValue =
const FakeAccessibilityFeatures(accessibleNavigation: true);
await tester.pumpAndSettle();
expect(render.text.style!.color!.opacity, 1.0);
// Turn off accessibleNavigation
tester.platformDispatcher.accessibilityFeaturesTestValue =
const FakeAccessibilityFeatures();
await tester.pumpAndSettle();
expect(render.text.style!.color!.opacity < 1.0, true);
});
testWidgets('!pinned && !floating && bottom ==> fade opacity', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
......
......@@ -69,7 +69,7 @@ void main() {
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.animateTo(RenderBigSliver.height + delegate.maxExtent - 5.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
final RenderBox box = tester.renderObject<RenderBox>(find.byType(Container));
final RenderBox box = tester.renderObject<RenderBox>(find.text('Sliver App Bar'));
final Rect rect = Rect.fromPoints(box.localToGlobal(Offset.zero), box.localToGlobal(box.size.bottomRight(Offset.zero)));
expect(rect, equals(const Rect.fromLTWH(0.0, -195.0, 800.0, 200.0)));
});
......@@ -95,14 +95,14 @@ void main() {
),
);
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.jumpTo(-50.0);
await tester.pump();
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
});
......@@ -127,16 +127,148 @@ void main() {
),
);
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.jumpTo(-50.0);
await tester.pump();
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
});
group('has correct semantics when', () {
testWidgets('within viewport', (WidgetTester tester) async {
const double cacheExtent = 250;
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
cacheExtent: cacheExtent,
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverPersistentHeader(delegate: TestDelegate()),
const SliverList(
delegate: SliverChildListDelegate.fixed(<Widget>[
SizedBox(
height: 300.0,
child: Text('X'),
),
]),
),
],
),
),
);
final SemanticsFinder sliverAppBar = find.semantics.byLabel(
'Sliver App Bar',
);
expect(sliverAppBar, findsOne);
expect(sliverAppBar, containsSemantics(isHidden: false));
handle.dispose();
});
testWidgets('partially scrolling off screen', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestDelegate delegate = TestDelegate();
final SemanticsHandle handle = tester.ensureSemantics();
const double cacheExtent = 250;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
cacheExtent: cacheExtent,
slivers: <Widget>[
SliverPersistentHeader(key: key, delegate: delegate),
const BigSliver(),
const BigSliver(),
],
),
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.animateTo(delegate.maxExtent - 20.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
final RenderBox box = tester.renderObject<RenderBox>(find.text('Sliver App Bar'));
final Rect rect = Rect.fromPoints(box.localToGlobal(Offset.zero), box.localToGlobal(box.size.bottomRight(Offset.zero)));
expect(rect, equals(const Rect.fromLTWH(0.0, -180.0, 800.0, 200.0)));
final SemanticsFinder sliverAppBar = find.semantics.byLabel(
'Sliver App Bar',
);
expect(sliverAppBar, findsOne);
expect(sliverAppBar, containsSemantics(isHidden: false));
handle.dispose();
});
testWidgets('completely scrolling off screen but within cache extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestDelegate delegate = TestDelegate();
final SemanticsHandle handle = tester.ensureSemantics();
const double cacheExtent = 250;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
cacheExtent: cacheExtent,
slivers: <Widget>[
SliverPersistentHeader(key: key, delegate: delegate),
const BigSliver(),
const BigSliver(),
],
),
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.animateTo(delegate.maxExtent + 20.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
final SemanticsFinder sliverAppBar = find.semantics.byLabel(
'Sliver App Bar',
);
expect(sliverAppBar, findsOne);
expect(sliverAppBar, containsSemantics(isHidden: true));
handle.dispose();
});
testWidgets('completely scrolling off screen and not within cache extent', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestDelegate delegate = TestDelegate();
final SemanticsHandle handle = tester.ensureSemantics();
const double cacheExtent = 250;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
cacheExtent: cacheExtent,
slivers: <Widget>[
SliverPersistentHeader(key: key, delegate: delegate),
const BigSliver(),
const BigSliver(),
],
),
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.animateTo(delegate.maxExtent + 300.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
final SemanticsFinder sliverAppBar = find.semantics.byLabel(
'Sliver App Bar',
);
expect(sliverAppBar, findsNothing);
handle.dispose();
});
});
}
class TestDelegate extends SliverPersistentHeaderDelegate {
......@@ -148,7 +280,7 @@ class TestDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(height: maxExtent);
return SizedBox(height: maxExtent, child: const Text('Sliver App Bar'),);
}
@override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment