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> {
leadingIcon: const Icon(Icons.search),
label: const Text('Icon'),
dropdownMenuEntries: iconEntries,
inputDecorationTheme: const InputDecorationTheme(filled: true),
inputDecorationTheme: const InputDecorationTheme(
filled: true,
contentPadding: EdgeInsets.symmetric(vertical: 5.0)),
onSelected: (IconLabel? icon) {
setState(() {
selectedIcon = icon;
......
......@@ -135,6 +135,7 @@ class DropdownMenu<T> extends StatefulWidget {
this.controller,
this.initialSelection,
this.onSelected,
this.requestFocusOnTap,
required this.dropdownMenuEntries,
});
......@@ -228,6 +229,19 @@ class DropdownMenu<T> extends StatefulWidget {
/// Defaults to null. If null, only the text field is updated.
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].
///
/// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
......@@ -242,7 +256,6 @@ class DropdownMenu<T> extends StatefulWidget {
class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey();
final FocusNode _textFocusNode = FocusNode();
final MenuController _controller = MenuController();
late final TextEditingController _textEditingController;
late bool _enableFilter;
......@@ -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() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
......@@ -428,7 +458,6 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
......@@ -489,13 +518,12 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
builder: (BuildContext context, MenuController controller, Widget? child) {
assert(_initialMenu != null);
final Widget trailingButton = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
padding: const EdgeInsets.all(4.0),
child: IconButton(
isSelected: controller.isOpen,
icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down),
selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up),
onPressed: () {
_textFocusNode.requestFocus();
handlePressed(controller);
},
),
......@@ -511,7 +539,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
width: widget.width,
children: <Widget>[
TextField(
focusNode: _textFocusNode,
canRequestFocus: canRequestFocus(),
enableInteractiveSelection: canRequestFocus(),
textAlignVertical: TextAlignVertical.center,
style: effectiveTextStyle,
controller: _textEditingController,
onEditingComplete: () {
......
......@@ -312,6 +312,7 @@ class TextField extends StatefulWidget {
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.canRequestFocus = true,
this.spellCheckConfiguration,
this.magnifierConfiguration,
}) : assert(obscuringCharacter.length == 1),
......@@ -762,6 +763,13 @@ class TextField extends StatefulWidget {
/// * [AdaptiveTextSelectionToolbar], which is built by default.
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) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
......@@ -976,7 +984,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if (widget.controller == null) {
_createLocalController();
}
_effectiveFocusNode.canRequestFocus = _isEnabled;
_effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
_effectiveFocusNode.addListener(_handleFocusChanged);
}
......@@ -984,7 +992,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return _isEnabled;
return widget.canRequestFocus && _isEnabled;
case NavigationMode.directional:
return true;
}
......
......@@ -13364,6 +13364,48 @@ void main() {
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', () {
testWidgets('Can right click to focus multiple times', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/pull/103228
......@@ -13518,6 +13560,34 @@ void main() {
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
}, 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', () {
......
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