Add requestFocusOnTap
to DropdownMenu
(#117504)
* Add canRequestFocus to TextField and requestFocusOnTap to DropdownMenu * Address comments * Address comments --------- Co-authored-by: Qun Cheng <quncheng@google.com>
This commit is contained in:
parent
fc3e8243ca
commit
ad1a44d0a7
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_textEditingController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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>> {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// * [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
|
|||||||
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
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ void main() {
|
|||||||
|
|
||||||
final Finder textField = find.byType(TextField);
|
final Finder textField = find.byType(TextField);
|
||||||
final Size anchorSize = tester.getSize(textField);
|
final Size anchorSize = tester.getSize(textField);
|
||||||
expect(anchorSize, const Size(180.0, 54.0));
|
expect(anchorSize, const Size(180.0, 56.0));
|
||||||
|
|
||||||
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@ -143,7 +143,7 @@ void main() {
|
|||||||
|
|
||||||
final Finder anchor = find.byType(TextField);
|
final Finder anchor = find.byType(TextField);
|
||||||
final Size size = tester.getSize(anchor);
|
final Size size = tester.getSize(anchor);
|
||||||
expect(size, const Size(200.0, 54.0));
|
expect(size, const Size(200.0, 56.0));
|
||||||
|
|
||||||
await tester.tap(anchor);
|
await tester.tap(anchor);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@ -428,7 +428,7 @@ void main() {
|
|||||||
expect(menuMaterial, findsOneWidget);
|
expect(menuMaterial, findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Down key can highlight the menu item', (WidgetTester tester) async {
|
testWidgets('Down key can highlight the menu item on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -468,9 +468,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
item0material = tester.widget<Material>(button0Material);
|
item0material = tester.widget<Material>(button0Material);
|
||||||
expect(item0material.color, Colors.transparent); // the previous item should not be highlighted.
|
expect(item0material.color, Colors.transparent); // the previous item should not be highlighted.
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Up key can highlight the menu item', (WidgetTester tester) async {
|
testWidgets('Up key can highlight the menu item on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -510,9 +510,10 @@ void main() {
|
|||||||
|
|
||||||
item5material = tester.widget<Material>(button5Material);
|
item5material = tester.widget<Material>(button5Material);
|
||||||
expect(item5material.color, Colors.transparent); // the previous item should not be highlighted.
|
expect(item5material.color, Colors.transparent); // the previous item should not be highlighted.
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('The text input should match the label of the menu item while pressing down key', (WidgetTester tester) async {
|
testWidgets('The text input should match the label of the menu item '
|
||||||
|
'while pressing down key on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -540,9 +541,10 @@ void main() {
|
|||||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget);
|
expect(find.widgetWithText(TextField, 'Item 2'), findsOneWidget);
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('The text input should match the label of the menu item while pressing up key', (WidgetTester tester) async {
|
testWidgets('The text input should match the label of the menu item '
|
||||||
|
'while pressing up key on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -570,9 +572,9 @@ void main() {
|
|||||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
|
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Disabled button will be skipped while pressing up/down key', (WidgetTester tester) async {
|
testWidgets('Disabled button will be skipped while pressing up/down key on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
|
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
|
||||||
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
|
||||||
@ -614,9 +616,32 @@ void main() {
|
|||||||
);
|
);
|
||||||
final Material item3Material = tester.widget<Material>(button3Material);
|
final Material item3Material = tester.widget<Material>(button3Material);
|
||||||
expect(item3Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
expect(item3Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Searching is enabled by default', (WidgetTester tester) async {
|
testWidgets('Searching is enabled by default on mobile platforms if initialSelection is non null', (WidgetTester tester) async {
|
||||||
|
final ThemeData themeData = ThemeData();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
theme: themeData,
|
||||||
|
home: Scaffold(
|
||||||
|
body: DropdownMenu<TestMenu>(
|
||||||
|
initialSelection: TestMenu.mainMenu1,
|
||||||
|
dropdownMenuEntries: menuChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
||||||
|
await tester.pump();
|
||||||
|
final Finder buttonMaterial = find.descendant(
|
||||||
|
of: find.widgetWithText(MenuItemButton, 'Menu 1').last,
|
||||||
|
matching: find.byType(Material),
|
||||||
|
);
|
||||||
|
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
||||||
|
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
|
||||||
|
}, variant: TargetPlatformVariant.mobile());
|
||||||
|
|
||||||
|
testWidgets('Searching is enabled by default on desktop platform', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -638,9 +663,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
||||||
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
|
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); // Menu 1 button is highlighted.
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Highlight can move up/down from the searching result', (WidgetTester tester) async {
|
testWidgets('Highlight can move up/down starting from the searching result on desktop platforms', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: themeData,
|
theme: themeData,
|
||||||
@ -684,7 +709,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
final Material item5Material = tester.widget<Material>(button5Material);
|
final Material item5Material = tester.widget<Material>(button5Material);
|
||||||
expect(item5Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
expect(item5Material.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Filtering is disabled by default', (WidgetTester tester) async {
|
testWidgets('Filtering is disabled by default', (WidgetTester tester) async {
|
||||||
final ThemeData themeData = ThemeData();
|
final ThemeData themeData = ThemeData();
|
||||||
@ -692,6 +717,7 @@ void main() {
|
|||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: DropdownMenu<TestMenu>(
|
body: DropdownMenu<TestMenu>(
|
||||||
|
requestFocusOnTap: true,
|
||||||
dropdownMenuEntries: menuChildren,
|
dropdownMenuEntries: menuChildren,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -715,6 +741,7 @@ void main() {
|
|||||||
theme: themeData,
|
theme: themeData,
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: DropdownMenu<TestMenu>(
|
body: DropdownMenu<TestMenu>(
|
||||||
|
requestFocusOnTap: true,
|
||||||
enableFilter: true,
|
enableFilter: true,
|
||||||
dropdownMenuEntries: menuChildren,
|
dropdownMenuEntries: menuChildren,
|
||||||
),
|
),
|
||||||
@ -748,6 +775,7 @@ void main() {
|
|||||||
builder: (BuildContext context, StateSetter setState) {
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: DropdownMenu<TestMenu>(
|
body: DropdownMenu<TestMenu>(
|
||||||
|
requestFocusOnTap: true,
|
||||||
enableFilter: true,
|
enableFilter: true,
|
||||||
dropdownMenuEntries: menuChildren,
|
dropdownMenuEntries: menuChildren,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@ -804,29 +832,47 @@ void main() {
|
|||||||
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
|
late final bool isMobile;
|
||||||
|
switch (themeData.platform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
isMobile = true;
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
isMobile = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int expectedCount = isMobile ? 0 : 1;
|
||||||
|
|
||||||
// Test onSelected on key press
|
// Test onSelected on key press
|
||||||
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(selectionCount, 1);
|
expect(selectionCount, expectedCount);
|
||||||
|
// The desktop platform closed the menu when a completion action is pressed. So we need to reopen it.
|
||||||
// Disabled item doesn't trigger onSelected callback.
|
if (!isMobile) {
|
||||||
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled item doesn't trigger onSelected callback.
|
||||||
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 1').last;
|
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 1').last;
|
||||||
await tester.tap(item1);
|
await tester.tap(item1);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(controller.text, 'Item 0');
|
expect(controller.text, isMobile ? '' : 'Item 0');
|
||||||
expect(selectionCount, 1);
|
expect(selectionCount, expectedCount);
|
||||||
|
|
||||||
final Finder item2 = find.widgetWithText(MenuItemButton, 'Item 2').last;
|
final Finder item2 = find.widgetWithText(MenuItemButton, 'Item 2').last;
|
||||||
await tester.tap(item2);
|
await tester.tap(item2);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(controller.text, 'Item 2');
|
expect(controller.text, 'Item 2');
|
||||||
expect(selectionCount, 2);
|
expect(selectionCount, ++expectedCount);
|
||||||
|
|
||||||
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
await tester.tap(find.byType(DropdownMenu<TestMenu>));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -835,18 +881,20 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(controller.text, 'Item 3');
|
expect(controller.text, 'Item 3');
|
||||||
expect(selectionCount, 3);
|
expect(selectionCount, ++expectedCount);
|
||||||
|
|
||||||
// When typing something in the text field without selecting any of the options,
|
// On desktop platforms, when typing something in the text field without selecting any of the options,
|
||||||
// the onSelected should not be called.
|
// the onSelected should not be called.
|
||||||
|
if (!isMobile) {
|
||||||
await tester.enterText(find.byType(TextField).first, 'New Item');
|
await tester.enterText(find.byType(TextField).first, 'New Item');
|
||||||
expect(controller.text, 'New Item');
|
expect(controller.text, 'New Item');
|
||||||
expect(selectionCount, 3);
|
expect(selectionCount, expectedCount);
|
||||||
expect(find.widgetWithText(TextField, 'New Item'), findsOneWidget);
|
expect(find.widgetWithText(TextField, 'New Item'), findsOneWidget);
|
||||||
await tester.enterText(find.byType(TextField).first, '');
|
await tester.enterText(find.byType(TextField).first, '');
|
||||||
expect(selectionCount, 3);
|
expect(selectionCount, expectedCount);
|
||||||
expect(controller.text.isEmpty, true);
|
expect(controller.text.isEmpty, true);
|
||||||
});
|
}
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
|
||||||
testWidgets('The selectedValue gives an initial text and highlights the according item', (WidgetTester tester) async {
|
testWidgets('The selectedValue gives an initial text and highlights the according item', (WidgetTester tester) async {
|
||||||
@ -882,6 +930,107 @@ void main() {
|
|||||||
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
|
||||||
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('The default text input field should not be focused on mobile platforms '
|
||||||
|
'when it is tapped', (WidgetTester tester) async {
|
||||||
|
final ThemeData themeData = ThemeData();
|
||||||
|
|
||||||
|
Widget buildDropdownMenu() => MaterialApp(
|
||||||
|
theme: themeData,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
DropdownMenu<TestMenu>(
|
||||||
|
dropdownMenuEntries: menuChildren,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test default condition.
|
||||||
|
await tester.pumpWidget(buildDropdownMenu());
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Finder textFieldFinder = find.byType(TextField);
|
||||||
|
final TextField result = tester.widget<TextField>(textFieldFinder);
|
||||||
|
expect(result.canRequestFocus, false);
|
||||||
|
}, variant: TargetPlatformVariant.mobile());
|
||||||
|
|
||||||
|
testWidgets('The text input field should be focused on desktop platforms '
|
||||||
|
'when it is tapped', (WidgetTester tester) async {
|
||||||
|
final ThemeData themeData = ThemeData();
|
||||||
|
|
||||||
|
Widget buildDropdownMenu() => MaterialApp(
|
||||||
|
theme: themeData,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
DropdownMenu<TestMenu>(
|
||||||
|
dropdownMenuEntries: menuChildren,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildDropdownMenu());
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Finder textFieldFinder = find.byType(TextField);
|
||||||
|
final TextField result = tester.widget<TextField>(textFieldFinder);
|
||||||
|
expect(result.canRequestFocus, true);
|
||||||
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
|
testWidgets('If requestFocusOnTap is true, the text input field can request focus, '
|
||||||
|
'otherwise it cannot request focus', (WidgetTester tester) async {
|
||||||
|
final ThemeData themeData = ThemeData();
|
||||||
|
|
||||||
|
Widget buildDropdownMenu({required bool requestFocusOnTap}) => MaterialApp(
|
||||||
|
theme: themeData,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
DropdownMenu<TestMenu>(
|
||||||
|
requestFocusOnTap: requestFocusOnTap,
|
||||||
|
dropdownMenuEntries: menuChildren,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set requestFocusOnTap to true.
|
||||||
|
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: true));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Finder textFieldFinder = find.byType(TextField);
|
||||||
|
final TextField textField = tester.widget<TextField>(textFieldFinder);
|
||||||
|
expect(textField.canRequestFocus, true);
|
||||||
|
// Open the dropdown menu.
|
||||||
|
await tester.tap(textFieldFinder);
|
||||||
|
await tester.pump();
|
||||||
|
// Make a selection.
|
||||||
|
await tester.tap(find.widgetWithText(MenuItemButton, 'Item 0').last);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
|
||||||
|
|
||||||
|
// Set requestFocusOnTap to false.
|
||||||
|
await tester.pumpWidget(Container());
|
||||||
|
await tester.pumpWidget(buildDropdownMenu(requestFocusOnTap: false));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Finder textFieldFinder1 = find.byType(TextField);
|
||||||
|
final TextField textField1 = tester.widget<TextField>(textFieldFinder1);
|
||||||
|
expect(textField1.canRequestFocus, false);
|
||||||
|
// Open the dropdown menu.
|
||||||
|
await tester.tap(textFieldFinder1);
|
||||||
|
await tester.pump();
|
||||||
|
// Make a selection.
|
||||||
|
await tester.tap(find.widgetWithText(MenuItemButton, 'Item 0').last);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.widgetWithText(TextField, 'Item 0'), findsOneWidget);
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TestMenu {
|
enum TestMenu {
|
||||||
|
@ -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() {
|
|||||||
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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user