From af958159b3e4f3fad06c24e64983aff5861a9482 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Fri, 4 Oct 2024 09:55:16 +0100 Subject: [PATCH] feat(UX-1064): Comms Buttons (#182) feat: comms buttons docs: changed comms button example to use assorted constructors docs: set initial values for comms button on widgetbook fix: added different constructors to zetacommsbutton chore(automated): Lint commit and format fix: set toggle related properties on non-toggle constructors to null docs: added comments to the class about named constructors fix: changed Colors.transparent to Zeta.of(context).colors.surfaceDefault test: comms goldens --- example/lib/home.dart | 2 + .../components/comms_button_example.dart | 262 +++++++++++ example/widgetbook/main.dart | 2 + .../components/comms_button_widgetbook.dart | 35 ++ .../components/comms_button/comms_button.dart | 428 ++++++++++++++++++ lib/src/components/components.dart | 1 + .../comms_button/comms_button_test.dart | 231 ++++++++++ .../golden/CommsButton_default.png | Bin 0 -> 6293 bytes .../golden/CommsButton_negative.png | Bin 0 -> 6236 bytes .../comms_button/golden/CommsButton_off.png | Bin 0 -> 6365 bytes .../comms_button/golden/CommsButton_on.png | Bin 0 -> 5444 bytes .../golden/CommsButton_positive.png | Bin 0 -> 6293 bytes .../golden/CommsButton_warning.png | Bin 0 -> 6309 bytes 13 files changed, 961 insertions(+) create mode 100644 example/lib/pages/components/comms_button_example.dart create mode 100644 example/widgetbook/pages/components/comms_button_widgetbook.dart create mode 100644 lib/src/components/comms_button/comms_button.dart create mode 100644 test/src/components/comms_button/comms_button_test.dart create mode 100644 test/src/components/comms_button/golden/CommsButton_default.png create mode 100644 test/src/components/comms_button/golden/CommsButton_negative.png create mode 100644 test/src/components/comms_button/golden/CommsButton_off.png create mode 100644 test/src/components/comms_button/golden/CommsButton_on.png create mode 100644 test/src/components/comms_button/golden/CommsButton_positive.png create mode 100644 test/src/components/comms_button/golden/CommsButton_warning.png 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 0000000000000000000000000000000000000000..3f4c2f7354bb91acf0bc3d256c9faebf71ec5d94 GIT binary patch literal 6293 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 U!yeuQEH?!I!TtO4Gk$mf51myUZvX%Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7b5a15f475d0a495fa8f922f93a238a412f3d4a6 GIT binary patch literal 6236 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 Xii1jjx_=*tcL=%PZ(r4(h>QOLqqps^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f624081394e0a9ea5997220ae4e85dc99bedfffb GIT binary patch literal 6365 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ebe1662fb5c7a06b30e292e002eebe924a511cd9 GIT binary patch literal 5444 zcmeHJZBUa}7Jm6?K|#@4MHvjN?Jn)02$YC?NLXu)Yz?3gBS?~h2oebL6+$3^)ZH<* zQJ~ATXrg9YT98DbHWDC_&@RPb5mJGWhBRQnpd={eA;|Tl=j&jFRert*{J4}JM;?UQNBrl( zANSIO38|joL-owQ2tjWe5)S<_wc?syg)j71Q0}QatThd9ezU{FrR1|;-Ep<=1$*9Q zK=(J!czHaBdFQ$%PQc&VpvWd;$Y{;^0z!dg^SkiriF&D<=lf@WewIQujgb@4V*-70 z(71)BDlG4uw#a;FCPy+2F<#_c3iXEjUFPPo{4RsuZ)vG>pyVjbM(xoGs9Gq%R zS#;}sStUZ#%`Wn8yBe;CN#uq8UkCCu#a`t4QlF=7~I5n;rm`M}+fHbUJuX+ons}pyQXXroO zlQEtZR#V$y^}@tGQKGh&Of=CiT7z zfZZAw9B-|@88C&HgQ>cnsI}Xly$r6(X?l2b>Tb_XF4t6fil@}KHo8O5-l^Qz1{pe~ zRw%wVTNn8pfgGFvIwZs}Gq1G@%-9<3?_HqpndZlk0<@%HlFJo#cfaJqI&D#NOB7Vg z7+4VEIF(TlwD1+`hV|nBQG7=V!N*U73P%tFdriT)pFg?l( zFhi<-m>XX>L9T1X?w&&)s91^0-B+!>lKZD^Op0o%ou?B{3PdbvGjh2XY<3-qinnIo z1h}Hnd|ztHRK#%qmfQQ3j@Cvu0NY5DYqqF2W=ojgWkNV*@z4C~6B|D!b>~3PqYBv8 zKt|s8AKs|;2d(6CC&^foqv%zIi{0yvR9R+zP!yPd)kXC*=fRp|^HVlK(0*>x>qq21 zGE2W>`C?scU5#TC9&Az^tMxw8``x-h^#@E>(Dz#1*r{)Y z{o8KI-*ai@WSo;nd8~@Z;>C4?x)`_Phyxy3>%jtY>FMozAA$NH>X%8c4`v3BhTd&Y zhH7u3mU9Tv}K%;H6#*wXE~`+PpF&ai5gwPoSHwQ4OeBa@6M$`O zRQ|J_f#4uzb>g8L(m#L&z~9LNfmFoZvi-uBh3DjGw8=bkCg6?-L{FiNB9{7SrR3Aq zRigIHiW`wloS!OtTg3r{FpjJ&I?hxA2I_;#SL`h1PYcT}5C-4+1S z3EGR<%g#} zRSz%Q)yY-@X$gH!q7}BsQM$X-j)KDnT(XnZS-l^E5IXPo_;G7-7 zwE+9h{RDa{T3?Q(IWu1=JeVxP?JJze7i#*9+POIaa33`K`W|1CVU#`M0zFEtg$1I~ zSj4!+i1CSB0iruH@&%le3nM8>v{juUB4D4^R8eqGV4c8igG{?!O1>4UCbT&zz(Ia9 z2|oYKaL!onvKSb-xg5(LHmi+uOKH6{e%Tags@PTP7f1?tjj?IR1yP4z9TI4~(RoyS zw(5kO)Tz{SiI&c3qjn`2`1XV{a)_09Pa=F19LDQF72W@ZBUv8F7WSy@15R7s#Y=~q zz`Qd!?B-Unkq7MZLY-4D6oU?jVks}N4aHb-6mPlUuid~ZAFa!BDGBv{pQxm*-a!N+ zt#D;C$EpL|nlC%fT>g3+KM!>@76kIP`qU%jq~F5GG!i2LiAZ&Z>bD!3ccD_69y(I9 z$HyxoxXT4uqhO2?Zw2`Pjd^5RIQtGT&ck$|H({$Pbun*cC1B+_bA)cU%AFZ~PDenT zZE2cj;aO(`oKLbj&aN6qqg7vHH}DZok<(73SyUY$1c(sp%D=YR3>X=I2?D9I+`$E5 z(_8TnMBlzj;~zai;(yQhA3`ID%8e~JP}V~9|9$WOqKviBH3ZfWSVLe9fi(oy5LiQC j4S_WT{#OJZI9fKhe?0&0~}APA8? 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 U!yeuQEH?!I!TtO4Gk$mf51myUZvX%Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..23d14fdc2829138725b2515d95d3dbde49399a6a GIT binary patch literal 6309 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 literal 0 HcmV?d00001