Unverified Commit b47126df authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Allow propagation to ancestor actions if actions lower down are disabled (#70404)

Change the semantics of Actions.invoke so that if the action it finds is disabled, then it keeps looking for an ancestor Actions widgets that has a matching intent where the action is not disabled.
parent 0b882698
...@@ -622,22 +622,26 @@ class Actions extends StatefulWidget { ...@@ -622,22 +622,26 @@ class Actions extends StatefulWidget {
/// ///
/// The `context`, `intent` and `nullOk` arguments must not be null. /// The `context`, `intent` and `nullOk` arguments must not be null.
/// ///
/// If the given `intent` isn't found in the first [Actions.actions] map, then /// If the given `intent` doesn't map to an action, or doesn't map to one that
/// it will look to the next [Actions] widget in the hierarchy until it /// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
/// reaches the root. /// then it will look to the next ancestor [Actions] widget in the hierarchy
/// /// until it reaches the root.
/// Will throw if no ambient [Actions] widget is found, or if the given ///
/// `intent` doesn't map to an action in any of the [Actions.actions] maps /// In debug mode, if `nullOk` is false, this method will throw an exception
/// that are found. /// if no ambient [Actions] widget is found, or if the given `intent` doesn't
/// /// map to an action in any of the [Actions.actions] maps that are found. In
/// Setting `nullOk` to true means that if no ambient [Actions] widget is /// release mode, this method will return null if no matching enabled action
/// found, then this method will return null instead of throwing. /// is found, regardless of the setting of `nullOk`.
/// ///
/// Returns the result of invoking the action's [Action.invoke] method. If /// Setting `nullOk` to true indicates that if no ambient [Actions] widget is
/// no action mapping was found for the specified intent, or if the action /// found, then in debug mode, this method should return null instead of
/// that was found was disabled, then this returns null. Callers can detect /// throwing an exception.
/// whether or not the action is available (found, and not disabled) using ///
/// [Actions.find] with its `nullOk` argument set to true. /// This method returns the result of invoking the action's [Action.invoke]
/// method. If no action mapping was found for the specified intent (and
/// `nullOk` is true), or if the actions that were found were disabled, or the
/// action itself returns null from [Action.invoke], then this method returns
/// null.
static Object? invoke<T extends Intent>( static Object? invoke<T extends Intent>(
BuildContext context, BuildContext context,
T intent, { T intent, {
...@@ -653,18 +657,17 @@ class Actions extends StatefulWidget { ...@@ -653,18 +657,17 @@ class Actions extends StatefulWidget {
final _ActionsMarker actions = element.widget as _ActionsMarker; final _ActionsMarker actions = element.widget as _ActionsMarker;
final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?; final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?;
if (result != null) { if (result != null) {
action = result;
actionElement = element; actionElement = element;
return true; if (result.isEnabled(intent)) {
action = result;
return true;
}
} }
return false; return false;
}); });
assert(() { assert(() {
if (nullOk) { if (!nullOk && actionElement == null) {
return true;
}
if (action == null) {
throw FlutterError('Unable to find an action for an Intent with type ' throw FlutterError('Unable to find an action for an Intent with type '
'${intent.runtimeType} in an $Actions widget in the given context.\n' '${intent.runtimeType} in an $Actions widget in the given context.\n'
'$Actions.invoke() was unable to find an $Actions widget that ' '$Actions.invoke() was unable to find an $Actions widget that '
...@@ -681,12 +684,9 @@ class Actions extends StatefulWidget { ...@@ -681,12 +684,9 @@ class Actions extends StatefulWidget {
if (actionElement == null || action == null) { if (actionElement == null || action == null) {
return null; return null;
} }
if (action!.isEnabled(intent)) { // Invoke the action we found using the relevant dispatcher from the Actions
// Invoke the action we found using the relevant dispatcher from the Actions // Element we found.
// Element we found. return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
}
return null;
} }
@override @override
......
...@@ -448,7 +448,6 @@ void main() { ...@@ -448,7 +448,6 @@ void main() {
expect(identical(result, sentinel), isTrue); expect(identical(result, sentinel), isTrue);
expect(invoked, isTrue); expect(invoked, isTrue);
}); });
testWidgets('ContextAction can return null', (WidgetTester tester) async { testWidgets('ContextAction can return null', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
const TestIntent intent = TestIntent(); const TestIntent intent = TestIntent();
...@@ -475,6 +474,52 @@ void main() { ...@@ -475,6 +474,52 @@ void main() {
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
expect(testAction.capturedContexts.single, containerKey.currentContext); expect(testAction.capturedContexts.single, containerKey.currentContext);
}); });
testWidgets('Disabled actions allow propagation to an ancestor', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const TestIntent intent = TestIntent();
final TestAction enabledTestAction = TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
);
enabledTestAction.enabled = true;
final TestAction disabledTestAction = TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
);
disabledTestAction.enabled = false;
await tester.pumpWidget(
Actions(
dispatcher: TestDispatcher1(postInvoke: collect),
actions: <Type, Action<Intent>>{
TestIntent: enabledTestAction,
},
child: Actions(
dispatcher: TestDispatcher(postInvoke: collect),
actions: <Type, Action<Intent>>{
TestIntent: disabledTestAction,
},
child: Container(key: containerKey),
),
),
);
await tester.pump();
final Object? result = Actions.invoke<TestIntent>(
containerKey.currentContext!,
intent,
);
expect(result, isTrue);
expect(invoked, isTrue);
expect(invokedIntent, equals(intent));
expect(invokedAction, equals(enabledTestAction));
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
});
}); });
group('Listening', () { group('Listening', () {
......
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