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 {
///
/// The `context`, `intent` and `nullOk` arguments must not be null.
///
/// If the given `intent` isn't found in the first [Actions.actions] map, then
/// it will look to the next [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
/// that are found.
///
/// Setting `nullOk` to true means that if no ambient [Actions] widget is
/// found, then this method will return null instead of throwing.
///
/// Returns the result of invoking the action's [Action.invoke] method. If
/// no action mapping was found for the specified intent, or if the action
/// that was found was disabled, then this returns null. Callers can detect
/// whether or not the action is available (found, and not disabled) using
/// [Actions.find] with its `nullOk` argument set to true.
/// If the given `intent` doesn't map to an action, or doesn't map to one that
/// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
/// then it will look to the next ancestor [Actions] widget in the hierarchy
/// until it reaches the root.
///
/// In debug mode, if `nullOk` is false, this method will throw an exception
/// 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
/// release mode, this method will return null if no matching enabled action
/// is found, regardless of the setting of `nullOk`.
///
/// Setting `nullOk` to true indicates that if no ambient [Actions] widget is
/// found, then in debug mode, this method should return null instead of
/// throwing an exception.
///
/// 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>(
BuildContext context,
T intent, {
......@@ -653,18 +657,17 @@ class Actions extends StatefulWidget {
final _ActionsMarker actions = element.widget as _ActionsMarker;
final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?;
if (result != null) {
action = result;
actionElement = element;
return true;
if (result.isEnabled(intent)) {
action = result;
return true;
}
}
return false;
});
assert(() {
if (nullOk) {
return true;
}
if (action == null) {
if (!nullOk && actionElement == null) {
throw FlutterError('Unable to find an action for an Intent with type '
'${intent.runtimeType} in an $Actions widget in the given context.\n'
'$Actions.invoke() was unable to find an $Actions widget that '
......@@ -681,12 +684,9 @@ class Actions extends StatefulWidget {
if (actionElement == null || action == null) {
return null;
}
if (action!.isEnabled(intent)) {
// Invoke the action we found using the relevant dispatcher from the Actions
// Element we found.
return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
}
return null;
// Invoke the action we found using the relevant dispatcher from the Actions
// Element we found.
return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
}
@override
......
......@@ -448,7 +448,6 @@ void main() {
expect(identical(result, sentinel), isTrue);
expect(invoked, isTrue);
});
testWidgets('ContextAction can return null', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
const TestIntent intent = TestIntent();
......@@ -475,6 +474,52 @@ void main() {
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
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', () {
......
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