mirror of
https://github.com/flutter/flutter
synced 2024-09-13 05:11:45 +00:00
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:
parent
4af2051f8e
commit
16e014e884
|
@ -21,6 +21,10 @@ import 'text_field.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'theme_data.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
|
/// A callback function that returns the index of the item that matches the
|
||||||
/// current contents of a text field.
|
/// current contents of a text field.
|
||||||
///
|
///
|
||||||
|
@ -155,6 +159,7 @@ class DropdownMenu<T> extends StatefulWidget {
|
||||||
this.controller,
|
this.controller,
|
||||||
this.initialSelection,
|
this.initialSelection,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
|
this.focusNode,
|
||||||
this.requestFocusOnTap,
|
this.requestFocusOnTap,
|
||||||
this.expandedInsets,
|
this.expandedInsets,
|
||||||
this.searchCallback,
|
this.searchCallback,
|
||||||
|
@ -276,17 +281,62 @@ 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;
|
||||||
|
|
||||||
|
/// 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
|
/// Determine if the dropdown button requests focus and the on-screen virtual
|
||||||
/// keyboard is shown in response to a touch event.
|
/// keyboard is shown in response to a touch event.
|
||||||
///
|
///
|
||||||
/// By default, on mobile platforms, tapping on the text field and opening
|
/// Ignored if a [focusNode] is explicitly provided (in which case,
|
||||||
/// the menu will not cause a focus request and the virtual keyboard will not
|
/// [FocusNode.canRequestFocus] controls the behavior).
|
||||||
/// 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
|
/// Defaults to null, which enables platform-specific behavior:
|
||||||
/// the implementation to choose based on the platform, can be useful for
|
///
|
||||||
/// applications that want to override the default 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;
|
final bool? requestFocusOnTap;
|
||||||
|
|
||||||
/// Descriptions of the menu items in the [DropdownMenu].
|
/// Descriptions of the menu items in the [DropdownMenu].
|
||||||
|
@ -419,10 +469,12 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canRequestFocus() {
|
bool canRequestFocus() {
|
||||||
|
if (widget.focusNode != null) {
|
||||||
|
return widget.focusNode!.canRequestFocus;
|
||||||
|
}
|
||||||
if (widget.requestFocusOnTap != null) {
|
if (widget.requestFocusOnTap != null) {
|
||||||
return widget.requestFocusOnTap!;
|
return widget.requestFocusOnTap!;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (Theme.of(context).platform) {
|
switch (Theme.of(context).platform) {
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
|
@ -676,6 +728,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||||
final Widget textField = TextField(
|
final Widget textField = TextField(
|
||||||
key: _anchorKey,
|
key: _anchorKey,
|
||||||
mouseCursor: effectiveMouseCursor,
|
mouseCursor: effectiveMouseCursor,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
canRequestFocus: canRequestFocus(),
|
canRequestFocus: canRequestFocus(),
|
||||||
enableInteractiveSelection: canRequestFocus(),
|
enableInteractiveSelection: canRequestFocus(),
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
|
|
@ -1932,6 +1932,45 @@ void main() {
|
||||||
|
|
||||||
expect(find.byType(Scrollbar), findsOneWidget);
|
expect(find.byType(Scrollbar), findsOneWidget);
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, 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 {
|
enum TestMenu {
|
||||||
|
|
Loading…
Reference in a new issue