mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add shortcuts and actions for default focus traversal (#40186)
This adds the default shortcuts and actions for keyboard-based focus traversal of apps. This list of shortcuts includes shortcuts for TAB, SHIFT TAB, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, DOWN_ARROW, and the four DPAD keys for game controllers (because the DPAD produces arrow key events). It doesn't yet include functionality for triggering a control (e.g. SPACE, ENTER, or controller buttons), because that involves restructuring some of the Flutter controls to trigger animations differently, and so will be done in another PR (#41220)
This commit is contained in:
parent
aeede20785
commit
bedf46d06e
@ -247,8 +247,8 @@ abstract class UndoableAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
class SetFocusActionBase extends UndoableAction {
|
||||
SetFocusActionBase(LocalKey name) : super(name);
|
||||
class UndoableFocusActionBase extends UndoableAction {
|
||||
UndoableFocusActionBase(LocalKey name) : super(name);
|
||||
|
||||
FocusNode _previousFocus;
|
||||
|
||||
@ -286,10 +286,8 @@ class SetFocusActionBase extends UndoableAction {
|
||||
}
|
||||
}
|
||||
|
||||
class SetFocusAction extends SetFocusActionBase {
|
||||
SetFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(SetFocusAction);
|
||||
class UndoableRequestFocusAction extends UndoableFocusActionBase {
|
||||
UndoableRequestFocusAction() : super(RequestFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -299,10 +297,8 @@ class SetFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
|
||||
/// Actions for manipulating focus.
|
||||
class NextFocusAction extends SetFocusActionBase {
|
||||
NextFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
||||
class UndoableNextFocusAction extends UndoableFocusActionBase {
|
||||
UndoableNextFocusAction() : super(NextFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -311,10 +307,8 @@ class NextFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
}
|
||||
|
||||
class PreviousFocusAction extends SetFocusActionBase {
|
||||
PreviousFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
||||
class UndoablePreviousFocusAction extends UndoableFocusActionBase {
|
||||
UndoablePreviousFocusAction() : super(PreviousFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -323,16 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
}
|
||||
|
||||
class DirectionalFocusIntent extends Intent {
|
||||
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
||||
|
||||
final TraversalDirection direction;
|
||||
}
|
||||
|
||||
class DirectionalFocusAction extends SetFocusActionBase {
|
||||
DirectionalFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
||||
class UndoableDirectionalFocusAction extends UndoableFocusActionBase {
|
||||
UndoableDirectionalFocusAction() : super(DirectionalFocusAction.key);
|
||||
|
||||
TraversalDirection direction;
|
||||
|
||||
@ -366,7 +352,7 @@ class _DemoButtonState extends State<DemoButton> {
|
||||
void _handleOnPressed() {
|
||||
print('Button ${widget.name} pressed.');
|
||||
setState(() {
|
||||
Actions.invoke(context, const Intent(SetFocusAction.key), focusNode: _focusNode);
|
||||
Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
|
||||
});
|
||||
}
|
||||
|
||||
@ -434,101 +420,91 @@ class _FocusDemoState extends State<FocusDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||
return Actions(
|
||||
dispatcher: dispatcher,
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
RequestFocusAction.key: () => UndoableRequestFocusAction(),
|
||||
NextFocusAction.key: () => UndoableNextFocusAction(),
|
||||
PreviousFocusAction.key: () => UndoablePreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => UndoableDirectionalFocusAction(),
|
||||
kUndoActionKey: () => kUndoAction,
|
||||
kRedoActionKey: () => kRedoAction,
|
||||
},
|
||||
child: Actions(
|
||||
dispatcher: dispatcher,
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
SetFocusAction.key: () => SetFocusAction(),
|
||||
NextFocusAction.key: () => NextFocusAction(),
|
||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||
kUndoActionKey: () => kUndoAction,
|
||||
kRedoActionKey: () => kRedoAction,
|
||||
},
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||
},
|
||||
child: FocusScope(
|
||||
debugLabel: 'Scope',
|
||||
autofocus: true,
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.display1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Actions Demo'),
|
||||
),
|
||||
body: Center(
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'One'),
|
||||
DemoButton(name: 'Two'),
|
||||
DemoButton(name: 'Three'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Four'),
|
||||
DemoButton(name: 'Five'),
|
||||
DemoButton(name: 'Six'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Seven'),
|
||||
DemoButton(name: 'Eight'),
|
||||
DemoButton(name: 'Nine'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('UNDO'),
|
||||
onPressed: canUndo
|
||||
? () {
|
||||
Actions.invoke(context, kUndoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||
},
|
||||
child: FocusScope(
|
||||
debugLabel: 'Scope',
|
||||
autofocus: true,
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.display1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Actions Demo'),
|
||||
),
|
||||
body: Center(
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'One'),
|
||||
DemoButton(name: 'Two'),
|
||||
DemoButton(name: 'Three'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Four'),
|
||||
DemoButton(name: 'Five'),
|
||||
DemoButton(name: 'Six'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Seven'),
|
||||
DemoButton(name: 'Eight'),
|
||||
DemoButton(name: 'Nine'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('UNDO'),
|
||||
onPressed: canUndo
|
||||
? () {
|
||||
Actions.invoke(context, kUndoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('REDO'),
|
||||
onPressed: canRedo
|
||||
? () {
|
||||
Actions.invoke(context, kRedoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('REDO'),
|
||||
onPressed: canRedo
|
||||
? () {
|
||||
Actions.invoke(context, kRedoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1199,11 +1199,21 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
|
||||
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||
},
|
||||
child: Actions(
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
DoNothingAction.key: () => const DoNothingAction(),
|
||||
RequestFocusAction.key: () => RequestFocusAction(),
|
||||
NextFocusAction.key: () => NextFocusAction(),
|
||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||
},
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
|
@ -2,9 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
import 'focus_manager.dart';
|
||||
@ -790,3 +793,138 @@ class DefaultFocusTraversal extends InheritedWidget {
|
||||
@override
|
||||
bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
|
||||
}
|
||||
|
||||
// A base class for all of the default actions that request focus for a node.
|
||||
class _RequestFocusActionBase extends Action {
|
||||
_RequestFocusActionBase(LocalKey name) : super(name);
|
||||
|
||||
FocusNode _previousFocus;
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent tag) {
|
||||
_previousFocus = WidgetsBinding.instance.focusManager.primaryFocus;
|
||||
node.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<FocusNode>('previous', _previousFocus));
|
||||
}
|
||||
}
|
||||
|
||||
/// An [Action] that requests the focus on the node it is invoked on.
|
||||
///
|
||||
/// This action can be used to request focus for a particular node, by calling
|
||||
/// [Action.invoke] like so:
|
||||
///
|
||||
/// ```dart
|
||||
/// Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
|
||||
/// ```
|
||||
///
|
||||
/// Where the `_focusNode` is the node for which the focus will be requested.
|
||||
///
|
||||
/// The difference between requesting focus in this way versus calling
|
||||
/// [_focusNode.requestFocus] directly is that it will use the [Action]
|
||||
/// registered in the nearest [Actions] widget associated with [key] to make the
|
||||
/// request, rather than just requesting focus directly. This allows the action
|
||||
/// to have additional side effects, like logging, or undo and redo
|
||||
/// functionality.
|
||||
///
|
||||
/// However, this [RequestFocusAction] is the default action associated with the
|
||||
/// [key] in the [WidgetsApp], and it simply requests focus and has no side
|
||||
/// effects.
|
||||
class RequestFocusAction extends _RequestFocusActionBase {
|
||||
/// Creates a [RequestFocusAction] with a fixed [key].
|
||||
RequestFocusAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent tag) {
|
||||
super.invoke(node, tag);
|
||||
node.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// An [Action] that moves the focus to the next focusable node in the focus
|
||||
/// order.
|
||||
///
|
||||
/// This action is the default action registered for the [key], and by default
|
||||
/// is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
|
||||
class NextFocusAction extends _RequestFocusActionBase {
|
||||
/// Creates a [NextFocusAction] with a fixed [key];
|
||||
NextFocusAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent tag) {
|
||||
super.invoke(node, tag);
|
||||
node.nextFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// An [Action] that moves the focus to the previous focusable node in the focus
|
||||
/// order.
|
||||
///
|
||||
/// This action is the default action registered for the [key], and by default
|
||||
/// is bound to a combination of the [LogicalKeyboardKey.tab] key and the
|
||||
/// [LogicalKeyboardKey.shift] key in the [WidgetsApp].
|
||||
class PreviousFocusAction extends _RequestFocusActionBase {
|
||||
/// Creates a [PreviousFocusAction] with a fixed [key];
|
||||
PreviousFocusAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent tag) {
|
||||
super.invoke(node, tag);
|
||||
node.previousFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// An [Intent] that represents moving to the next focusable node in the given
|
||||
/// [direction].
|
||||
///
|
||||
/// This is the [Intent] bound by default to the [LogicalKeyboardKey.arrowUp],
|
||||
/// [LogicalKeyboardKey.arrowDown], [LogicalKeyboardKey.arrowLeft], and
|
||||
/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
|
||||
/// appropriate associated directions.
|
||||
class DirectionalFocusIntent extends Intent {
|
||||
/// Creates a [DirectionalFocusIntent] with a fixed [key], and the given
|
||||
/// [direction].
|
||||
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
||||
|
||||
/// The direction in which to look for the next focusable node when the
|
||||
/// associated [DirectionalFocusAction] is invoked.
|
||||
final TraversalDirection direction;
|
||||
}
|
||||
|
||||
/// An [Action] that moves the focus to the focusable node in the given
|
||||
/// [direction] configured by the associated [DirectionalFocusIntent].
|
||||
///
|
||||
/// This is the [Action] associated with the [key] and bound by default to the
|
||||
/// [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
|
||||
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
|
||||
/// the [WidgetsApp], with the appropriate associated directions.
|
||||
class DirectionalFocusAction extends _RequestFocusActionBase {
|
||||
/// Creates a [DirectionalFocusAction] with a fixed [key];
|
||||
DirectionalFocusAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely identifies this action to [DirectionalFocusIntent].
|
||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
||||
|
||||
/// The direction in which to look for the next focusable node when invoked.
|
||||
TraversalDirection direction;
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, DirectionalFocusIntent tag) {
|
||||
super.invoke(node, tag);
|
||||
final DirectionalFocusIntent args = tag;
|
||||
node.focusInDirection(args.direction);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -914,5 +916,112 @@ void main() {
|
||||
expect(focusCenter.hasFocus, isFalse);
|
||||
expect(focusTop.hasFocus, isTrue);
|
||||
});
|
||||
testWidgets('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async {
|
||||
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
|
||||
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
|
||||
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
|
||||
final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey');
|
||||
|
||||
await tester.pumpWidget(
|
||||
WidgetsApp(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
return TestRoute(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: FocusScope(
|
||||
debugLabel: 'scope',
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Focus(
|
||||
autofocus: true,
|
||||
debugLabel: 'upperLeft',
|
||||
child: Container(width: 100, height: 100, key: upperLeftKey),
|
||||
),
|
||||
Focus(
|
||||
debugLabel: 'upperRight',
|
||||
child: Container(width: 100, height: 100, key: upperRightKey),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Focus(
|
||||
debugLabel: 'lowerLeft',
|
||||
child: Container(width: 100, height: 100, key: lowerLeftKey),
|
||||
),
|
||||
Focus(
|
||||
debugLabel: 'lowerRight',
|
||||
child: Container(width: 100, height: 100, key: lowerRightKey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Initial focus happens.
|
||||
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
|
||||
// Traverse in a direction
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
// Initial focus happens.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestRoute extends PageRouteBuilder<void> {
|
||||
TestRoute({Widget child})
|
||||
: super(
|
||||
pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user