diff --git a/example/lib/home.dart b/example/lib/home.dart index 72fbbd44..783f36ec 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -10,6 +10,7 @@ import 'package:zeta_example/pages/components/button_example.dart'; import 'package:zeta_example/pages/components/chat_item_example.dart'; import 'package:zeta_example/pages/components/checkbox_example.dart'; import 'package:zeta_example/pages/components/chip_example.dart'; +import 'package:zeta_example/pages/components/comms_button_example.dart'; import 'package:zeta_example/pages/components/contact_item_example.dart'; import 'package:zeta_example/pages/components/date_input_example.dart'; import 'package:zeta_example/pages/components/dialog_example.dart'; @@ -69,6 +70,7 @@ final List components = [ Component(ChatItemExample.name, (context) => const ChatItemExample()), Component(CheckBoxExample.name, (context) => const CheckBoxExample()), Component(ChipExample.name, (context) => const ChipExample()), + Component(CommsButtonExample.name, (context) => const CommsButtonExample()), Component(ContactItemExample.name, (context) => const ContactItemExample()), Component(ListExample.name, (context) => const ListExample()), Component(ListItemExample.name, (context) => const ListItemExample()), diff --git a/example/lib/pages/components/comms_button_example.dart b/example/lib/pages/components/comms_button_example.dart new file mode 100644 index 00000000..9c1c5939 --- /dev/null +++ b/example/lib/pages/components/comms_button_example.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class CommsButtonExample extends StatelessWidget { + static const String name = 'CommsButton'; + + const CommsButtonExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: name, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + runSpacing: Zeta.of(context).spacing.xl_3, + alignment: WrapAlignment.start, + children: [ + Column( + children: [ + ZetaCommsButton.reject( + label: 'Reject', + size: ZetaWidgetSize.large, + ), + ZetaCommsButton.reject( + label: 'Reject', + size: ZetaWidgetSize.medium, + ), + ZetaCommsButton.reject( + label: 'Reject', + size: ZetaWidgetSize.small, + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.answer( + label: 'Answer', + size: ZetaWidgetSize.large, + ), + ZetaCommsButton.answer( + label: 'Answer', + size: ZetaWidgetSize.medium, + ), + ZetaCommsButton.answer( + label: 'Answer', + size: ZetaWidgetSize.small, + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.mute( + label: 'Mute', + size: ZetaWidgetSize.large, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Un-Mute', + ), + ZetaCommsButton.mute( + label: 'Mute', + size: ZetaWidgetSize.medium, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Un-Mute', + ), + ZetaCommsButton.mute( + label: 'Mute', + size: ZetaWidgetSize.small, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Un-Mute', + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.video( + label: 'Hide Video', + size: ZetaWidgetSize.large, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Show Video', + ), + ZetaCommsButton.video( + label: 'Hide Video', + size: ZetaWidgetSize.medium, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Show Video', + ), + ZetaCommsButton.video( + label: 'Hide Video', + size: ZetaWidgetSize.small, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Show Video', + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.transfer( + label: 'Transfer', + size: ZetaWidgetSize.large, + ), + ZetaCommsButton.transfer( + label: 'Transfer', + size: ZetaWidgetSize.medium, + ), + ZetaCommsButton.transfer( + label: 'Transfer', + size: ZetaWidgetSize.small, + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.hold( + label: 'Hold Call', + size: ZetaWidgetSize.large, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'On Hold', + ), + ZetaCommsButton.hold( + label: 'Hold Call', + size: ZetaWidgetSize.medium, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'On Hold', + ), + ZetaCommsButton.hold( + label: 'Hold Call', + size: ZetaWidgetSize.small, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'On Hold', + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.speaker( + label: 'Speaker On', + size: ZetaWidgetSize.large, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Speaker Off', + ), + ZetaCommsButton.speaker( + label: 'Speaker On', + size: ZetaWidgetSize.medium, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Speaker Off', + ), + ZetaCommsButton.speaker( + label: 'Speaker On', + size: ZetaWidgetSize.small, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Speaker Off', + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.record( + label: 'Record', + size: ZetaWidgetSize.large, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Stop', + ), + ZetaCommsButton.record( + label: 'Record', + size: ZetaWidgetSize.medium, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Stop', + ), + ZetaCommsButton.record( + label: 'Record', + size: ZetaWidgetSize.small, + onToggle: (isToggled) { + print('Toggled'); + print(isToggled); + }, + toggledLabel: 'Stop', + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.add( + label: 'Add', + size: ZetaWidgetSize.large, + ), + ZetaCommsButton.add( + label: 'Add', + size: ZetaWidgetSize.medium, + ), + ZetaCommsButton.add( + label: 'Add', + size: ZetaWidgetSize.small, + ), + ].gap(Zeta.of(context).spacing.large), + ), + Column( + children: [ + ZetaCommsButton.security( + label: 'Security', + size: ZetaWidgetSize.large, + ), + ZetaCommsButton.security( + label: 'Security', + size: ZetaWidgetSize.medium, + ), + ZetaCommsButton.security( + label: 'Security', + size: ZetaWidgetSize.small, + ), + ].gap(Zeta.of(context).spacing.large), + ), + ].gap(Zeta.of(context).spacing.large), + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/search_bar_example.dart b/example/lib/pages/components/search_bar_example.dart index b44ee768..5395fa47 100644 --- a/example/lib/pages/components/search_bar_example.dart +++ b/example/lib/pages/components/search_bar_example.dart @@ -27,6 +27,7 @@ class _SearchBarExampleState extends State { padding: const EdgeInsets.all(20), child: ZetaSearchBar( onChange: (value) {}, + showSpeechToText: false, textInputAction: TextInputAction.search, onFieldSubmitted: (text) { print(text); diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index d807ba58..5397a190 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -20,6 +20,7 @@ import 'pages/components/button_widgetbook.dart'; import 'pages/components/chat_item_widgetbook.dart'; import 'pages/components/checkbox_widgetbook.dart'; import 'pages/components/chip_widgetbook.dart'; +import 'pages/components/comms_button_widgetbook.dart'; import 'pages/components/contact_item_widgetbook.dart'; import 'pages/components/date_input_widgetbook.dart'; import 'pages/components/dial_pad_widgetbook.dart'; @@ -185,6 +186,7 @@ class _HotReloadState extends State { WidgetbookUseCase(name: 'Bottom Sheet', builder: (context) => bottomSheetContentUseCase(context)), WidgetbookUseCase(name: 'BreadCrumbs', builder: (context) => breadCrumbsUseCase(context)), WidgetbookUseCase(name: 'Checkbox', builder: (context) => checkboxUseCase(context)), + WidgetbookUseCase(name: 'Comms Button', builder: (context) => commsButtonUseCase(context)), WidgetbookUseCase(name: 'Date Input', builder: (context) => dateInputUseCase(context)), WidgetbookUseCase(name: 'Dial Pad', builder: (context) => dialPadUseCase(context)), WidgetbookUseCase(name: 'Dialog', builder: (context) => dialogUseCase(context)), diff --git a/example/widgetbook/pages/components/button_widgetbook.dart b/example/widgetbook/pages/components/button_widgetbook.dart index 74ad65bc..ed1980e8 100644 --- a/example/widgetbook/pages/components/button_widgetbook.dart +++ b/example/widgetbook/pages/components/button_widgetbook.dart @@ -139,7 +139,7 @@ class _FabWidgetState extends State { itemBuilder: (context, index) => Text("$index"), ), floatingActionButton: ZetaFAB( - expanded: true, + expanded: widget.c.knobs.boolean(label: 'Expanded'), scrollController: _scrollController, label: widget.c.knobs.string(label: 'Label', initialValue: 'Floating Action Button'), onPressed: widget.c.knobs.boolean(label: 'Disabled') ? null : () {}, diff --git a/example/widgetbook/pages/components/comms_button_widgetbook.dart b/example/widgetbook/pages/components/comms_button_widgetbook.dart new file mode 100644 index 00000000..cc462b73 --- /dev/null +++ b/example/widgetbook/pages/components/comms_button_widgetbook.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../utils/scaffold.dart'; +import '../../utils/utils.dart'; + +Widget commsButtonUseCase(BuildContext context) { + return WidgetbookScaffold( + builder: (context, _) => ZetaCommsButton( + label: context.knobs.string( + label: 'Text', + initialValue: 'Answer', + ), + size: context.knobs.list( + label: 'Size', + options: ZetaWidgetSize.values, + labelBuilder: enumLabelBuilder, + initialOption: ZetaWidgetSize.medium, + ), + type: context.knobs.list( + label: 'Type', + options: ZetaCommsButtonType.values, + labelBuilder: enumLabelBuilder, + initialOption: ZetaCommsButtonType.positive, + ), + icon: iconKnob( + context, + nullable: false, + name: "Icon", + initial: ZetaIcons.phone, + ), + ), + ); +} diff --git a/example/widgetbook/pages/components/search_bar_widgetbook.dart b/example/widgetbook/pages/components/search_bar_widgetbook.dart index 9072142b..fc767a8d 100644 --- a/example/widgetbook/pages/components/search_bar_widgetbook.dart +++ b/example/widgetbook/pages/components/search_bar_widgetbook.dart @@ -49,7 +49,7 @@ Widget searchBarUseCase(BuildContext context) { size: size, shape: shape, disabled: disabled, - hintText: hint, + placeholder: hint, showSpeechToText: showSpeechToText, onChange: (value) { if (value == null) return; diff --git a/lib/generated/icons/icons.g.dart b/lib/generated/icons/icons.g.dart index 5ad78da3..a75631a4 100644 --- a/lib/generated/icons/icons.g.dart +++ b/lib/generated/icons/icons.g.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; // DO NOT MODIFY /// Current version of Zeta Icons -const zetaIconsVersion = '0.5.5'; +// x-release-please-start-version +const zetaIconsVersion = '0.6.2'; +// x-release-please-end -/// Zeta Icons. Full list of icons can be found at [Zeta Icons](https://zeta-icons.web.app/). +/// Zeta Icons. Full list of icons can be found at [Zeta Icons](https://design.zebra.com/icons/). /// {@category Assets} abstract class ZetaIcons { /// Default icon font name diff --git a/lib/generated/icons/zeta-icons-round.ttf b/lib/generated/icons/zeta-icons-round.ttf index dc0a88d8..e40044dc 100644 Binary files a/lib/generated/icons/zeta-icons-round.ttf and b/lib/generated/icons/zeta-icons-round.ttf differ diff --git a/lib/generated/icons/zeta-icons-sharp.ttf b/lib/generated/icons/zeta-icons-sharp.ttf index 2d8a9d16..6c7e6a5b 100644 Binary files a/lib/generated/icons/zeta-icons-sharp.ttf and b/lib/generated/icons/zeta-icons-sharp.ttf differ diff --git a/lib/src/components/accordion/accordion.dart b/lib/src/components/accordion/accordion.dart index 16095819..834b0f18 100644 --- a/lib/src/components/accordion/accordion.dart +++ b/lib/src/components/accordion/accordion.dart @@ -7,6 +7,10 @@ import '../../../zeta_flutter.dart'; /// the content associated with that item. There can be zero expanded items, exactly one, /// or more than one item expanded at a time, depending on the configuration. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=3427-67874 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/accordion class ZetaAccordion extends ZetaStatefulWidget { /// The constructor of the component [ZetaAccordion]. const ZetaAccordion({ diff --git a/lib/src/components/avatars/avatar.dart b/lib/src/components/avatars/avatar.dart index 1dde9530..d4ba76bf 100644 --- a/lib/src/components/avatars/avatar.dart +++ b/lib/src/components/avatars/avatar.dart @@ -35,6 +35,10 @@ enum ZetaAvatarSize { /// An avatar is a visual representation of a user or entity. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=20816-388 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/avatar class ZetaAvatar extends ZetaStatelessWidget { /// Constructor for [ZetaAvatar] const ZetaAvatar({ @@ -184,7 +188,7 @@ class ZetaAvatar extends ZetaStatelessWidget { (initials != null ? Center( child: Text( - size == ZetaAvatarSize.xs ? initials!.substring(0, 1) : initials!, + initials!, style: initialTextStyle ?? TextStyle( fontSize: size.fontSize(context), @@ -284,11 +288,10 @@ class ZetaAvatar extends ZetaStatelessWidget { ..add(StringProperty('semanticLowerBadgeValue', semanticLowerBadgeLabel)) ..add(DiagnosticsProperty('initialTextStyle', initialTextStyle)); } -} -extension on ZetaAvatarSize { - double pixelSize(BuildContext context) { - switch (this) { + /// Returns pixel size for [ZetaAvatarSize] + static double pixelSize(BuildContext context, ZetaAvatarSize size) { + switch (size) { case ZetaAvatarSize.xxxl: return Zeta.of(context).spacing.minimum * 50; // TODO(UX-1202): ZetaSpacingBase // return ZetaSpacingBase.x50; @@ -312,6 +315,17 @@ extension on ZetaAvatarSize { } } + /// Font size for initials + static double fontSize(BuildContext context, ZetaAvatarSize size) { + return pixelSize(context, size) * 4 / 9; + } +} + +extension on ZetaAvatarSize { + double pixelSize(BuildContext context) { + return ZetaAvatar.pixelSize(context, this); + } + double borderSize(BuildContext context) { switch (this) { case ZetaAvatarSize.xxxl: @@ -331,7 +345,7 @@ extension on ZetaAvatarSize { } double fontSize(BuildContext context) { - return pixelSize(context) * 4 / 9; + return ZetaAvatar.fontSize(context, this); } } diff --git a/lib/src/components/badges/indicator.dart b/lib/src/components/badges/indicator.dart index 339722d6..b517cae5 100644 --- a/lib/src/components/badges/indicator.dart +++ b/lib/src/components/badges/indicator.dart @@ -13,6 +13,10 @@ enum ZetaIndicatorType { /// Indicators are used to show the status of a user or any messages/notifications they might have. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=22000-10045&mode=design&t=6mhOcUUr3tgxxFdd-0 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/badge/indicators class ZetaIndicator extends ZetaStatelessWidget { /// Constructor for [ZetaIndicator]. const ZetaIndicator({ diff --git a/lib/src/components/badges/label.dart b/lib/src/components/badges/label.dart index 1b2dfe2f..53f7f02d 100644 --- a/lib/src/components/badges/label.dart +++ b/lib/src/components/badges/label.dart @@ -9,6 +9,10 @@ typedef ZetaBadge = ZetaLabel; /// Text badges notify users of line items that need attention. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=21926-2099 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/badge/label class ZetaLabel extends ZetaStatelessWidget { ///Constructs [ZetaLabel]. const ZetaLabel({ diff --git a/lib/src/components/badges/priority_pill.dart b/lib/src/components/badges/priority_pill.dart index a295b61e..6e107e42 100644 --- a/lib/src/components/badges/priority_pill.dart +++ b/lib/src/components/badges/priority_pill.dart @@ -61,6 +61,10 @@ extension on ZetaPriorityPillType { /// This badge is used to indicate the order of importance. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=22000-15955 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/badge/priority-pill class ZetaPriorityPill extends ZetaStatelessWidget { ///Constructs [ZetaPriorityPill] const ZetaPriorityPill({ diff --git a/lib/src/components/badges/status_label.dart b/lib/src/components/badges/status_label.dart index dd3ff091..77fc7d83 100644 --- a/lib/src/components/badges/status_label.dart +++ b/lib/src/components/badges/status_label.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// To help some information, labels, or errors stand out, we present them with badges. /// They can look like buttons, but users can’t select them. They just guide users to things they should pay attention to. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=21836-37274 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/badge/status-label class ZetaStatusLabel extends ZetaStatelessWidget { ///Constructs [ZetaStatusLabel]. const ZetaStatusLabel({ diff --git a/lib/src/components/badges/tag.dart b/lib/src/components/badges/tag.dart index 067e1945..c6ecea98 100644 --- a/lib/src/components/badges/tag.dart +++ b/lib/src/components/badges/tag.dart @@ -15,6 +15,10 @@ enum ZetaTagDirection { /// Tags are used to draw attention to a specific area or information. /// The arrow shape helps direct the users attention to the desired place. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?type=design&node-id=22000-13170 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/badge/tags class ZetaTag extends ZetaStatelessWidget { /// Constructs a [ZetaTag]. const ZetaTag({ diff --git a/lib/src/components/banner/banner.dart b/lib/src/components/banner/banner.dart index 61bec762..cbd5a35b 100644 --- a/lib/src/components/banner/banner.dart +++ b/lib/src/components/banner/banner.dart @@ -29,6 +29,10 @@ enum ZetaBannerStatus { /// A banner displays an important, succinct message, and provides action for users to address. /// It draws the attention to the message by displaying it at the top in various colors. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=22195-43965 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/banners class ZetaBanner extends MaterialBanner { /// Constructor for [ZetaBanner]. See [MaterialBanner] for more information. ZetaBanner({ @@ -98,7 +102,14 @@ class ZetaBanner extends MaterialBanner { size: Zeta.of(context).spacing.xl_2, ), ), - Flexible(child: Text(title)), + Flexible( + child: Text( + title, + style: ZetaTextStyles.labelLarge.copyWith( + color: Zeta.of(context).colors.mainInverse, + ), + ), + ), ], ), ), diff --git a/lib/src/components/bottom sheets/bottom_sheet.dart b/lib/src/components/bottom sheets/bottom_sheet.dart index 439a300f..fcde59a8 100644 --- a/lib/src/components/bottom sheets/bottom_sheet.dart +++ b/lib/src/components/bottom sheets/bottom_sheet.dart @@ -9,6 +9,10 @@ import '../../../zeta_flutter.dart'; /// /// Content should typically consist of a [List] of [ZetaMenuItem]s. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21541-2225 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/bottom-sheet class ZetaBottomSheet extends ZetaStatelessWidget { /// Constructor for [ZetaBottomSheet]. const ZetaBottomSheet({ diff --git a/lib/src/components/bottom sheets/menu_items.dart b/lib/src/components/bottom sheets/menu_items.dart index 52585cdf..bb733b6b 100644 --- a/lib/src/components/bottom sheets/menu_items.dart +++ b/lib/src/components/bottom sheets/menu_items.dart @@ -13,6 +13,10 @@ enum ZetaMenuItemType { /// Menu Item component, typically used as body of [ZetaBottomSheet]. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21541-2225 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/bottom-sheet class ZetaMenuItem extends ZetaStatelessWidget { /// Constructor for the component [ZetaMenuItem]. /// diff --git a/lib/src/components/breadcrumbs/breadcrumbs.dart b/lib/src/components/breadcrumbs/breadcrumbs.dart index 204feaeb..fbde10c7 100644 --- a/lib/src/components/breadcrumbs/breadcrumbs.dart +++ b/lib/src/components/breadcrumbs/breadcrumbs.dart @@ -9,6 +9,10 @@ import '../../../zeta_flutter.dart'; /// [children] should consist of [ZetaBreadCrumb]s. /// /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-5&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/breadcrumbs class ZetaBreadCrumbs extends ZetaStatefulWidget { ///Constructor for [ZetaBreadCrumbs] const ZetaBreadCrumbs({ diff --git a/lib/src/components/button_group/button_group.dart b/lib/src/components/button_group/button_group.dart index 90570723..f07f6ec1 100644 --- a/lib/src/components/button_group/button_group.dart +++ b/lib/src/components/button_group/button_group.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Zeta Button Group /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-45&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/buttons/group-button class ZetaButtonGroup extends ZetaStatelessWidget { /// Constructs [ZetaButtonGroup] from a list of [ZetaGroupButton]s const ZetaButtonGroup({ diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index c14ec484..fb81b239 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Zeta Button /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=23126-110945 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/buttons/button class ZetaButton extends ZetaStatelessWidget { ///Constructs [ZetaButton] const ZetaButton({ diff --git a/lib/src/components/buttons/icon_button.dart b/lib/src/components/buttons/icon_button.dart index 27cea181..16ec30cd 100644 --- a/lib/src/components/buttons/icon_button.dart +++ b/lib/src/components/buttons/icon_button.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Component [ZetaIconButton] /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=23126-110314 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/buttons/icon-button class ZetaIconButton extends ZetaStatelessWidget { /// Constructor for [ZetaIconButton] const ZetaIconButton({ diff --git a/lib/src/components/chat_item/chat_item.dart b/lib/src/components/chat_item/chat_item.dart index bba78930..84bd7ba1 100644 --- a/lib/src/components/chat_item/chat_item.dart +++ b/lib/src/components/chat_item/chat_item.dart @@ -7,6 +7,10 @@ import '../../../zeta_flutter.dart'; /// Chat item widget that can be dragged to reveal contextual actions. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=24828-46282&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/chat-list-item class ZetaChatItem extends ZetaStatelessWidget { /// Creates a [ZetaChatItem] const ZetaChatItem({ diff --git a/lib/src/components/checkbox/checkbox.dart b/lib/src/components/checkbox/checkbox.dart index 55f96721..1eec21c4 100644 --- a/lib/src/components/checkbox/checkbox.dart +++ b/lib/src/components/checkbox/checkbox.dart @@ -10,6 +10,10 @@ import '../../../zeta_flutter.dart'; /// rebuild the checkbox with a new [value] to update the visual appearance of /// the checkbox. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21510-54003 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/checkbox class ZetaCheckbox extends FormField { /// Constructs a [ZetaCheckbox]. ZetaCheckbox({ diff --git a/lib/src/components/chips/assist_chip.dart b/lib/src/components/chips/assist_chip.dart index 2b74da5d..343bba38 100644 --- a/lib/src/components/chips/assist_chip.dart +++ b/lib/src/components/chips/assist_chip.dart @@ -6,6 +6,10 @@ import '../../../zeta_flutter.dart'; /// /// These chips use [Draggable] and can be dragged around the screen and placed in new locations using [DragTarget]. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21265-14215 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/chips/assist-chip class ZetaAssistChip extends ZetaChip { /// Creates a [ZetaAssistChip]. const ZetaAssistChip({ diff --git a/lib/src/components/chips/chip.dart b/lib/src/components/chips/chip.dart index c9fdf159..71945d8c 100644 --- a/lib/src/components/chips/chip.dart +++ b/lib/src/components/chips/chip.dart @@ -10,7 +10,6 @@ export './input_chip.dart'; /// This covers the broad functionality of [ZetaAssistChip], [ZetaFilterChip] and [ZetaInputChip]. /// /// If [selected] is not null, the chip will have the toggle behavior of [ZetaFilterChip]. -/// {@category Components} class ZetaChip extends ZetaStatefulWidget { /// Constructs a [ZetaChip]. const ZetaChip({ diff --git a/lib/src/components/chips/filter_chip.dart b/lib/src/components/chips/filter_chip.dart index 68cb3e90..c6f8c22c 100644 --- a/lib/src/components/chips/filter_chip.dart +++ b/lib/src/components/chips/filter_chip.dart @@ -8,6 +8,10 @@ import '../../../zeta_flutter.dart'; /// /// These chips use [Draggable] and can be dragged around the screen and placed in new locations using [DragTarget]. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21265-14112 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/chips/filter-chip class ZetaFilterChip extends ZetaChip { /// Creates a [ZetaFilterChip]. const ZetaFilterChip({ diff --git a/lib/src/components/chips/input_chip.dart b/lib/src/components/chips/input_chip.dart index 0b57bb19..c079a67f 100644 --- a/lib/src/components/chips/input_chip.dart +++ b/lib/src/components/chips/input_chip.dart @@ -4,6 +4,10 @@ import '../../../zeta_flutter.dart'; /// /// Leading widget should typically be a [ZetaAvatar]. /// {@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 ZetaInputChip extends ZetaChip { /// Creates a [ZetaInputChip]. const ZetaInputChip({ diff --git a/lib/src/components/comms_button/comms_button.dart b/lib/src/components/comms_button/comms_button.dart new file mode 100644 index 00000000..36ff0e4e --- /dev/null +++ b/lib/src/components/comms_button/comms_button.dart @@ -0,0 +1,428 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Enum for the type of comms button. +enum ZetaCommsButtonType { + /// Green background, no border, white icon. + positive, + + /// Red background, no border, white icon. + negative, + + /// Light grey background, dark grey border, black icon. + on, + + /// Dark grey background, light grey border, white icon. + off, + + /// White background, red border, red icon. + warning, +} + +/// Comms button component. +/// This component is used to display a button for communication action. Answer, reject, mute, hold, speaker, etc. +/// +/// Use the constructors to create preconfigured comms buttons. +/// +/// `ZetaCommsButton.answer()`, `ZetaCommsButton.reject()`, `ZetaCommsButton.mute()`, +/// `ZetaCommsButton.hold()`, `ZetaCommsButton.speaker()`, `ZetaCommsButton.record()`, etc. +/// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=20816-7765&node-type=canvas&t=nc1YR061CeZRr6IJ-0 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/comms-button +class ZetaCommsButton extends StatefulWidget { + /// Constructs [ZetaCommsButton] + const ZetaCommsButton({ + super.key, + this.label, + this.type = ZetaCommsButtonType.on, + this.size = ZetaWidgetSize.medium, + this.icon, + this.onToggle, + this.toggledIcon, + this.toggledLabel, + this.toggledType, + this.onPressed, + this.focusNode, + this.semanticLabel, + }); + + /// Constructs answer call [ZetaCommsButton] + const ZetaCommsButton.answer({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.positive, + icon = ZetaIcons.phone, + onToggle = null, + toggledIcon = null, + toggledLabel = null, + toggledType = null; + + /// Constructs reject call [ZetaCommsButton] + const ZetaCommsButton.reject({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.negative, + icon = ZetaIcons.end_call, + onToggle = null, + toggledIcon = null, + toggledLabel = null, + toggledType = null; + + /// Constructs mute [ZetaCommsButton] + const ZetaCommsButton.mute({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onToggle, + this.toggledLabel, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.microphone, + toggledIcon = ZetaIcons.microphone_off, + toggledType = ZetaCommsButtonType.off; + + /// Constructs video [ZetaCommsButton] + const ZetaCommsButton.video({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onToggle, + this.toggledLabel, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.video, + toggledIcon = ZetaIcons.video_off, + toggledType = ZetaCommsButtonType.off; + + /// Constructs transfer [ZetaCommsButton] + const ZetaCommsButton.transfer({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.forward, + onToggle = null, + toggledIcon = null, + toggledLabel = null, + toggledType = null; + + /// Constructs hold [ZetaCommsButton] + const ZetaCommsButton.hold({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onToggle, + this.toggledLabel, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.pause, + toggledIcon = ZetaIcons.pause, + toggledType = ZetaCommsButtonType.off; + + /// Constructs speaker [ZetaCommsButton] + const ZetaCommsButton.speaker({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onToggle, + this.toggledLabel, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.volume_up, + toggledIcon = ZetaIcons.volume_off, + toggledType = ZetaCommsButtonType.off; + + /// Constructs record [ZetaCommsButton] + const ZetaCommsButton.record({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onToggle, + this.toggledLabel, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.recording, + toggledIcon = ZetaIcons.stop, + toggledType = ZetaCommsButtonType.off; + + /// Constructs add [ZetaCommsButton] + const ZetaCommsButton.add({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.on, + icon = ZetaIcons.add_group, + onToggle = null, + toggledIcon = null, + toggledLabel = null, + toggledType = null; + + /// Constructs security [ZetaCommsButton] + const ZetaCommsButton.security({ + super.key, + this.label, + this.size = ZetaWidgetSize.medium, + this.onPressed, + this.focusNode, + this.semanticLabel, + }) : type = ZetaCommsButtonType.warning, + icon = ZetaIcons.alert_active, + onToggle = null, + toggledIcon = null, + toggledLabel = null, + toggledType = null; + + /// Comms button label + final String? label; + + /// Called when the comms button is toggled. + /// If null, the comms button will not be toggleable. + final ValueChanged? onToggle; + + /// Icon to display when the comms button is toggled. + final IconData? toggledIcon; + + /// Label to display when the comms button is toggled. + /// If null, the [label] will be used instead. + /// If both [label] and [toggledLabel] are null, the comms button will not display a label. + final String? toggledLabel; + + /// The coloring type of the comms button when toggled. + /// Defaults to [ZetaCommsButtonType.on]. + final ZetaCommsButtonType? toggledType; + + /// Called when the comms button is tapped or otherwise activated. + /// + /// {@macro zeta-widget-change-disable} + final VoidCallback? onPressed; + + /// The coloring type of the comms button + final ZetaCommsButtonType type; + + /// Size of the comms button. Defaults to [ZetaWidgetSize.medium]. + final ZetaWidgetSize size; + + /// Icon of comms button. Goes in centre of button. + final IconData? icon; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// The semantic label of the comms button. + /// + /// {@macro zeta-widget-semantic-label} + /// + /// If this property is null, [label] or [toggledLabel] will be used instead. + final String? semanticLabel; + + @override + State createState() => _ZetaCommsButtonState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('label', label)) + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(ObjectFlagProperty?>.has('onToggle', onToggle)) + ..add(ObjectFlagProperty.has('toggledIcon', toggledIcon)) + ..add(StringProperty('toggledLabel', toggledLabel)) + ..add(EnumProperty('type', type)) + ..add(EnumProperty('toggledType', toggledType)) + ..add(EnumProperty('size', size)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(StringProperty('semanticLabel', semanticLabel)); + } +} + +class _ZetaCommsButtonState extends State { + late ZetaCommsButtonType type; + bool isToggled = false; + + @override + void initState() { + super.initState(); + type = widget.type; + } + + @override + Widget build(BuildContext context) { + Color iconColor = _iconColor(context, type); + Color backgroundColor = _backgroundColor(context, type); + Color borderColor = _borderColor(context, type); + final iconSize = _iconSize(context); + final labelSize = _labelSize(context); + + return Semantics( + button: true, + label: widget.semanticLabel ?? (isToggled ? widget.toggledLabel : widget.label), + toggled: isToggled, + excludeSemantics: true, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton.filled( + constraints: BoxConstraints( + minWidth: _minConstraints(context), + minHeight: _minConstraints(context), + ), + iconSize: iconSize, + onPressed: () { + if (widget.onToggle != null) { + widget.onToggle!(isToggled); + setState(() { + isToggled = !isToggled; + if (widget.toggledType != null) { + type = isToggled ? widget.toggledType! : widget.type; + iconColor = _iconColor(context, type); + backgroundColor = _backgroundColor(context, type); + borderColor = _borderColor(context, type); + } + }); + } + widget.onPressed?.call(); + }, + isSelected: isToggled, + icon: Icon( + widget.icon, + semanticLabel: isToggled ? widget.toggledLabel : widget.label, + ), + selectedIcon: Icon(widget.toggledIcon), + style: ButtonStyle( + iconColor: WidgetStateProperty.all(iconColor), + backgroundColor: WidgetStateProperty.all(backgroundColor), + side: WidgetStateProperty.all( + BorderSide(color: borderColor, width: 2), + ), + ), + ), + if (widget.label != null) + Text( + isToggled + ? widget.toggledLabel != null + ? widget.toggledLabel! + : widget.label! + : widget.label!, + style: labelSize, + ), + ], + ), + ); + } + + /// Gets the border color based on the type + Color _borderColor(BuildContext context, ZetaCommsButtonType type) { + switch (type) { + case ZetaCommsButtonType.positive: + case ZetaCommsButtonType.negative: + return Zeta.of(context).colors.surfaceDefault; + case ZetaCommsButtonType.off: + case ZetaCommsButtonType.on: + return Zeta.of(context).colors.borderSubtle; + case ZetaCommsButtonType.warning: + return Zeta.of(context).colors.surfaceNegative; + } + } + + /// Gets the background color based on the type + Color _backgroundColor(BuildContext context, ZetaCommsButtonType type) { + switch (type) { + case ZetaCommsButtonType.positive: + return Zeta.of(context).colors.surfacePositive; + case ZetaCommsButtonType.negative: + return Zeta.of(context).colors.surfaceNegative; + case ZetaCommsButtonType.off: + return Zeta.of(context).colors.textDefault; + case ZetaCommsButtonType.on: + return Zeta.of(context).colors.textInverse; + case ZetaCommsButtonType.warning: + return Zeta.of(context).colors.surfaceDefault; + } + } + + /// Gets the icon color based on the type + Color _iconColor(BuildContext context, ZetaCommsButtonType type) { + switch (type) { + case ZetaCommsButtonType.positive: + case ZetaCommsButtonType.negative: + case ZetaCommsButtonType.off: + return Zeta.of(context).colors.iconInverse; + case ZetaCommsButtonType.on: + return Zeta.of(context).colors.iconDefault; + case ZetaCommsButtonType.warning: + return Zeta.of(context).colors.surfaceNegative; + } + } + + /// Gets the label size + TextStyle? _labelSize(BuildContext context) { + switch (widget.size) { + case ZetaWidgetSize.small: + return Theme.of(context).textTheme.labelSmall; + case ZetaWidgetSize.medium: + case ZetaWidgetSize.large: + return Theme.of(context).textTheme.labelLarge; + } + } + + /// Gets the icon size + double _iconSize(BuildContext context) { + switch (widget.size) { + case ZetaWidgetSize.small: + return Zeta.of(context).spacing.xl_2; + case ZetaWidgetSize.medium: + return Zeta.of(context).spacing.xl_4; + case ZetaWidgetSize.large: + return Zeta.of(context).spacing.xl_6; + } + } + + /// Gets the minimum constraints to set the size of the button + double _minConstraints(BuildContext context) { + switch (widget.size) { + case ZetaWidgetSize.large: + return Zeta.of(context).spacing.xl_10; + case ZetaWidgetSize.medium: + return Zeta.of(context).spacing.xl_9; + case ZetaWidgetSize.small: + return Zeta.of(context).spacing.xl_7; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('isToggled', isToggled)) + ..add(EnumProperty('type', type)); + } +} diff --git a/lib/src/components/components.dart b/lib/src/components/components.dart index 4fbd61ab..ab887229 100644 --- a/lib/src/components/components.dart +++ b/lib/src/components/components.dart @@ -16,6 +16,7 @@ export 'buttons/icon_button.dart'; export 'chat_item/chat_item.dart'; export 'checkbox/checkbox.dart' hide ZetaInternalCheckbox; export 'chips/chip.dart'; +export 'comms_button/comms_button.dart'; export 'contact_item/contact_item.dart'; export 'date_input/date_input.dart'; export 'dial_pad/dial_pad.dart'; diff --git a/lib/src/components/contact_item/contact_item.dart b/lib/src/components/contact_item/contact_item.dart index 452eb06d..72f7dd05 100644 --- a/lib/src/components/contact_item/contact_item.dart +++ b/lib/src/components/contact_item/contact_item.dart @@ -4,6 +4,10 @@ import '../../../zeta_flutter.dart'; /// A single row that contains avatar, title and subtitle. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=24828-46282&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/contact-item class ZetaContactItem extends ZetaStatelessWidget { /// Constructs [ZetaContactItem]. const ZetaContactItem({ diff --git a/lib/src/components/date_input/date_input.dart b/lib/src/components/date_input/date_input.dart index 73b60599..fabad949 100644 --- a/lib/src/components/date_input/date_input.dart +++ b/lib/src/components/date_input/date_input.dart @@ -13,6 +13,10 @@ import '../text_input/internal_text_input.dart'; /// /// Can be used and validated the same way as a [TextFormField]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-36&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/date-input class ZetaDateInput extends ZetaFormField { /// Creates a new [ZetaDateInput] ZetaDateInput({ @@ -43,6 +47,7 @@ class ZetaDateInput extends ZetaFormField { return InternalTextInput( label: label, + constrained: true, hintText: hintText, errorText: field.errorText ?? errorText, size: size, diff --git a/lib/src/components/dial_pad/dial_pad.dart b/lib/src/components/dial_pad/dial_pad.dart index c0245ef8..992abaee 100644 --- a/lib/src/components/dial_pad/dial_pad.dart +++ b/lib/src/components/dial_pad/dial_pad.dart @@ -6,6 +6,10 @@ import '../../../zeta_flutter.dart'; /// Dial pad gives the user the ability to dial a number and start a call. It also has a quick dial security action and a delete entry action. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21156-16370&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/dial-pad class ZetaDialPad extends StatefulWidget { /// Constructs a [ZetaDialPad]. const ZetaDialPad({ diff --git a/lib/src/components/dialog/dialog.dart b/lib/src/components/dialog/dialog.dart index b985439b..7e88b776 100644 --- a/lib/src/components/dialog/dialog.dart +++ b/lib/src/components/dialog/dialog.dart @@ -47,6 +47,12 @@ Future showZetaDialog( ), ); +/// The Zeta Dialog component. +/// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=23954-93337&node-type=frame&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/dialog class _ZetaDialog extends ZetaStatelessWidget { const _ZetaDialog({ this.headerAlignment = ZetaDialogHeaderAlignment.center, diff --git a/lib/src/components/dropdown/dropdown.dart b/lib/src/components/dropdown/dropdown.dart index ed86be48..d4309bc7 100644 --- a/lib/src/components/dropdown/dropdown.dart +++ b/lib/src/components/dropdown/dropdown.dart @@ -27,6 +27,10 @@ class _DropdownControllerImpl implements ZetaDropdownController { /// An item used in a [ZetaDropdown] or a [ZetaSelectInput]. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=22391-10146 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/dropdown class ZetaDropdownItem { /// Creates a new [ZetaDropdownItem] ZetaDropdownItem({ @@ -51,6 +55,10 @@ class ZetaDropdownItem { /// Class for [ZetaDropdown] /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=22391-10146 +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/dropdown class ZetaDropdown extends ZetaStatefulWidget { /// Creates a new [ZetaDropdown]. const ZetaDropdown({ diff --git a/lib/src/components/fabs/fab.dart b/lib/src/components/fabs/fab.dart index aac834c6..ce6dff67 100644 --- a/lib/src/components/fabs/fab.dart +++ b/lib/src/components/fabs/fab.dart @@ -27,6 +27,10 @@ enum ZetaFabSize { /// Zeta Floating Action Button Component. /// {@category Components} +/// +/// Figma: https://www.figma.com/file/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21816-4283&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/buttons/floating-action-button class ZetaFAB extends StatefulWidget { ///Constructs [ZetaFAB]. const ZetaFAB({ @@ -116,62 +120,79 @@ class _ZetaFABState extends State { final Color backgroundColorHover = widget.type.hoverColor(colors); final Color backgroundColorSelected = widget.type.selectedColor(colors); - return FilledButton( - onPressed: widget.onPressed, - focusNode: widget.focusNode, - style: ButtonStyle( - padding: const WidgetStatePropertyAll(EdgeInsets.zero), - shape: WidgetStatePropertyAll( - widget.shape.buttonShape(isExpanded: widget.expanded, size: widget.size, context: context), - ), - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return colors.stateDisabledDisabled; - } - if (states.contains(WidgetState.pressed)) { - return backgroundColorSelected; - } - if (states.contains(WidgetState.hovered)) { - return backgroundColorHover; - } - return backgroundColor; - }), - side: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.focused)) { - // TODO(UX-1134): This removes a defualt border when focused, rather than adding a second border when focused. - return BorderSide(color: Zeta.of(context).colors.borderPrimary, width: ZetaBorders.medium); - } - return null; - }, - ), - ), - child: AnimatedContainer( - duration: ZetaAnimationLength.normal, - child: Padding( - padding: widget.expanded - ? EdgeInsets.symmetric( - horizontal: Zeta.of(context).spacing.large, - vertical: Zeta.of(context).spacing.medium, - ) - : EdgeInsets.all(widget.size.padding(context)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ZetaIcon( - widget.icon, - size: widget.size.iconSize(context), - color: foregroundColor, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton( + onPressed: widget.onPressed, + focusNode: widget.focusNode, + style: ButtonStyle( + padding: const WidgetStatePropertyAll(EdgeInsets.zero), + shape: WidgetStatePropertyAll( + widget.shape.buttonShape(isExpanded: widget.expanded, size: widget.size, context: context), + ), + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return colors.stateDisabledDisabled; + } + if (states.contains(WidgetState.pressed)) { + return backgroundColorSelected; + } + if (states.contains(WidgetState.hovered)) { + return backgroundColorHover; + } + return backgroundColor; + }), + side: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.focused)) { + // TODO(UX-1134): This removes a defualt border when focused, rather than adding a second border when focused. + return BorderSide(color: Zeta.of(context).colors.borderPrimary, width: ZetaBorders.medium); + } + return null; + }, + ), + ), + child: AnimatedContainer( + duration: ZetaAnimationLength.normal, + child: Padding( + padding: widget.expanded + ? EdgeInsets.symmetric( + horizontal: Zeta.of(context).spacing.large, + vertical: Zeta.of(context).spacing.medium, + ) + : EdgeInsets.all(widget.size.padding(context)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ZetaIcon(widget.icon, size: widget.size.iconSize(context), color: foregroundColor), + if (widget.expanded && widget.label != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.label!, + style: ZetaTextStyles.labelLarge.apply(color: foregroundColor), + ), + ], + ), + ].divide(SizedBox(width: Zeta.of(context).spacing.small)).toList(), ), - if (widget.expanded && widget.label != null) - Row( - mainAxisSize: MainAxisSize.min, - children: [Text(widget.label!, style: ZetaTextStyles.labelLarge.apply(color: foregroundColor))], - ), - ].divide(SizedBox(width: Zeta.of(context).spacing.small)).toList(), + ), ), ), - ), + if (!widget.expanded && widget.label != null) + Container( + margin: EdgeInsets.only(top: Zeta.of(context).spacing.minimum), + width: 100, // TODODE: Is there a better way to do this? + alignment: Alignment.center, + child: Text( + widget.label!, + style: ZetaTextStyles.bodyMedium, + textAlign: TextAlign.center, + ), + ), + ], ); } } diff --git a/lib/src/components/filter_selection/filter_selection.dart b/lib/src/components/filter_selection/filter_selection.dart index 34e0a477..bd14fb62 100644 --- a/lib/src/components/filter_selection/filter_selection.dart +++ b/lib/src/components/filter_selection/filter_selection.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Component [ZetaFilterSelection] /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=24607-737&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/filter-selection class ZetaFilterSelection extends ZetaStatelessWidget { /// Constructor for the component [ZetaFilterSelection] const ZetaFilterSelection({ diff --git a/lib/src/components/global_header/global_header.dart b/lib/src/components/global_header/global_header.dart index cb87730f..1578d224 100644 --- a/lib/src/components/global_header/global_header.dart +++ b/lib/src/components/global_header/global_header.dart @@ -6,6 +6,10 @@ import '../../../zeta_flutter.dart'; /// Global header component /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=1120-26358&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/global-header class ZetaGlobalHeader extends ZetaStatefulWidget { /// Constructor for [ZetaGlobalHeader] const ZetaGlobalHeader({ diff --git a/lib/src/components/global_header/header_tab_item.dart b/lib/src/components/global_header/header_tab_item.dart index 8c482e05..14afecf3 100644 --- a/lib/src/components/global_header/header_tab_item.dart +++ b/lib/src/components/global_header/header_tab_item.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Tab item to be used in [ZetaGlobalHeader]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=1120-26358&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/global-header class ZetaGlobalHeaderItem extends ZetaStatefulWidget { ///Constructor for tab item const ZetaGlobalHeaderItem({ diff --git a/lib/src/components/icon/icon.dart b/lib/src/components/icon/icon.dart index 15e16772..95cf997d 100644 --- a/lib/src/components/icon/icon.dart +++ b/lib/src/components/icon/icon.dart @@ -13,6 +13,8 @@ import '../../../zeta_flutter.dart'; /// Custom wrapper for [Icon] that applies the Zeta icon family. /// {@category Components} +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=assets/icons class ZetaIcon extends ZetaStatelessWidget { /// Constructs a [ZetaIcon]. const ZetaIcon( diff --git a/lib/src/components/in_page_banner/in_page_banner.dart b/lib/src/components/in_page_banner/in_page_banner.dart index 827a570b..ba302d4f 100644 --- a/lib/src/components/in_page_banner/in_page_banner.dart +++ b/lib/src/components/in_page_banner/in_page_banner.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// In page banners display an important, succinct message, and may provide actions for users to address. Banners should be displayed at the top of the screen,below a top app bar. Only one banner should be shown at a time. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=21156-20085&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/in-page-banners class ZetaInPageBanner extends ZetaStatelessWidget { /// Constructs [ZetaInPageBanner]. const ZetaInPageBanner({ diff --git a/lib/src/components/list_item/dropdown_list_item.dart b/lib/src/components/list_item/dropdown_list_item.dart index 6bace75b..bcfbac5c 100644 --- a/lib/src/components/list_item/dropdown_list_item.dart +++ b/lib/src/components/list_item/dropdown_list_item.dart @@ -6,6 +6,10 @@ import 'list_scope.dart'; /// An expandable list item containing other [ZetaListItem]s within it. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-17&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/dropdown-list-item class ZetaDropdownListItem extends ZetaStatefulWidget { /// Creates a new [ZetaDropdownListItem] const ZetaDropdownListItem({ diff --git a/lib/src/components/list_item/list_item.dart b/lib/src/components/list_item/list_item.dart index 7beb9973..01d69254 100644 --- a/lib/src/components/list_item/list_item.dart +++ b/lib/src/components/list_item/list_item.dart @@ -9,6 +9,10 @@ import 'list_scope.dart'; /// /// This wraps [ListView.builder] so it needs to be used in a widget with a constrained height. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-17&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/list-item class ZetaList extends ZetaStatelessWidget { /// Creates a new [ZetaList]. const ZetaList({ @@ -50,6 +54,10 @@ class ZetaList extends ZetaStatelessWidget { /// /// To create list items with a [ZetaSwitch], [ZetaCheckbox], or [ZetaRadio], use the [ZetaListItem.toggle], [ZetaListItem.checkbox] or the [ZetaListItem.radio] named constructors respectively. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-17&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/list-item class ZetaListItem extends ZetaStatelessWidget { /// Creates a [ZetaListItem]. const ZetaListItem({ diff --git a/lib/src/components/list_item/notification_list_item.dart b/lib/src/components/list_item/notification_list_item.dart index 7d86d7df..2a687839 100644 --- a/lib/src/components/list_item/notification_list_item.dart +++ b/lib/src/components/list_item/notification_list_item.dart @@ -6,6 +6,10 @@ import '../../../zeta_flutter.dart'; /// Notification list items are used in notification lists. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=25043-100110&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/list-items/notification-list-item class ZetaNotificationListItem extends ZetaStatelessWidget { /// Constructor for [ZetaNotificationListItem] const ZetaNotificationListItem({ diff --git a/lib/src/components/navigation bar/navigation_bar.dart b/lib/src/components/navigation bar/navigation_bar.dart index 849e9dd9..fb699305 100644 --- a/lib/src/components/navigation bar/navigation_bar.dart +++ b/lib/src/components/navigation bar/navigation_bar.dart @@ -26,6 +26,10 @@ class ZetaNavigationBarItem { /// Navigation Bars (Bottom navigation) allow movement between primary destinations in an app. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=1052-24751&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/navigation-bar class ZetaNavigationBar extends ZetaStatelessWidget { /// Creates a new [ZetaNavigationBar]. const ZetaNavigationBar({ diff --git a/lib/src/components/navigation_rail/navigation_rail.dart b/lib/src/components/navigation_rail/navigation_rail.dart index ff9bb193..5f238949 100644 --- a/lib/src/components/navigation_rail/navigation_rail.dart +++ b/lib/src/components/navigation_rail/navigation_rail.dart @@ -9,6 +9,10 @@ import '../../../zeta_flutter.dart'; /// navigation item. /// Should be used with [ZetaNavigationRailItem]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-43&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/navigation-rail class ZetaNavigationRail extends ZetaStatelessWidget { /// Constructor for [ZetaNavigationRail]. const ZetaNavigationRail({ diff --git a/lib/src/components/pagination/pagination.dart b/lib/src/components/pagination/pagination.dart index 047adbdf..872fdcc3 100644 --- a/lib/src/components/pagination/pagination.dart +++ b/lib/src/components/pagination/pagination.dart @@ -16,6 +16,10 @@ enum ZetaPaginationType { /// Pagination is used to switch between pages. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-24&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/pagination class ZetaPagination extends ZetaStatefulWidget { /// Creates a new [ZetaPagination] const ZetaPagination({ diff --git a/lib/src/components/password/password_input.dart b/lib/src/components/password/password_input.dart index 2fa85a9f..152de9f8 100644 --- a/lib/src/components/password/password_input.dart +++ b/lib/src/components/password/password_input.dart @@ -7,6 +7,10 @@ import '../text_input/internal_text_input.dart'; /// Zeta Password Input /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=948-13002&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/password-input class ZetaPasswordInput extends ZetaTextFormField { ///Constructs [ZetaPasswordInput] ZetaPasswordInput({ @@ -46,6 +50,7 @@ class ZetaPasswordInput extends ZetaTextFormField { requirementLevel: requirementLevel, errorText: field.errorText ?? errorText, onSubmit: onSubmit, + constrained: true, disabled: disabled, obscureText: state._obscureText, semanticLabel: semanticLabel, diff --git a/lib/src/components/phone_input/phone_input.dart b/lib/src/components/phone_input/phone_input.dart index 4e6764bf..8dff36ad 100644 --- a/lib/src/components/phone_input/phone_input.dart +++ b/lib/src/components/phone_input/phone_input.dart @@ -26,6 +26,10 @@ class PhoneNumber { /// ZetaPhoneInput allows entering phone numbers. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=916-10934&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/phone-input class ZetaPhoneInput extends ZetaFormField { /// Constructor for [ZetaPhoneInput]. ZetaPhoneInput({ diff --git a/lib/src/components/progress/progress_bar.dart b/lib/src/components/progress/progress_bar.dart index cc83675e..92c05987 100644 --- a/lib/src/components/progress/progress_bar.dart +++ b/lib/src/components/progress/progress_bar.dart @@ -19,6 +19,10 @@ enum ZetaProgressBarType { /// /// Linear progress bar. Uses progress percentage value to fill bar. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-22&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/progress/bar class ZetaProgressBar extends ZetaProgress { ///Constructor for [ZetaProgressBar]. const ZetaProgressBar({ diff --git a/lib/src/components/progress/progress_circle.dart b/lib/src/components/progress/progress_circle.dart index 6dbd1b23..1dc684cd 100644 --- a/lib/src/components/progress/progress_circle.dart +++ b/lib/src/components/progress/progress_circle.dart @@ -25,6 +25,10 @@ enum ZetaCircleSizes { /// Progress indicators express an unspecified wait time or display the length of a process. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-22&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/progress/circle class ZetaProgressCircle extends ZetaProgress { /// Constructor for [ZetaProgressCircle] const ZetaProgressCircle({ diff --git a/lib/src/components/radio/radio.dart b/lib/src/components/radio/radio.dart index 515c84b8..75cde7d9 100644 --- a/lib/src/components/radio/radio.dart +++ b/lib/src/components/radio/radio.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Radio buttons are used for mutually exclusive choices, not for multiple choices. Only one radio button can be selected at a time. When a user chooses a new item, the previous choice is automatically deselected. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-7&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/radio-button class ZetaRadio extends ZetaStatefulWidget { /// Constructor for [ZetaRadio]. const ZetaRadio({ diff --git a/lib/src/components/screen_header_bar/screen_header_bar.dart b/lib/src/components/screen_header_bar/screen_header_bar.dart index 6411d8c0..544eefe6 100644 --- a/lib/src/components/screen_header_bar/screen_header_bar.dart +++ b/lib/src/components/screen_header_bar/screen_header_bar.dart @@ -4,6 +4,10 @@ import '../../../zeta_flutter.dart'; /// [ZetaScreenHeaderBar] /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=24601-6781&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/screen-header-bar class ZetaScreenHeaderBar extends ZetaStatelessWidget { /// Constructor for [ZetaScreenHeaderBar]. const ZetaScreenHeaderBar({ diff --git a/lib/src/components/search_bar/search_bar.dart b/lib/src/components/search_bar/search_bar.dart index a9a5f371..8735983a 100644 --- a/lib/src/components/search_bar/search_bar.dart +++ b/lib/src/components/search_bar/search_bar.dart @@ -3,9 +3,14 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; import '../buttons/input_icon_button.dart'; +import '../text_input/internal_text_input.dart'; /// ZetaSearchBar provides input field for searching. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=875-17463&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/search-bar class ZetaSearchBar extends ZetaTextFormField { /// Constructor for [ZetaSearchBar]. ZetaSearchBar({ @@ -21,8 +26,9 @@ class ZetaSearchBar extends ZetaTextFormField { super.initialValue, this.size = ZetaWidgetSize.medium, this.shape = ZetaWidgetBorder.rounded, - @Deprecated('Use hintText instead. ' 'deprecated as of 0.15.0') String? hint, - this.hintText, + @Deprecated('hint has been removed. ' 'deprecated as of 0.15.0') String? hint, + @Deprecated('Use placeholder instead. ' 'deprecated as of 0.16.0') String? hintText, + this.placeholder, this.onSpeechToText, this.showSpeechToText = true, @Deprecated('Use disabled instead. ' 'enabled is deprecated as of 0.11.0') bool enabled = true, @@ -45,130 +51,74 @@ class ZetaSearchBar extends ZetaTextFormField { _ => zeta.radius.none, }; - final defaultInputBorder = OutlineInputBorder( - borderRadius: borderRadius, - borderSide: BorderSide(color: zeta.colors.borderDefault), - ); - - final focusedBorder = defaultInputBorder.copyWith( - borderSide: BorderSide( - color: zeta.colors.borderPrimary, - width: zeta.spacing.minimum, - ), - ); - - final disabledborder = defaultInputBorder.copyWith( - borderSide: BorderSide(color: zeta.colors.borderDisabled), - ); - late final double iconSize; - late final double padding; switch (size) { case ZetaWidgetSize.large: iconSize = zeta.spacing.xl_2; - padding = zeta.spacing.medium; case ZetaWidgetSize.medium: iconSize = zeta.spacing.xl; - padding = zeta.spacing.small; case ZetaWidgetSize.small: iconSize = zeta.spacing.large; - padding = zeta.spacing.minimum; } return ZetaRoundedScope( rounded: shape != ZetaWidgetBorder.sharp, child: Semantics( excludeSemantics: disabled, - label: disabled ? hintText ?? 'Search' : null, // TODO(UX-1003): Localize + label: disabled ? placeholder ?? 'Search' : null, // TODO(UX-1003): Localize enabled: disabled ? false : null, - child: TextFormField( + child: InternalTextInput( focusNode: focusNode, - enabled: !disabled, + size: size, + disabled: disabled, + constrained: true, + borderRadius: borderRadius, controller: state.effectiveController, keyboardType: TextInputType.text, textInputAction: textInputAction, - onFieldSubmitted: onFieldSubmitted, - onChanged: state.onChange, - style: ZetaTextStyles.bodyMedium, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric( - horizontal: 10, - vertical: padding, - ), - hintText: hintText ?? 'Search', // TODO(UX-1003): Localize - hintStyle: ZetaTextStyles.bodyMedium.copyWith( - color: !disabled ? zeta.colors.mainSubtle : zeta.colors.mainDisabled, - ), - prefixIcon: Padding( - padding: EdgeInsets.only(left: zeta.spacing.medium, right: zeta.spacing.small), - child: ZetaIcon( - ZetaIcons.search, - color: !disabled ? zeta.colors.mainSubtle : zeta.colors.mainDisabled, - size: iconSize, - ), - ), - prefixIconConstraints: BoxConstraints( - minHeight: zeta.spacing.xl_2, - minWidth: zeta.spacing.xl_2, - ), - suffixIcon: IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.effectiveController.text.isNotEmpty && !disabled) ...[ - Semantics( - container: true, - button: true, - excludeSemantics: true, - label: clearSemanticLabel, - child: InputIconButton( - icon: ZetaIcons.cancel, - onTap: () => state.onChange(''), - disabled: disabled, - size: size, - color: zeta.colors.mainSubtle, - key: const ValueKey('search-clear-btn'), - ), - ), - if (showSpeechToText) - SizedBox( - height: iconSize, - child: VerticalDivider( - color: zeta.colors.mainSubtle, - width: 5, - thickness: 1, - ), - ), - ], - if (showSpeechToText) - Semantics( - label: microphoneSemanticLabel, - container: true, - button: true, - excludeSemantics: true, - child: InputIconButton( - icon: ZetaIcons.microphone, - onTap: state.onSpeechToText, - key: const ValueKey('speech-to-text-btn'), - disabled: disabled, - size: size, - color: zeta.colors.mainDefault, - ), + placeholder: placeholder ?? 'Search', // TODO(UX-1003): Localize + onSubmit: onFieldSubmitted, + onChange: state.onChange, + prefix: ZetaIcon( + ZetaIcons.search, + color: !disabled ? zeta.colors.mainSubtle : zeta.colors.mainDisabled, + size: iconSize, + ), + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state.effectiveController.text.isNotEmpty && !disabled) ...[ + InputIconButton( + icon: ZetaIcons.cancel, + onTap: () => state.onChange(''), + disabled: disabled, + size: size, + semanticLabel: clearSemanticLabel, + color: zeta.colors.mainSubtle, + key: const ValueKey('search-clear-btn'), + ), + if (showSpeechToText) + SizedBox( + height: iconSize, + child: VerticalDivider( + color: zeta.colors.mainSubtle, + width: 5, + thickness: 1, ), - ], - ), - ), - suffixIconConstraints: BoxConstraints( - minHeight: zeta.spacing.xl_2, - minWidth: zeta.spacing.xl_2, - ), - filled: !disabled ? null : true, - fillColor: !disabled ? null : zeta.colors.surfaceDisabled, - enabledBorder: defaultInputBorder, - focusedBorder: focusedBorder, - disabledBorder: disabledborder, + ), + ], + if (showSpeechToText) + InputIconButton( + icon: ZetaIcons.microphone, + onTap: state.onSpeechToText, + key: const ValueKey('speech-to-text-btn'), + disabled: disabled, + semanticLabel: microphoneSemanticLabel, + size: size, + color: zeta.colors.mainDefault, + ), + ], ), ), ), @@ -180,14 +130,13 @@ class ZetaSearchBar extends ZetaTextFormField { /// Default is [ZetaWidgetSize.medium] final ZetaWidgetSize size; + /// Placeholder text for the search field. + final String? placeholder; + /// Determines the shape of the input field. /// Default is [ZetaWidgetBorder.rounded] final ZetaWidgetBorder shape; - /// If provided, displays a hint inside the input field. - /// Default is `Search`. - final String? hintText; - /// The type of action button to use for the keyboard. final TextInputAction? textInputAction; @@ -220,14 +169,14 @@ class ZetaSearchBar extends ZetaTextFormField { properties ..add(EnumProperty('size', size)) ..add(EnumProperty('shape', shape)) - ..add(StringProperty('hintText', hintText)) ..add(StringProperty('initialValue', initialValue)) ..add(ObjectFlagProperty.has('onSpeechToText', onSpeechToText)) ..add(DiagnosticsProperty('showSpeechToText', showSpeechToText)) ..add(DiagnosticsProperty('focusNode', focusNode)) ..add(EnumProperty('textInputAction', textInputAction)) ..add(StringProperty('microphoneSemanticLabel', microphoneSemanticLabel)) - ..add(StringProperty('clearSemanticLabel', clearSemanticLabel)); + ..add(StringProperty('clearSemanticLabel', clearSemanticLabel)) + ..add(StringProperty('placeholder', placeholder)); } } diff --git a/lib/src/components/segmented_control/segmented_control.dart b/lib/src/components/segmented_control/segmented_control.dart index 5b849bac..24355f12 100644 --- a/lib/src/components/segmented_control/segmented_control.dart +++ b/lib/src/components/segmented_control/segmented_control.dart @@ -9,6 +9,10 @@ import '../../../zeta_flutter.dart'; /// functions as a mutually exclusive button. Like buttons, segments can contain /// text or images. Segmented controls are often used to display different views. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=1046-20148&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/segmented-control class ZetaSegmentedControl extends ZetaStatefulWidget { /// Constructs an segmented control bar. const ZetaSegmentedControl({ diff --git a/lib/src/components/select_input/select_input.dart b/lib/src/components/select_input/select_input.dart index 4d9fe513..df9401bf 100644 --- a/lib/src/components/select_input/select_input.dart +++ b/lib/src/components/select_input/select_input.dart @@ -10,6 +10,10 @@ import '../text_input/internal_text_input.dart'; /// Class for [ZetaSelectInput]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-39&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/select-input class ZetaSelectInput extends ZetaFormField { ///Constructor of [ZetaSelectInput] ZetaSelectInput({ @@ -50,6 +54,7 @@ class ZetaSelectInput extends ZetaFormField { builder: (context, _, controller) { return InternalTextInput( size: size, + constrained: true, requirementLevel: requirementLevel, disabled: disabled, controller: state.inputController, diff --git a/lib/src/components/slider/slider.dart b/lib/src/components/slider/slider.dart index 6288ad9a..9df25cb1 100644 --- a/lib/src/components/slider/slider.dart +++ b/lib/src/components/slider/slider.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Slider component with customized styling. /// {@category Components} +/// +/// Figma: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/slider +/// +/// Widgetbook: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=875-11860&node-type=canvas&m=dev class ZetaSlider extends ZetaStatefulWidget { /// Default constructor for [ZetaSlider] const ZetaSlider({ diff --git a/lib/src/components/snack_bar/snack_bar.dart b/lib/src/components/snack_bar/snack_bar.dart index 8e00a522..f321bdb3 100644 --- a/lib/src/components/snack_bar/snack_bar.dart +++ b/lib/src/components/snack_bar/snack_bar.dart @@ -34,6 +34,10 @@ enum ZetaSnackBarType { /// /// Different styles can be applied to [ZetaSnackBar] with [ZetaSnackBarType]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-13&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/snack-bar class ZetaSnackBar extends SnackBar { /// Sets basic styles for the [SnackBar]. ZetaSnackBar({ diff --git a/lib/src/components/stepper/stepper.dart b/lib/src/components/stepper/stepper.dart index 0dd50501..cfa6251b 100644 --- a/lib/src/components/stepper/stepper.dart +++ b/lib/src/components/stepper/stepper.dart @@ -7,6 +7,10 @@ import '../../../zeta_flutter.dart'; /// requires the completion of another one, or where multiple steps need to be /// completed in order to submit the whole form. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=3420-67488&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/stepper class ZetaStepper extends ZetaStatefulWidget { /// Creates a stepper from a list of steps. /// diff --git a/lib/src/components/stepper_input/stepper_input.dart b/lib/src/components/stepper_input/stepper_input.dart index 3bc140af..34e7e843 100644 --- a/lib/src/components/stepper_input/stepper_input.dart +++ b/lib/src/components/stepper_input/stepper_input.dart @@ -17,6 +17,10 @@ enum ZetaStepperInputSize { /// allows users to input a number or value simply by clicking the plus and /// minus buttons. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-47&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/stepper-input class ZetaStepperInput extends ZetaStatefulWidget { /// Creates a new [ZetaStepperInput] const ZetaStepperInput({ diff --git a/lib/src/components/switch/zeta_switch.dart b/lib/src/components/switch/zeta_switch.dart index 6f85ca96..76d2433a 100644 --- a/lib/src/components/switch/zeta_switch.dart +++ b/lib/src/components/switch/zeta_switch.dart @@ -26,6 +26,11 @@ enum ZetaSwitchType { /// /// Switch has styles for Android, iOS and Web. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-41&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/switch +/// // TODO(UX-1137): Add web icon support. class ZetaSwitch extends StatelessWidget { /// Constructor for [ZetaSwitch]. diff --git a/lib/src/components/tabs/tab.dart b/lib/src/components/tabs/tab.dart index 51fe5052..759d4aac 100644 --- a/lib/src/components/tabs/tab.dart +++ b/lib/src/components/tabs/tab.dart @@ -4,6 +4,10 @@ import '../../../zeta_flutter.dart'; /// Defines how the bounds of the selected tab indicator are computed. /// Intended to be used with [ZetaTabBar]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-18&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/tabs class ZetaTab extends Tab { /// Creates a Zeta Design tab bar. ZetaTab({ diff --git a/lib/src/components/tabs/tab_bar.dart b/lib/src/components/tabs/tab_bar.dart index bfa88804..b4d70ce2 100644 --- a/lib/src/components/tabs/tab_bar.dart +++ b/lib/src/components/tabs/tab_bar.dart @@ -3,6 +3,10 @@ import '../../../zeta_flutter.dart'; /// A Zeta Design primary tab bar. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-18&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/tabs class ZetaTabBar extends TabBar { /// Creates a Zeta Design primary tab bar. ZetaTabBar({ diff --git a/lib/src/components/text_input/internal_text_input.dart b/lib/src/components/text_input/internal_text_input.dart index 015f047b..dbddc3bb 100644 --- a/lib/src/components/text_input/internal_text_input.dart +++ b/lib/src/components/text_input/internal_text_input.dart @@ -36,6 +36,8 @@ class InternalTextInput extends ZetaStatefulWidget { this.externalPrefix, this.semanticLabel, this.borderRadius, + this.textInputAction, + this.constrained = false, }) : requirementLevel = requirementLevel ?? ZetaFormFieldRequirement.none, assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'), assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'); @@ -130,6 +132,12 @@ class InternalTextInput extends ZetaStatefulWidget { /// The widget displayed before the input. final Widget? externalPrefix; + /// The action to take when the user submits the input. + final TextInputAction? textInputAction; + + /// Determines if the prefix and suffix should be constrained. + final bool constrained; + @override State createState() => InternalTextInputState(); @override @@ -156,7 +164,9 @@ class InternalTextInput extends ZetaStatefulWidget { ..add(DiagnosticsProperty('keyboardType', keyboardType)) ..add(DiagnosticsProperty('focusNode', focusNode)) ..add(DiagnosticsProperty('borderRadius', borderRadius)) - ..add(StringProperty('semanticLabel', semanticLabel)); + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(EnumProperty('textInputAction', textInputAction)) + ..add(DiagnosticsProperty('constrained', constrained)); } } @@ -197,7 +207,7 @@ class InternalTextInputState extends State { case ZetaWidgetSize.medium: return EdgeInsets.symmetric( horizontal: Zeta.of(context).spacing.medium, - vertical: Zeta.of(context).spacing.small, + vertical: Zeta.of(context).spacing.medium, ); } } @@ -221,7 +231,7 @@ class InternalTextInputState extends State { width = Zeta.of(context).spacing.xl_6; height = Zeta.of(context).spacing.xl_6; case ZetaWidgetSize.small: - width = Zeta.of(context).spacing.xl_6; + width = Zeta.of(context).spacing.xl_4; height = Zeta.of(context).spacing.xl_4; } return BoxConstraints( @@ -279,6 +289,7 @@ class InternalTextInputState extends State { OutlineInputBorder _focusedBorder(bool rounded) => _baseBorder(rounded).copyWith( borderSide: BorderSide(color: _colors.borderPrimary, width: ZetaBorders.medium), ); + OutlineInputBorder _errorBorder(bool rounded) => _baseBorder(rounded).copyWith( borderSide: BorderSide(color: _colors.borderNegative, width: ZetaBorders.medium), ); @@ -337,6 +348,7 @@ class InternalTextInputState extends State { onSubmitted: widget.onSubmit, style: _baseTextStyle, cursorErrorColor: _colors.mainNegative, + textInputAction: widget.textInputAction, obscureText: widget.obscureText, focusNode: widget.focusNode, decoration: InputDecoration( @@ -344,9 +356,11 @@ class InternalTextInputState extends State { contentPadding: _contentPadding, filled: true, prefixIcon: _prefix, - prefixIconConstraints: widget.prefixText != null ? _affixConstraints : null, + prefixIconConstraints: + widget.prefixText != null || widget.constrained ? _affixConstraints : null, suffixIcon: _suffix, - suffixIconConstraints: widget.suffixText != null ? _affixConstraints : null, + suffixIconConstraints: + widget.suffixText != null || widget.constrained ? _affixConstraints : null, focusColor: _backgroundColor, hoverColor: _backgroundColor, fillColor: _backgroundColor, @@ -357,7 +371,9 @@ class InternalTextInputState extends State { errorBorder: widget.disabled ? _baseBorder(rounded) : _errorBorder(rounded), hintText: widget.placeholder, errorText: widget.errorText, - hintStyle: _baseTextStyle, + hintStyle: _baseTextStyle.copyWith( + color: widget.disabled ? _colors.mainDisabled : _colors.mainSubtle, + ), errorStyle: const TextStyle(height: 0.001, color: Colors.transparent), ), ), diff --git a/lib/src/components/text_input/text_input.dart b/lib/src/components/text_input/text_input.dart index 2cd537da..946193d4 100644 --- a/lib/src/components/text_input/text_input.dart +++ b/lib/src/components/text_input/text_input.dart @@ -11,6 +11,10 @@ import 'internal_text_input.dart'; /// To show error messages on the text input, use the [validator]. The string returned from this function will be displayed as the error message. /// Error messages can also be managed outside the text input by setting [errorText]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-38&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/text-input class ZetaTextInput extends ZetaTextFormField { /// Creates a new [ZetaTextInput] ZetaTextInput({ diff --git a/lib/src/components/time_input/time_input.dart b/lib/src/components/time_input/time_input.dart index a587a200..cbe22a76 100644 --- a/lib/src/components/time_input/time_input.dart +++ b/lib/src/components/time_input/time_input.dart @@ -15,6 +15,10 @@ const _max12HrValue = 12; /// /// Can be used and validated the same way as a [TextFormField]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=724-6821&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/time-input class ZetaTimeInput extends ZetaFormField { /// Creates a new [ZetaTimeInput] ZetaTimeInput({ @@ -45,6 +49,7 @@ class ZetaTimeInput extends ZetaFormField { return InternalTextInput( label: label, hintText: hintText, + constrained: true, errorText: field.errorText ?? errorText, size: size, placeholder: state.timeFormat, diff --git a/lib/src/components/tooltip/tooltip.dart b/lib/src/components/tooltip/tooltip.dart index 063cb895..17eced21 100644 --- a/lib/src/components/tooltip/tooltip.dart +++ b/lib/src/components/tooltip/tooltip.dart @@ -23,6 +23,10 @@ enum ZetaTooltipArrowDirection { /// Tooltips display informative text when users hover over, focus on, or tap an element. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-16&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/tooltip class ZetaTooltip extends ZetaStatelessWidget { /// Constructor for [ZetaTooltip]. const ZetaTooltip({ diff --git a/lib/src/components/top_app_bar/extended_top_app_bar.dart b/lib/src/components/top_app_bar/extended_top_app_bar.dart index 96178fb6..c24673c1 100644 --- a/lib/src/components/top_app_bar/extended_top_app_bar.dart +++ b/lib/src/components/top_app_bar/extended_top_app_bar.dart @@ -4,6 +4,10 @@ import '../../../zeta_flutter.dart'; /// Delegate for creating an extended app bar, that grows and shrinks when scrolling. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-37&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/top-app-bar/extended class ZetaExtendedAppBarDelegate extends SliverPersistentHeaderDelegate { /// Constructs a [ZetaExtendedAppBarDelegate]. ZetaExtendedAppBarDelegate({ diff --git a/lib/src/components/top_app_bar/search_top_app_bar.dart b/lib/src/components/top_app_bar/search_top_app_bar.dart index cec14bb8..25d093c2 100644 --- a/lib/src/components/top_app_bar/search_top_app_bar.dart +++ b/lib/src/components/top_app_bar/search_top_app_bar.dart @@ -5,6 +5,10 @@ import '../../../zeta_flutter.dart'; /// Creates a search field used on a [ZetaTopAppBar]. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-37&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/top-app-bar/search class ZetaTopAppBarSearchField extends ZetaStatefulWidget { /// Constructs a [ZetaTopAppBarSearchField]. const ZetaTopAppBarSearchField({ diff --git a/lib/src/components/top_app_bar/top_app_bar.dart b/lib/src/components/top_app_bar/top_app_bar.dart index da894baf..3d51b9a4 100644 --- a/lib/src/components/top_app_bar/top_app_bar.dart +++ b/lib/src/components/top_app_bar/top_app_bar.dart @@ -9,6 +9,10 @@ export 'search_top_app_bar.dart' hide ZetaTopAppBarSearchField; /// Top app bars provide content and actions related to the current screen. /// {@category Components} +/// +/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=229-37&node-type=canvas&m=dev +/// +/// Widgetbook: https://zeta-ds.web.app/flutter/widgetbook/index.html#/?path=components/top-app-bar/default class ZetaTopAppBar extends ZetaStatefulWidget implements PreferredSizeWidget { /// Creates a ZetaTopAppBar. const ZetaTopAppBar({ diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index b54aea82..8ba366f5 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -75,7 +75,7 @@ extension StringExtensions on String? { final List nameParts = this!.split(RegExp(r'\W+'))..removeWhere((item) => item.isEmpty); if (nameParts.isEmpty) return ''; return (nameParts.length > 1 - ? nameParts[0].substring(0, 1) + nameParts[1].substring(0, 1) + ? nameParts[0].substring(0, 1) + nameParts[nameParts.length - 1].substring(0, 1) : nameParts[0].length > 1 ? nameParts[0].substring(0, 2) : nameParts[0]) diff --git a/pubspec.yaml b/pubspec.yaml index e0f3a8ed..8b35b798 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: web: ^1.0.0 dev_dependencies: + analyzer: ^6.7.0 build_runner: ^2.4.10 flutter_test: sdk: flutter diff --git a/test/TESTING_README.mdx b/test/TESTING_README.mdx new file mode 100644 index 00000000..0789e872 --- /dev/null +++ b/test/TESTING_README.mdx @@ -0,0 +1,84 @@ +# Testing Conventions Flutter Components + +### Helper Functions + +As you are writing tests think about helper function you could write and add them to the `test_utils/utils.dart` file. This will help you and others write tests faster and more consistently. + +- For golden tests + `goldenTest(GoldenFiles goldenFile, Widget widget, Type widgetType, String fileName, {bool darkMode = false})` +- For debugFillProperties tests + `debugFillPropertiesTest(Widget widget, Map debugFillProperties)` + +### Groups + +- Accessibility Tests + Semantic labels, touch areas, contrast ratios, etc. +- Content Tests + Finds the widget, parameter statuses, etc. +- Dimensions Tests + Size, padding, margin, alignment, etc. +- Styling Tests + Rendered colors, fonts, borders, radii etc. +- Interaction Tests + Gesture recognizers, taps, drags, etc. +- Golden Tests + Compares the rendered widget with the golden file. Use the `goldenTest()` function from test_utils/utils.dart. +- Performance Tests + Animation performance, rendering performance, data manupulation performance, etc. + +### Testing File Template + +``` +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +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 componentName = 'ENTER_COMPONENT_NAME (e.g. ZetaButton)'; + const String parentFolder = 'ENTER_PARENT_FOLDER (e.g. button)'; + + const goldenFile = GoldenFiles(component: parentFolder); + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + group('$componentName Accessibility Tests', () {}); + group('$componentName Content Tests', () { + final debugFillProperties = { + '': '', + }; + debugFillPropertiesTest( + widget, + debugFillProperties, + ); + }); + group('$componentName Dimensions Tests', () {}); + group('$componentName Styling Tests', () {}); + group('$componentName Interaction Tests', () {}); + group('$componentName Golden Tests', () { + goldenTest(goldenFile, widget, widgetType, 'PNG_FILE_NAME'); + }); + group('$componentName Performance Tests', () {}); +} +``` + +### Test Visibility Table + +You can find the test visibility table at the following path: 'test/scripts/output/test_table.mdx' + +To generate the table run the following command from the root of the project: + +```bash +dart test/scripts/test_counter.dart +``` + +#### Visibility Excel Sheet + +https://zebra-my.sharepoint.com/:x:/p/de7924/Ea0l7BF7AzJJoBVPrg4cIswBZRyek6iNT3zzwDcLn-5ZGg?e=NTJIZU diff --git a/test/scripts/test_counter.dart b/test/scripts/test_counter.dart new file mode 100644 index 00000000..72dafe74 --- /dev/null +++ b/test/scripts/test_counter.dart @@ -0,0 +1,241 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import 'utils/utils.dart'; + +/// A visitor that recursively visits AST nodes to identify and process test groups. +/// +/// This class extends `RecursiveAstVisitor` and overrides necessary methods +/// to traverse the abstract syntax tree (AST) of Dart code. It is specifically +/// designed to locate and handle test groups within the code, which are typically +/// defined using the `group` function in test files. +/// +/// By implementing this visitor, you can analyze the structure of your test files, +/// extract information about test groups, and perform any required operations on them. +/// This can be useful for generating reports, performing static analysis, or +/// automating certain tasks related to your test suite. +class TestGroupVisitor extends RecursiveAstVisitor { + final List> groups = []; + + /// Visits a method invocation node in the abstract syntax tree (AST). + /// + /// This method is typically used in the context of traversing or analyzing + /// Dart code. It processes a [MethodInvocation] node, which represents + /// a method call in the source code. + /// + /// [node] - The [MethodInvocation] node to visit. + /// The method checks if the method invocation is one of the following: + /// - `group` + /// - `testWidgets` + /// - `test` + /// - `goldenTest` + /// - `debugFillPropertiesTest` + /// Then it extracts the group name and test names from the method invocation. + /// + /// - Parameter node: The [MethodInvocation] node to visit. + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.hasNullParentNode()) { + if (node.methodIsOneOf(['group'])) { + final groupName = node.getGroupName(); + final groupBody = node.argumentList.arguments.last; + + final tests = >[]; + + if (groupBody is FunctionExpression) { + final body = groupBody.body; + if (body is BlockFunctionBody) { + body.block.visitChildren(TestVisitor(tests)); + } + } + + groups.add({ + 'group': groupName, + 'tests': tests, + }); + } else if (node.methodIsOneOf(['testWidgets', 'test', 'goldenTest', 'debugFillPropertiesTest'])) { + final testName = node.getTestName(); + + if (groups.any((el) => el['group'] == 'unorganised')) { + final unorganisedGroup = groups.firstWhere((el) => el['group'] == 'unorganised'); + (unorganisedGroup['tests'] as List).add({ + 'name': testName, + }); + } else { + groups.add({ + 'group': 'unorganised', + 'tests': [ + { + 'name': testName, + }, + ], + }); + } + } + } + super.visitMethodInvocation(node); + } +} + +/// A visitor class that extends `RecursiveAstVisitor` to traverse +/// the Abstract Syntax Tree (AST) of Dart code. This class is specifically +/// designed to extract test names from test files. +/// +/// The `TestVisitor` class overrides necessary methods to visit nodes +/// in the AST and identify test definitions. It collects the names of +/// the tests, which can then be used for various purposes such as +/// generating test reports or running specific tests. +/// +class TestVisitor extends RecursiveAstVisitor { + TestVisitor(this.tests); + + final List> tests; + + /// Visits a method invocation node in the abstract syntax tree (AST). + /// This method checks if the method invocation is one of the following: + /// - `testWidgets` + /// - `test` + /// - `goldenTest` + /// - `debugFillPropertiesTest` + /// Then it extracts the test name from the method invocation. + /// + /// [node] - The [MethodInvocation] node to visit. + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.methodIsOneOf(['testWidgets', 'test'])) { + final testName = node.getTestName(); + tests.add({ + 'name': testName, + }); + } else if (node.methodIsOneOf(['debugFillPropertiesTest'])) { + tests.add({ + 'name': node.getMethodName(), + }); + } else if (node.methodIsOneOf(['goldenTest'])) { + tests.add({ + 'name': node.toString(), + }); + } + + super.visitMethodInvocation(node); + } +} + +/// Generates an MDX (Markdown Extended) table representation of the test counts. +/// +/// The function takes a nested map where the outer map's keys are test group names, +/// and the inner map's keys are test names with their corresponding integer counts. +/// +/// Example input: +/// ```dart +/// { +/// "test/src/components\\banner\\banner_test.dart": { +/// "Accessibility": 3, +/// }, +/// } +/// ``` +/// +/// Example output: +/// ```mdx +/// | Component | Accessibility | Content | Dimensions | Styling | Interaction | Golden | Performance | Unorganised | Total Tests | +/// | ----------- | ------------- | ------- | ---------- | ------- | ----------- | ------ | ----------- | ----------- | ----------- | +/// | Banner | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | +/// | Total Tests | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | +/// ``` +/// +/// Parameters: +/// - `testCount`: A map where the keys are test group names and the values are maps +/// of test names with their corresponding counts. +/// +/// Returns: +/// - A string in MDX format representing the test counts in a table with totals. +String generateMDX(Map> testCount) { + final Map groupTotals = { + 'Accessibility': 0, + 'Content': 0, + 'Dimensions': 0, + 'Styling': 0, + 'Interaction': 0, + 'Golden': 0, + 'Performance': 0, + 'unorganised': 0, + }; + + final List data = [ + '| Component | Accessibility | Content | Dimensions | Styling | Interaction | Golden | Performance | Unorganised | Total Tests |', + '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |', + ] + ..addComponentRows(testCount, groupTotals) + ..addCategoryTotalRow(testCount, groupTotals); + + return data.join('\n'); +} + +/// Parses a collection of test files and returns a map where the keys are +/// strings and the values are lists of maps containing dynamic data. +/// +/// The function takes an iterable of [FileSystemEntity] objects representing +/// the test files to be parsed. It processes these files asynchronously and +/// returns a [Future] that completes with a map. Each key in the map is a +/// string, and each value is a list of maps with string keys and dynamic values. +/// +/// - Parameter testFiles: An iterable collection of [FileSystemEntity] +/// objects representing the test files to be parsed. +/// - Returns: A [Future] that completes with a map where the keys are strings +/// and the values are lists of maps containing dynamic data. +Future parseTestFiles(Iterable testFiles) async { + final TestGroups testGroups = {}; + for (final FileSystemEntity file in testFiles) { + final contents = await File(file.path).readAsString(); + final parseResult = parseString(content: contents); + final visitor = TestGroupVisitor(); + parseResult.unit.visitChildren(visitor); + testGroups[file.path] = visitor.groups; + } + return testGroups; +} + +/// Counts the number of tests in each test group and returns a map with the counts. +/// +/// - Parameters: +/// - testGroups: A map where the keys are group names and the values are lists of test maps. +/// - Returns: A map where the keys are component names and the values are maps containing the count of tests in each test group. +Map> countTests(TestGroups testGroups) { + final TestCount testCount = {}; + testGroups.forEach((filePath, groups) { + final Map groupCounts = {}; + for (final group in groups) { + final groupName = group['group'] as String; + final tests = group['tests'] as List; + groupCounts[groupName] = tests.length; + } + testCount[filePath] = groupCounts; + }); + return testCount; +} + +void main() async { + // check for output directory and create if it doesn't exist + final Directory outputDirectory = await outputPath('test/scripts/output'); + + // get all test files + final Iterable testFiles = getTestFiles('test/src/components'); + + // parse each test file and extract test groups + final TestGroups testGroups = await parseTestFiles(testFiles); + + // write test groups to file + await writeJSONToFile('${outputDirectory.path}/test_groups.json', testGroups); + + // count the number of tests in each group + final TestCount testCount = countTests(testGroups); + + // write test counts to file + await writeJSONToFile('${outputDirectory.path}/test_counts.json', testCount); + + // generate MDX table + await writeMDXToFile('${outputDirectory.path}/test_table.mdx', generateMDX(testCount)); +} diff --git a/test/scripts/utils/utils.dart b/test/scripts/utils/utils.dart new file mode 100644 index 00000000..bcfc30c3 --- /dev/null +++ b/test/scripts/utils/utils.dart @@ -0,0 +1,249 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/ast/ast.dart'; + +/// A typedef for a map that associates a string key with a list of maps. +/// Each map in the list contains string keys and dynamic values. +/// +/// This can be used to group test cases or data sets by a specific category or identifier. +/// +/// Example: +/// ```dart +/// TestGroups testGroups = "path/to/test/file_test.dart": [ +/// { +/// 'group': 'Accessibility', +/// 'tests': [ +/// {'name': 'value1'}, +/// {'name': 'value2'}, +/// ], +/// }, +/// { +/// 'group': 'Content', +/// 'tests': [ +/// {'name': 'value1'}, +/// ], +/// }, +/// }; +/// ``` +typedef TestGroups = Map>>; + +/// A typedef for a nested map structure that counts tests. +/// +/// The outer map uses a `String` as the key, which represents a test file path. +/// The inner map also uses a `String` as the key, which represents a test category. +/// The value of the inner map is an `int` that represents the count of occurrences or results for that specific test category. +/// +/// Example: +/// ```dart +/// TestCount testCount = { +/// "path/to/test/file_test.dart": { +/// "Accessibility": 3, +/// "Content": 2, +/// }, +/// }; +/// ``` +typedef TestCount = Map>; + +extension NodeExtension on MethodInvocation { + /// Checks if the current node has a null parent node. + /// + /// Returns `true` if the parent node is null, otherwise `false`. + bool hasNullParentNode() { + return parent!.parent!.thisOrAncestorMatching((node) => node is MethodInvocation) == null; + } + + /// Retrieves and sanitizes the name of the test group. + /// + /// Returns: + /// A [String] containing the sanitized name of the group. + String getGroupName() { + return argumentList.arguments.first + .toString() + .replaceAll("'", '') + .replaceAll(r'$componentName ', '') + .replaceAll(' Tests', ''); + } + + /// Retrieves and sanitizes the name of the test. + /// + /// Returns: + /// A [String] containing the sanitized name of the test. + String getTestName() { + return argumentList.arguments.first.toString().replaceAll("'", ''); + } + + /// Returns the name of the method as a string. + /// + /// Returns: + /// A [String] representing the method name. + String getMethodName() { + return methodName.name; + } + + /// Checks if the current method is one of the specified methods. + /// + /// This function takes a list of method names and checks if the current + /// method is included in that list. + /// + /// - Parameter methods: A list of method names to check against. + /// - Returns: `true` if the current method is one of the specified methods, + /// otherwise `false`. + bool methodIsOneOf(List methods) { + return methods.contains(methodName.name); + } +} + +extension StringExtension on String { + /// Capitalizes the first letter of the string. + /// + /// Returns a new string with the first letter converted to uppercase + /// and the remaining letters unchanged. + /// + /// Example: + /// + /// ```dart + /// String text = "hello"; + /// String capitalizedText = text.capitalize(); + /// print(capitalizedText); // Output: Hello + /// ``` + String capitalize() { + if (isEmpty) return this; + return this[0].toUpperCase() + substring(1); + } + + /// Capitalizes the first letter of each word in a string. + /// + /// This method splits the string by spaces, capitalizes the first letter + /// of each word, and then joins the words back together with spaces. + /// + /// Returns a new string with each word capitalized. + String capitalizeEachWord() { + return split(' ').map((word) => word.capitalize()).join(' '); + } +} + +/// Extracts the component name from a given test file path. +/// +/// This function takes a test file path as input and returns the component name +/// inferred from the path. The component name is extracted by splitting the path +/// and removing the file extension and test suffix. +/// +/// Example: +/// ```dart +/// String componentName = getComponentNameFromTestPath('test/src/components/comms_button/comms_button_test.dart'); +/// print(componentName); // Output: Comms Button +/// ``` +/// +/// - Parameter path: The file path from which to extract the component name. +/// - Returns: The component name as a string. +String getComponentNameFromTestPath(String path) { + return path.split(r'\').last.split('_test').first.replaceAll('_', ' ').capitalizeEachWord(); +} + +/// Returns a [Future] that completes with a [Directory] at the specified [path]. +/// +/// This function takes a [String] [path] and asynchronously returns a [Directory] +/// object representing the directory at the given path. +/// +/// Example: +/// ```dart +/// Directory dir = await outputPath('/path/to/directory'); +/// ``` +Future outputPath(String path) async { + final outputDirectory = Directory(path); + if (!outputDirectory.existsSync()) { + await outputDirectory.create(recursive: true); + } + return outputDirectory; +} + +/// Retrieves an iterable of file system entities from the specified path. +/// +/// This function takes a [path] as a string and returns an [Iterable] of +/// [FileSystemEntity] objects representing the test files located at the given path. +/// +/// - Parameter path: The path to the directory from which to retrieve the files. +/// - Returns: An iterable collection of file system entities found at the specified path. +Iterable getTestFiles(String path) { + final testDirectory = Directory(path); + return testDirectory + .listSync(recursive: true) + .where((entity) => entity is File && entity.path.endsWith('_test.dart')); +} + +/// Writes the given JSON content to a file at the specified path. +/// +/// This function takes a file path and JSON content, and writes the content +/// to the file asynchronously. If the file does not exist, it will be created. +/// +/// [path] The file path where the JSON content should be written. +/// [content] The JSON content to write to the file. +Future writeJSONToFile(String path, dynamic content) async { + final jsonOutputGroups = jsonEncode(content); + final outputFileGroups = File(path); + await outputFileGroups.writeAsString(jsonOutputGroups); +} + +/// Writes the given MDX data to a file at the specified path. +/// +/// This function asynchronously writes the provided MDX data to a file +/// located at the given path. If the file does not exist, it will be created. +/// +/// [path] The file path where the MDX data should be written. +/// [mdxData] The MDX data to write to the file. +Future writeMDXToFile(String path, String mdxData) async { + final mdxFile = File(path); + await mdxFile.writeAsString(mdxData); +} + +extension ListExtension on List { + /// Adds rows of components to the specified target. + /// + /// This function iterates over the test count and group totals + /// to add the component rows to the target. + void addComponentRows( + Map> testCount, + Map groupTotals, + ) { + testCount.forEach((filePath, groups) { + final componentName = getComponentNameFromTestPath(filePath); + + int unorganisedTestsInComponent = 0; + groups.forEach((key, value) { + if (!groupTotals.keys.contains(key)) { + unorganisedTestsInComponent += value; + } + }); + unorganisedTestsInComponent += groups['unorganised'] ?? 0; + + final totalTestsForComponent = groups.values.fold(0, (previousValue, element) => previousValue + element); + + return add( + '| $componentName | ${groups['Accessibility'] ?? 0} | ${groups['Content'] ?? 0} | ${groups['Dimensions'] ?? 0} | ${groups['Styling'] ?? 0} | ${groups['Interaction'] ?? 0} | ${groups['Golden'] ?? 0} | ${groups['Performance'] ?? 0} | $unorganisedTestsInComponent | $totalTestsForComponent |', + ); + }); + } + + /// Adds a total row for a category in the data table. + /// + /// This method calculates the total for a specific category and appends + /// a row to the data table displaying the totals. + void addCategoryTotalRow( + Map> testCount, + Map groupTotals, + ) { + testCount.forEach((filePath, groups) { + groups.forEach((key, value) { + if (!groupTotals.keys.contains(key)) { + groupTotals['unorganised'] = groupTotals['unorganised']! + value; + } else { + groupTotals[key] = groupTotals[key]! + value; + } + }); + }); + return add( + '| Total Tests | ${groupTotals['Accessibility']} | ${groupTotals['Content']} | ${groupTotals['Dimensions']} | ${groupTotals['Styling']} | ${groupTotals['Interaction']} | ${groupTotals['Golden']} | ${groupTotals['Performance']} | ${groupTotals['unorganised']} | ${groupTotals.values.fold(0, (previousValue, element) => previousValue + element)} |', + ); + } +} diff --git a/test/src/components/avatar/assets/maxresdefault.jpg b/test/src/components/avatar/assets/maxresdefault.jpg new file mode 100644 index 00000000..be07c7f1 Binary files /dev/null and b/test/src/components/avatar/assets/maxresdefault.jpg differ diff --git a/test/src/components/avatar/avatar_test.dart b/test/src/components/avatar/avatar_test.dart new file mode 100644 index 00000000..9376f890 --- /dev/null +++ b/test/src/components/avatar/avatar_test.dart @@ -0,0 +1,400 @@ +import 'dart:io'; + +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 goldenFile = GoldenFiles(component: 'avatar'); + + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + group('ZetaAvatar Accessibility Tests', () { + testWidgets('ZetaAvatar meets accessibility requirements', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const TestApp( + home: ZetaAvatar.initials( + initials: 'AB', + backgroundColor: Colors.black, + borderColor: Colors.white, + lowerBadge: ZetaAvatarBadge( + icon: Icons.abc, + ), + upperBadge: ZetaAvatarBadge( + icon: Icons.abc, + ), + size: ZetaAvatarSize.l, + ), + ), + ); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + }); + + group('ZetaAvatar Content Tests', () { + const names = [ + 'John Doe', + 'Jane Doe', + 'John Jim Smith', + 'Jane Lane Smith', + 'Emily Bukowsik Johnson', + 'Michael John Brad Brown', + 'Emma Amy Davis', + 'William Charlie Wilson', + 'Olivia Johnston', + 'James Oliver', + 'Isabella Smith', + ]; + + // TODODE: We need options for which initials should be displayed. Could use a bitmask? + for (final name in names) { + testWidgets( + 'ZetaAvatar intiatls show the first letter of the first name and the last name $name', + (WidgetTester tester) async { + final nameParts = name.split(' '); + final initials = nameParts[0][0].toUpperCase() + nameParts[nameParts.length - 1][0].toUpperCase(); + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.fromName( + name: name, + ), + ), + ); + + expect(find.text(initials), findsOneWidget); + }, + skip: true, + ); + } + + for (final size in ZetaAvatarSize.values) { + testWidgets('ZetaAvatar with initials $size text is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.initials( + size: size, + initials: 'AB', + ), + ), + ); + + expect(find.byType(Text), findsOneWidget); + expect(find.text('AB'), findsOneWidget); + }); + } + }); + + group('ZetaAvatar Dimensions Tests', () { + for (final size in ZetaAvatarSize.values) { + testWidgets( + 'ZetaAvatar size $size with upper badge', + (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + size: size, + upperBadge: const ZetaAvatarBadge.notification(value: 3), + ), + ), + ); + + expect(find.byType(ZetaAvatarBadge), findsOneWidget); + + final badgePosition = tester.getRect(find.byType(ZetaAvatarBadge)); + final pixelSize = ZetaAvatar.pixelSize(tester.element(find.byType(ZetaAvatar)), size); + + expect(badgePosition.top, equals(0.0)); + expect(badgePosition.right, equals(pixelSize)); + }, + ); + + testWidgets('ZetaAvatar size $size with lower badge', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + size: size, + lowerBadge: const ZetaAvatarBadge.icon(icon: Icons.star), + ), + ), + ); + + expect(find.byType(ZetaAvatarBadge), findsOneWidget); + + final badgePosition = tester.getRect(find.byType(ZetaAvatarBadge)); + final pixelSize = ZetaAvatar.pixelSize(tester.element(find.byType(ZetaAvatar)), size); + + expect(badgePosition.bottom, equals(pixelSize)); + expect(badgePosition.right, equals(pixelSize)); + }); + + testWidgets('ZetaAvatar size $size', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + size: size, + ), + ), + ); + + final avatarSize = tester.getSize(find.byType(ZetaAvatar)); + final typeSize = ZetaAvatar.pixelSize( + tester.element(find.byType(ZetaAvatar)), + size, + ); + + expect(avatarSize.width, equals(typeSize)); + expect(avatarSize.height, equals(typeSize)); + }); + + testWidgets('ZetaAvatar size $size with image', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.image( + size: size, + image: Image.file(File('/assets/maxresdefault.jpg')), + ), + ), + ); + + final avatarSize = tester.getSize(find.byType(ZetaAvatar)); + final typeSize = ZetaAvatar.pixelSize(tester.element(find.byType(ZetaAvatar)), size); + + expect(avatarSize.width, equals(typeSize)); + expect(avatarSize.height, equals(typeSize)); + }); + + testWidgets('ZetaAvatar size $size with initials', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.initials( + size: size, + initials: 'AB', + ), + ), + ); + + final avatarSize = tester.getSize(find.byType(ZetaAvatar)); + final typeSize = ZetaAvatar.pixelSize(tester.element(find.byType(ZetaAvatar)), size); + + expect(avatarSize.width, equals(typeSize)); + expect(avatarSize.height, equals(typeSize)); + }); + + testWidgets('ZetaAvatar size $size with fromName', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.fromName( + size: size, + name: 'John Doe', + ), + ), + ); + + final avatarSize = tester.getSize(find.byType(ZetaAvatar)); + final typeSize = ZetaAvatar.pixelSize(tester.element(find.byType(ZetaAvatar)), size); + + expect(avatarSize.width, equals(typeSize)); + expect(avatarSize.height, equals(typeSize)); + }); + } + }); + + group('ZetaAvatar Styling Tests', () { + for (final size in ZetaAvatarSize.values) { + testWidgets('ZetaAvatar with initials $size text size is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.initials( + size: size, + initials: 'AB', + ), + ), + ); + + final text = tester.firstWidget(find.byType(Text)) as Text; + expect( + text.style?.fontSize, + equals( + ZetaAvatar.fontSize(tester.element(find.byType(ZetaAvatar)), size), + ), + ); + }); + } + + testWidgets('ZetaAvatar with initials text color is correct', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaAvatar.initials( + initials: 'AB', + initialTextStyle: TextStyle(color: Colors.red), + ), + ), + ); + + final text = tester.firstWidget(find.byType(Text)) as Text; + expect(text.style!.color, equals(Colors.red)); + }); + testWidgets('ZetaAvatar default background colour', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaAvatar(), + ), + ); + + final avatarBackgroundColor = + ((tester.firstWidget(find.byType(Container)) as Container).decoration! as BoxDecoration).color; + + final context = tester.element(find.byType(ZetaAvatar)); + expect(avatarBackgroundColor, equals(Zeta.of(context).colors.surfacePrimary)); + }); + + testWidgets('ZetaAvatar with background colour', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaAvatar( + backgroundColor: Colors.red, + ), + ), + ); + + final avatarBackgroundColor = + ((tester.firstWidget(find.byType(Container)) as Container).decoration! as BoxDecoration).color; + + expect(avatarBackgroundColor, equals(Colors.red)); + }); + + for (final size in ZetaAvatarSize.values) { + testWidgets('ZetaAvatar with border colour for $size', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + borderColor: Colors.red, + size: size, + ), + ), + ); + + final avatarBorderColor = + (((tester.firstWidget(find.byType(Container)) as Container).decoration! as BoxDecoration).border! as Border) + .top + .color; + expect(avatarBorderColor, equals(Colors.red)); + }); + } + }); + + group('ZetaAvatar Interaction Tests', () {}); + + group('ZetaAvatar Golden Tests', () { + for (final size in ZetaAvatarSize.values) { + testWidgets('ZetaAvatar default ${size.toString().split('.').last}', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_default_${size.toString().split('.').last}')), + ); + }); + + testWidgets('ZetaAvatar with initials', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.initials( + initials: 'AB', + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_initials_${size.toString().split('.').last}')), + ); + }); + + testWidgets('ZetaAvatar with image', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.image( + image: Image.file(File('/assets/maxresdefault.jpg')), + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_image_${size.toString().split('.').last}')), + ); + }); + + testWidgets('ZetaAvatar with fromName', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar.fromName( + name: 'John Doe', + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_from_name_${size.toString().split('.').last}')), + ); + }); + + testWidgets('ZetaAvatar with upper badge', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + upperBadge: const ZetaAvatarBadge.notification(value: 3), + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_upper_badge_${size.toString().split('.').last}')), + ); + }); + + testWidgets('ZetaAvatar with lower badge', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaAvatar( + lowerBadge: const ZetaAvatarBadge.icon(icon: Icons.star), + size: size, + ), + ), + ); + + await expectLater( + find.byType(ZetaAvatar), + matchesGoldenFile(goldenFile.getFileUri('avatar_lower_badge_${size.toString().split('.').last}')), + ); + }); + } + }); + + group('ZetaAvatar Performance Tests', () {}); +} diff --git a/test/src/components/avatar/golden/avatar_default_l.png b/test/src/components/avatar/golden/avatar_default_l.png new file mode 100644 index 00000000..f1b7e806 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_l.png differ diff --git a/test/src/components/avatar/golden/avatar_default_m.png b/test/src/components/avatar/golden/avatar_default_m.png new file mode 100644 index 00000000..1cdbb01c Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_m.png differ diff --git a/test/src/components/avatar/golden/avatar_default_s.png b/test/src/components/avatar/golden/avatar_default_s.png new file mode 100644 index 00000000..6968568a Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_s.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xl.png b/test/src/components/avatar/golden/avatar_default_xl.png new file mode 100644 index 00000000..93ff79dd Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xs.png b/test/src/components/avatar/golden/avatar_default_xs.png new file mode 100644 index 00000000..56dcc7dc Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xxl.png b/test/src/components/avatar/golden/avatar_default_xxl.png new file mode 100644 index 00000000..81462459 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xxs.png b/test/src/components/avatar/golden/avatar_default_xxs.png new file mode 100644 index 00000000..0ddee64c Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xxxl.png b/test/src/components/avatar/golden/avatar_default_xxxl.png new file mode 100644 index 00000000..35dded72 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_default_xxxs.png b/test/src/components/avatar/golden/avatar_default_xxxs.png new file mode 100644 index 00000000..590958fc Binary files /dev/null and b/test/src/components/avatar/golden/avatar_default_xxxs.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_l.png b/test/src/components/avatar/golden/avatar_from_name_l.png new file mode 100644 index 00000000..54a7498a Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_l.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_m.png b/test/src/components/avatar/golden/avatar_from_name_m.png new file mode 100644 index 00000000..00d0cc42 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_m.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_s.png b/test/src/components/avatar/golden/avatar_from_name_s.png new file mode 100644 index 00000000..29d4d0b5 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_s.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xl.png b/test/src/components/avatar/golden/avatar_from_name_xl.png new file mode 100644 index 00000000..c9288ff3 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xs.png b/test/src/components/avatar/golden/avatar_from_name_xs.png new file mode 100644 index 00000000..ec8f22c7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xxl.png b/test/src/components/avatar/golden/avatar_from_name_xxl.png new file mode 100644 index 00000000..27293658 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xxs.png b/test/src/components/avatar/golden/avatar_from_name_xxs.png new file mode 100644 index 00000000..d43b1625 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xxxl.png b/test/src/components/avatar/golden/avatar_from_name_xxxl.png new file mode 100644 index 00000000..b16b99d7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_from_name_xxxs.png b/test/src/components/avatar/golden/avatar_from_name_xxxs.png new file mode 100644 index 00000000..e03fc338 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_from_name_xxxs.png differ diff --git a/test/src/components/avatar/golden/avatar_image_l.png b/test/src/components/avatar/golden/avatar_image_l.png new file mode 100644 index 00000000..43bda33c Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_l.png differ diff --git a/test/src/components/avatar/golden/avatar_image_m.png b/test/src/components/avatar/golden/avatar_image_m.png new file mode 100644 index 00000000..0373e854 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_m.png differ diff --git a/test/src/components/avatar/golden/avatar_image_s.png b/test/src/components/avatar/golden/avatar_image_s.png new file mode 100644 index 00000000..f1f5d2be Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_s.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xl.png b/test/src/components/avatar/golden/avatar_image_xl.png new file mode 100644 index 00000000..40e80684 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xs.png b/test/src/components/avatar/golden/avatar_image_xs.png new file mode 100644 index 00000000..b7b83468 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xxl.png b/test/src/components/avatar/golden/avatar_image_xxl.png new file mode 100644 index 00000000..3fd71247 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xxs.png b/test/src/components/avatar/golden/avatar_image_xxs.png new file mode 100644 index 00000000..db0eafb7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xxxl.png b/test/src/components/avatar/golden/avatar_image_xxxl.png new file mode 100644 index 00000000..a677b2cb Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_image_xxxs.png b/test/src/components/avatar/golden/avatar_image_xxxs.png new file mode 100644 index 00000000..162703a4 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_image_xxxs.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_l.png b/test/src/components/avatar/golden/avatar_initials_l.png new file mode 100644 index 00000000..54a7498a Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_l.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_m.png b/test/src/components/avatar/golden/avatar_initials_m.png new file mode 100644 index 00000000..00d0cc42 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_m.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_s.png b/test/src/components/avatar/golden/avatar_initials_s.png new file mode 100644 index 00000000..29d4d0b5 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_s.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xl.png b/test/src/components/avatar/golden/avatar_initials_xl.png new file mode 100644 index 00000000..c9288ff3 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xs.png b/test/src/components/avatar/golden/avatar_initials_xs.png new file mode 100644 index 00000000..ec8f22c7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xxl.png b/test/src/components/avatar/golden/avatar_initials_xxl.png new file mode 100644 index 00000000..27293658 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xxs.png b/test/src/components/avatar/golden/avatar_initials_xxs.png new file mode 100644 index 00000000..d43b1625 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xxxl.png b/test/src/components/avatar/golden/avatar_initials_xxxl.png new file mode 100644 index 00000000..b16b99d7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_initials_xxxs.png b/test/src/components/avatar/golden/avatar_initials_xxxs.png new file mode 100644 index 00000000..e03fc338 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_initials_xxxs.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_l.png b/test/src/components/avatar/golden/avatar_lower_badge_l.png new file mode 100644 index 00000000..e463ddce Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_l.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_m.png b/test/src/components/avatar/golden/avatar_lower_badge_m.png new file mode 100644 index 00000000..13700bb6 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_m.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_s.png b/test/src/components/avatar/golden/avatar_lower_badge_s.png new file mode 100644 index 00000000..c38225f4 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_s.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xl.png b/test/src/components/avatar/golden/avatar_lower_badge_xl.png new file mode 100644 index 00000000..dea92ef7 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xs.png b/test/src/components/avatar/golden/avatar_lower_badge_xs.png new file mode 100644 index 00000000..f25da5df Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xxl.png b/test/src/components/avatar/golden/avatar_lower_badge_xxl.png new file mode 100644 index 00000000..7faec2b4 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xxs.png b/test/src/components/avatar/golden/avatar_lower_badge_xxs.png new file mode 100644 index 00000000..dcba2c3e Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xxxl.png b/test/src/components/avatar/golden/avatar_lower_badge_xxxl.png new file mode 100644 index 00000000..bb9be878 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_lower_badge_xxxs.png b/test/src/components/avatar/golden/avatar_lower_badge_xxxs.png new file mode 100644 index 00000000..dfb93618 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_lower_badge_xxxs.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_l.png b/test/src/components/avatar/golden/avatar_upper_badge_l.png new file mode 100644 index 00000000..7e42a459 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_l.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_m.png b/test/src/components/avatar/golden/avatar_upper_badge_m.png new file mode 100644 index 00000000..7b9bf5ab Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_m.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_s.png b/test/src/components/avatar/golden/avatar_upper_badge_s.png new file mode 100644 index 00000000..1e6b9e3f Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_s.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xl.png b/test/src/components/avatar/golden/avatar_upper_badge_xl.png new file mode 100644 index 00000000..b02b399f Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xl.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xs.png b/test/src/components/avatar/golden/avatar_upper_badge_xs.png new file mode 100644 index 00000000..faf37477 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xs.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xxl.png b/test/src/components/avatar/golden/avatar_upper_badge_xxl.png new file mode 100644 index 00000000..5e3eb610 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xxl.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xxs.png b/test/src/components/avatar/golden/avatar_upper_badge_xxs.png new file mode 100644 index 00000000..8808ac8c Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xxs.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xxxl.png b/test/src/components/avatar/golden/avatar_upper_badge_xxxl.png new file mode 100644 index 00000000..8abced31 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xxxl.png differ diff --git a/test/src/components/avatar/golden/avatar_upper_badge_xxxs.png b/test/src/components/avatar/golden/avatar_upper_badge_xxxs.png new file mode 100644 index 00000000..ea50d404 Binary files /dev/null and b/test/src/components/avatar/golden/avatar_upper_badge_xxxs.png differ diff --git a/test/src/components/banner/banner_test.dart b/test/src/components/banner/banner_test.dart new file mode 100644 index 00000000..c733a75f --- /dev/null +++ b/test/src/components/banner/banner_test.dart @@ -0,0 +1,359 @@ +import 'package:flutter/foundation.dart'; +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'; + +ZetaColorSwatch _backgroundColorFromType(BuildContext context, ZetaBannerStatus type) { + final zeta = Zeta.of(context); + + switch (type) { + case ZetaBannerStatus.primary: + return zeta.colors.primary; + case ZetaBannerStatus.positive: + return zeta.colors.surfacePositive; + case ZetaBannerStatus.warning: + return zeta.colors.orange; + case ZetaBannerStatus.negative: + return zeta.colors.surfaceNegative; + } +} + +void main() { + const goldenFile = GoldenFiles(component: 'banner'); + + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + group('ZetaBanner Accessibility Tests', () { + for (final type in ZetaBannerStatus.values) { + testWidgets('meets contrast ratio guideline for $type', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: Icons.info, + trailing: const ZetaIconButton(icon: Icons.close), + type: type, + ); + }, + ), + ), + ); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + }); + } + + testWidgets('semantic label works correctly', (WidgetTester tester) async { + String semanticLabelText = 'Banner Title'; + StateSetter? setState; + + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + semanticLabel: semanticLabelText, + ); + }, + ), + ); + }, + ), + ); + + final Semantics titleSematicLabel = tester.widgetList(find.byType(Semantics)).last; + expect(titleSematicLabel.properties.label, equals('Banner Title')); + + setState?.call(() => semanticLabelText = ''); + await tester.pumpAndSettle(); + + final Semantics titleSematicLabel2 = tester.widgetList(find.byType(Semantics)).last; + expect(titleSematicLabel2.properties.label, equals('')); + }); + + testWidgets('uses title for sematic label if nessaccary', (WidgetTester tester) async { + String titleText = 'Banner Title'; + StateSetter? setState; + + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState2) { + setState = setState2; + return TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: titleText, + ); + }, + ), + ); + }, + ), + ); + + final Semantics titleSematicLabel = tester.widgetList(find.byType(Semantics)).last; + expect(titleSematicLabel.properties.label, equals('Banner Title')); + + setState?.call(() => titleText = ''); + await tester.pumpAndSettle(); + + final Semantics titleSematicLabel2 = tester.widgetList(find.byType(Semantics)).last; + expect(titleSematicLabel2.properties.label, equals('')); + }); + }); + + group('ZetaBanner Content Tests', () { + testWidgets('ZetaBanner title is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + ); + }, + ), + ), + ); + final Finder textFinder = find.text('Banner Title'); + expect(textFinder, findsOneWidget); + }); + + testWidgets('ZetaBanner leading icon is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: Icons.info, + ); + }, + ), + ), + ); + final Finder iconFinder = find.byIcon(Icons.info); + expect(iconFinder, findsOneWidget); + + final Icon iconWidget = tester.widget(iconFinder); + expect(iconWidget.icon, equals(Icons.info)); + }); + + testWidgets('trailing widget is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + trailing: const ZetaIconButton(icon: ZetaIcons.close), + ); + }, + ), + ), + ); + + final Finder iconButtonFinder = find.byType(ZetaIconButton); + expect(iconButtonFinder, findsOneWidget); + + final ZetaIconButton button = tester.widget(iconButtonFinder); + expect(button.icon, equals(ZetaIcons.close)); + }); + + testWidgets('debugFillProperties works correctly', (WidgetTester tester) async { + final diagnostics = DiagnosticPropertiesBuilder(); + const ZetaAccordion( + title: 'Title', + ).debugFillProperties(diagnostics); + + expect(diagnostics.finder('title'), '"Title"'); + expect(diagnostics.finder('rounded'), 'null'); + expect(diagnostics.finder('contained'), 'false'); + expect(diagnostics.finder('isOpen'), 'false'); + }); + }); + + group('ZetaBanner Dimension Tests', () { + testWidgets('icon is the correct size', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: ZetaIcons.info, + ); + }, + ), + ), + ); + final Finder iconFinder = find.byIcon(ZetaIcons.info); + + final Icon iconWidget = tester.widget(iconFinder); + + expect(iconWidget.size, Zeta.of(tester.element(iconFinder)).spacing.xl_2); + }); + + testWidgets('icon padding is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: ZetaIcons.info, + ); + }, + ), + ), + ); + final Finder paddingFinder = find.widgetWithIcon(Padding, ZetaIcons.info); + + final Padding paddingWidget = tester.firstWidget(paddingFinder); + + expect(paddingWidget.padding, equals(const EdgeInsets.only(right: 8))); + }); + + testWidgets('banner padding is correct', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + ); + }, + ), + ), + ); + final Finder paddingFinder = find.byType(Padding); + + final Padding paddingWidget = tester.widgetList(paddingFinder).elementAt(1); + + expect(paddingWidget.padding, equals(const EdgeInsetsDirectional.only(start: 16, top: 2))); + }); + }); + + group('ZetaBanner Styling Tests', () { + for (final type in ZetaBannerStatus.values) { + testWidgets('title styles are correct for $type', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + type: type, + ); + }, + ), + ), + ); + final Finder textFinder = find.text('Banner Title'); + final Text textWidget = tester.widget(find.byType(Text)); + expect( + textWidget.style, + equals( + ZetaTextStyles.labelLarge.copyWith( + color: Zeta.of(tester.element(textFinder)).colors.textInverse, + ), + ), + ); + }); + + testWidgets('icon color is correct for $type', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: Icons.info, + type: type, + ); + }, + ), + ), + ); + + final Finder iconFinder = find.byIcon(Icons.info); + + final Icon iconWidget = tester.widget(iconFinder); + expect(iconWidget.color, _backgroundColorFromType(tester.element(iconFinder), type).onColor); + }); + + testWidgets('background colors are correct for $type', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + type: type, + ); + }, + ), + ), + ); + final Finder finder = find.byType(ZetaBanner); + final ZetaBanner widget = tester.firstWidget(finder); + + expect(widget.backgroundColor, equals(_backgroundColorFromType(tester.element(finder), type))); + }); + } + }); + + group('ZetaBanner Interaction Tests', () {}); + + group('ZetaBanner Golden Tests', () { + for (final type in ZetaBannerStatus.values) { + testWidgets('ZetaBanner ${type.toString().split('.').last} golden', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return ZetaBanner( + context: context, + title: 'Banner Title', + leadingIcon: Icons.info, + trailing: const ZetaIcon(Icons.chevron_right), + type: type, + ); + }, + ), + ), + ); + + await expectLater( + find.byType(ZetaBanner), + matchesGoldenFile(goldenFile.getFileUri('banner_${type.toString().split('.').last}')), + ); + }); + } + }); +} diff --git a/test/src/components/banner/golden/banner_negative.png b/test/src/components/banner/golden/banner_negative.png new file mode 100644 index 00000000..31ff7fd7 Binary files /dev/null and b/test/src/components/banner/golden/banner_negative.png differ diff --git a/test/src/components/banner/golden/banner_positive.png b/test/src/components/banner/golden/banner_positive.png new file mode 100644 index 00000000..b96308fb Binary files /dev/null and b/test/src/components/banner/golden/banner_positive.png differ diff --git a/test/src/components/banner/golden/banner_primary.png b/test/src/components/banner/golden/banner_primary.png new file mode 100644 index 00000000..c1ff44d8 Binary files /dev/null and b/test/src/components/banner/golden/banner_primary.png differ diff --git a/test/src/components/banner/golden/banner_warning.png b/test/src/components/banner/golden/banner_warning.png new file mode 100644 index 00000000..18786b14 Binary files /dev/null and b/test/src/components/banner/golden/banner_warning.png differ diff --git a/test/src/components/comms_button/comms_button_test.dart b/test/src/components/comms_button/comms_button_test.dart new file mode 100644 index 00000000..0322682e --- /dev/null +++ b/test/src/components/comms_button/comms_button_test.dart @@ -0,0 +1,231 @@ +import 'package:flutter/foundation.dart'; +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 goldenFile = GoldenFiles(component: 'comms_button'); + + setUpAll(() { + goldenFileComparator = TolerantComparator(goldenFile.uri); + }); + + group('ZetaCommsButton Tests', () { + testWidgets('Initializes with correct label', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaCommsButton(label: 'Label', icon: ZetaIcons.phone, type: ZetaCommsButtonType.positive), + ), + ); + + expect(find.text('Label'), findsOneWidget); + + await expectLater( + find.byType(ZetaCommsButton), + matchesGoldenFile(goldenFile.getFileUri('CommsButton_default')), + ); + }); + + testWidgets('Initializes with correct icon', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaCommsButton(label: 'Label', icon: ZetaIcons.phone, type: ZetaCommsButtonType.positive), + ), + ); + + expect(find.widgetWithIcon(ZetaCommsButton, ZetaIcons.phone), findsOneWidget); + }); + + testWidgets('Initializes with correct type', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaCommsButton(label: 'Label', icon: ZetaIcons.phone, type: ZetaCommsButtonType.positive), + ), + ); + + expect(find.byType(ZetaCommsButton), findsOneWidget); + }); + + testWidgets('Changes label, icon, and type when toggled', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaCommsButton( + label: 'Label', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + toggledLabel: 'Toggled Label', + toggledIcon: ZetaIcons.end_call, + toggledType: ZetaCommsButtonType.negative, + onToggle: (isToggled) {}, + ), + ), + ); + + expect(find.text('Label'), findsOneWidget); + expect(find.widgetWithIcon(ZetaCommsButton, ZetaIcons.phone), findsOneWidget); + expect(tester.widget(find.byType(ZetaCommsButton)).type, ZetaCommsButtonType.positive); + var iconButton = tester.widget(find.byType(IconButton)); + final context = tester.element(find.byType(ZetaCommsButton)); + expect(iconButton.style?.backgroundColor?.resolve({}), Zeta.of(context).colors.surfacePositive); + + await tester.tap(find.byType(ZetaCommsButton)); + await tester.pumpAndSettle(); + + expect(find.text('Toggled Label'), findsOneWidget); + expect(find.widgetWithIcon(ZetaCommsButton, ZetaIcons.end_call), findsOneWidget); + expect(tester.widget(find.byType(ZetaCommsButton)).toggledType, ZetaCommsButtonType.negative); + iconButton = tester.widget(find.byType(IconButton)); + expect(iconButton.style?.backgroundColor?.resolve({}), Zeta.of(context).colors.surfaceNegative); + }); + + testWidgets('Button is not toggleable when onToggle is null', (WidgetTester tester) async { + await tester.pumpWidget( + const TestApp( + home: ZetaCommsButton( + label: 'Label', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + toggledLabel: 'Toggled Label', + toggledIcon: ZetaIcons.end_call, + toggledType: ZetaCommsButtonType.negative, + ), + ), + ); + + expect(find.text('Label'), findsOneWidget); + expect(find.widgetWithIcon(ZetaCommsButton, ZetaIcons.phone), findsOneWidget); + expect(tester.widget(find.byType(ZetaCommsButton)).type, ZetaCommsButtonType.positive); + var iconButton = tester.widget(find.byType(IconButton)); + final context = tester.element(find.byType(ZetaCommsButton)); + expect(iconButton.style?.backgroundColor?.resolve({}), Zeta.of(context).colors.surfacePositive); + + await tester.tap(find.byType(ZetaCommsButton)); + await tester.pumpAndSettle(); + + expect(find.text('Label'), findsOneWidget); + expect(find.widgetWithIcon(ZetaCommsButton, ZetaIcons.phone), findsOneWidget); + expect(tester.widget(find.byType(ZetaCommsButton)).type, ZetaCommsButtonType.positive); + iconButton = tester.widget(find.byType(IconButton)); + expect(iconButton.style?.backgroundColor?.resolve({}), Zeta.of(context).colors.surfacePositive); + }); + + testWidgets('Button calls onPressed callback when pressed', (WidgetTester tester) async { + var pressed = false; + + await tester.pumpWidget( + TestApp( + home: ZetaCommsButton( + label: 'Label', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + onPressed: () { + pressed = true; + }, + ), + ), + ); + + await tester.tap(find.byType(ZetaCommsButton)); + await tester.pumpAndSettle(); + + expect(pressed, isTrue); + }); + + testWidgets('debugFillProperties Test', (WidgetTester tester) async { + final diagnostic = DiagnosticPropertiesBuilder(); + const ZetaCommsButton( + label: 'Label', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + ).debugFillProperties(diagnostic); + + expect(diagnostic.finder('label'), '"Label"'); + expect(diagnostic.finder('onPressed'), 'null'); + expect(diagnostic.finder('onToggle'), 'null'); + expect(diagnostic.finder('toggledIcon'), 'null'); + expect(diagnostic.finder('toggledLabel'), 'null'); + expect(diagnostic.finder('toggleType'), null); + expect(diagnostic.finder('focusNode'), 'null'); + expect(diagnostic.finder('semanticLabel'), 'null'); + expect(diagnostic.finder('type'), 'positive'); + expect(diagnostic.finder('size'), 'medium'); + expect(diagnostic.finder('icon'), 'IconData(U+0E16B)'); + }); + + testWidgets('Button meets accessibility requirements', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const TestApp( + home: ZetaCommsButton( + label: 'Label', + semanticLabel: 'Phone', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + ), + ), + ); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + + testWidgets('Button meets accessibility requirements when toggled', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + TestApp( + home: ZetaCommsButton( + label: 'Label', + semanticLabel: 'Phone', + icon: ZetaIcons.phone, + type: ZetaCommsButtonType.positive, + toggledLabel: 'Toggled Label', + toggledIcon: ZetaIcons.end_call, + toggledType: ZetaCommsButtonType.negative, + onToggle: (isToggled) {}, + ), + ), + ); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + await tester.tap(find.byType(ZetaCommsButton)); + await tester.pumpAndSettle(); + + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + }); + + group('ZetaCommsButton Golden Tests', () { + for (final type in ZetaCommsButtonType.values) { + testWidgets('ZetaCommsButton with type $type', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + home: ZetaCommsButton( + label: 'Label', + icon: ZetaIcons.phone, + type: type, + ), + ), + ); + + await expectLater( + find.byType(ZetaCommsButton), + matchesGoldenFile(goldenFile.getFileUri('CommsButton_${type.name}')), + ); + }); + } + }); +} diff --git a/test/src/components/comms_button/golden/CommsButton_default.png b/test/src/components/comms_button/golden/CommsButton_default.png new file mode 100644 index 00000000..3f4c2f73 Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_default.png differ diff --git a/test/src/components/comms_button/golden/CommsButton_negative.png b/test/src/components/comms_button/golden/CommsButton_negative.png new file mode 100644 index 00000000..7b5a15f4 Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_negative.png differ diff --git a/test/src/components/comms_button/golden/CommsButton_off.png b/test/src/components/comms_button/golden/CommsButton_off.png new file mode 100644 index 00000000..f6240813 Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_off.png differ diff --git a/test/src/components/comms_button/golden/CommsButton_on.png b/test/src/components/comms_button/golden/CommsButton_on.png new file mode 100644 index 00000000..ebe1662f Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_on.png differ diff --git a/test/src/components/comms_button/golden/CommsButton_positive.png b/test/src/components/comms_button/golden/CommsButton_positive.png new file mode 100644 index 00000000..3f4c2f73 Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_positive.png differ diff --git a/test/src/components/comms_button/golden/CommsButton_warning.png b/test/src/components/comms_button/golden/CommsButton_warning.png new file mode 100644 index 00000000..23d14fdc Binary files /dev/null and b/test/src/components/comms_button/golden/CommsButton_warning.png differ diff --git a/test/src/components/fabs/fab_test.dart b/test/src/components/fabs/fab_test.dart index e1b4c3f4..1152f2aa 100644 --- a/test/src/components/fabs/fab_test.dart +++ b/test/src/components/fabs/fab_test.dart @@ -189,7 +189,7 @@ void main() { expect(diagnostics.finder('focusNode'), 'null'); }); - testWidgets('Expanded changes when label is null', (WidgetTester tester) async { + testWidgets('Label is correct', (WidgetTester tester) async { final scrollController = ScrollController(); StateSetter? setState; bool expanded = false; @@ -212,7 +212,7 @@ void main() { final labelFinder = find.text('Label'); - expect(labelFinder, findsNothing); + expect(labelFinder, findsOne); setState?.call(() => expanded = true); diff --git a/test/src/components/search_bar/search_bar_test.dart b/test/src/components/search_bar/search_bar_test.dart index 9cfaef8b..120b326a 100644 --- a/test/src/components/search_bar/search_bar_test.dart +++ b/test/src/components/search_bar/search_bar_test.dart @@ -47,7 +47,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(TextFormField), findsOneWidget); + expect(find.byType(TextField), findsOneWidget); }); testWidgets('golden: renders initializes correctly', (WidgetTester tester) async { @@ -178,7 +178,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextFormField), 'New text'); + await tester.enterText(find.byType(TextField), 'New text'); await tester.pump(); verify(callbacks.onChange.call('New text')).called(1); @@ -194,7 +194,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextFormField), 'Submit text'); + await tester.enterText(find.byType(TextField), 'Submit text'); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pump(); @@ -231,7 +231,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextFormField), 'Disabled input'); + await tester.enterText(find.byType(TextField), 'Disabled input'); await tester.pump(); expect(find.text('Disabled input'), findsNothing); @@ -263,7 +263,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextFormField), 'New text'); + await tester.enterText(find.byType(TextField), 'New text'); await tester.pump(); verify(callbacks.onChange.call('New text')).called(1); @@ -280,7 +280,7 @@ void main() { expect(diagnostics.finder('size'), 'medium'); expect(diagnostics.finder('shape'), 'rounded'); - expect(diagnostics.finder('hintText'), 'null'); + expect(diagnostics.finder('placeholder'), 'null'); expect(diagnostics.finder('textInputAction'), 'null'); expect(diagnostics.finder('onSpeechToText'), 'null'); expect(diagnostics.finder('showSpeechToText'), 'true'); diff --git a/test/src/utils/extensions_test.dart b/test/src/utils/extensions_test.dart index cd844f43..f67b7e2d 100644 --- a/test/src/utils/extensions_test.dart +++ b/test/src/utils/extensions_test.dart @@ -218,7 +218,7 @@ void main() { group('StringExtensions extension', () { test('initials returns correct initials', () { expect('John Doe'.initials, 'JD'); - expect('A B C'.initials, 'AB'); + expect('A B C'.initials, 'AC'); expect('Single'.initials, 'SI'); expect('A'.initials, 'A'); expect('a'.initials, 'A'); diff --git a/test/test_utils/utils.dart b/test/test_utils/utils.dart index e8e7acc2..1bfb59f1 100644 --- a/test/test_utils/utils.dart +++ b/test/test_utils/utils.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart'; extension Util on DiagnosticPropertiesBuilder { @@ -27,3 +29,7 @@ class GoldenFiles { .replace(scheme: 'file'); } } + +BuildContext getBuildContext(WidgetTester tester, Type type) { + return tester.element(find.byType(type)); +}