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>(
|
ActivateIntent: CallbackAction<ActivateIntent>(
|
||||||
onInvoke: (ActivateIntent intent) => _handleTap(),
|
onInvoke: (ActivateIntent intent) => _handleTap(),
|
||||||
),
|
),
|
||||||
|
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
|
||||||
|
onInvoke: (ButtonActivateIntent intent) => _handleTap(),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
focusNode!.addListener(_handleFocusChanged);
|
focusNode!.addListener(_handleFocusChanged);
|
||||||
final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
|
final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
|
||||||
|
@ -734,6 +734,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
|||||||
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
|
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
|
||||||
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
|
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
|
||||||
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _simulateTap),
|
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _simulateTap),
|
||||||
|
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _simulateTap),
|
||||||
};
|
};
|
||||||
|
|
||||||
bool get highlightsExist => _highlights.values.where((InkHighlight? highlight) => highlight != null).isNotEmpty;
|
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;
|
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
|
||||||
|
|
||||||
void _simulateTap([ActivateIntent? intent]) {
|
void _simulateTap([Intent? intent]) {
|
||||||
_startSplash(context: context);
|
_startSplash(context: context);
|
||||||
_handleTap();
|
_handleTap();
|
||||||
}
|
}
|
||||||
|
@ -1248,12 +1248,41 @@ class DoNothingAction extends Action<Intent> {
|
|||||||
void invoke(Intent 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 {
|
class ActivateIntent extends Intent {
|
||||||
/// Creates a const [ActivateIntent] so subclasses can be const.
|
/// Creates a const [ActivateIntent] so subclasses can be const.
|
||||||
const ActivateIntent();
|
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.
|
/// An action that activates the currently focused control.
|
||||||
///
|
///
|
||||||
/// This is an abstract class that serves as a base class for actions that
|
/// 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>{
|
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// Activation
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
|
// On the web, enter activates buttons, but not other controls.
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
|
||||||
|
|
||||||
// Dismissal
|
// Dismissal
|
||||||
LogicalKeySet(LogicalKeyboardKey.escape): const DismissIntent(),
|
LogicalKeySet(LogicalKeyboardKey.escape): const DismissIntent(),
|
||||||
@ -1046,7 +1048,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Default shortcuts for the macOS platform.
|
// Default shortcuts for the macOS platform.
|
||||||
static final Map<LogicalKeySet, Intent> _defaultMacOsShortcuts = <LogicalKeySet, Intent>{
|
static final Map<LogicalKeySet, Intent> _defaultAppleOsShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// Activation
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
@ -1086,13 +1088,10 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
return _defaultShortcuts;
|
return _defaultShortcuts;
|
||||||
case TargetPlatform.macOS:
|
|
||||||
return _defaultMacOsShortcuts;
|
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
// No keyboard support on iOS yet.
|
case TargetPlatform.macOS:
|
||||||
break;
|
return _defaultAppleOsShortcuts;
|
||||||
}
|
}
|
||||||
return <LogicalKeySet, Intent>{};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default value of [WidgetsApp.actions].
|
/// 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))));
|
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');
|
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton');
|
||||||
String? value = 'one';
|
String? value = 'one';
|
||||||
|
|
||||||
@ -2397,15 +2397,69 @@ void main() {
|
|||||||
await tester.pump(); // Pump a frame for autofocus to take effect.
|
await tester.pump(); // Pump a frame for autofocus to take effect.
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
|
|
||||||
// Web doesn't respond to enter, only space.
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||||
await tester.sendKeyEvent(kIsWeb ? LogicalKeyboardKey.space : LogicalKeyboardKey.enter);
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
||||||
expect(value, equals('one'));
|
expect(value, equals('one'));
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab); // Focus 'two'
|
||||||
await tester.pump();
|
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();
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/src/services/keyboard_key.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
@ -75,6 +76,38 @@ void main() {
|
|||||||
expect(log, equals(<String>['tap-down', 'tap-cancel']));
|
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 {
|
testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const Material(
|
await tester.pumpWidget(const Material(
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user