Unverified Commit ad1a44d0 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Add `requestFocusOnTap` to `DropdownMenu` (#117504)

* Add canRequestFocus to TextField and requestFocusOnTap to DropdownMenu

* Address comments

* Address comments

---------
Co-authored-by: 's avatarQun Cheng <quncheng@google.com>
parent fc3e8243
...@@ -67,7 +67,9 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> { ...@@ -67,7 +67,9 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
leadingIcon: const Icon(Icons.search), leadingIcon: const Icon(Icons.search),
label: const Text('Icon'), label: const Text('Icon'),
dropdownMenuEntries: iconEntries, dropdownMenuEntries: iconEntries,
inputDecorationTheme: const InputDecorationTheme(filled: true), inputDecorationTheme: const InputDecorationTheme(
filled: true,
contentPadding: EdgeInsets.symmetric(vertical: 5.0)),
onSelected: (IconLabel? icon) { onSelected: (IconLabel? icon) {
setState(() { setState(() {
selectedIcon = icon; selectedIcon = icon;
......
...@@ -135,6 +135,7 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -135,6 +135,7 @@ class DropdownMenu<T> extends StatefulWidget {
this.controller, this.controller,
this.initialSelection, this.initialSelection,
this.onSelected, this.onSelected,
this.requestFocusOnTap,
required this.dropdownMenuEntries, required this.dropdownMenuEntries,
}); });
...@@ -228,6 +229,19 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -228,6 +229,19 @@ class DropdownMenu<T> extends StatefulWidget {
/// Defaults to null. If null, only the text field is updated. /// Defaults to null. If null, only the text field is updated.
final ValueChanged<T?>? onSelected; final ValueChanged<T?>? onSelected;
/// Determine if the dropdown button requests focus and the on-screen virtual
/// keyboard is shown in response to a touch event.
///
/// By default, on mobile platforms, tapping on the text field and opening
/// the menu will not cause a focus request and the virtual keyboard will not
/// appear. The default behavior for desktop platforms is for the dropdown to
/// take the focus.
///
/// Defaults to null. Setting this field to true or false, rather than allowing
/// the implementation to choose based on the platform, can be useful for
/// applications that want to override the default behavior.
final bool? requestFocusOnTap;
/// Descriptions of the menu items in the [DropdownMenu]. /// Descriptions of the menu items in the [DropdownMenu].
/// ///
/// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
...@@ -242,7 +256,6 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -242,7 +256,6 @@ class DropdownMenu<T> extends StatefulWidget {
class _DropdownMenuState<T> extends State<DropdownMenu<T>> { class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey(); final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey(); final GlobalKey _leadingKey = GlobalKey();
final FocusNode _textFocusNode = FocusNode();
final MenuController _controller = MenuController(); final MenuController _controller = MenuController();
late final TextEditingController _textEditingController; late final TextEditingController _textEditingController;
late bool _enableFilter; late bool _enableFilter;
...@@ -288,6 +301,23 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -288,6 +301,23 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
} }
} }
bool canRequestFocus() {
if (widget.requestFocusOnTap != null) {
return widget.requestFocusOnTap!;
}
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return false;
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return true;
}
}
void refreshLeadingPadding() { void refreshLeadingPadding() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
...@@ -428,7 +458,6 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -428,7 +458,6 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override @override
void dispose() { void dispose() {
_textEditingController.dispose();
super.dispose(); super.dispose();
} }
...@@ -489,13 +518,12 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -489,13 +518,12 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
builder: (BuildContext context, MenuController controller, Widget? child) { builder: (BuildContext context, MenuController controller, Widget? child) {
assert(_initialMenu != null); assert(_initialMenu != null);
final Widget trailingButton = Padding( final Widget trailingButton = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.all(4.0),
child: IconButton( child: IconButton(
isSelected: controller.isOpen, isSelected: controller.isOpen,
icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down),
selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up),
onPressed: () { onPressed: () {
_textFocusNode.requestFocus();
handlePressed(controller); handlePressed(controller);
}, },
), ),
...@@ -511,7 +539,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -511,7 +539,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
width: widget.width, width: widget.width,
children: <Widget>[ children: <Widget>[
TextField( TextField(
focusNode: _textFocusNode, canRequestFocus: canRequestFocus(),
enableInteractiveSelection: canRequestFocus(),
textAlignVertical: TextAlignVertical.center,
style: effectiveTextStyle, style: effectiveTextStyle,
controller: _textEditingController, controller: _textEditingController,
onEditingComplete: () { onEditingComplete: () {
......
...@@ -312,6 +312,7 @@ class TextField extends StatefulWidget { ...@@ -312,6 +312,7 @@ class TextField extends StatefulWidget {
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.contextMenuBuilder = _defaultContextMenuBuilder, this.contextMenuBuilder = _defaultContextMenuBuilder,
this.canRequestFocus = true,
this.spellCheckConfiguration, this.spellCheckConfiguration,
this.magnifierConfiguration, this.magnifierConfiguration,
}) : assert(obscuringCharacter.length == 1), }) : assert(obscuringCharacter.length == 1),
...@@ -762,6 +763,13 @@ class TextField extends StatefulWidget { ...@@ -762,6 +763,13 @@ class TextField extends StatefulWidget {
/// * [AdaptiveTextSelectionToolbar], which is built by default. /// * [AdaptiveTextSelectionToolbar], which is built by default.
final EditableTextContextMenuBuilder? contextMenuBuilder; final EditableTextContextMenuBuilder? contextMenuBuilder;
/// Determine whether this text field can request the primary focus.
///
/// Defaults to true. If false, the text field will not request focus
/// when tapped, or when its context menu is displayed. If false it will not
/// be possible to move the focus to the text field with tab key.
final bool canRequestFocus;
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.editableText( return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState, editableTextState: editableTextState,
...@@ -976,7 +984,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -976,7 +984,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if (widget.controller == null) { if (widget.controller == null) {
_createLocalController(); _createLocalController();
} }
_effectiveFocusNode.canRequestFocus = _isEnabled; _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
_effectiveFocusNode.addListener(_handleFocusChanged); _effectiveFocusNode.addListener(_handleFocusChanged);
} }
...@@ -984,7 +992,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -984,7 +992,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) { switch (mode) {
case NavigationMode.traditional: case NavigationMode.traditional:
return _isEnabled; return widget.canRequestFocus && _isEnabled;
case NavigationMode.directional: case NavigationMode.directional:
return true; return true;
} }
......
...@@ -13364,6 +13364,48 @@ void main() { ...@@ -13364,6 +13364,48 @@ void main() {
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
); );
testWidgets('Cannot request focus when canRequestFocus is false', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
// Default test. The canRequestFocus is true by default and the text field can be focused
await tester.pumpWidget(
boilerplate(
child: TextField(
focusNode: focusNode,
),
),
);
expect(focusNode.hasFocus, isFalse);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, isTrue);
// Set canRequestFocus to false: the text field cannot be focused when it is tapped/long pressed.
await tester.pumpWidget(
boilerplate(
child: TextField(
focusNode: focusNode,
canRequestFocus: false,
),
),
);
expect(focusNode.hasFocus, isFalse);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, isFalse);
// The text field cannot be focused if it is tapped.
await tester.tap(find.byType(TextField));
await tester.pump();
expect(focusNode.hasFocus, isFalse);
// The text field cannot be focused if it is long pressed.
await tester.longPress(find.byType(TextField));
await tester.pump();
expect(focusNode.hasFocus, isFalse);
});
group('Right click focus', () { group('Right click focus', () {
testWidgets('Can right click to focus multiple times', (WidgetTester tester) async { testWidgets('Can right click to focus multiple times', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/pull/103228 // Regression test for https://github.com/flutter/flutter/pull/103228
...@@ -13518,6 +13560,34 @@ void main() { ...@@ -13518,6 +13560,34 @@ void main() {
expect(controller.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5); expect(controller.selection.extentOffset, 5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Right clicking cannot request focus if canRequestFocus is false', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
TextField(
key: key,
focusNode: focusNode,
canRequestFocus: false,
),
],
),
),
),
);
await tester.tapAt(
tester.getCenter(find.byKey(key)),
buttons: kSecondaryButton,
);
await tester.pump();
expect(focusNode.hasFocus, isFalse);
});
}); });
group('context menu', () { group('context menu', () {
......
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