Make web buttons respond to enter key (#72162)
This commit is contained in:
parent
70f5f7a79d
commit
0351c74aeb
@ -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', should work on web too.
|
||||
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.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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user