Keyboard scrolling of Scrollable (#45019)
This adds the ability to scroll and page up/down in a Scrollable using the keyboard. Currently, the macOS bindings use Platform.isMacOS as a check, but we'll switch that to be defaultTargetPlatform == TargetPlatform.macOS once that exists.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection' show HashMap;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -20,6 +21,7 @@ import 'media_query.dart';
|
||||
import 'navigator.dart';
|
||||
import 'pages.dart';
|
||||
import 'performance_overlay.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'semantics_debugger.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'text.dart';
|
||||
@@ -1041,12 +1043,46 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
final Map<LogicalKeySet, Intent> _keyMap = <LogicalKeySet, Intent>{
|
||||
// Next/previous keyboard traversal.
|
||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||
|
||||
// Directional keyboard traversal. Not available on web.
|
||||
if (!kIsWeb) ...<LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up)
|
||||
},
|
||||
|
||||
// Keyboard scrolling.
|
||||
// TODO(gspencergoog): Convert all of the Platform.isMacOS checks to be
|
||||
// defaultTargetPlatform == TargetPlatform.macOS, once that exists.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
if (!kIsWeb && !Platform.isMacOS) ...<LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
|
||||
},
|
||||
if (!kIsWeb && Platform.isMacOS) ...<LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
|
||||
},
|
||||
|
||||
// Web scrolling.
|
||||
if (kIsWeb) ...<LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
|
||||
},
|
||||
|
||||
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||
LogicalKeySet(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
|
||||
};
|
||||
@@ -1057,6 +1093,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
NextFocusAction.key: () => NextFocusAction(),
|
||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||
ScrollAction.key: () => ScrollAction(),
|
||||
};
|
||||
|
||||
@override
|
||||
@@ -1169,7 +1206,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
: _locale;
|
||||
|
||||
assert(_debugCheckLocalizations(appLocale));
|
||||
|
||||
return Shortcuts(
|
||||
shortcuts: _keyMap,
|
||||
child: Actions(
|
||||
|
||||
@@ -1002,8 +1002,8 @@ class DirectionalFocusIntent extends Intent {
|
||||
final bool ignoreTextFields;
|
||||
}
|
||||
|
||||
/// An [Action] that moves the focus to the focusable node in the given
|
||||
/// [direction] configured by the associated [DirectionalFocusIntent].
|
||||
/// An [Action] that moves the focus to the focusable node in the direction
|
||||
/// configured by the associated [DirectionalFocusIntent.direction].
|
||||
///
|
||||
/// This is the [Action] associated with the [key] and bound by default to the
|
||||
/// [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
|
||||
@@ -1016,9 +1016,6 @@ class DirectionalFocusAction extends _RequestFocusActionBase {
|
||||
/// The [LocalKey] that uniquely identifies this action to [DirectionalFocusIntent].
|
||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
||||
|
||||
/// The direction in which to look for the next focusable node when invoked.
|
||||
TraversalDirection direction;
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, DirectionalFocusIntent intent) {
|
||||
if (!intent.ignoreTextFields || node.context.widget is! EditableText) {
|
||||
|
||||
@@ -11,13 +11,16 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'scroll_configuration.dart';
|
||||
import 'scroll_context.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scroll_position_with_single_context.dart';
|
||||
@@ -81,6 +84,7 @@ class Scrollable extends StatefulWidget {
|
||||
this.controller,
|
||||
this.physics,
|
||||
@required this.viewportBuilder,
|
||||
this.incrementCalculator,
|
||||
this.excludeFromSemantics = false,
|
||||
this.semanticChildCount,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
@@ -155,6 +159,19 @@ class Scrollable extends StatefulWidget {
|
||||
/// slivers and sizes itself based on the size of the slivers.
|
||||
final ViewportBuilder viewportBuilder;
|
||||
|
||||
/// An optional function that will be called to calculate the distance to
|
||||
/// scroll when the scrollable is asked to scroll via the keyboard using a
|
||||
/// [ScrollAction].
|
||||
///
|
||||
/// If not supplied, the [Scrollable] will scroll a default amount when a
|
||||
/// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow,
|
||||
/// etc.), or otherwise invoked by a [ScrollAction].
|
||||
///
|
||||
/// If [incrementCalculator] is null, the default for
|
||||
/// [ScrollIncrementType.page] is 80% of the size of the scroll window, and
|
||||
/// for [ScrollIncrementType.line], 50 logical pixels.
|
||||
final ScrollIncrementCalculator incrementCalculator;
|
||||
|
||||
/// Whether the scroll actions introduced by this [Scrollable] are exposed
|
||||
/// in the semantics tree.
|
||||
///
|
||||
@@ -767,3 +784,231 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
_innerNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A typedef for a function that can calculate the offset for a type of scroll
|
||||
/// increment given a [ScrollIncrementDetails].
|
||||
///
|
||||
/// This function is used as the type for [Scrollable.incrementCalculator],
|
||||
/// which is called from a [ScrollAction].
|
||||
typedef ScrollIncrementCalculator = double Function(ScrollIncrementDetails details);
|
||||
|
||||
/// Describes the type of scroll increment that will be performed by a
|
||||
/// [ScrollAction] on a [Scrollable].
|
||||
///
|
||||
/// This is used to configure a [ScrollIncrementDetails] object to pass to a
|
||||
/// [ScrollIncrementCalculator] function on a [Scrollable].
|
||||
///
|
||||
/// {@template flutter.widgets.scrollable.scroll_increment_type.intent}
|
||||
/// This indicates the *intent* of the scroll, not necessarily the size. Not all
|
||||
/// scrollable areas will have the concept of a "line" or "page", but they can
|
||||
/// respond to the different standard key bindings that cause scrolling, which
|
||||
/// are bound to keys that people use to indicate a "line" scroll (e.g.
|
||||
/// control-arrowDown keys) or a "page" scroll (e.g. pageDown key). It is
|
||||
/// recommended that at least the relative magnitudes of the scrolls match
|
||||
/// expectations.
|
||||
/// {@endtemplate}
|
||||
enum ScrollIncrementType {
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "line".
|
||||
///
|
||||
/// The distance a "line" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a line" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
line,
|
||||
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "page".
|
||||
///
|
||||
/// The distance a "page" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a page" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
page,
|
||||
}
|
||||
|
||||
/// A details object that describes the type of scroll increment being requested
|
||||
/// of a [ScrollIncrementCalculator] function, as well as the current metrics
|
||||
/// for the scrollable.
|
||||
class ScrollIncrementDetails {
|
||||
/// A const constructor for a [ScrollIncrementDetails].
|
||||
///
|
||||
/// All of the arguments must not be null, and are required.
|
||||
const ScrollIncrementDetails({
|
||||
@required this.type,
|
||||
@required this.metrics,
|
||||
}) : assert(type != null),
|
||||
assert(metrics != null);
|
||||
|
||||
/// The type of scroll this is (e.g. line, page, etc.).
|
||||
///
|
||||
/// {@macro flutter.widgets.scrollable.scroll_increment_type.intent}
|
||||
final ScrollIncrementType type;
|
||||
|
||||
/// The current metrics of the scrollable that is being scrolled.
|
||||
final ScrollMetrics metrics;
|
||||
}
|
||||
|
||||
/// An [Intent] that represents scrolling the nearest scrollable by an amount
|
||||
/// appropriate for the [type] specified.
|
||||
///
|
||||
/// The actual amount of the scroll is determined by the
|
||||
/// [Scrollable.incrementCalculator], or by its defaults if that is not
|
||||
/// specified.
|
||||
class ScrollIntent extends Intent {
|
||||
/// Creates a const [ScrollIntent] that requests scrolling in the given
|
||||
/// [direction], with the given [type].
|
||||
///
|
||||
/// If [reversed] is specified, then the scroll will happen in the opposite
|
||||
/// direction from the normal scroll direction.
|
||||
const ScrollIntent({
|
||||
@required this.direction,
|
||||
this.type = ScrollIncrementType.line,
|
||||
}) : assert(direction != null),
|
||||
assert(type != null),
|
||||
super(ScrollAction.key);
|
||||
|
||||
/// The direction in which to scroll the scrollable containing the focused
|
||||
/// widget.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The type of scrolling that is intended.
|
||||
final ScrollIncrementType type;
|
||||
|
||||
@override
|
||||
bool isEnabled(BuildContext context) {
|
||||
return Scrollable.of(context) != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// An [Action] that scrolls the [Scrollable] that encloses the current
|
||||
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it.
|
||||
///
|
||||
/// If [Scrollable.incrementCalculator] is null for the scrollable, the default
|
||||
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
|
||||
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
|
||||
/// pixels.
|
||||
class ScrollAction extends Action {
|
||||
/// Creates a const [ScrollAction].
|
||||
ScrollAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely connects this action to a [ScrollIntent].
|
||||
static const LocalKey key = ValueKey<Type>(ScrollAction);
|
||||
|
||||
// Returns the scroll increment for a single scroll request, for use when
|
||||
// scrolling using a hardware keyboard.
|
||||
//
|
||||
// Must not be called when the position is null, or when any of the position
|
||||
// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
|
||||
// null. The type and state arguments must not be null, and the widget must
|
||||
// have already been laid out so that the position fields are valid.
|
||||
double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
|
||||
assert(type != null);
|
||||
assert(state.position != null);
|
||||
assert(state.position.pixels != null);
|
||||
assert(state.position.viewportDimension != null);
|
||||
assert(state.position.maxScrollExtent != null);
|
||||
assert(state.position.minScrollExtent != null);
|
||||
assert(state.widget.physics == null || state.widget.physics.shouldAcceptUserOffset(state.position));
|
||||
if (state.widget.incrementCalculator != null) {
|
||||
return state.widget.incrementCalculator(
|
||||
ScrollIncrementDetails(
|
||||
type: type,
|
||||
metrics: state.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
switch (type) {
|
||||
case ScrollIncrementType.line:
|
||||
return 50.0;
|
||||
case ScrollIncrementType.page:
|
||||
return 0.8 * state.position.viewportDimension;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Find out how much of an increment to move by, taking the different
|
||||
// directions into account.
|
||||
double _getIncrement(ScrollableState state, ScrollIntent intent) {
|
||||
final double increment = _calculateScrollIncrement(state, type: intent.type);
|
||||
switch (intent.direction) {
|
||||
case AxisDirection.down:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return -increment;
|
||||
break;
|
||||
case AxisDirection.down:
|
||||
return increment;
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
break;
|
||||
case AxisDirection.up:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return increment;
|
||||
break;
|
||||
case AxisDirection.down:
|
||||
return -increment;
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
break;
|
||||
case AxisDirection.left:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return -increment;
|
||||
break;
|
||||
case AxisDirection.left:
|
||||
return increment;
|
||||
break;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return increment;
|
||||
break;
|
||||
case AxisDirection.left:
|
||||
return -increment;
|
||||
break;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, ScrollIntent intent) {
|
||||
final ScrollableState state = Scrollable.of(node.context);
|
||||
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
|
||||
assert(state.position.pixels != null, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
|
||||
assert(state.position.viewportDimension != null);
|
||||
assert(state.position.maxScrollExtent != null);
|
||||
assert(state.position.minScrollExtent != null);
|
||||
|
||||
// Don't do anything if the user isn't allowed to scroll.
|
||||
if (state.widget.physics != null && !state.widget.physics.shouldAcceptUserOffset(state.position)) {
|
||||
return;
|
||||
}
|
||||
final double increment = _getIncrement(state, intent);
|
||||
if (increment == 0.0) {
|
||||
return;
|
||||
}
|
||||
state.position.moveTo(
|
||||
state.position.pixels + increment,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -13,12 +16,14 @@ Future<void> pumpTest(
|
||||
TargetPlatform platform, {
|
||||
bool scrollable = true,
|
||||
bool reverse = false,
|
||||
ScrollController controller,
|
||||
}) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: platform,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
reverse: reverse,
|
||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
slivers: const <Widget>[
|
||||
@@ -31,6 +36,14 @@ Future<void> pumpTest(
|
||||
|
||||
const double dragOffset = 200.0;
|
||||
|
||||
// TODO(gspencergoog): Change this to use TargetPlatform.macOS once that is available.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
// Can't be const, since Platform.macOS asserts if called in const context.
|
||||
// ignore: prefer_const_declarations
|
||||
final LogicalKeyboardKey modifierKey = (!kIsWeb && Platform.isMacOS)
|
||||
? LogicalKeyboardKey.metaLeft
|
||||
: LogicalKeyboardKey.controlLeft;
|
||||
|
||||
double getScrollOffset(WidgetTester tester) {
|
||||
final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
|
||||
return viewport.offset.pixels;
|
||||
@@ -267,4 +280,354 @@ void main() {
|
||||
|
||||
expect(getScrollOffset(tester), 20.0);
|
||||
});
|
||||
|
||||
testWidgets("Keyboard scrolling doesn't happen if scroll physics are set to NeverScrollableScrollPhysics", (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
autofocus: index == 0,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
autofocus: index == 0,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, -50.0, 800.0, 0.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, -400.0, 800.0, -350.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
autofocus: index == 0,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), width: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 50.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(-50.0, 0.0, 0.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 50.0, 600.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
autofocus: index == 0,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), width: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(750.0, 0.0, 800.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(800.0, 0.0, 850.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(750.0, 0.0, 800.0, 600.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'SizedBox');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
reverse: true,
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
focusNode: focusNode,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 550.0, 800.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 600.0, 800.0, 650.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 550.0, 800.0, 600.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 950.0, 800.0, 1000.0)));
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 550.0, 800.0, 600.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'SizedBox');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
slivers: List<Widget>.generate(
|
||||
20,
|
||||
(int index) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Focus(
|
||||
focusNode: focusNode,
|
||||
child: SizedBox(key: ValueKey<String>('Box $index'), width: 50.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(750.0, 0.0, 800.0, 600.00)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(800.0, 0.0, 850.0, 600.0)));
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
final List<String> items = List<String>.generate(20, (int index) => 'Item $index');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
platform: TargetPlatform.fuchsia,
|
||||
),
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
center: const ValueKey<String>('Center'),
|
||||
slivers: items.map<Widget>(
|
||||
(String item) {
|
||||
return SliverToBoxAdapter(
|
||||
key: item == 'Item 10' ? const ValueKey<String>('Center') : null,
|
||||
child: Focus(
|
||||
autofocus: item == 'Item 10',
|
||||
child: Container(
|
||||
key: ValueKey<String>(item),
|
||||
alignment: Alignment.center,
|
||||
height: 100,
|
||||
child: Text(item),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.position.pixels, equals(0.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Item 10'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 100.0)));
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
// Starts at #10 already, so doesn't work out to 500.0 because it hits bottom.
|
||||
expect(controller.position.pixels, equals(400.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Item 10'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, -400.0, 800.0, -300.0)));
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
await tester.sendKeyDownEvent(modifierKey);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.sendKeyUpEvent(modifierKey);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
// Goes up two past "center" where it started, so negative.
|
||||
expect(controller.position.pixels, equals(-100.0));
|
||||
expect(tester.getRect(find.byKey(const ValueKey<String>('Item 10'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)));
|
||||
|
||||
// TODO(gspencergoog): Once we can test against TargetPlatform.macOS instead
|
||||
// of Platform.isMacOS, don't skip this on web anymore.
|
||||
// https://github.com/flutter/flutter/issues/31366
|
||||
}, skip: kIsWeb);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user