Skip to content

Commit 6c3010e

Browse files
authored
Add delete button support to FilterChip (flutter#136645)
fixes [`FilterChip` should have `DeletableChipAttributes`/`trailing` to match Material 3 spec.](flutter#135595) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @OverRide Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData(useMaterial3: true), home: const Example(), ); } } class Example extends StatelessWidget { const Example({super.key}); @OverRide Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text('FilterChip'), const SizedBox(height: 8), FilterChip( avatar: const Icon(Icons.favorite_rounded), label: const Text('FilterChip'), selected: true, showCheckmark: false, onSelected: (bool value) {}, onDeleted: () {}, deleteButtonTooltipMessage: 'Delete Me!', ), const SizedBox(height: 16), FilterChip( avatar: const Icon(Icons.favorite_rounded), label: const Text('FilterChip'), onSelected: (bool value) {}, onDeleted: () {}, ), const SizedBox(height: 48), const Text('FilterChip.elevated'), const SizedBox(height: 8), FilterChip.elevated( avatar: const Icon(Icons.favorite_rounded), label: const Text('FilterChip'), selected: true, showCheckmark: false, onSelected: (bool value) {}, onDeleted: () {}, ), const SizedBox(height: 16), FilterChip.elevated( avatar: const Icon(Icons.favorite_rounded), label: const Text('FilterChip'), onSelected: (bool value) {}, onDeleted: () {}, ), ], ), ), ); } } ``` </details> ### Before Not possible to add delete button ### After ![Screenshot 2023-10-16 at 17 56 51](https://github.com/flutter/flutter/assets/48603081/ad751ef9-c2bc-4184-ae5f-4d1017eff664)
1 parent 5e8b5f4 commit 6c3010e

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'chip_theme.dart';
1010
import 'color_scheme.dart';
1111
import 'colors.dart';
1212
import 'debug.dart';
13+
import 'icons.dart';
1314
import 'material_state.dart';
1415
import 'text_theme.dart';
1516
import 'theme.dart';
@@ -56,6 +57,7 @@ enum _ChipVariant { flat, elevated }
5657
class FilterChip extends StatelessWidget
5758
implements
5859
ChipAttributes,
60+
DeletableChipAttributes,
5961
SelectableChipAttributes,
6062
CheckmarkableChipAttributes,
6163
DisabledChipAttributes {
@@ -72,6 +74,10 @@ class FilterChip extends StatelessWidget
7274
this.labelPadding,
7375
this.selected = false,
7476
required this.onSelected,
77+
this.deleteIcon,
78+
this.onDeleted,
79+
this.deleteIconColor,
80+
this.deleteButtonTooltipMessage,
7581
this.pressElevation,
7682
this.disabledColor,
7783
this.selectedColor,
@@ -111,6 +117,10 @@ class FilterChip extends StatelessWidget
111117
this.labelPadding,
112118
this.selected = false,
113119
required this.onSelected,
120+
this.deleteIcon,
121+
this.onDeleted,
122+
this.deleteIconColor,
123+
this.deleteButtonTooltipMessage,
114124
this.pressElevation,
115125
this.disabledColor,
116126
this.selectedColor,
@@ -150,6 +160,14 @@ class FilterChip extends StatelessWidget
150160
@override
151161
final ValueChanged<bool>? onSelected;
152162
@override
163+
final Widget? deleteIcon;
164+
@override
165+
final VoidCallback? onDeleted;
166+
@override
167+
final Color? deleteIconColor;
168+
@override
169+
final String? deleteButtonTooltipMessage;
170+
@override
153171
final double? pressElevation;
154172
@override
155173
final Color? disabledColor;
@@ -205,13 +223,19 @@ class FilterChip extends StatelessWidget
205223
final ChipThemeData? defaults = Theme.of(context).useMaterial3
206224
? _FilterChipDefaultsM3(context, isEnabled, selected, _chipVariant)
207225
: null;
226+
final Widget? resolvedDeleteIcon = deleteIcon
227+
?? (Theme.of(context).useMaterial3 ? const Icon(Icons.clear, size: 18) : null);
208228
return RawChip(
209229
defaultProperties: defaults,
210230
avatar: avatar,
211231
label: label,
212232
labelStyle: labelStyle,
213233
labelPadding: labelPadding,
214234
onSelected: onSelected,
235+
deleteIcon: resolvedDeleteIcon,
236+
onDeleted: onDeleted,
237+
deleteIconColor: deleteIconColor,
238+
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
215239
pressElevation: pressElevation,
216240
selected: selected,
217241
tooltip: tooltip,

packages/flutter/test/material/filter_chip_test.dart

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
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:flutter/foundation.dart';
56
import 'package:flutter/material.dart';
67
import 'package:flutter_test/flutter_test.dart';
78
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
89

10+
import 'feedback_tester.dart';
11+
912
/// Adds the basic requirements for a Chip.
1013
Widget wrapForChip({
1114
required Widget child,
@@ -722,4 +725,198 @@ void main() {
722725

723726
expect(getIconData(tester).color, const Color(0xff00ff00));
724727
});
728+
729+
testWidgetsWithLeakTracking('Material3 - FilterChip supports delete button', (WidgetTester tester) async {
730+
final ThemeData theme = ThemeData();
731+
await tester.pumpWidget(
732+
MaterialApp(
733+
theme: theme,
734+
home: Material(
735+
child: Center(
736+
child: FilterChip(
737+
onDeleted: () { },
738+
onSelected: (bool valueChanged) { },
739+
label: const Text('FilterChip'),
740+
),
741+
),
742+
),
743+
),
744+
);
745+
746+
// Test the chip size with delete button.
747+
expect(find.text('FilterChip'), findsOneWidget);
748+
expect(tester.getSize(find.byType(FilterChip)), const Size(195.0, 48.0));
749+
750+
// Test the delete button icon.
751+
expect(tester.getSize(find.byIcon(Icons.clear)), const Size(18.0, 18.0));
752+
expect(getIconData(tester).color, theme.colorScheme.onSecondaryContainer);
753+
754+
await tester.pumpWidget(
755+
MaterialApp(
756+
theme: theme,
757+
home: Material(
758+
child: Center(
759+
child: FilterChip.elevated(
760+
onDeleted: () { },
761+
onSelected: (bool valueChanged) { },
762+
label: const Text('Elevated FilterChip'),
763+
),
764+
),
765+
),
766+
),
767+
);
768+
769+
// Test the elevated chip size with delete button.
770+
expect(find.text('Elevated FilterChip'), findsOneWidget);
771+
expect(
772+
tester.getSize(find.byType(FilterChip)),
773+
within(distance: 0.001, from: const Size(321.9, 48.0)),
774+
);
775+
776+
// Test the delete button icon.
777+
expect(tester.getSize(find.byIcon(Icons.clear)), const Size(18.0, 18.0));
778+
expect(getIconData(tester).color, theme.colorScheme.onSecondaryContainer);
779+
}, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933
780+
781+
testWidgetsWithLeakTracking('Material2 - FilterChip supports delete button', (WidgetTester tester) async {
782+
final ThemeData theme = ThemeData(useMaterial3: false);
783+
await tester.pumpWidget(
784+
MaterialApp(
785+
theme: theme,
786+
home: Material(
787+
child: Center(
788+
child: FilterChip(
789+
onDeleted: () { },
790+
onSelected: (bool valueChanged) { },
791+
label: const Text('FilterChip'),
792+
),
793+
),
794+
),
795+
),
796+
);
797+
798+
// Test the chip size with delete button.
799+
expect(find.text('FilterChip'), findsOneWidget);
800+
expect(tester.getSize(find.byType(FilterChip)), const Size(188.0, 48.0));
801+
802+
// Test the delete button icon.
803+
expect(tester.getSize(find.byIcon(Icons.cancel)), const Size(18.0, 18.0));
804+
expect(getIconData(tester).color, theme.iconTheme.color?.withAlpha(0xde));
805+
806+
await tester.pumpWidget(
807+
MaterialApp(
808+
theme: theme,
809+
home: Material(
810+
child: Center(
811+
child: FilterChip.elevated(
812+
onDeleted: () { },
813+
onSelected: (bool valueChanged) { },
814+
label: const Text('Elevated FilterChip'),
815+
),
816+
),
817+
),
818+
),
819+
);
820+
821+
// Test the elevated chip size with delete button.
822+
expect(find.text('Elevated FilterChip'), findsOneWidget);
823+
expect(tester.getSize(find.byType(FilterChip)), const Size(314.0, 48.0));
824+
825+
// Test the delete button icon.
826+
expect(tester.getSize(find.byIcon(Icons.cancel)), const Size(18.0, 18.0));
827+
expect(getIconData(tester).color, theme.iconTheme.color?.withAlpha(0xde));
828+
});
829+
830+
testWidgetsWithLeakTracking('Customize FilterChip delete button', (WidgetTester tester) async {
831+
final ThemeData theme = ThemeData();
832+
Widget buildChip({
833+
Widget? deleteIcon,
834+
Color? deleteIconColor,
835+
String? deleteButtonTooltipMessage,
836+
}) {
837+
return MaterialApp(
838+
theme: theme,
839+
home: Material(
840+
child: Center(
841+
child: FilterChip(
842+
deleteIcon: deleteIcon,
843+
deleteIconColor: deleteIconColor,
844+
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
845+
onDeleted: () { },
846+
onSelected: (bool valueChanged) { },
847+
label: const Text('FilterChip'),
848+
),
849+
),
850+
),
851+
);
852+
}
853+
854+
// Test the custom delete icon.
855+
await tester.pumpWidget(buildChip(deleteIcon: const Icon(Icons.delete)));
856+
857+
expect(find.byIcon(Icons.clear), findsNothing);
858+
expect(find.byIcon(Icons.delete), findsOneWidget);
859+
860+
// Test the custom delete icon color.
861+
await tester.pumpWidget(buildChip(
862+
deleteIcon: const Icon(Icons.delete),
863+
deleteIconColor: const Color(0xff00ff00)),
864+
);
865+
await tester.pumpAndSettle();
866+
867+
expect(find.byIcon(Icons.clear), findsNothing);
868+
expect(find.byIcon(Icons.delete), findsOneWidget);
869+
expect(getIconData(tester).color, const Color(0xff00ff00));
870+
871+
// Test the custom delete button tooltip message.
872+
await tester.pumpWidget(buildChip(deleteButtonTooltipMessage: 'Delete FilterChip'));
873+
await tester.pumpAndSettle();
874+
875+
// Hover over the delete icon of the chip
876+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byIcon(Icons.clear)));
877+
878+
await tester.pumpAndSettle();
879+
880+
// Verify the tooltip message is set.
881+
expect(find.widgetWithText(Tooltip, 'Delete FilterChip'), findsOneWidget);
882+
883+
await gesture.up();
884+
});
885+
886+
testWidgetsWithLeakTracking('FilterChip delete button control test', (WidgetTester tester) async {
887+
final FeedbackTester feedback = FeedbackTester();
888+
final List<String> deletedButtonStrings = <String>[];
889+
await tester.pumpWidget(
890+
MaterialApp(
891+
home: Material(
892+
child: Center(
893+
child: FilterChip(
894+
onDeleted: () {
895+
deletedButtonStrings.add('A');
896+
},
897+
onSelected: (bool valueChanged) { },
898+
label: const Text('FilterChip'),
899+
),
900+
),
901+
),
902+
),
903+
);
904+
905+
expect(feedback.clickSoundCount, 0);
906+
907+
expect(deletedButtonStrings, isEmpty);
908+
await tester.tap(find.byIcon(Icons.clear));
909+
expect(deletedButtonStrings, equals(<String>['A']));
910+
911+
await tester.pumpAndSettle(const Duration(seconds: 1));
912+
expect(feedback.clickSoundCount, 1);
913+
914+
await tester.tap(find.byIcon(Icons.clear));
915+
expect(deletedButtonStrings, equals(<String>['A', 'A']));
916+
917+
await tester.pumpAndSettle(const Duration(seconds: 1));
918+
expect(feedback.clickSoundCount, 2);
919+
920+
feedback.dispose();
921+
});
725922
}

0 commit comments

Comments
 (0)