Skip to content

Commit 6c6938e

Browse files
authored
fix: Dropdown text not clearing when its value is reset (#5800)
* initial commit * improve docs * improve code * test clearing dropdown * test clearing dropdown
1 parent be928bd commit 6c6938e

File tree

8 files changed

+140
-84
lines changed

8 files changed

+140
-84
lines changed

packages/flet/lib/src/controls/dropdown.dart

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:flet/flet.dart';
23
import 'package:flutter/material.dart';
34
import 'package:flutter/scheduler.dart';
@@ -16,21 +17,27 @@ class DropdownControl extends StatefulWidget {
1617
class _DropdownControlState extends State<DropdownControl> {
1718
late final FocusNode _focusNode;
1819
late final TextEditingController _controller;
20+
String? _value;
21+
bool _suppressTextChange = false;
1922

2023
@override
2124
void initState() {
2225
super.initState();
23-
_focusNode = FocusNode();
2426

25-
_controller = TextEditingController(text: widget.control.getString("text"));
27+
// initialize controller
28+
_value = widget.control.getString("value");
29+
final text = widget.control.getString("text") ?? _value ?? "";
30+
_controller = TextEditingController(text: text);
31+
_controller.addListener(_onTextChange);
32+
33+
_focusNode = FocusNode();
2634
_focusNode.addListener(_onFocusChange);
2735
widget.control.addInvokeMethodListener(_invokeMethod);
28-
29-
_controller.addListener(_onTextChange);
3036
}
3137

3238
void _onTextChange() {
33-
debugPrint("Typed text: ${_controller.text}");
39+
if (_suppressTextChange) return;
40+
3441
if (_controller.text != widget.control.getString("text")) {
3542
widget.control.updateProperties({"text": _controller.text});
3643
widget.control.triggerEvent("text_change", _controller.text);
@@ -41,6 +48,17 @@ class _DropdownControlState extends State<DropdownControl> {
4148
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
4249
}
4350

51+
/// Updates text without triggering a text change event.
52+
void _updateControllerText(String text) {
53+
_suppressTextChange = true;
54+
_controller.value = TextEditingValue(
55+
text: text,
56+
selection: TextSelection.collapsed(offset: text.length),
57+
);
58+
_suppressTextChange = false;
59+
widget.control.updateProperties({"text": text}, python: false);
60+
}
61+
4462
@override
4563
void dispose() {
4664
_focusNode.removeListener(_onFocusChange);
@@ -65,13 +83,12 @@ class _DropdownControlState extends State<DropdownControl> {
6583
debugPrint("DropdownMenu build: ${widget.control.id}");
6684

6785
var theme = Theme.of(context);
68-
bool editable = widget.control.getBool("editable", false)!;
69-
bool autofocus = widget.control.getBool("autofocus", false)!;
86+
var editable = widget.control.getBool("editable", false)!;
87+
var autofocus = widget.control.getBool("autofocus", false)!;
7088
var textSize = widget.control.getDouble("text_size");
7189
var color = widget.control.getColor("color", context);
7290

73-
TextAlign textAlign =
74-
widget.control.getTextAlign("text_align", TextAlign.start)!;
91+
var textAlign = widget.control.getTextAlign("text_align", TextAlign.start)!;
7592

7693
var fillColor = widget.control.getColor("fill_color", context);
7794
var borderColor = widget.control.getColor("border_color", context);
@@ -85,7 +102,7 @@ class _DropdownControlState extends State<DropdownControl> {
85102
var bgColor = widget.control.getWidgetStateColor("bgcolor", theme);
86103
var elevation = widget.control.getWidgetStateDouble("elevation");
87104

88-
FormFieldInputBorder inputBorder = widget.control
105+
var inputBorder = widget.control
89106
.getFormFieldInputBorder("border", FormFieldInputBorder.outline)!;
90107

91108
InputBorder? border;
@@ -115,7 +132,7 @@ class _DropdownControlState extends State<DropdownControl> {
115132
? BorderSide.none
116133
: BorderSide(
117134
color: borderColor ??
118-
theme.colorScheme.onSurface.withOpacity(0.38),
135+
theme.colorScheme.onSurface.withValues(alpha: 0.38),
119136
width: borderWidth ?? 1.0));
120137
}
121138
}
@@ -176,30 +193,54 @@ class _DropdownControlState extends State<DropdownControl> {
176193
color: color ?? theme.colorScheme.onSurface);
177194
}
178195

179-
var items = widget.control
196+
// build dropdown items
197+
var options = widget.control
180198
.children("options")
181-
.map<DropdownMenuEntry<String>>((Control itemCtrl) {
182-
bool itemDisabled = widget.control.disabled || itemCtrl.disabled;
183-
ButtonStyle? style = itemCtrl.getButtonStyle("style", theme);
184-
185-
return DropdownMenuEntry<String>(
186-
enabled: !itemDisabled,
187-
value: itemCtrl.getString("key") ??
188-
itemCtrl.getString("text") ??
189-
itemCtrl.id.toString(),
190-
label: itemCtrl.getString("text") ??
191-
itemCtrl.getString("key") ??
192-
itemCtrl.id.toString(),
193-
labelWidget: itemCtrl.buildWidget("content"),
194-
leadingIcon: itemCtrl.buildIconOrWidget("leading_icon"),
195-
trailingIcon: itemCtrl.buildIconOrWidget("trailing_icon"),
196-
style: style,
197-
);
198-
}).toList();
199-
200-
String? value = widget.control.getString("value");
201-
if (items.where((item) => item.value == value).isEmpty) {
202-
value = null;
199+
.map<DropdownMenuEntry<String>?>((Control itemCtrl) {
200+
bool itemDisabled = widget.control.disabled || itemCtrl.disabled;
201+
ButtonStyle? style = itemCtrl.getButtonStyle("style", theme);
202+
203+
var optionKey = itemCtrl.getString("key");
204+
var optionText = itemCtrl.getString("text");
205+
206+
var optionValue = optionKey ?? optionText;
207+
var optionLabel = optionText ?? optionKey;
208+
if (optionValue == null || optionLabel == null) {
209+
return null;
210+
}
211+
212+
return DropdownMenuEntry<String>(
213+
enabled: !itemDisabled,
214+
value: optionValue,
215+
label: optionLabel,
216+
labelWidget: itemCtrl.buildWidget("content"),
217+
leadingIcon: itemCtrl.buildIconOrWidget("leading_icon"),
218+
trailingIcon: itemCtrl.buildIconOrWidget("trailing_icon"),
219+
style: style,
220+
);
221+
})
222+
.nonNulls
223+
.toList();
224+
225+
var value = widget.control.getString("value");
226+
var selectedOption = options.firstWhereOrNull((o) => o.value == value);
227+
value = selectedOption?.value;
228+
229+
// keep controller text in sync with backend-driven value changes
230+
if (_value != value) {
231+
if (value == null) {
232+
if (_value != null && _controller.text.isNotEmpty) {
233+
// clears dropdown field
234+
_updateControllerText("");
235+
}
236+
} else {
237+
final String entryLabel =
238+
selectedOption?.label ?? widget.control.getString("text") ?? value;
239+
if (_controller.text != entryLabel) {
240+
_updateControllerText(entryLabel);
241+
}
242+
}
243+
_value = value;
203244
}
204245

205246
TextCapitalization textCapitalization = widget.control
@@ -237,20 +278,16 @@ class _DropdownControlState extends State<DropdownControl> {
237278
errorText: widget.control.getString("error_text"),
238279
hintText: widget.control.getString("hint_text"),
239280
helperText: widget.control.getString("helper_text"),
240-
// menuStyle: MenuStyle(
241-
// backgroundColor: widget.control.getWidgetStateColor("bgcolor", theme),
242-
// elevation: widget.control.getWidgetStateDouble("elevation"),
243-
// fixedSize: WidgetStateProperty.all(Size.fromWidth(menuWidth)),
244-
// ),
245281
menuStyle: menuStyle,
246282
inputDecorationTheme: inputDecorationTheme,
247283
onSelected: widget.control.disabled
248284
? null
249-
: (String? value) {
250-
widget.control.updateProperties({"value": value});
251-
widget.control.triggerEvent("select", value);
285+
: (String? selection) {
286+
_value = selection;
287+
widget.control.updateProperties({"value": selection});
288+
widget.control.triggerEvent("select", selection);
252289
},
253-
dropdownMenuEntries: items,
290+
dropdownMenuEntries: options,
254291
);
255292

256293
var didAutoFocus = false;
-6.75 KB
Loading
-3.33 KB
Loading
-3.52 KB
Loading
-3.4 KB
Loading
-3.27 KB
Loading

sdk/python/packages/flet/integration_tests/controls/material/test_dropdown.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@ def flet_app(flet_app_function):
1313

1414
@pytest.mark.asyncio(loop_scope="function")
1515
async def test_basic(flet_app: ftt.FletTestApp, request):
16-
colors = [ft.Colors.RED, ft.Colors.BLUE, ft.Colors.GREEN]
17-
dd = ft.Dropdown(
18-
label="Color",
19-
text="Select a color",
20-
options=[
21-
ft.DropdownOption(
22-
key=color.value, content=ft.Text(value=color.value, color=color)
23-
)
24-
for color in colors
25-
],
26-
key="dd",
27-
)
2816
flet_app.page.enable_screenshots = True
29-
flet_app.resize_page(400, 600)
30-
flet_app.page.add(dd)
17+
flet_app.resize_page(350, 300)
18+
19+
colors = ["red", "blue", "green"]
20+
flet_app.page.add(
21+
dd := ft.Dropdown(
22+
key="dd",
23+
label="Color",
24+
text="Select a color",
25+
options=[
26+
ft.DropdownOption(key=color, content=ft.Text(value=color, color=color))
27+
for color in colors
28+
],
29+
)
30+
)
3131
await flet_app.tester.pump_and_settle()
3232

3333
# normal state
@@ -48,10 +48,10 @@ async def test_basic(flet_app: ftt.FletTestApp, request):
4848
),
4949
)
5050

51-
blue_option = await flet_app.tester.find_by_text("blue")
52-
assert blue_option.count == 2
53-
54-
await flet_app.tester.tap(blue_option.last)
51+
# select red option
52+
red_options = await flet_app.tester.find_by_text("red")
53+
assert red_options.count == 2 # Flutter Finder bug - should be 1
54+
await flet_app.tester.tap(red_options.last)
5555
await flet_app.tester.pump_and_settle()
5656
flet_app.assert_screenshot(
5757
"basic_2",
@@ -60,9 +60,22 @@ async def test_basic(flet_app: ftt.FletTestApp, request):
6060
),
6161
)
6262

63+
# clear value
64+
dd.value = None
65+
dd.update()
66+
await flet_app.tester.pump_and_settle()
67+
flet_app.assert_screenshot(
68+
"basic_0",
69+
await flet_app.page.take_screenshot(
70+
pixel_ratio=flet_app.screenshots_pixel_ratio
71+
),
72+
)
73+
6374

6475
@pytest.mark.asyncio(loop_scope="function")
6576
async def test_theme(flet_app: ftt.FletTestApp, request):
77+
flet_app.page.enable_screenshots = True
78+
flet_app.resize_page(350, 300)
6679
flet_app.page.theme = ft.Theme(
6780
dropdown_theme=ft.DropdownTheme(
6881
text_style=ft.TextStyle(color=ft.Colors.PURPLE, size=20),
@@ -73,20 +86,19 @@ async def test_theme(flet_app: ftt.FletTestApp, request):
7386
),
7487
)
7588
)
89+
7690
colors = [ft.Colors.RED, ft.Colors.BLUE, ft.Colors.GREEN]
77-
dd = ft.Dropdown(
78-
label="Color",
79-
text="Select a color",
80-
options=[
81-
ft.DropdownOption(key=color.value, content=ft.Text(value=color.value))
82-
for color in colors
83-
],
84-
key="dd",
91+
flet_app.page.add(
92+
ft.Dropdown(
93+
key="dd",
94+
label="Color",
95+
text="Select a color",
96+
options=[
97+
ft.DropdownOption(key=color.value, content=ft.Text(value=color.value))
98+
for color in colors
99+
],
100+
)
85101
)
86-
flet_app.page.enable_screenshots = True
87-
flet_app.resize_page(400, 600)
88-
89-
flet_app.page.add(dd)
90102
await flet_app.tester.pump_and_settle()
91103

92104
# normal state

sdk/python/packages/flet/src/flet/controls/material/dropdown.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,27 @@
2828
@control("DropdownOption")
2929
class DropdownOption(Control):
3030
"""
31-
Represents an item in a dropdown. Either `key` or `text` must be specified, else an
32-
A `ValueError` will be raised.
31+
Represents an item in a dropdown.
3332
"""
3433

3534
key: Optional[str] = None
3635
"""
37-
Option's key. If not specified [`text`][(c).] will
38-
be used as fallback.
36+
Option's key.
37+
38+
If not specified [`text`][(c).] will be used as fallback.
39+
40+
Raises:
41+
ValueError: If neither `key` nor [`text`][(c).] are provided.
3942
"""
4043

4144
text: Optional[str] = None
4245
"""
43-
Option's display text. If not specified `key` will be used as fallback.
46+
Option's display text.
47+
48+
If not specified [`key`][(c).] will be used as fallback.
4449
4550
Raises:
46-
ValueError: If neither [`key`][(c).] nor [`text`][(c).] are provided.
51+
ValueError: If neither [`key`][(c).] nor `text` are provided.
4752
"""
4853

4954
content: Optional[Control] = None
@@ -79,24 +84,26 @@ def before_update(self):
7984
@control("Dropdown")
8085
class Dropdown(LayoutControl):
8186
"""
82-
A dropdown control that allows users to select a single option from a list of
83-
options.
87+
A dropdown control that allows users to select a single option
88+
from a list of [`options`][(c).].
8489
90+
Example:
8591
```python
8692
ft.Dropdown(
8793
width=220,
8894
value="alice",
8995
options=[
90-
ft.dropdown.Option(key="alice", text="Alice"),
91-
ft.dropdown.Option(key="bob", text="Bob"),
96+
ft.DropdownOption(key="alice", text="Alice"),
97+
ft.DropdownOption(key="bob", text="Bob"),
9298
],
9399
)
94100
```
95101
"""
96102

97103
value: Optional[str] = None
98104
"""
99-
[`key`][(c).] value of the selected option.
105+
The [`key`][flet.DropdownOption.] of the dropdown [`options`][(c).]
106+
corresponding to the selected option.
100107
"""
101108

102109
options: list[DropdownOption] = field(default_factory=list)

0 commit comments

Comments
 (0)