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 {
/// the nearest enclosing [ScrollableState] in that [Axis] is returned, or
/// 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:
///
/// * [Scrollable.of], which is similar to this method, but asserts
......@@ -359,6 +363,10 @@ class Scrollable extends StatefulWidget {
/// target [Scrollable] is not the closest instance. When [axis] is provided,
/// 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
/// debug mode, and throw an exception in release mode.
///
......@@ -943,7 +951,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
Widget result = _ScrollableScope(
scrollable: this,
position: position,
// TODO(ianh): Having all these global keys is sad.
child: Listener(
onPointerSignal: _receivedPointerSignal,
child: RawGestureDetector(
......
......@@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart';
import 'actions.dart';
import 'basic.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_configuration.dart';
......@@ -377,10 +376,10 @@ class ScrollIntent extends Intent {
final ScrollIncrementType type;
}
/// An [Action] that scrolls the [Scrollable] that encloses the current
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it.
/// An [Action] that scrolls the relevant [Scrollable] by the amount configured
/// 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
/// [ScrollAction]s.
///
......@@ -388,21 +387,17 @@ class ScrollIntent extends Intent {
/// 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<ScrollIntent> {
class ScrollAction extends ContextAction<ScrollIntent> {
@override
bool isEnabled(ScrollIntent intent) {
final FocusNode? focus = primaryFocus;
final bool contextIsValid = focus != null && focus.context != null;
if (contextIsValid) {
// Check for primary scrollable within the current context
if (Scrollable.maybeOf(focus.context!) != null) {
return true;
bool isEnabled(ScrollIntent intent, [BuildContext? context]) {
if (context == null) {
return false;
}
// Check for fallback scrollable with context from PrimaryScrollController
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(focus.context!);
return primaryScrollController != null && primaryScrollController.hasClients;
if (Scrollable.maybeOf(context) != null) {
return true;
}
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
......@@ -480,10 +475,11 @@ class ScrollAction extends Action<ScrollIntent> {
}
@override
void invoke(ScrollIntent intent) {
ScrollableState? state = Scrollable.maybeOf(primaryFocus!.context!);
void invoke(ScrollIntent intent, [BuildContext? context]) {
assert(context != null, 'Cannot scroll without a context.');
ScrollableState? state = Scrollable.maybeOf(context!);
if (state == null) {
final ScrollController primaryScrollController = PrimaryScrollController.of(primaryFocus!.context!);
final ScrollController primaryScrollController = PrimaryScrollController.of(context);
assert (() {
if (primaryScrollController.positions.length != 1) {
throw FlutterError.fromParts(<DiagnosticsNode>[
......
......@@ -843,12 +843,16 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
primaryContext,
intent: matchedIntent,
);
if (action != null && action.isEnabled(matchedIntent)) {
final Object? invokeResult = Actions.of(primaryContext).invokeAction(action, matchedIntent, primaryContext);
if (action != null) {
final (bool enabled, Object? invokeResult) = Actions.of(primaryContext).invokeActionIfEnabled(
action, matchedIntent, primaryContext,
);
if (enabled) {
return action.toKeyEventResult(matchedIntent, invokeResult);
}
}
}
}
return modal ? KeyEventResult.skipRemainingHandlers : KeyEventResult.ignored;
}
......
......@@ -549,4 +549,33 @@ void main() {
equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)),
);
}, 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