Unverified Commit 0351c74a authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Make web buttons respond to enter key (#72162)

parent 70f5f7a7
......@@ -1113,6 +1113,9 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (ActivateIntent intent) => _handleTap(),
),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
onInvoke: (ButtonActivateIntent intent) => _handleTap(),
),
};
focusNode!.addListener(_handleFocusChanged);
final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
......
......@@ -734,6 +734,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _simulateTap),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _simulateTap),
};
bool get highlightsExist => _highlights.values.where((InkHighlight? highlight) => highlight != null).isNotEmpty;
......@@ -756,7 +757,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
void _simulateTap([ActivateIntent? intent]) {
void _simulateTap([Intent? intent]) {
_startSplash(context: context);
_handleTap();
}
......
......@@ -1248,12 +1248,41 @@ class DoNothingAction extends Action<Intent> {
void invoke(Intent intent) {}
}
/// An intent that activates the currently focused control.
/// An [Intent] that activates the currently focused control.
///
/// This intent is bound by default to the [LogicalKeyboardKey.space] key on all
/// platforms, and also to the [LogicalKeyboardKey.enter] key on all platforms
/// except the web, where ENTER doesn't toggle selection. On the web, ENTER is
/// bound to [ButtonActivateIntent] instead.
///
/// See also:
///
/// * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
/// in apps.
/// * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
/// application (and defaults to [WidgetsApp.defaultShortcuts]).
class ActivateIntent extends Intent {
/// Creates a const [ActivateIntent] so subclasses can be const.
const ActivateIntent();
}
/// An [Intent] that activates the currently focused button.
///
/// This intent is bound by default to the [LogicalKeyboardKey.enter] key on the
/// web, where ENTER can be used to activate buttons, but not toggle selection.
/// All other platforms bind [LogicalKeyboardKey.enter] to [ActivateIntent].
///
/// See also:
///
/// * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
/// in apps.
/// * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
/// application (and defaults to [WidgetsApp.defaultShortcuts]).
class ButtonActivateIntent extends Intent {
/// Creates a const [ButtonActivateIntent] so subclasses can be const.
const ButtonActivateIntent();
}
/// An action that activates the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
......
......@@ -1028,6 +1028,8 @@ class WidgetsApp extends StatefulWidget {
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
// On the web, enter activates buttons, but not other controls.
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
// Dismissal
LogicalKeySet(LogicalKeyboardKey.escape): const DismissIntent(),
......@@ -1046,7 +1048,7 @@ class WidgetsApp extends StatefulWidget {
};
// Default shortcuts for the macOS platform.
static final Map<LogicalKeySet, Intent> _defaultMacOsShortcuts = <LogicalKeySet, Intent>{
static final Map<LogicalKeySet, Intent> _defaultAppleOsShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
......@@ -1086,13 +1088,10 @@ class WidgetsApp extends StatefulWidget {
case TargetPlatform.linux:
case TargetPlatform.windows:
return _defaultShortcuts;
case TargetPlatform.macOS:
return _defaultMacOsShortcuts;
case TargetPlatform.iOS:
// No keyboard support on iOS yet.
break;
case TargetPlatform.macOS:
return _defaultAppleOsShortcuts;
}
return <LogicalKeySet, Intent>{};
}
/// The default value of [WidgetsApp.actions].
......
......@@ -2358,7 +2358,7 @@ void main() {
expect(find.byKey(buttonKey), isNot(paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0), color: const Color(0xff00ff00))));
});
testWidgets('DropdownButton is activated with the enter/space key', (WidgetTester tester) async {
testWidgets('DropdownButton is activated with the enter key', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton');
String? value = 'one';
......@@ -2397,15 +2397,69 @@ void main() {
await tester.pump(); // Pump a frame for autofocus to take effect.
expect(focusNode.hasPrimaryFocus, isTrue);
// Web doesn't respond to enter, only space.
await tester.sendKeyEvent(kIsWeb ? LogicalKeyboardKey.space : LogicalKeyboardKey.enter);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('one'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select 'two'.
await tester.pump();
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('two'));
});
testWidgets('DropdownButton is activated with the space key', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton');
String? value = 'one';
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
body: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return DropdownButton<String>(
focusNode: focusNode,
autofocus: true,
onChanged: (String? newValue) {
setState(() {
value = newValue;
});
},
value: value,
itemHeight: null,
items: menuItems.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>(item),
value: item,
child: Text(item, key: ValueKey<String>(item + 'Text')),
);
}).toList(),
);
},
),
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.pump(); // Pump a frame for autofocus to take effect.
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('one'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter); // Select 'two', should work on web too.
await tester.sendKeyEvent(LogicalKeyboardKey.space); // Select 'two'.
await tester.pump();
await tester.pump();
......
......@@ -4,6 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/services/keyboard_key.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
......@@ -75,6 +76,38 @@ void main() {
expect(log, equals(<String>['tap-down', 'tap-cancel']));
});
testWidgets('InkWell invokes activation actions when expected', (WidgetTester tester) async {
final List<String> log = <String>[];
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
},
child: Material(
child: Center(
child: InkWell(
autofocus: true,
onTap: () {
log.add('tap');
},
),
),
),
),
));
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
expect(log, equals(<String>['tap']));
log.clear();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(log, equals(<String>['tap']));
});
testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async {
await tester.pumpWidget(const Material(
child: Directionality(
......
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