From 17d789545e717bd9149988da54562fc3a7980156 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Mon, 4 Nov 2024 17:34:50 +0000 Subject: [PATCH 1/4] feat: created status chip --- .../lib/pages/components/chip_example.dart | 16 ++++ lib/src/components/chips/chip.dart | 1 + lib/src/components/chips/status_chip.dart | 87 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 lib/src/components/chips/status_chip.dart diff --git a/example/lib/pages/components/chip_example.dart b/example/lib/pages/components/chip_example.dart index 72a96407..899a8c38 100644 --- a/example/lib/pages/components/chip_example.dart +++ b/example/lib/pages/components/chip_example.dart @@ -63,6 +63,21 @@ class _ChipExampleState extends State { ), ), ]); + + final Widget statusChipExample = Column(children: [ + Text( + 'Status Chip', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + ZetaStatusChip( + label: 'Label', + data: 'Status chip', + draggable: true, + ), + ]); + final colors = Zeta.of(context).colors; return ExampleScaffold( name: ChipExample.name, @@ -74,6 +89,7 @@ class _ChipExampleState extends State { inputChipExample, assistChipExample, filterChipExample, + statusChipExample, const SizedBox(height: 100), DragTarget( onAcceptWithDetails: (details) => setState(() => chipType = details.data), diff --git a/lib/src/components/chips/chip.dart b/lib/src/components/chips/chip.dart index 545f0410..9f3a0fca 100644 --- a/lib/src/components/chips/chip.dart +++ b/lib/src/components/chips/chip.dart @@ -6,6 +6,7 @@ import '../../../zeta_flutter.dart'; export './assist_chip.dart'; export './filter_chip.dart'; export './input_chip.dart'; +export './status_chip.dart'; /// This covers the broad functionality of [ZetaAssistChip], [ZetaFilterChip] and [ZetaInputChip]. /// diff --git a/lib/src/components/chips/status_chip.dart b/lib/src/components/chips/status_chip.dart new file mode 100644 index 00000000..db6aa0b9 --- /dev/null +++ b/lib/src/components/chips/status_chip.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// +/// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21265-2159 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/chips/input-chip +class ZetaStatusChip extends ZetaStatelessWidget { + /// Creates a [ZetaStatusChip]. + const ZetaStatusChip({ + super.key, + required this.label, + super.rounded, + this.draggable = false, + this.data, + this.onDragCompleted, + this.semanticLabel, + }); + + /// The label on the [ZetaStatusChip] + final String label; + + /// Whether the chip can be dragged. + final bool draggable; + + /// Draggable data. + final dynamic data; + + /// Called when the draggable is dropped and accepted by a [DragTarget]. + /// + /// See also: + /// * [DragTarget] + /// * [Draggable] + final VoidCallback? onDragCompleted; + + /// A semantic label for the chip. + final String? semanticLabel; + + @override + Widget build(BuildContext context) { + return ZetaRoundedScope( + rounded: context.rounded, + child: MouseRegion( + cursor: draggable ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: Semantics( + label: semanticLabel, + child: SelectionContainer.disabled( + child: draggable + ? Draggable( + feedback: Material( + color: Colors.transparent, + child: child(context), + ), + childWhenDragging: const Nothing(), + data: data, + onDragCompleted: onDragCompleted, + child: child(context), + ) + : child(context), + ), + ), + ), + ); + } + + /// The child widget of the chip. + Widget child(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Zeta.of(context).colors.surfaceWarm, + borderRadius: BorderRadius.all( + Radius.circular( + Zeta.of(context).spacing.small, + ), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: Zeta.of(context).spacing.small, + vertical: Zeta.of(context).spacing.minimum, + ), + child: Text(label, style: ZetaTextStyles.bodyXSmall), + ); + } +} From 5505885e6bbbbc4c087f0c83de679b701d608b22 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Tue, 5 Nov 2024 17:08:51 +0000 Subject: [PATCH 2/4] docs: added description for status chip fix: converted child widget function to a stateless widget class in status chip --- lib/src/components/chips/status_chip.dart | 41 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/src/components/chips/status_chip.dart b/lib/src/components/chips/status_chip.dart index db6aa0b9..7cad4fa1 100644 --- a/lib/src/components/chips/status_chip.dart +++ b/lib/src/components/chips/status_chip.dart @@ -1,7 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; +/// The [ZetaStatusChip] is a chip that displays a status/label. +/// It can be dragged around the screen and placed in new locations using [DragTarget]. /// /// {@category Components} /// @@ -52,22 +55,42 @@ class ZetaStatusChip extends ZetaStatelessWidget { ? Draggable( feedback: Material( color: Colors.transparent, - child: child(context), + child: _Child(context: context, label: label), ), childWhenDragging: const Nothing(), data: data, onDragCompleted: onDragCompleted, - child: child(context), + child: _Child(context: context, label: label), ) - : child(context), + : _Child(context: context, label: label), ), ), ), ); } - /// The child widget of the chip. - Widget child(BuildContext context) { + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('draggable', draggable)) + ..add(DiagnosticsProperty('data', data)) + ..add(ObjectFlagProperty.has('onDragCompleted', onDragCompleted)) + ..add(StringProperty('semanticLabel', semanticLabel)); + } +} + +/// The child widget of the [ZetaStatusChip]. +class _Child extends StatelessWidget { + const _Child({required this.context, required this.label}); + + final BuildContext context; + + final String label; + + @override + Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Zeta.of(context).colors.surfaceWarm, @@ -84,4 +107,12 @@ class ZetaStatusChip extends ZetaStatelessWidget { child: Text(label, style: ZetaTextStyles.bodyXSmall), ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('context', context)) + ..add(StringProperty('label', label)); + } } From 5f0c6e51455916eb41a2eaca8af0efab9c46f091 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Tue, 5 Nov 2024 17:20:41 +0000 Subject: [PATCH 3/4] feat: implemented status chip widgetbook use case fix: set rounded to default to true --- example/widgetbook/main.dart | 1 + .../pages/components/chip_widgetbook.dart | 11 +++++++++++ lib/src/components/chips/status_chip.dart | 14 ++++++++------ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index 6a5202c6..7eeba843 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -163,6 +163,7 @@ class _HotReloadState extends State { WidgetbookUseCase(name: 'Filter Chip', builder: (context) => filterChipUseCase(context)), WidgetbookUseCase(name: 'Input Chip', builder: (context) => inputChipUseCase(context)), WidgetbookUseCase(name: 'Assist Chip', builder: (context) => assistChipUseCase(context)), + WidgetbookUseCase(name: 'Status Chip', builder: (context) => statusChipUseCase(context)), ], ), WidgetbookComponent( diff --git a/example/widgetbook/pages/components/chip_widgetbook.dart b/example/widgetbook/pages/components/chip_widgetbook.dart index d8e7848a..057d6d6c 100644 --- a/example/widgetbook/pages/components/chip_widgetbook.dart +++ b/example/widgetbook/pages/components/chip_widgetbook.dart @@ -39,3 +39,14 @@ Widget assistChipUseCase(BuildContext context) { ), ); } + +Widget statusChipUseCase(BuildContext context) { + return WidgetbookScaffold( + builder: (context, _) => ZetaStatusChip( + label: context.knobs.string(label: 'Label', initialValue: 'Label'), + draggable: context.knobs.boolean(label: 'Draggable', initialValue: false), + rounded: context.knobs.boolean(label: 'Rounded', initialValue: true), + onDragCompleted: () => print('Drag completed'), + ), + ); +} diff --git a/lib/src/components/chips/status_chip.dart b/lib/src/components/chips/status_chip.dart index 7cad4fa1..079fd2e7 100644 --- a/lib/src/components/chips/status_chip.dart +++ b/lib/src/components/chips/status_chip.dart @@ -16,7 +16,7 @@ class ZetaStatusChip extends ZetaStatelessWidget { const ZetaStatusChip({ super.key, required this.label, - super.rounded, + super.rounded = true, this.draggable = false, this.data, this.onDragCompleted, @@ -94,11 +94,13 @@ class _Child extends StatelessWidget { return Container( decoration: BoxDecoration( color: Zeta.of(context).colors.surfaceWarm, - borderRadius: BorderRadius.all( - Radius.circular( - Zeta.of(context).spacing.small, - ), - ), + borderRadius: context.rounded + ? BorderRadius.all( + Radius.circular( + Zeta.of(context).spacing.small, + ), + ) + : null, ), padding: EdgeInsets.symmetric( horizontal: Zeta.of(context).spacing.small, From bdfdb951e7f534100af923e5edaf6c25574ac066 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Wed, 6 Nov 2024 13:49:43 +0000 Subject: [PATCH 4/4] test: wrote tests for status chip --- lib/src/components/chips/status_chip.dart | 9 +- .../chips/golden/status_chip_default.png | Bin 0 -> 3340 bytes .../chips/golden/status_chip_long.png | Bin 0 -> 3405 bytes .../chips/golden/status_chip_sharp.png | Bin 0 -> 3340 bytes .../components/chips/status_chip_test.dart | 226 ++++++++++++++++++ 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 test/src/components/chips/golden/status_chip_default.png create mode 100644 test/src/components/chips/golden/status_chip_long.png create mode 100644 test/src/components/chips/golden/status_chip_sharp.png create mode 100644 test/src/components/chips/status_chip_test.dart diff --git a/lib/src/components/chips/status_chip.dart b/lib/src/components/chips/status_chip.dart index 079fd2e7..cdd205dd 100644 --- a/lib/src/components/chips/status_chip.dart +++ b/lib/src/components/chips/status_chip.dart @@ -49,7 +49,7 @@ class ZetaStatusChip extends ZetaStatelessWidget { child: MouseRegion( cursor: draggable ? SystemMouseCursors.click : SystemMouseCursors.basic, child: Semantics( - label: semanticLabel, + label: semanticLabel ?? label, child: SelectionContainer.disabled( child: draggable ? Draggable( @@ -106,7 +106,12 @@ class _Child extends StatelessWidget { horizontal: Zeta.of(context).spacing.small, vertical: Zeta.of(context).spacing.minimum, ), - child: Text(label, style: ZetaTextStyles.bodyXSmall), + child: Text( + label, + style: ZetaTextStyles.bodyXSmall.copyWith( + color: Zeta.of(context).colors.textDefault, + ), + ), ); } diff --git a/test/src/components/chips/golden/status_chip_default.png b/test/src/components/chips/golden/status_chip_default.png new file mode 100644 index 0000000000000000000000000000000000000000..e0bad968a24acb93bbf1b514eb3f04d4d9225063 GIT binary patch literal 3340 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefq`e6r;B4q#hkY{H~KmoiZ}$aZ{)hkmD=txpRvQmQKhWoMgvn>v9Oy* zUDs@-=U2{u%U-`{X7N0B1_p-*FYj>!4eWPr0Fsi7ObiSuEF26B69g52lFxo#I`I2p z@*KbW;qgDVy*d8;!Bgh|G^&Szk%7U8iG_jT1cv|vgM#v?;Anu1CWg_BFj_K%JlSHi`iVJYD@<);T3K0RV>I;Nk!P literal 0 HcmV?d00001 diff --git a/test/src/components/chips/golden/status_chip_long.png b/test/src/components/chips/golden/status_chip_long.png new file mode 100644 index 0000000000000000000000000000000000000000..03f566255de485a2e620d0272ea3198f4a748c56 GIT binary patch literal 3405 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefr00{r;B4q#hkY{W3|{4B^n+oT7L|l=9I{~iT$f(fJ^{ihwTYInGGUO z)0K-qMIQRzTO@qF^v{_uS8n^<-K+iB;v72zL&Jer{`x?p|Hw13Ffg3p5MW?XP<8-{ z_b@OrFc@Wbv+3!rZ~OhbeQA7!k+s~q{CCsl%|F}MKAZEiyLmdKI;Vst04i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefq`e6r;B4q#hkY{H~KmoiZ}$aZ{)hkmD=txpRvQmQKhWoMgvn>v9Oy* zUDs@-=U2{u%U-`{X7N0B1_p-*FYj>!4eWPr0Fsi7ObiSuEF26B69g52lFxo#I`I2p z@*KbW;qgDVy*d8;!Bgh|G^&Szk%7U8iG_jT1cv|vgM#v?;Anu1CWg_BFj_K%JlSHi`iVJYD@<);T3K0RV>I;Nk!P literal 0 HcmV?d00001 diff --git a/test/src/components/chips/status_chip_test.dart b/test/src/components/chips/status_chip_test.dart new file mode 100644 index 00000000..5d993228 --- /dev/null +++ b/test/src/components/chips/status_chip_test.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/tolerant_comparator.dart'; +import '../../../test_utils/utils.dart'; + +void main() { + const String parentFolder = 'chips'; + + const goldenFile = GoldenFiles(component: parentFolder); + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + group('Accessibility Tests', () { + testWidgets('label meets accessibility requirements', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + ), + ), + ); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + }); + + group('Content Tests', () { + final debugFillProperties = { + 'label': '"Label"', + 'draggable': 'false', + 'data': 'null', + 'onDragCompleted': 'null', + 'semanticLabel': 'null', + }; + debugFillPropertiesTest( + const ZetaStatusChip( + label: 'Label', + ), + debugFillProperties, + ); + + testWidgets('renders label correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + ), + ), + ); + + expect(find.text('Label'), findsOneWidget); + }); + + testWidgets('renders label correctly when label is long', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label that is very very long', + ), + ), + ); + + final Finder textFinder = find.text('Label that is very very long'); + + expect(textFinder, findsOneWidget); + }); + }); + + group('Dimensions Tests', () { + testWidgets('horizontal padding is 8 and vertical padding is 4', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + ), + ), + ); + final Finder finder = find.byType(Container); + expect(finder, findsOneWidget); + final Container widget = tester.widget(finder); + expect( + widget.padding, + const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + ); + }); + }); + + group('Styling Tests', () { + testWidgets('background color is ZetaColors.surfaceWarm and border radius is 8', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + ), + ), + ); + final Finder finder = find.byType(Container); + expect(finder, findsOneWidget); + final Container widget = tester.widget(finder); + expect( + widget.decoration, + BoxDecoration( + color: ZetaColors.light().surfaceWarm, + borderRadius: BorderRadius.circular(8), + ), + ); + }); + + testWidgets('border radius is null when rounded is false', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + rounded: false, + ), + ), + ); + final Finder finder = find.byType(Container); + expect(finder, findsOneWidget); + final Container widget = tester.widget(finder); + expect( + widget.decoration, + BoxDecoration( + color: ZetaColors.light().surfaceWarm, + ), + ); + }); + + testWidgets('text style is ZetaTextStyles.bodyXSmall and text color is textDefault', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaStatusChip( + label: 'Label', + ), + ), + ); + final Finder finder = find.byType(Text); + expect(finder, findsOneWidget); + final Text widget = tester.widget(finder); + expect(widget.style, ZetaTextStyles.bodyXSmall.copyWith(color: ZetaColors.light().textDefault)); + }); + }); + + group('Interaction Tests', () { + testWidgets('chip calls onDragCompleted and parses data to DragTarget when drag is completed', + (WidgetTester tester) async { + bool dragCompleted = false; + String? dragData; + + await tester.pumpWidget( + TestApp( + home: Column( + children: [ + ZetaStatusChip( + label: 'Label', + draggable: true, + data: 'data', + onDragCompleted: () { + dragCompleted = true; + }, + ), + DragTarget( + onAcceptWithDetails: (details) => dragData = details.data, + builder: (BuildContext context, List candidateData, List rejectedData) { + return const SizedBox( + width: 100, + height: 100, + ); + }, + ), + ], + ), + ), + ); + + final Finder finder = find.byType(Draggable); + expect(finder, findsOneWidget); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(finder)); + await gesture.moveTo(tester.getCenter(find.byType(DragTarget))); + await gesture.up(); + + expect(dragCompleted, isTrue); + expect(dragData, 'data'); + }); + }); + + group('Golden Tests', () { + goldenTest( + goldenFile, + const ZetaStatusChip( + label: 'Label', + ), + 'status_chip_default', + ); + goldenTest( + goldenFile, + const ZetaStatusChip( + label: 'Label', + rounded: false, + ), + 'status_chip_sharp', + ); + goldenTest( + goldenFile, + const ZetaStatusChip( + label: 'Label that is very very long', + ), + 'status_chip_long', + ); + }); + + group('Performance Tests', () {}); +}