Skip to content

Commit 21381d8

Browse files
authored
Fix DropdownMenu does not rematch initialSelection when entries have changed (#155757)
## Description This PR makes DropdownMenu rematching the initialSelection when the entries are updated. If the new entries contains one entry whose value matches `initialSelection` this entry's label is used to initialize the inner text field, if no entries matches `initialSelection` the text field is emptied. ## Related Issue Fixes [DropdownMenu.didUpdateWidget should re-match initialSelection when dropdownMenuEntries have changed](flutter/flutter#155660). ## Tests Adds 3 tests.
1 parent 91c621e commit 21381d8

File tree

5 files changed

+198
-61
lines changed

5 files changed

+198
-61
lines changed

examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:collection/collection.dart';
56
import 'package:flutter/material.dart';
67

78
// Flutter code sample for [DropdownMenu]s. The first dropdown menu
@@ -14,6 +15,8 @@ void main() {
1415
runApp(const DropdownMenuExample());
1516
}
1617

18+
typedef ColorEntry = DropdownMenuEntry<ColorLabel>;
19+
1720
// DropdownMenuEntry labels and values for the first dropdown menu.
1821
enum ColorLabel {
1922
blue('Blue', Colors.blue),
@@ -25,21 +28,43 @@ enum ColorLabel {
2528
const ColorLabel(this.label, this.color);
2629
final String label;
2730
final Color color;
31+
32+
static final List<ColorEntry> entries = UnmodifiableListView<ColorEntry>(
33+
values.map<ColorEntry>(
34+
(ColorLabel color) => ColorEntry(
35+
value: color,
36+
label: color.label,
37+
enabled: color.label != 'Grey',
38+
style: MenuItemButton.styleFrom(
39+
foregroundColor: color.color,
40+
),
41+
),
42+
),
43+
);
2844
}
2945

46+
typedef IconEntry = DropdownMenuEntry<IconLabel>;
47+
3048
// DropdownMenuEntry labels and values for the second dropdown menu.
3149
enum IconLabel {
3250
smile('Smile', Icons.sentiment_satisfied_outlined),
33-
cloud(
34-
'Cloud',
35-
Icons.cloud_outlined,
36-
),
51+
cloud('Cloud', Icons.cloud_outlined),
3752
brush('Brush', Icons.brush_outlined),
3853
heart('Heart', Icons.favorite);
3954

4055
const IconLabel(this.label, this.icon);
4156
final String label;
4257
final IconData icon;
58+
59+
static final List<IconEntry> entries = UnmodifiableListView<IconEntry>(
60+
values.map<IconEntry>(
61+
(IconLabel icon) => IconEntry(
62+
value: icon,
63+
label: icon.label,
64+
leadingIcon: Icon(icon.icon),
65+
),
66+
),
67+
);
4368
}
4469

4570
class DropdownMenuExample extends StatefulWidget {
@@ -85,18 +110,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
85110
selectedColor = color;
86111
});
87112
},
88-
dropdownMenuEntries: ColorLabel.values.map<DropdownMenuEntry<ColorLabel>>(
89-
(ColorLabel color) {
90-
return DropdownMenuEntry<ColorLabel>(
91-
value: color,
92-
label: color.label,
93-
enabled: color.label != 'Grey',
94-
style: MenuItemButton.styleFrom(
95-
foregroundColor: color.color,
96-
),
97-
);
98-
}
99-
).toList(),
113+
dropdownMenuEntries: ColorLabel.entries,
100114
),
101115
const SizedBox(width: 24),
102116
DropdownMenu<IconLabel>(
@@ -114,15 +128,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
114128
selectedIcon = icon;
115129
});
116130
},
117-
dropdownMenuEntries: IconLabel.values.map<DropdownMenuEntry<IconLabel>>(
118-
(IconLabel icon) {
119-
return DropdownMenuEntry<IconLabel>(
120-
value: icon,
121-
label: icon.label,
122-
leadingIcon: Icon(icon.icon),
123-
);
124-
},
125-
).toList(),
131+
dropdownMenuEntries: IconLabel.entries,
126132
),
127133
],
128134
),

examples/api/lib/material/dropdown_menu/dropdown_menu.1.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:collection/collection.dart';
56
import 'package:flutter/material.dart';
67

78
/// Flutter code sample for [DropdownMenu].
@@ -34,7 +35,12 @@ class DropdownMenuExample extends StatefulWidget {
3435
State<DropdownMenuExample> createState() => _DropdownMenuExampleState();
3536
}
3637

38+
typedef MenuEntry = DropdownMenuEntry<String>;
39+
3740
class _DropdownMenuExampleState extends State<DropdownMenuExample> {
41+
static final List<MenuEntry> menuEntries = UnmodifiableListView<MenuEntry>(
42+
list.map<MenuEntry>((String name) => MenuEntry(value: name, label: name)),
43+
);
3844
String dropdownValue = list.first;
3945

4046
@override
@@ -47,12 +53,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
4753
dropdownValue = value!;
4854
});
4955
},
50-
dropdownMenuEntries: list.map<DropdownMenuEntry<String>>((String value) {
51-
return DropdownMenuEntry<String>(
52-
value: value,
53-
label: value
54-
);
55-
}).toList(),
56+
dropdownMenuEntries: menuEntries,
5657
);
5758
}
5859
}

examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:collection/collection.dart';
56
import 'package:flutter/material.dart';
67

78
/// Flutter code sample for [DropdownMenu].
@@ -33,7 +34,12 @@ class DropdownMenuExample extends StatefulWidget {
3334
State<DropdownMenuExample> createState() => _DropdownMenuExampleState();
3435
}
3536

37+
typedef MenuEntry = DropdownMenuEntry<String>;
38+
3639
class _DropdownMenuExampleState extends State<DropdownMenuExample> {
40+
static final List<MenuEntry> menuEntries = UnmodifiableListView<MenuEntry>(
41+
list.map<MenuEntry>((String name) => MenuEntry(value: name, label: name)),
42+
);
3743
String dropdownValue = list.first;
3844

3945
@override
@@ -62,10 +68,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
6268
dropdownValue = value!;
6369
});
6470
},
65-
dropdownMenuEntries:
66-
list.map<DropdownMenuEntry<String>>((String value) {
67-
return DropdownMenuEntry<String>(value: value, label: value);
68-
}).toList(),
71+
dropdownMenuEntries: menuEntries,
6972
),
7073
const Text('Text cursor is shown when hovering over the DropdownMenu.'),
7174
],
@@ -92,10 +95,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
9295
dropdownValue = value!;
9396
});
9497
},
95-
dropdownMenuEntries:
96-
list.map<DropdownMenuEntry<String>>((String value) {
97-
return DropdownMenuEntry<String>(value: value, label: value);
98-
}).toList(),
98+
dropdownMenuEntries: menuEntries,
9999
),
100100
const Text('Clickable cursor is shown when hovering over the DropdownMenu.'),
101101
],
@@ -123,10 +123,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
123123
dropdownValue = value!;
124124
});
125125
},
126-
dropdownMenuEntries:
127-
list.map<DropdownMenuEntry<String>>((String value) {
128-
return DropdownMenuEntry<String>(value: value, label: value);
129-
}).toList(),
126+
dropdownMenuEntries: menuEntries,
130127
),
131128
const Text('Default cursor is shown when hovering over the DropdownMenu.'),
132129
],
@@ -154,10 +151,7 @@ class _DropdownMenuExampleState extends State<DropdownMenuExample> {
154151
dropdownValue = value!;
155152
});
156153
},
157-
dropdownMenuEntries:
158-
list.map<DropdownMenuEntry<String>>((String value) {
159-
return DropdownMenuEntry<String>(value: value, label: value);
160-
}).toList(),
154+
dropdownMenuEntries: menuEntries,
161155
),
162156
const Text('Default cursor is shown when hovering over the DropdownMenu.'),
163157
],

packages/flutter/lib/src/material/dropdown_menu.dart

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,18 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
497497
bool _menuHasEnabledItem = false;
498498
TextEditingController? _localTextEditingController;
499499

500+
TextEditingValue get _initialTextEditingValue {
501+
for (final DropdownMenuEntry<T> entry in filteredEntries) {
502+
if (entry.value == widget.initialSelection) {
503+
return TextEditingValue(
504+
text: entry.label,
505+
selection: TextSelection.collapsed(offset: entry.label.length),
506+
);
507+
}
508+
}
509+
return TextEditingValue.empty;
510+
}
511+
500512
@override
501513
void initState() {
502514
super.initState();
@@ -509,14 +521,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
509521
filteredEntries = widget.dropdownMenuEntries;
510522
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
511523
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
524+
_localTextEditingController?.value = _initialTextEditingValue;
512525

513-
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
514-
if (index != -1) {
515-
_localTextEditingController?.value = TextEditingValue(
516-
text: filteredEntries[index].label,
517-
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
518-
);
519-
}
520526
refreshLeadingPadding();
521527
}
522528

@@ -554,18 +560,19 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
554560
filteredEntries = widget.dropdownMenuEntries;
555561
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
556562
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
563+
// If the text field content matches one of the new entries do not rematch the initialSelection.
564+
final bool isCurrentSelectionValid = filteredEntries.any(
565+
(DropdownMenuEntry<T> entry) => entry.label == _localTextEditingController?.text
566+
);
567+
if (!isCurrentSelectionValid) {
568+
_localTextEditingController?.value = _initialTextEditingValue;
569+
}
557570
}
558571
if (oldWidget.leadingIcon != widget.leadingIcon) {
559572
refreshLeadingPadding();
560573
}
561574
if (oldWidget.initialSelection != widget.initialSelection) {
562-
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
563-
if (index != -1) {
564-
_localTextEditingController?.value = TextEditingValue(
565-
text: filteredEntries[index].label,
566-
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
567-
);
568-
}
575+
_localTextEditingController?.value = _initialTextEditingValue;
569576
}
570577
}
571578

0 commit comments

Comments
 (0)