Unverified Commit 34f39a20 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

ContextAction.isEnabled needs a context (#127721)

...and lots of things that fall out from that
parent 4effd9c4
...@@ -320,6 +320,10 @@ class Scrollable extends StatefulWidget { ...@@ -320,6 +320,10 @@ class Scrollable extends StatefulWidget {
/// the nearest enclosing [ScrollableState] in that [Axis] is returned, or /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or
/// null if there is none. /// null if there is none.
/// ///
/// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
/// means that if the `context` is that of a [Scrollable], it will _not_ find
/// _that_ [Scrollable].
///
/// See also: /// See also:
/// ///
/// * [Scrollable.of], which is similar to this method, but asserts /// * [Scrollable.of], which is similar to this method, but asserts
...@@ -359,6 +363,10 @@ class Scrollable extends StatefulWidget { ...@@ -359,6 +363,10 @@ class Scrollable extends StatefulWidget {
/// target [Scrollable] is not the closest instance. When [axis] is provided, /// target [Scrollable] is not the closest instance. When [axis] is provided,
/// the nearest enclosing [ScrollableState] in that [Axis] is returned. /// the nearest enclosing [ScrollableState] in that [Axis] is returned.
/// ///
/// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
/// means that if the `context` is that of a [Scrollable], it will _not_ find
/// _that_ [Scrollable].
///
/// If no [Scrollable] ancestor is found, then this method will assert in /// If no [Scrollable] ancestor is found, then this method will assert in
/// debug mode, and throw an exception in release mode. /// debug mode, and throw an exception in release mode.
/// ///
...@@ -943,7 +951,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -943,7 +951,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
Widget result = _ScrollableScope( Widget result = _ScrollableScope(
scrollable: this, scrollable: this,
position: position, position: position,
// TODO(ianh): Having all these global keys is sad.
child: Listener( child: Listener(
onPointerSignal: _receivedPointerSignal, onPointerSignal: _receivedPointerSignal,
child: RawGestureDetector( child: RawGestureDetector(
......
...@@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart'; ...@@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart';
import 'actions.dart'; import 'actions.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'primary_scroll_controller.dart'; import 'primary_scroll_controller.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
...@@ -377,10 +376,10 @@ class ScrollIntent extends Intent { ...@@ -377,10 +376,10 @@ class ScrollIntent extends Intent {
final ScrollIncrementType type; final ScrollIncrementType type;
} }
/// An [Action] that scrolls the [Scrollable] that encloses the current /// An [Action] that scrolls the relevant [Scrollable] by the amount configured
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it. /// in the [ScrollIntent] given to it.
/// ///
/// If a Scrollable cannot be found above the current [primaryFocus], the /// If a Scrollable cannot be found above the given [BuildContext], the
/// [PrimaryScrollController] will be considered for default handling of /// [PrimaryScrollController] will be considered for default handling of
/// [ScrollAction]s. /// [ScrollAction]s.
/// ///
...@@ -388,21 +387,17 @@ class ScrollIntent extends Intent { ...@@ -388,21 +387,17 @@ class ScrollIntent extends Intent {
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the /// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical /// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
/// pixels. /// pixels.
class ScrollAction extends Action<ScrollIntent> { class ScrollAction extends ContextAction<ScrollIntent> {
@override @override
bool isEnabled(ScrollIntent intent) { bool isEnabled(ScrollIntent intent, [BuildContext? context]) {
final FocusNode? focus = primaryFocus; if (context == null) {
final bool contextIsValid = focus != null && focus.context != null; return false;
if (contextIsValid) { }
// Check for primary scrollable within the current context if (Scrollable.maybeOf(context) != null) {
if (Scrollable.maybeOf(focus.context!) != null) { return true;
return true;
}
// Check for fallback scrollable with context from PrimaryScrollController
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(focus.context!);
return primaryScrollController != null && primaryScrollController.hasClients;
} }
return false; final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(context);
return (primaryScrollController != null) && (primaryScrollController.hasClients);
} }
/// Returns the scroll increment for a single scroll request, for use when /// Returns the scroll increment for a single scroll request, for use when
...@@ -480,10 +475,11 @@ class ScrollAction extends Action<ScrollIntent> { ...@@ -480,10 +475,11 @@ class ScrollAction extends Action<ScrollIntent> {
} }
@override @override
void invoke(ScrollIntent intent) { void invoke(ScrollIntent intent, [BuildContext? context]) {
ScrollableState? state = Scrollable.maybeOf(primaryFocus!.context!); assert(context != null, 'Cannot scroll without a context.');
ScrollableState? state = Scrollable.maybeOf(context!);
if (state == null) { if (state == null) {
final ScrollController primaryScrollController = PrimaryScrollController.of(primaryFocus!.context!); final ScrollController primaryScrollController = PrimaryScrollController.of(context);
assert (() { assert (() {
if (primaryScrollController.positions.length != 1) { if (primaryScrollController.positions.length != 1) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
......
...@@ -843,9 +843,13 @@ class ShortcutManager with Diagnosticable, ChangeNotifier { ...@@ -843,9 +843,13 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
primaryContext, primaryContext,
intent: matchedIntent, intent: matchedIntent,
); );
if (action != null && action.isEnabled(matchedIntent)) { if (action != null) {
final Object? invokeResult = Actions.of(primaryContext).invokeAction(action, matchedIntent, primaryContext); final (bool enabled, Object? invokeResult) = Actions.of(primaryContext).invokeActionIfEnabled(
return action.toKeyEventResult(matchedIntent, invokeResult); action, matchedIntent, primaryContext,
);
if (enabled) {
return action.toKeyEventResult(matchedIntent, invokeResult);
}
} }
} }
} }
......
...@@ -549,4 +549,33 @@ void main() { ...@@ -549,4 +549,33 @@ void main() {
equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)), equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)),
); );
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Can scroll using intents only', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView(
children: const <Widget>[
SizedBox(height: 600.0, child: Text('The cow as white as milk')),
SizedBox(height: 600.0, child: Text('The cape as red as blood')),
SizedBox(height: 600.0, child: Text('The hair as yellow as corn')),
],
),
),
);
expect(find.text('The cow as white as milk'), findsOneWidget);
expect(find.text('The cape as red as blood'), findsNothing);
expect(find.text('The hair as yellow as corn'), findsNothing);
Actions.invoke(tester.element(find.byType(SliverList)), const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page));
await tester.pump(); // start scroll
await tester.pump(const Duration(milliseconds: 1000)); // end scroll
expect(find.text('The cow as white as milk'), findsOneWidget);
expect(find.text('The cape as red as blood'), findsOneWidget);
expect(find.text('The hair as yellow as corn'), findsNothing);
Actions.invoke(tester.element(find.byType(SliverList)), const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page));
await tester.pump(); // start scroll
await tester.pump(const Duration(milliseconds: 1000)); // end scroll
expect(find.text('The cow as white as milk'), findsNothing);
expect(find.text('The cape as red as blood'), findsOneWidget);
expect(find.text('The hair as yellow as corn'), findsOneWidget);
});
} }
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