From 16e014e884bca395338b7f7d5fd3f190d4e014bf Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 30 Jan 2024 22:12:20 -0800 Subject: [PATCH] Add `DropdownMenu.focusNode` (#142516) fixes [`DropdownMenu` doesn't have a focusNode](https://github.com/flutter/flutter/issues/142384) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; enum TShirtSize { s('S'), m('M'), l('L'), xl('XL'), xxl('XXL'), ; const TShirtSize(this.label); final String label; } void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { final FocusNode _focusNode = FocusNode(); @override void dispose() { _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: AppBar( title: const Text('DropdownMenu Sample'), ), body: Center( child: DropdownMenu( focusNode: _focusNode, initialSelection: TShirtSize.m, label: const Text('T-Shirt Size'), dropdownMenuEntries: TShirtSize.values.map((e) { return DropdownMenuEntry( value: e, label: e.label, ); }).toList(), ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () { _focusNode.requestFocus(); }, label: const Text('Request Focus on DropdownMenu'), ), ), ); } } ```
--- .../lib/src/material/dropdown_menu.dart | 69 ++++++++++++++++--- .../test/material/dropdown_menu_test.dart | 39 +++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index e8f9aceb5f4..7d73db013a7 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -21,6 +21,10 @@ import 'text_field.dart'; import 'theme.dart'; import 'theme_data.dart'; +// Examples can assume: +// late BuildContext context; +// late FocusNode myFocusNode; + /// A callback function that returns the index of the item that matches the /// current contents of a text field. /// @@ -155,6 +159,7 @@ class DropdownMenu extends StatefulWidget { this.controller, this.initialSelection, this.onSelected, + this.focusNode, this.requestFocusOnTap, this.expandedInsets, this.searchCallback, @@ -276,17 +281,62 @@ class DropdownMenu extends StatefulWidget { /// Defaults to null. If null, only the text field is updated. final ValueChanged? onSelected; + /// Defines the keyboard focus for this widget. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + /// + /// ## Keyboard + /// + /// Requesting the focus will typically cause the keyboard to be shown + /// if it's not showing already. + /// + /// On Android, the user can hide the keyboard - without changing the focus - + /// with the system back button. They can restore the keyboard's visibility + /// by tapping on a text field. The user might hide the keyboard and + /// switch to a physical keyboard, or they might just need to get it + /// out of the way for a moment, to expose something it's + /// obscuring. In this case requesting the focus again will not + /// cause the focus to change, and will not make the keyboard visible. + /// + /// If this is non-null, the behaviour of [requestFocusOnTap] is overridden + /// by the [FocusNode.canRequestFocus] property. + final FocusNode? focusNode; + /// 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. + /// Ignored if a [focusNode] is explicitly provided (in which case, + /// [FocusNode.canRequestFocus] controls the behavior). /// - /// 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. + /// Defaults to null, which enables platform-specific behavior: + /// + /// * On mobile platforms, acts as if set to false; tapping on the text + /// field and opening the menu will not cause a focus request and the + /// virtual keyboard will not appear. + /// + /// * On desktop platforms, acts as if set to true; the dropdown takes the + /// focus when activated. + /// + /// Set this to true or false explicitly to override the default behavior. final bool? requestFocusOnTap; /// Descriptions of the menu items in the [DropdownMenu]. @@ -419,10 +469,12 @@ class _DropdownMenuState extends State> { } bool canRequestFocus() { + if (widget.focusNode != null) { + return widget.focusNode!.canRequestFocus; + } if (widget.requestFocusOnTap != null) { return widget.requestFocusOnTap!; } - switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.android: @@ -676,6 +728,7 @@ class _DropdownMenuState extends State> { final Widget textField = TextField( key: _anchorKey, mouseCursor: effectiveMouseCursor, + focusNode: widget.focusNode, canRequestFocus: canRequestFocus(), enableInteractiveSelection: canRequestFocus(), textAlignVertical: TextAlignVertical.center, diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 475631c2888..2976d4bb20f 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -1932,6 +1932,45 @@ void main() { expect(find.byType(Scrollbar), findsOneWidget); }, variant: TargetPlatformVariant.all()); + + testWidgets('DropdownMenu.focusNode can focus text input field', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + final ThemeData theme = ThemeData(); + + await tester.pumpWidget(MaterialApp( + theme: theme, + home: Scaffold( + body: DropdownMenu( + focusNode: focusNode, + dropdownMenuEntries: const >[ + DropdownMenuEntry( + value: 'Yolk', + label: 'Yolk', + ), + DropdownMenuEntry( + value: 'Eggbert', + label: 'Eggbert', + ), + ], + ), + ), + )); + + RenderBox box = tester.renderObject(find.byType(InputDecorator)); + + // Test input border when not focused. + expect(box, paints..rrect(color: theme.colorScheme.outline)); + + focusNode.requestFocus(); + await tester.pump(); + // Advance input decorator animation. + await tester.pump(const Duration(milliseconds: 200)); + + box = tester.renderObject(find.byType(InputDecorator)); + + // Test input border when focused. + expect(box, paints..rrect(color: theme.colorScheme.primary)); + }); } enum TestMenu {