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/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/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/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/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