Unverified Commit 3fd3447f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Add PrioritizedIntents to support multiple shortcut configurations (#72560)

parent 542084d0
...@@ -1321,3 +1321,50 @@ class DismissIntent extends Intent { ...@@ -1321,3 +1321,50 @@ class DismissIntent extends Intent {
/// ///
/// This is an abstract class that serves as a base class for dismiss actions. /// This is an abstract class that serves as a base class for dismiss actions.
abstract class DismissAction extends Action<DismissIntent> {} abstract class DismissAction extends Action<DismissIntent> {}
/// An [Intent] that evaluates a series of specified [orderedIntents] for
/// execution.
class PrioritizedIntents extends Intent {
/// Creates a set of const [PrioritizedIntents].
const PrioritizedIntents({
required this.orderedIntents,
}) : assert(orderedIntents != null);
/// List of intents to be evaluated in order for execution. When an
/// [Action.isEnabled] returns true, that action will be invoked and
/// progression through the ordered intents stops.
final List<Intent> orderedIntents;
}
/// An [Action] that iterates through a list of [Intent]s, invoking the first
/// that is enabled.
class PrioritizedAction extends Action<PrioritizedIntents> {
late Action<dynamic> _selectedAction;
late Intent _selectedIntent;
@override
bool isEnabled(PrioritizedIntents intent) {
final FocusNode? focus = primaryFocus;
if (focus == null || focus.context == null)
return false;
for (final Intent candidateIntent in intent.orderedIntents) {
final Action<Intent>? candidateAction = Actions.maybeFind<Intent>(
focus.context!,
intent: candidateIntent,
);
if (candidateAction != null && candidateAction.isEnabled(candidateIntent)) {
_selectedAction = candidateAction;
_selectedIntent = candidateIntent;
return true;
}
}
return false;
}
@override
Object? invoke(PrioritizedIntents intent) {
assert(_selectedAction != null);
assert(_selectedIntent != null);
_selectedAction.invoke(_selectedIntent);
}
}
...@@ -1024,7 +1024,12 @@ class WidgetsApp extends StatefulWidget { ...@@ -1024,7 +1024,12 @@ class WidgetsApp extends StatefulWidget {
// Default shortcuts for the web platform. // Default shortcuts for the web platform.
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{ static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
// Activation // Activation
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(), LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents(
orderedIntents: <Intent>[
ActivateIntent(),
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
]
),
// On the web, enter activates buttons, but not other controls. // On the web, enter activates buttons, but not other controls.
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(), LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
...@@ -1100,6 +1105,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -1100,6 +1105,7 @@ class WidgetsApp extends StatefulWidget {
PreviousFocusIntent: PreviousFocusAction(), PreviousFocusIntent: PreviousFocusAction(),
DirectionalFocusIntent: DirectionalFocusAction(), DirectionalFocusIntent: DirectionalFocusAction(),
ScrollIntent: ScrollAction(), ScrollIntent: ScrollAction(),
PrioritizedIntents: PrioritizedAction(),
}; };
@override @override
......
...@@ -579,5 +579,86 @@ void main() { ...@@ -579,5 +579,86 @@ void main() {
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})')); expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}')); expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
}); });
testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp() {
return MaterialApp(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents(
orderedIntents: <Intent>[
ActivateIntent(),
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
]
),
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
},
home: Material(
child: Center(
child: ListView(
primary: true,
children: <Widget> [
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: value,
onChanged: (bool? newValue) => setState(() { value = newValue; }),
focusColor: Colors.orange[500],
);
},
),
Container(
color: Colors.blue,
height: 1000,
)
],
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
tester.binding.focusManager.primaryFocus!.toStringShort(),
equalsIgnoringHashCodes('FocusScopeNode#00000(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])'),
);
final ScrollController controller = PrimaryScrollController.of(
tester.element(find.byType(ListView))
)!;
expect(controller.position.pixels, 0.0);
expect(value, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
// ScrollView scrolls
expect(controller.position.pixels, 448.0);
expect(value, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
// Focus is now on the checkbox.
expect(
tester.binding.focusManager.primaryFocus!.toStringShort(),
equalsIgnoringHashCodes('FocusNode#00000([PRIMARY FOCUS])'),
);
expect(value, isTrue);
expect(controller.position.pixels, 0.0);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
// Checkbox is toggled, scroll view does not scroll.
expect(value, isFalse);
expect(controller.position.pixels, 0.0);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isTrue);
expect(controller.position.pixels, 0.0);
});
}); });
} }
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