Add DropdownMenu.focusNode (#142516)

fixes [`DropdownMenu` doesn't have a focusNode](https://github.com/flutter/flutter/issues/142384)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```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<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  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<TShirtSize>(
            focusNode: _focusNode,
            initialSelection: TShirtSize.m,
            label: const Text('T-Shirt Size'),
            dropdownMenuEntries: TShirtSize.values.map((e) {
              return DropdownMenuEntry<TShirtSize>(
                value: e,
                label: e.label,
              );
            }).toList(),
          ),
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: () {
            _focusNode.requestFocus();
          },
          label: const Text('Request Focus on DropdownMenu'),
        ),
      ),
    );
  }
}
```

</details>
This commit is contained in:
Ian Hickson 2024-01-30 22:12:20 -08:00 committed by GitHub
parent 4af2051f8e
commit 16e014e884
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 100 additions and 8 deletions

View file

@ -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<T> extends StatefulWidget {
this.controller,
this.initialSelection,
this.onSelected,
this.focusNode,
this.requestFocusOnTap,
this.expandedInsets,
this.searchCallback,
@ -276,17 +281,62 @@ class DropdownMenu<T> extends StatefulWidget {
/// Defaults to null. If null, only the text field is updated.
final ValueChanged<T?>? 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<T> extends State<DropdownMenu<T>> {
}
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<T> extends State<DropdownMenu<T>> {
final Widget textField = TextField(
key: _anchorKey,
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
canRequestFocus: canRequestFocus(),
enableInteractiveSelection: canRequestFocus(),
textAlignVertical: TextAlignVertical.center,

View file

@ -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<String>(
focusNode: focusNode,
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
DropdownMenuEntry<String>(
value: 'Yolk',
label: 'Yolk',
),
DropdownMenuEntry<String>(
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 {