flutter
flutter copied to clipboard
Add Dropdown support to `FilterChip`
Use case
This will allow a filter chip to make filter selections via a drop down menu, as showcased in the Material 3 Design specifications. This is mostly useful for desktop, but mobile in some situations too. DropdownButton
is not customizable enough to support this use-case, and there is no simple way to open a drop down menu from a click (see here).
Proposal
Either the introduction of a new widget, perhaps DropdownFilterChip
, modifications to the existing FilterChip
, or a programmatic way to open up a dropdown menu from an arbitrary widget (preferred, seeing as this would be useful for various other use cases).
My current workaround using flutter_hooks
and font_awesome_flutter
(neither of these packages are required for this workaround, but they make it easier):
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class DropdownFilterChip<T> extends HookWidget {
const DropdownFilterChip({
Key? key,
required this.label,
this.value,
required this.items,
required this.onChanged,
}) : super(key: key);
final Widget label;
final T? value;
final List<DropdownMenuItem<T>> items;
final void Function(T? value) onChanged;
@override
Widget build(BuildContext context) {
final dropdownButtonKey = useMemoized(() => GlobalKey());
final focusNode = useFocusNode();
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
label,
const SizedBox(width: 8),
const FaIcon(FontAwesomeIcons.caretDown),
Offstage(
child: DropdownButton<T>(
key: dropdownButtonKey,
focusNode: focusNode,
value: value,
items: items,
onChanged: (value) {
onChanged(value);
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.unfocus();
});
},
),
),
],
),
onSelected: (_) {
dropdownButtonKey.currentContext?.visitChildElements((element) {
if (element.widget is Semantics) {
element.visitChildElements((element) {
if (element.widget is Actions) {
element.visitChildElements((element) {
Actions.invoke(element, const ActivateIntent());
});
}
});
}
});
},
);
}
}
Which can be used like so:
DropdownFilterChip<String>(
label: Text(
selectedCategories.value.isEmpty
? 'category'
: selectedCategories.value.first,
),
value: selectedCategories.value.isEmpty
? null
: selectedCategories.value.first,
items: [
const DropdownMenuItem(value: null, child: Text('all')),
for (final category in library.categories)
DropdownMenuItem(value: category, child: Text(category)),
],
onChanged: (category) {
selectedCategories.value = {if (category != null) category};
},
)
any update on this ?
This issue is assigned but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!
+1, this would be great
any updates? so currently it is impossible to make such a design as specified in the m3 specs?
M3 has been marked done but this is still missing
This feature would be a valuable addition if it were to be implemented. It appears that many individuals are searching for this particular missing feature: https://stackoverflow.com/q/76482263
for this I combine FilterChip with MenuAnchor, to anyone who wanna use it here is the code:
class ChipMenu extends StatefulWidget {
ChipMenu({required this.menuItemList, required this.initialSelectionIndex, this.onSelected, super.key});
final List<(String, Widget?)> menuItemList;
final int initialSelectionIndex;
final Function(String)? onSelected;
@override
State<ChipMenu> createState() => _ChipMenuState();
}
class _ChipMenuState extends State<ChipMenu> {
late String _selectedText;
@override
void initState() {
super.initState();
_selectedText = widget.menuItemList[widget.initialSelectionIndex].$1;
}
@override
Widget build(BuildContext context) {
return MenuAnchor(
builder: (context, controller, child) {
return FilterChip(
label: Text(_selectedText),
selected: true,
onSelected: (_) {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
);
},
menuChildren: [
for (final rec in widget.menuItemList)
MenuItemButton(
leadingIcon: rec.$2,
child: Text(rec.$1),
onPressed: () {
setState(() {
_selectedText = rec.$1;
});
widget.onSelected?.call(rec.$1);
},
),
],
);
}
}
and the dartpad example
@Mr-1311 can you add a pointer too ?
+1 for this addition
Here is my solution to the problem:
https://dartpad.dev/?id=692d161174550822d13ce0c34d7eeca9