Unverified Commit 6c3010e7 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Add delete button support to `FilterChip` (#136645)

fixes [`FilterChip` should have `DeletableChipAttributes`/`trailing` to match Material 3 spec.](https://github.com/flutter/flutter/issues/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)
parent 5e8b5f4e
......@@ -10,6 +10,7 @@ import 'chip_theme.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'debug.dart';
import 'icons.dart';
import 'material_state.dart';
import 'text_theme.dart';
import 'theme.dart';
......@@ -56,6 +57,7 @@ enum _ChipVariant { flat, elevated }
class FilterChip extends StatelessWidget
implements
ChipAttributes,
DeletableChipAttributes,
SelectableChipAttributes,
CheckmarkableChipAttributes,
DisabledChipAttributes {
......@@ -72,6 +74,10 @@ class FilterChip extends StatelessWidget
this.labelPadding,
this.selected = false,
required this.onSelected,
this.deleteIcon,
this.onDeleted,
this.deleteIconColor,
this.deleteButtonTooltipMessage,
this.pressElevation,
this.disabledColor,
this.selectedColor,
......@@ -111,6 +117,10 @@ class FilterChip extends StatelessWidget
this.labelPadding,
this.selected = false,
required this.onSelected,
this.deleteIcon,
this.onDeleted,
this.deleteIconColor,
this.deleteButtonTooltipMessage,
this.pressElevation,
this.disabledColor,
this.selectedColor,
......@@ -150,6 +160,14 @@ class FilterChip extends StatelessWidget
@override
final ValueChanged<bool>? onSelected;
@override
final Widget? deleteIcon;
@override
final VoidCallback? onDeleted;
@override
final Color? deleteIconColor;
@override
final String? deleteButtonTooltipMessage;
@override
final double? pressElevation;
@override
final Color? disabledColor;
......@@ -205,6 +223,8 @@ class FilterChip extends StatelessWidget
final ChipThemeData? defaults = Theme.of(context).useMaterial3
? _FilterChipDefaultsM3(context, isEnabled, selected, _chipVariant)
: null;
final Widget? resolvedDeleteIcon = deleteIcon
?? (Theme.of(context).useMaterial3 ? const Icon(Icons.clear, size: 18) : null);
return RawChip(
defaultProperties: defaults,
avatar: avatar,
......@@ -212,6 +232,10 @@ class FilterChip extends StatelessWidget
labelStyle: labelStyle,
labelPadding: labelPadding,
onSelected: onSelected,
deleteIcon: resolvedDeleteIcon,
onDeleted: onDeleted,
deleteIconColor: deleteIconColor,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
pressElevation: pressElevation,
selected: selected,
tooltip: tooltip,
......
......@@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'feedback_tester.dart';
/// Adds the basic requirements for a Chip.
Widget wrapForChip({
required Widget child,
......@@ -722,4 +725,198 @@ void main() {
expect(getIconData(tester).color, const Color(0xff00ff00));
});
testWidgetsWithLeakTracking('Material3 - FilterChip supports delete button', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: FilterChip(
onDeleted: () { },
onSelected: (bool valueChanged) { },
label: const Text('FilterChip'),
),
),
),
),
);
// Test the chip size with delete button.
expect(find.text('FilterChip'), findsOneWidget);
expect(tester.getSize(find.byType(FilterChip)), const Size(195.0, 48.0));
// Test the delete button icon.
expect(tester.getSize(find.byIcon(Icons.clear)), const Size(18.0, 18.0));
expect(getIconData(tester).color, theme.colorScheme.onSecondaryContainer);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: FilterChip.elevated(
onDeleted: () { },
onSelected: (bool valueChanged) { },
label: const Text('Elevated FilterChip'),
),
),
),
),
);
// Test the elevated chip size with delete button.
expect(find.text('Elevated FilterChip'), findsOneWidget);
expect(
tester.getSize(find.byType(FilterChip)),
within(distance: 0.001, from: const Size(321.9, 48.0)),
);
// Test the delete button icon.
expect(tester.getSize(find.byIcon(Icons.clear)), const Size(18.0, 18.0));
expect(getIconData(tester).color, theme.colorScheme.onSecondaryContainer);
}, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933
testWidgetsWithLeakTracking('Material2 - FilterChip supports delete button', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: FilterChip(
onDeleted: () { },
onSelected: (bool valueChanged) { },
label: const Text('FilterChip'),
),
),
),
),
);
// Test the chip size with delete button.
expect(find.text('FilterChip'), findsOneWidget);
expect(tester.getSize(find.byType(FilterChip)), const Size(188.0, 48.0));
// Test the delete button icon.
expect(tester.getSize(find.byIcon(Icons.cancel)), const Size(18.0, 18.0));
expect(getIconData(tester).color, theme.iconTheme.color?.withAlpha(0xde));
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: FilterChip.elevated(
onDeleted: () { },
onSelected: (bool valueChanged) { },
label: const Text('Elevated FilterChip'),
),
),
),
),
);
// Test the elevated chip size with delete button.
expect(find.text('Elevated FilterChip'), findsOneWidget);
expect(tester.getSize(find.byType(FilterChip)), const Size(314.0, 48.0));
// Test the delete button icon.
expect(tester.getSize(find.byIcon(Icons.cancel)), const Size(18.0, 18.0));
expect(getIconData(tester).color, theme.iconTheme.color?.withAlpha(0xde));
});
testWidgetsWithLeakTracking('Customize FilterChip delete button', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
Widget buildChip({
Widget? deleteIcon,
Color? deleteIconColor,
String? deleteButtonTooltipMessage,
}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: FilterChip(
deleteIcon: deleteIcon,
deleteIconColor: deleteIconColor,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
onDeleted: () { },
onSelected: (bool valueChanged) { },
label: const Text('FilterChip'),
),
),
),
);
}
// Test the custom delete icon.
await tester.pumpWidget(buildChip(deleteIcon: const Icon(Icons.delete)));
expect(find.byIcon(Icons.clear), findsNothing);
expect(find.byIcon(Icons.delete), findsOneWidget);
// Test the custom delete icon color.
await tester.pumpWidget(buildChip(
deleteIcon: const Icon(Icons.delete),
deleteIconColor: const Color(0xff00ff00)),
);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.clear), findsNothing);
expect(find.byIcon(Icons.delete), findsOneWidget);
expect(getIconData(tester).color, const Color(0xff00ff00));
// Test the custom delete button tooltip message.
await tester.pumpWidget(buildChip(deleteButtonTooltipMessage: 'Delete FilterChip'));
await tester.pumpAndSettle();
// Hover over the delete icon of the chip
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byIcon(Icons.clear)));
await tester.pumpAndSettle();
// Verify the tooltip message is set.
expect(find.widgetWithText(Tooltip, 'Delete FilterChip'), findsOneWidget);
await gesture.up();
});
testWidgetsWithLeakTracking('FilterChip delete button control test', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester();
final List<String> deletedButtonStrings = <String>[];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: FilterChip(
onDeleted: () {
deletedButtonStrings.add('A');
},
onSelected: (bool valueChanged) { },
label: const Text('FilterChip'),
),
),
),
),
);
expect(feedback.clickSoundCount, 0);
expect(deletedButtonStrings, isEmpty);
await tester.tap(find.byIcon(Icons.clear));
expect(deletedButtonStrings, equals(<String>['A']));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
await tester.tap(find.byIcon(Icons.clear));
expect(deletedButtonStrings, equals(<String>['A', 'A']));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 2);
feedback.dispose();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment