diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index d07219de9e..2ddf3f8439 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -49,6 +49,10 @@ typedef SetSelectionHandler = void Function(TextSelection selection); /// current text with the input `text`. typedef SetTextHandler = void Function(String text); +/// Signature for the [SemanticsAction.scrollToOffset] handlers to scroll the +/// scrollable container to the given `targetOffset`. +typedef ScrollToOffsetHandler = void Function(Offset targetOffset); + /// Signature for a handler of a [SemanticsAction]. /// /// Returned by [SemanticsConfiguration.getActionHandler]. @@ -3920,6 +3924,29 @@ class SemanticsConfiguration { _onScrollDown = value; } + /// The handler for [SemanticsAction.scrollToOffset]. + /// + /// This handler is only called on iOS by UIKit, when the iOS focus engine + /// switches its focus to an item too close to a scrollable edge of a + /// scrollable container, to make sure the focused item is always fully + /// visible. + /// + /// The callback, if not `null`, should typically set the scroll offset of + /// the associated scrollable container to the given `targetOffset` without + /// animation as it is already animated by the caller: the iOS focus engine + /// invokes [onScrollToOffset] every frame during the scroll animation with + /// animated scroll offset values. + ScrollToOffsetHandler? get onScrollToOffset => _onScrollToOffset; + ScrollToOffsetHandler? _onScrollToOffset; + set onScrollToOffset(ScrollToOffsetHandler? value) { + assert(value != null); + _addAction(SemanticsAction.scrollToOffset, (Object? args) { + final Float64List list = args! as Float64List; + value!(Offset(list[0], list[1])); + }); + _onScrollToOffset = value; + } + /// The handler for [SemanticsAction.increase]. /// /// This is a request to increase the value represented by the widget. For diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index bd00ea99cd..22df141e9c 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -611,12 +611,10 @@ class ScrollableState extends State with TickerProviderStateMixin, R // Only call this from places that will definitely trigger a rebuild. void _updatePosition() { _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context); - _physics = _configuration.getScrollPhysics(context); - if (widget.physics != null) { - _physics = widget.physics!.applyTo(_physics); - } else if (widget.scrollBehavior != null) { - _physics = widget.scrollBehavior!.getScrollPhysics(context).applyTo(_physics); - } + final ScrollPhysics? physicsFromWidget = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); + _physics = _configuration.getScrollPhysics(context); + _physics = physicsFromWidget?.applyTo(_physics) ?? _physics; + final ScrollPosition? oldPosition = _position; if (oldPosition != null) { _effectiveScrollController.detach(oldPosition); @@ -1032,6 +1030,7 @@ class ScrollableState extends State with TickerProviderStateMixin, R key: _scrollSemanticsKey, position: position, allowImplicitScrolling: _physics!.allowImplicitScrolling, + axis: widget.axis, semanticChildCount: widget.semanticChildCount, child: result, ) @@ -1556,6 +1555,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { super.key, required this.position, required this.allowImplicitScrolling, + required this.axis, required this.semanticChildCount, super.child, }) : assert(semanticChildCount == null || semanticChildCount >= 0); @@ -1563,6 +1563,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { final ScrollPosition position; final bool allowImplicitScrolling; final int? semanticChildCount; + final Axis axis; @override _RenderScrollSemantics createRenderObject(BuildContext context) { @@ -1570,6 +1571,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { position: position, allowImplicitScrolling: allowImplicitScrolling, semanticChildCount: semanticChildCount, + axis: axis, ); } @@ -1577,6 +1579,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) { renderObject ..allowImplicitScrolling = allowImplicitScrolling + ..axis = axis ..position = position ..semanticChildCount = semanticChildCount; } @@ -1586,6 +1589,7 @@ class _RenderScrollSemantics extends RenderProxyBox { _RenderScrollSemantics({ required ScrollPosition position, required bool allowImplicitScrolling, + required this.axis, required int? semanticChildCount, RenderBox? child, }) : _position = position, @@ -1619,6 +1623,8 @@ class _RenderScrollSemantics extends RenderProxyBox { markNeedsSemanticsUpdate(); } + Axis axis; + int? get semanticChildCount => _semanticChildCount; int? _semanticChildCount; set semanticChildCount(int? value) { @@ -1629,6 +1635,14 @@ class _RenderScrollSemantics extends RenderProxyBox { markNeedsSemanticsUpdate(); } + void _onScrollToOffset(Offset targetOffset) { + final double offset = switch (axis) { + Axis.horizontal => targetOffset.dx, + Axis.vertical => targetOffset.dy, + }; + _position.jumpTo(offset); + } + @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); @@ -1640,6 +1654,9 @@ class _RenderScrollSemantics extends RenderProxyBox { ..scrollExtentMax = _position.maxScrollExtent ..scrollExtentMin = _position.minScrollExtent ..scrollChildCount = semanticChildCount; + if (position.maxScrollExtent > position.minScrollExtent && allowImplicitScrolling) { + config.onScrollToOffset = _onScrollToOffset; + } } } diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart index a98f76a57f..093e63a2d6 100644 --- a/packages/flutter/test/material/flexible_space_bar_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_test.dart @@ -333,7 +333,7 @@ void main() { id: 14, flags: [SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 5, @@ -441,7 +441,7 @@ void main() { id: 14, flags: [SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, - actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 5, @@ -598,7 +598,7 @@ void main() { id: 14, flags: [SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 5, @@ -706,7 +706,7 @@ void main() { id: 14, flags: [SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, - actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 5, diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 9d56ef3786..e51a014687 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -3677,7 +3677,7 @@ void main() { const String tab10title = 'This is a very wide tab #10\nTab 11 of 20'; const List hiddenFlags = [SemanticsFlag.isHidden, SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState]; - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset])); expect(semantics, includesNodeWith(label: tab0title)); expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags)); @@ -3685,18 +3685,18 @@ void main() { await tester.pumpAndSettle(); expect(semantics, includesNodeWith(label: tab0title, flags: hiddenFlags)); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollRight])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollRight, SemanticsAction.scrollToOffset])); expect(semantics, includesNodeWith(label: tab10title)); controller.index = 19; await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight, SemanticsAction.scrollToOffset])); controller.index = 0; await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset])); expect(semantics, includesNodeWith(label: tab0title)); expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags)); diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index c1e5976f00..c92f5d25d3 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -908,6 +908,7 @@ void main() { expect(config.onScrollUp, isNull); expect(config.onScrollLeft, isNull); expect(config.onScrollRight, isNull); + expect(config.onScrollToOffset, isNull); expect(config.onLongPress, isNull); expect(config.onDecrease, isNull); expect(config.onIncrease, isNull); @@ -932,6 +933,7 @@ void main() { void onScrollUp() { } void onScrollLeft() { } void onScrollRight() { } + void onScrollToOffset(Offset _) { } void onLongPress() { } void onDecrease() { } void onIncrease() { } @@ -945,6 +947,7 @@ void main() { config.onScrollUp = onScrollUp; config.onScrollLeft = onScrollLeft; config.onScrollRight = onScrollRight; + config.onScrollToOffset = onScrollToOffset; config.onLongPress = onLongPress; config.onDecrease = onDecrease; config.onIncrease = onIncrease; @@ -969,6 +972,7 @@ void main() { expect(config.onScrollUp, same(onScrollUp)); expect(config.onScrollLeft, same(onScrollLeft)); expect(config.onScrollRight, same(onScrollRight)); + expect(config.onScrollToOffset, same(onScrollToOffset)); expect(config.onLongPress, same(onLongPress)); expect(config.onDecrease, same(onDecrease)); expect(config.onIncrease, same(onIncrease)); diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart index 23d870a988..6e7ed76e61 100644 --- a/packages/flutter/test/widgets/custom_painter_test.dart +++ b/packages/flutter/test/widgets/custom_painter_test.dart @@ -356,9 +356,7 @@ void _defineTests() { final Set allActions = SemanticsAction.values.toSet() ..remove(SemanticsAction.customAction) // customAction is not user-exposed. ..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed - // TODO(LongCatIsLooong): change to `SemanticsAction.scrollToOffset` when available. - // https://github.com/flutter/flutter/issues/159515. - ..removeWhere((SemanticsAction action) => action.index == 1 << 23); + ..remove(SemanticsAction.scrollToOffset); // scrollToOffset is not user-exposed const int expectedId = 2; final TestSemantics expectedSemantics = TestSemantics.root( @@ -381,9 +379,6 @@ void _defineTests() { final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; int expectedLength = 1; for (final SemanticsAction action in allActions) { - // TODO(LongCatIsLooong): remove after `SemanticsAction.scrollToOffset` is added to dart:ui. - // https://github.com/flutter/flutter/issues/159515. - // ignore: exhaustive_cases switch (action) { case SemanticsAction.moveCursorBackwardByCharacter: case SemanticsAction.moveCursorForwardByCharacter: @@ -411,6 +406,7 @@ void _defineTests() { case SemanticsAction.scrollLeft: case SemanticsAction.scrollRight: case SemanticsAction.scrollUp: + case SemanticsAction.scrollToOffset: case SemanticsAction.showOnScreen: case SemanticsAction.tap: case SemanticsAction.focus: diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index b2999e5fbf..57c3b04a79 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -3362,7 +3362,7 @@ void main() { TestSemantics( id: 9, flags: [SemanticsFlag.hasImplicitScrolling], - actions: [SemanticsAction.scrollLeft], + actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 5, @@ -3430,6 +3430,7 @@ void main() { children: [ TestSemantics( id: 9, + actions: [SemanticsAction.scrollToOffset], flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( diff --git a/packages/flutter/test/widgets/list_view_semantics_test.dart b/packages/flutter/test/widgets/list_view_semantics_test.dart index 88727fff1f..4841fa9b4c 100644 --- a/packages/flutter/test/widgets/list_view_semantics_test.dart +++ b/packages/flutter/test/widgets/list_view_semantics_test.dart @@ -35,12 +35,12 @@ void main() { ), ); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset])); // Jump to the end. controller.jumpTo(itemCount * itemHeight); await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown, SemanticsAction.scrollToOffset])); semantics.dispose(); }); @@ -67,12 +67,12 @@ void main() { ), ); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown, SemanticsAction.scrollToOffset])); // Jump to the end. controller.jumpTo(itemCount * itemHeight); await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset])); semantics.dispose(); }); @@ -99,12 +99,12 @@ void main() { ), ); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset])); // Jump to the end. controller.jumpTo(itemCount * itemHeight); await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight, SemanticsAction.scrollToOffset])); semantics.dispose(); }); @@ -132,12 +132,12 @@ void main() { ), ); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollRight, SemanticsAction.scrollToOffset])); // Jump to the end. controller.jumpTo(itemCount * itemHeight); await tester.pumpAndSettle(); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset])); semantics.dispose(); }); diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index c8486ff97a..fdcc0e532b 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -25,23 +26,99 @@ void main() { ), ); - expect(semantics,includesNodeWith(actions: [SemanticsAction.scrollUp])); + expect(semantics,includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset])); await flingUp(tester); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset])); await flingDown(tester, repetitions: 2); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset])); await flingUp(tester, repetitions: 5); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown, SemanticsAction.scrollToOffset])); await flingDown(tester); - expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset])); semantics.dispose(); }); + testWidgets('Vertical scrollable responds to scrollToOffset', (WidgetTester tester) async { + semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + controller: controller, + children: List.generate(60, (int i) => Text('$i')), + ), + ), + ); + final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; + final int scrollableId = semantics.nodesWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]).single.id; + + assert(controller.offset == 0); + semanticsOwner.performAction(scrollableId, SemanticsAction.scrollToOffset, Float64List.fromList([123.0, 456.0])); + expect(controller.offset, 456.0); + controller.dispose(); + semantics.dispose(); + }); + + testWidgets('Horizontal scrollable responds to scrollToOffset', (WidgetTester tester) async { + semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + controller: controller, + scrollDirection: Axis.horizontal, + children: List.generate(60, (int i) => Text('$i')), + ), + ), + ); + final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; + final int scrollableId = semantics.nodesWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]).single.id; + + assert(controller.offset == 0); + semanticsOwner.performAction(scrollableId, SemanticsAction.scrollToOffset, Float64List.fromList([123.0, 456.0])); + expect(controller.offset, 123.0); + controller.dispose(); + semantics.dispose(); + }); + + testWidgets('Unscrollable scrollable does not respond to scrollToOffset', (WidgetTester tester) async { + semantics = SemanticsTester(tester); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + children: List.generate(3, (int i) => Text('$i')), + ), + ), + ); + expect(semantics.nodesWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]), isEmpty); + semantics.dispose(); + }); + + testWidgets('scrollToOffset respects implicit scrolling configuration', (WidgetTester tester) async { + semantics = SemanticsTester(tester); + final ScrollPhysics physics = _NoImplicitScrollingScrollPhysics(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + physics: physics, + children: List.generate(60, (int i) => Text('$i')), + ), + ), + ); + expect(semantics.nodesWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]), isEmpty); + semantics.dispose(); + }); + + testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation @@ -227,6 +304,7 @@ void main() { scrollExtentMax: 520.0, actions: [ SemanticsAction.scrollUp, + SemanticsAction.scrollToOffset, ], )); @@ -239,6 +317,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], )); @@ -250,6 +329,7 @@ void main() { scrollExtentMax: 520.0, actions: [ SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], )); @@ -276,6 +356,7 @@ void main() { scrollExtentMax: double.infinity, actions: [ SemanticsAction.scrollUp, + SemanticsAction.scrollToOffset, ], )); @@ -288,6 +369,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], )); @@ -300,6 +382,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], )); @@ -354,7 +437,7 @@ void main() { flags: [ SemanticsFlag.hasImplicitScrolling, ], - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( label: r'item 0', @@ -614,7 +697,7 @@ void main() { ), ); - final SemanticsNode rootScrollNode = semantics.nodesWith(actions: [SemanticsAction.scrollUp]).single; + final SemanticsNode rootScrollNode = semantics.nodesWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]).single; final SemanticsNode innerListPane = semantics.nodesWith(ancestor: rootScrollNode, scrollExtentMax: 0).single; final SemanticsNode outerListPane = innerListPane.parent!; final List hiddenNodes = semantics.nodesWith(flags: [SemanticsFlag.isHidden]).toList(); @@ -664,3 +747,11 @@ Rect nodeGlobalRect(SemanticsNode node) { } return MatrixUtils.transformRect(globalTransform, node.rect); } + +class _NoImplicitScrollingScrollPhysics extends ScrollPhysics { + @override + bool get allowImplicitScrolling => false; + + @override + ScrollPhysics applyTo(ScrollPhysics? ancestor) => this; +} diff --git a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart index 96a41661f1..cee63a605e 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart @@ -71,6 +71,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -245,6 +246,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -376,6 +378,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -515,6 +518,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -671,6 +675,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: children, ), @@ -744,6 +749,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 746bbf0b3b..a345b774e9 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -2625,7 +2625,7 @@ void main() { children: [ TestSemantics( flags: [SemanticsFlag.hasImplicitScrolling], - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( actions: [SemanticsAction.longPress], diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index eb89e3be4b..ea552844cc 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -525,9 +525,7 @@ void main() { ..remove(SemanticsAction.moveCursorBackwardByWord) ..remove(SemanticsAction.customAction) // customAction is not user-exposed. ..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed - // TODO(LongCatIsLooong): change to `SemanticsAction.scrollToOffset` when available. - // https://github.com/flutter/flutter/issues/159515. - ..removeWhere((SemanticsAction action) => action.index == 1 << 23); + ..remove(SemanticsAction.scrollToOffset); // scrollToOffset is not user-exposed const int expectedId = 1; final TestSemantics expectedSemantics = TestSemantics.root( @@ -545,9 +543,6 @@ void main() { final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; int expectedLength = 1; for (final SemanticsAction action in allActions) { - // TODO(LongCatIsLooong): remove after `SemanticsAction.scrollToOffset` is added to dart:ui. - // https://github.com/flutter/flutter/issues/159515. - // ignore: exhaustive_cases switch (action) { case SemanticsAction.moveCursorBackwardByCharacter: case SemanticsAction.moveCursorForwardByCharacter: @@ -575,6 +570,7 @@ void main() { case SemanticsAction.scrollLeft: case SemanticsAction.scrollRight: case SemanticsAction.scrollUp: + case SemanticsAction.scrollToOffset: case SemanticsAction.showOnScreen: case SemanticsAction.tap: case SemanticsAction.focus: diff --git a/packages/flutter/test/widgets/single_child_scroll_view_test.dart b/packages/flutter/test/widgets/single_child_scroll_view_test.dart index d84f52fd6e..6f6aac8410 100644 --- a/packages/flutter/test/widgets/single_child_scroll_view_test.dart +++ b/packages/flutter/test/widgets/single_child_scroll_view_test.dart @@ -354,6 +354,7 @@ void main() { ], actions: [ SemanticsAction.scrollUp, + SemanticsAction.scrollToOffset, ], children: generateSemanticsChildren(endHidden: 3), ), @@ -375,6 +376,7 @@ void main() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: generateSemanticsChildren(startHidden: 14, endHidden: 18), ), @@ -395,6 +397,7 @@ void main() { ], actions: [ SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: generateSemanticsChildren(startHidden: 26), ), diff --git a/packages/flutter/test/widgets/sliver_semantics_test.dart b/packages/flutter/test/widgets/sliver_semantics_test.dart index c7d5981a8e..596128f656 100644 --- a/packages/flutter/test/widgets/sliver_semantics_test.dart +++ b/packages/flutter/test/widgets/sliver_semantics_test.dart @@ -94,7 +94,7 @@ void _tests() { flags: [ SemanticsFlag.hasImplicitScrolling, ], - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( id: 3, @@ -165,6 +165,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], flags: [SemanticsFlag.hasImplicitScrolling], children: [ @@ -244,6 +245,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -339,6 +341,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -521,6 +524,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], flags: [SemanticsFlag.hasImplicitScrolling], children: [ @@ -633,6 +637,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], flags: [SemanticsFlag.hasImplicitScrolling], children: [ @@ -736,6 +741,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -851,6 +857,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( @@ -1007,6 +1014,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], flags: [SemanticsFlag.hasImplicitScrolling], children: [ @@ -1069,6 +1077,7 @@ void _tests() { actions: [ SemanticsAction.scrollUp, SemanticsAction.scrollDown, + SemanticsAction.scrollToOffset, ], children: [ TestSemantics( diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart index cab6bf787a..1118d51962 100644 --- a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart @@ -106,7 +106,7 @@ void main() { ], ), TestSemantics( - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], flags: [SemanticsFlag.hasImplicitScrolling], scrollIndex: 0, children: [ @@ -171,7 +171,7 @@ void main() { ], ), TestSemantics( - actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset], flags: [SemanticsFlag.hasImplicitScrolling], scrollIndex: 11, children: [ diff --git a/packages/flutter/test/widgets/text_test.dart b/packages/flutter/test/widgets/text_test.dart index e38fa88c04..13a5762a71 100644 --- a/packages/flutter/test/widgets/text_test.dart +++ b/packages/flutter/test/widgets/text_test.dart @@ -674,7 +674,7 @@ void main() { children: [ TestSemantics( flags: [SemanticsFlag.hasImplicitScrolling], - actions: [SemanticsAction.scrollUp], + actions: [SemanticsAction.scrollUp, SemanticsAction.scrollToOffset], children: [ TestSemantics( children: [ diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 2760d7f0d0..5e495bab1d 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -71,7 +71,8 @@ class SemanticsController { SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index | SemanticsAction.scrollLeft.index | - SemanticsAction.scrollRight.index; + SemanticsAction.scrollRight.index | + SemanticsAction.scrollToOffset.index; /// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java). static final int _importantFlagsForAccessibility =