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
123 changes: 80 additions & 43 deletions packages/flet/lib/src/controls/dropdown.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
Expand All @@ -16,21 +17,27 @@ class DropdownControl extends StatefulWidget {
class _DropdownControlState extends State<DropdownControl> {
late final FocusNode _focusNode;
late final TextEditingController _controller;
String? _value;
bool _suppressTextChange = false;

@override
void initState() {
super.initState();
_focusNode = FocusNode();

_controller = TextEditingController(text: widget.control.getString("text"));
// initialize controller
_value = widget.control.getString("value");
final text = widget.control.getString("text") ?? _value ?? "";
_controller = TextEditingController(text: text);
_controller.addListener(_onTextChange);

_focusNode = FocusNode();
_focusNode.addListener(_onFocusChange);
widget.control.addInvokeMethodListener(_invokeMethod);

_controller.addListener(_onTextChange);
}

void _onTextChange() {
debugPrint("Typed text: ${_controller.text}");
if (_suppressTextChange) return;

if (_controller.text != widget.control.getString("text")) {
widget.control.updateProperties({"text": _controller.text});
widget.control.triggerEvent("text_change", _controller.text);
Expand All @@ -41,6 +48,17 @@ class _DropdownControlState extends State<DropdownControl> {
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
}

/// Updates text without triggering a text change event.
void _updateControllerText(String text) {
_suppressTextChange = true;
_controller.value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
_suppressTextChange = false;
widget.control.updateProperties({"text": text}, python: false);
}

@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
Expand All @@ -65,13 +83,12 @@ class _DropdownControlState extends State<DropdownControl> {
debugPrint("DropdownMenu build: ${widget.control.id}");

var theme = Theme.of(context);
bool editable = widget.control.getBool("editable", false)!;
bool autofocus = widget.control.getBool("autofocus", false)!;
var editable = widget.control.getBool("editable", false)!;
var autofocus = widget.control.getBool("autofocus", false)!;
var textSize = widget.control.getDouble("text_size");
var color = widget.control.getColor("color", context);

TextAlign textAlign =
widget.control.getTextAlign("text_align", TextAlign.start)!;
var textAlign = widget.control.getTextAlign("text_align", TextAlign.start)!;

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

FormFieldInputBorder inputBorder = widget.control
var inputBorder = widget.control
.getFormFieldInputBorder("border", FormFieldInputBorder.outline)!;

InputBorder? border;
Expand Down Expand Up @@ -115,7 +132,7 @@ class _DropdownControlState extends State<DropdownControl> {
? BorderSide.none
: BorderSide(
color: borderColor ??
theme.colorScheme.onSurface.withOpacity(0.38),
theme.colorScheme.onSurface.withValues(alpha: 0.38),
width: borderWidth ?? 1.0));
}
}
Expand Down Expand Up @@ -176,30 +193,54 @@ class _DropdownControlState extends State<DropdownControl> {
color: color ?? theme.colorScheme.onSurface);
}

var items = widget.control
// build dropdown items
var options = widget.control
.children("options")
.map<DropdownMenuEntry<String>>((Control itemCtrl) {
bool itemDisabled = widget.control.disabled || itemCtrl.disabled;
ButtonStyle? style = itemCtrl.getButtonStyle("style", theme);

return DropdownMenuEntry<String>(
enabled: !itemDisabled,
value: itemCtrl.getString("key") ??
itemCtrl.getString("text") ??
itemCtrl.id.toString(),
label: itemCtrl.getString("text") ??
itemCtrl.getString("key") ??
itemCtrl.id.toString(),
labelWidget: itemCtrl.buildWidget("content"),
leadingIcon: itemCtrl.buildIconOrWidget("leading_icon"),
trailingIcon: itemCtrl.buildIconOrWidget("trailing_icon"),
style: style,
);
}).toList();

String? value = widget.control.getString("value");
if (items.where((item) => item.value == value).isEmpty) {
value = null;
.map<DropdownMenuEntry<String>?>((Control itemCtrl) {
bool itemDisabled = widget.control.disabled || itemCtrl.disabled;
ButtonStyle? style = itemCtrl.getButtonStyle("style", theme);

var optionKey = itemCtrl.getString("key");
var optionText = itemCtrl.getString("text");

var optionValue = optionKey ?? optionText;
var optionLabel = optionText ?? optionKey;
if (optionValue == null || optionLabel == null) {
return null;
}

return DropdownMenuEntry<String>(
enabled: !itemDisabled,
value: optionValue,
label: optionLabel,
labelWidget: itemCtrl.buildWidget("content"),
leadingIcon: itemCtrl.buildIconOrWidget("leading_icon"),
trailingIcon: itemCtrl.buildIconOrWidget("trailing_icon"),
style: style,
);
})
.nonNulls
.toList();

var value = widget.control.getString("value");
var selectedOption = options.firstWhereOrNull((o) => o.value == value);
value = selectedOption?.value;

// keep controller text in sync with backend-driven value changes
if (_value != value) {
if (value == null) {
if (_value != null && _controller.text.isNotEmpty) {
// clears dropdown field
_updateControllerText("");
}
} else {
final String entryLabel =
selectedOption?.label ?? widget.control.getString("text") ?? value;
if (_controller.text != entryLabel) {
_updateControllerText(entryLabel);
}
}
_value = value;
}

TextCapitalization textCapitalization = widget.control
Expand Down Expand Up @@ -237,20 +278,16 @@ class _DropdownControlState extends State<DropdownControl> {
errorText: widget.control.getString("error_text"),
hintText: widget.control.getString("hint_text"),
helperText: widget.control.getString("helper_text"),
// menuStyle: MenuStyle(
// backgroundColor: widget.control.getWidgetStateColor("bgcolor", theme),
// elevation: widget.control.getWidgetStateDouble("elevation"),
// fixedSize: WidgetStateProperty.all(Size.fromWidth(menuWidth)),
// ),
menuStyle: menuStyle,
inputDecorationTheme: inputDecorationTheme,
onSelected: widget.control.disabled
? null
: (String? value) {
widget.control.updateProperties({"value": value});
widget.control.triggerEvent("select", value);
: (String? selection) {
_value = selection;
widget.control.updateProperties({"value": selection});
widget.control.triggerEvent("select", selection);
},
dropdownMenuEntries: items,
dropdownMenuEntries: options,
);

var didAutoFocus = false;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ def flet_app(flet_app_function):

@pytest.mark.asyncio(loop_scope="function")
async def test_basic(flet_app: ftt.FletTestApp, request):
colors = [ft.Colors.RED, ft.Colors.BLUE, ft.Colors.GREEN]
dd = ft.Dropdown(
label="Color",
text="Select a color",
options=[
ft.DropdownOption(
key=color.value, content=ft.Text(value=color.value, color=color)
)
for color in colors
],
key="dd",
)
flet_app.page.enable_screenshots = True
flet_app.resize_page(400, 600)
flet_app.page.add(dd)
flet_app.resize_page(350, 300)

colors = ["red", "blue", "green"]
flet_app.page.add(
dd := ft.Dropdown(
key="dd",
label="Color",
text="Select a color",
options=[
ft.DropdownOption(key=color, content=ft.Text(value=color, color=color))
for color in colors
],
)
)
await flet_app.tester.pump_and_settle()

# normal state
Expand All @@ -48,10 +48,10 @@ async def test_basic(flet_app: ftt.FletTestApp, request):
),
)

blue_option = await flet_app.tester.find_by_text("blue")
assert blue_option.count == 2

await flet_app.tester.tap(blue_option.last)
# select red option
red_options = await flet_app.tester.find_by_text("red")
assert red_options.count == 2 # Flutter Finder bug - should be 1
await flet_app.tester.tap(red_options.last)
await flet_app.tester.pump_and_settle()
flet_app.assert_screenshot(
"basic_2",
Expand All @@ -60,9 +60,22 @@ async def test_basic(flet_app: ftt.FletTestApp, request):
),
)

# clear value
dd.value = None
dd.update()
await flet_app.tester.pump_and_settle()
flet_app.assert_screenshot(
"basic_0",
await flet_app.page.take_screenshot(
pixel_ratio=flet_app.screenshots_pixel_ratio
),
)


@pytest.mark.asyncio(loop_scope="function")
async def test_theme(flet_app: ftt.FletTestApp, request):
flet_app.page.enable_screenshots = True
flet_app.resize_page(350, 300)
flet_app.page.theme = ft.Theme(
dropdown_theme=ft.DropdownTheme(
text_style=ft.TextStyle(color=ft.Colors.PURPLE, size=20),
Expand All @@ -73,20 +86,19 @@ async def test_theme(flet_app: ftt.FletTestApp, request):
),
)
)

colors = [ft.Colors.RED, ft.Colors.BLUE, ft.Colors.GREEN]
dd = ft.Dropdown(
label="Color",
text="Select a color",
options=[
ft.DropdownOption(key=color.value, content=ft.Text(value=color.value))
for color in colors
],
key="dd",
flet_app.page.add(
ft.Dropdown(
key="dd",
label="Color",
text="Select a color",
options=[
ft.DropdownOption(key=color.value, content=ft.Text(value=color.value))
for color in colors
],
)
)
flet_app.page.enable_screenshots = True
flet_app.resize_page(400, 600)

flet_app.page.add(dd)
await flet_app.tester.pump_and_settle()

# normal state
Expand Down
29 changes: 18 additions & 11 deletions sdk/python/packages/flet/src/flet/controls/material/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,27 @@
@control("DropdownOption")
class DropdownOption(Control):
"""
Represents an item in a dropdown. Either `key` or `text` must be specified, else an
A `ValueError` will be raised.
Represents an item in a dropdown.
"""

key: Optional[str] = None
"""
Option's key. If not specified [`text`][(c).] will
be used as fallback.
Option's key.

If not specified [`text`][(c).] will be used as fallback.

Raises:
ValueError: If neither `key` nor [`text`][(c).] are provided.
"""

text: Optional[str] = None
"""
Option's display text. If not specified `key` will be used as fallback.
Option's display text.

If not specified [`key`][(c).] will be used as fallback.

Raises:
ValueError: If neither [`key`][(c).] nor [`text`][(c).] are provided.
ValueError: If neither [`key`][(c).] nor `text` are provided.
"""

content: Optional[Control] = None
Expand Down Expand Up @@ -79,24 +84,26 @@ def before_update(self):
@control("Dropdown")
class Dropdown(LayoutControl):
"""
A dropdown control that allows users to select a single option from a list of
options.
A dropdown control that allows users to select a single option
from a list of [`options`][(c).].

Example:
```python
ft.Dropdown(
width=220,
value="alice",
options=[
ft.dropdown.Option(key="alice", text="Alice"),
ft.dropdown.Option(key="bob", text="Bob"),
ft.DropdownOption(key="alice", text="Alice"),
ft.DropdownOption(key="bob", text="Bob"),
],
)
```
"""

value: Optional[str] = None
"""
[`key`][(c).] value of the selected option.
The [`key`][flet.DropdownOption.] of the dropdown [`options`][(c).]
corresponding to the selected option.
"""

options: list[DropdownOption] = field(default_factory=list)
Expand Down
Loading