Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 72 additions & 53 deletions packages/flutter/lib/src/material/dropdown_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,35 +348,54 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _leadingKey = GlobalKey();
late List<GlobalKey> buttonItemKeys;
final MenuController _controller = MenuController();
late final TextEditingController _textEditingController;
late bool _enableFilter;
late List<DropdownMenuEntry<T>> filteredEntries;
List<Widget>? _initialMenu;
int? currentHighlight;
double? leadingPadding;
bool _menuHasEnabledItem = false;
TextEditingController? _localTextEditingController;
TextEditingController get _textEditingController {
return widget.controller ?? (_localTextEditingController ??= TextEditingController());
}

@override
void initState() {
super.initState();
_textEditingController = widget.controller ?? TextEditingController();
_enableFilter = widget.enableFilter;
filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);

final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) {
_textEditingController.text = filteredEntries[index].label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
_textEditingController.value = TextEditingValue(
text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
);
}
refreshLeadingPadding();
}

@override
void dispose() {
if (_localTextEditingController != null) {
debugPrint('Disposing of $_textEditingController');
}
_localTextEditingController?.dispose();
_localTextEditingController = null;
super.dispose();
}

@override
void didUpdateWidget(DropdownMenu<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code wasn't handling the case where the widget got a new controller later on. It would just ignore any controller that was set later, and continue using the one it originally constructed.

if (widget.controller != null) {
_localTextEditingController?.dispose();
_localTextEditingController = null;
}
}
if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) {
currentHighlight = null;
Expand All @@ -394,9 +413,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (oldWidget.initialSelection != widget.initialSelection) {
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) {
_textEditingController.text = filteredEntries[index].label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
_textEditingController.value = TextEditingValue(
text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
);
}
}
}
Expand Down Expand Up @@ -463,7 +483,6 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {

List<Widget> _buildButtons(
List<DropdownMenuEntry<T>> filteredEntries,
TextEditingController textEditingController,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the problem here was that the text editing controller that was passed in was being captured by the closure, rather than being accessed new each time, so it would capture an instance that was no longer current. There's no need to pass it in, since it's a member of the state class.

TextDirection textDirection,
{ int? focusedIndex, bool enableScrollToHighlight = true}
) {
Expand Down Expand Up @@ -519,9 +538,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
trailingIcon: entry.trailingIcon,
onPressed: entry.enabled
? () {
textEditingController.text = entry.label;
textEditingController.selection =
TextSelection.collapsed(offset: textEditingController.text.length);
_textEditingController.value = TextEditingValue(
text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length),
);
currentHighlight = widget.enableSearch ? i : null;
widget.onSelected?.call(entry.value);
}
Expand All @@ -535,37 +555,43 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
return result;
}

void handleUpKeyInvoke(_) => setState(() {
if (!_menuHasEnabledItem || !_controller.isOpen) {
return;
}
_enableFilter = false;
currentHighlight ??= 0;
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
while (!filteredEntries[currentHighlight!].enabled) {
void handleUpKeyInvoke(_) {
setState(() {
if (!_menuHasEnabledItem || !_controller.isOpen) {
return;
}
_enableFilter = false;
currentHighlight ??= 0;
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
}
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.text = currentLabel;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
});
while (!filteredEntries[currentHighlight!].enabled) {
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
}
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.value = TextEditingValue(
text: currentLabel,
selection: TextSelection.collapsed(offset: currentLabel.length),
);
});
}

void handleDownKeyInvoke(_) => setState(() {
if (!_menuHasEnabledItem || !_controller.isOpen) {
return;
}
_enableFilter = false;
currentHighlight ??= -1;
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
while (!filteredEntries[currentHighlight!].enabled) {
void handleDownKeyInvoke(_) {
setState(() {
if (!_menuHasEnabledItem || !_controller.isOpen) {
return;
}
_enableFilter = false;
currentHighlight ??= -1;
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
}
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.text = currentLabel;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
});
while (!filteredEntries[currentHighlight!].enabled) {
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
}
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.value = TextEditingValue(
text: currentLabel,
selection: TextSelection.collapsed(offset: currentLabel.length),
);
});
}

void handlePressed(MenuController controller) {
if (controller.isOpen) {
Expand All @@ -580,18 +606,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
setState(() {});
}

@override
void dispose() {
if (widget.controller == null) {
_textEditingController.dispose();
}
super.dispose();
}

@override
Widget build(BuildContext context) {
final TextDirection textDirection = Directionality.of(context);
_initialMenu ??= _buildButtons(widget.dropdownMenuEntries, _textEditingController, textDirection, enableScrollToHighlight: false);
_initialMenu ??= _buildButtons(widget.dropdownMenuEntries, textDirection, enableScrollToHighlight: false);
final DropdownMenuThemeData theme = DropdownMenuTheme.of(context);
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);

Expand All @@ -610,7 +628,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
}
}

final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight);
final List<Widget> menu = _buildButtons(filteredEntries, textDirection, focusedIndex: currentHighlight);

final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle;

Expand Down Expand Up @@ -670,9 +688,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (currentHighlight != null) {
final DropdownMenuEntry<T> entry = filteredEntries[currentHighlight!];
if (entry.enabled) {
_textEditingController.text = entry.label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
_textEditingController.value = TextEditingValue(
text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length),
);
widget.onSelected?.call(entry.value);
}
} else {
Expand Down
103 changes: 103 additions & 0 deletions packages/flutter/test/material/dropdown_menu_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,109 @@ void main() {
await tester.pump();
checkExpectedHighlight(searchResult: 'Read', otherItems: <String>['All', 'Unread']);
});

testWidgetsWithLeakTracking('onSelected gets called when a selection is made in a nested menu',
(WidgetTester tester) async {
int selectionCount = 0;

final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
];

await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: MenuAnchor(
menuChildren: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
],
builder: (BuildContext context, MenuController controller, Widget? widget) {
return IconButton(
icon: const Icon(Icons.smartphone_rounded),
onPressed: () {
controller.open();
},
);
},
),
);
}),
));

// Open the first menu
await tester.tap(find.byType(IconButton));
await tester.pump();
// Open the dropdown menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();

final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last;
await tester.tap(item1);
await tester.pumpAndSettle();

expect(selectionCount, 1);
});

testWidgetsWithLeakTracking('When onSelected is called and menu is closed, no textEditingController exception is thrown',
(WidgetTester tester) async {
int selectionCount = 0;

final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
];

await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: MenuAnchor(
menuChildren: <Widget>[
DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
],
builder: (BuildContext context, MenuController controller, Widget? widget) {
return IconButton(
icon: const Icon(Icons.smartphone_rounded),
onPressed: () {
controller.open();
},
);
},
),
);
}),
));

// Open the first menu
await tester.tap(find.byType(IconButton));
await tester.pump();
// Open the dropdown menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();

final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last;
await tester.tap(item1);
await tester.pumpAndSettle();

expect(selectionCount, 1);
expect(tester.takeException(), isNull);
});
}

enum TestMenu {
Expand Down