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 {
|
class UndoableFocusActionBase extends UndoableAction {
|
||||||
SetFocusActionBase(LocalKey name) : super(name);
|
UndoableFocusActionBase(LocalKey name) : super(name);
|
||||||
|
|
||||||
FocusNode _previousFocus;
|
FocusNode _previousFocus;
|
||||||
|
|
||||||
@ -286,10 +286,8 @@ class SetFocusActionBase extends UndoableAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SetFocusAction extends SetFocusActionBase {
|
class UndoableRequestFocusAction extends UndoableFocusActionBase {
|
||||||
SetFocusAction() : super(key);
|
UndoableRequestFocusAction() : super(RequestFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(SetFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -299,10 +297,8 @@ class SetFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Actions for manipulating focus.
|
/// Actions for manipulating focus.
|
||||||
class NextFocusAction extends SetFocusActionBase {
|
class UndoableNextFocusAction extends UndoableFocusActionBase {
|
||||||
NextFocusAction() : super(key);
|
UndoableNextFocusAction() : super(NextFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -311,10 +307,8 @@ class NextFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviousFocusAction extends SetFocusActionBase {
|
class UndoablePreviousFocusAction extends UndoableFocusActionBase {
|
||||||
PreviousFocusAction() : super(key);
|
UndoablePreviousFocusAction() : super(PreviousFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -323,16 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectionalFocusIntent extends Intent {
|
class UndoableDirectionalFocusAction extends UndoableFocusActionBase {
|
||||||
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
UndoableDirectionalFocusAction() : super(DirectionalFocusAction.key);
|
||||||
|
|
||||||
final TraversalDirection direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DirectionalFocusAction extends SetFocusActionBase {
|
|
||||||
DirectionalFocusAction() : super(key);
|
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
|
||||||
|
|
||||||
TraversalDirection direction;
|
TraversalDirection direction;
|
||||||
|
|
||||||
@ -366,7 +352,7 @@ class _DemoButtonState extends State<DemoButton> {
|
|||||||
void _handleOnPressed() {
|
void _handleOnPressed() {
|
||||||
print('Button ${widget.name} pressed.');
|
print('Button ${widget.name} pressed.');
|
||||||
setState(() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
return Shortcuts(
|
return Actions(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
dispatcher: dispatcher,
|
||||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
actions: <LocalKey, ActionFactory>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
RequestFocusAction.key: () => UndoableRequestFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
NextFocusAction.key: () => UndoableNextFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
PreviousFocusAction.key: () => UndoablePreviousFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
DirectionalFocusAction.key: () => UndoableDirectionalFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
kUndoActionKey: () => kUndoAction,
|
||||||
|
kRedoActionKey: () => kRedoAction,
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: DefaultFocusTraversal(
|
||||||
dispatcher: dispatcher,
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
actions: <LocalKey, ActionFactory>{
|
child: Shortcuts(
|
||||||
SetFocusAction.key: () => SetFocusAction(),
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
NextFocusAction.key: () => NextFocusAction(),
|
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
},
|
||||||
kUndoActionKey: () => kUndoAction,
|
child: FocusScope(
|
||||||
kRedoActionKey: () => kRedoAction,
|
debugLabel: 'Scope',
|
||||||
},
|
autofocus: true,
|
||||||
child: DefaultFocusTraversal(
|
child: DefaultTextStyle(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
style: textTheme.display1,
|
||||||
child: Shortcuts(
|
child: Scaffold(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
appBar: AppBar(
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
title: const Text('Actions Demo'),
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
),
|
||||||
},
|
body: Center(
|
||||||
child: FocusScope(
|
child: Builder(builder: (BuildContext context) {
|
||||||
debugLabel: 'Scope',
|
return Column(
|
||||||
autofocus: true,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: DefaultTextStyle(
|
children: <Widget>[
|
||||||
style: textTheme.display1,
|
Row(
|
||||||
child: Scaffold(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
appBar: AppBar(
|
children: const <Widget>[
|
||||||
title: const Text('Actions Demo'),
|
DemoButton(name: 'One'),
|
||||||
),
|
DemoButton(name: 'Two'),
|
||||||
body: Center(
|
DemoButton(name: 'Three'),
|
||||||
child: Builder(builder: (BuildContext context) {
|
],
|
||||||
return Column(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Row(
|
||||||
children: <Widget>[
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: const <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
DemoButton(name: 'Four'),
|
||||||
children: const <Widget>[
|
DemoButton(name: 'Five'),
|
||||||
DemoButton(name: 'One'),
|
DemoButton(name: 'Six'),
|
||||||
DemoButton(name: 'Two'),
|
],
|
||||||
DemoButton(name: 'Three'),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: const <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
DemoButton(name: 'Seven'),
|
||||||
children: const <Widget>[
|
DemoButton(name: 'Eight'),
|
||||||
DemoButton(name: 'Four'),
|
DemoButton(name: 'Nine'),
|
||||||
DemoButton(name: 'Five'),
|
],
|
||||||
DemoButton(name: 'Six'),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Padding(
|
||||||
children: const <Widget>[
|
padding: const EdgeInsets.all(8.0),
|
||||||
DemoButton(name: 'Seven'),
|
child: RaisedButton(
|
||||||
DemoButton(name: 'Eight'),
|
child: const Text('UNDO'),
|
||||||
DemoButton(name: 'Nine'),
|
onPressed: canUndo
|
||||||
],
|
? () {
|
||||||
),
|
Actions.invoke(context, kUndoIntent);
|
||||||
Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
: null,
|
||||||
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),
|
Padding(
|
||||||
child: RaisedButton(
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: const Text('REDO'),
|
child: RaisedButton(
|
||||||
onPressed: canRedo
|
child: const Text('REDO'),
|
||||||
? () {
|
onPressed: canRedo
|
||||||
Actions.invoke(context, kRedoIntent);
|
? () {
|
||||||
}
|
Actions.invoke(context, kRedoIntent);
|
||||||
: null,
|
}
|
||||||
),
|
: null,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1199,11 +1199,21 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
|
|||||||
|
|
||||||
return Shortcuts(
|
return Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
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),
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <LocalKey, ActionFactory>{
|
||||||
DoNothingAction.key: () => const DoNothingAction(),
|
DoNothingAction.key: () => const DoNothingAction(),
|
||||||
|
RequestFocusAction.key: () => RequestFocusAction(),
|
||||||
|
NextFocusAction.key: () => NextFocusAction(),
|
||||||
|
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||||
|
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||||
},
|
},
|
||||||
child: DefaultFocusTraversal(
|
child: DefaultFocusTraversal(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
import 'actions.dart';
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'binding.dart';
|
import 'binding.dart';
|
||||||
import 'focus_manager.dart';
|
import 'focus_manager.dart';
|
||||||
@ -790,3 +793,138 @@ class DefaultFocusTraversal extends InheritedWidget {
|
|||||||
@override
|
@override
|
||||||
bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
|
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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// 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_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -914,5 +916,112 @@ void main() {
|
|||||||
expect(focusCenter.hasFocus, isFalse);
|
expect(focusCenter.hasFocus, isFalse);
|
||||||
expect(focusTop.hasFocus, isTrue);
|
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