diff --git a/example/lib/home.dart b/example/lib/home.dart index 2c96e4fe..a122e837 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/checkbox_example.dart'; import 'package:zeta_example/pages/components/chip_example.dart'; import 'package:zeta_example/pages/components/date_input_example.dart'; +import 'package:zeta_example/pages/components/dialog_example.dart'; import 'package:zeta_example/pages/components/dialpad_example.dart'; import 'package:zeta_example/pages/components/dropdown_example.dart'; import 'package:zeta_example/pages/components/list_item_example.dart'; @@ -60,6 +61,7 @@ final List components = [ Component(RadioButtonExample.name, (context) => const RadioButtonExample()), Component(SwitchExample.name, (context) => const SwitchExample()), Component(DateInputExample.name, (context) => const DateInputExample()), + Component(DialogExample.name, (context) => const DialogExample()), ]; final List theme = [ diff --git a/example/lib/pages/components/dialog_example.dart b/example/lib/pages/components/dialog_example.dart new file mode 100644 index 00000000..66d52334 --- /dev/null +++ b/example/lib/pages/components/dialog_example.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class DialogExample extends StatelessWidget { + static const String name = 'Dialog'; + + const DialogExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final zeta = Zeta.of(context); + return ExampleScaffold( + name: 'Dialog', + child: Center( + child: Column( + children: [ + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + ), + child: Text('Show dialog with one button'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + ), + child: Text('Show dialog with two buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + tertiaryButtonLabel: 'Learn more', + onTertiaryButtonPressed: () {}, + ), + child: Text('Show dialog with three buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + title: 'Dialog Title', + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + headerAlignment: ZetaDialogHeaderAlignment.left, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + rounded: false, + ), + child: Text( + 'Show dialog with header to the left\nand sharp buttons', + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index 96a46c7f..0720feb4 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -14,6 +14,7 @@ import 'pages/components/checkbox_widgetbook.dart'; import 'pages/components/chip_widgetbook.dart'; import 'pages/components/date_input_widgetbook.dart'; import 'pages/components/dial_pad_widgetbook.dart'; +import 'pages/components/dialog_widgetbook.dart'; import 'pages/components/dropdown_widgetbook.dart'; import 'pages/components/in_page_banner_widgetbook.dart'; import 'pages/components/list_item_widgetbook.dart'; @@ -108,6 +109,7 @@ class HotReload extends StatelessWidget { builder: (context) => stepperUseCase(context), ), WidgetbookUseCase(name: 'Tabs', builder: (context) => tabsUseCase(context)), + WidgetbookUseCase(name: 'Dialog', builder: (context) => dialogUseCase(context)), ]..sort((a, b) => a.name.compareTo(b.name)), ), WidgetbookCategory( diff --git a/example/widgetbook/pages/components/dialog_widgetbook.dart b/example/widgetbook/pages/components/dialog_widgetbook.dart new file mode 100644 index 00000000..e7445e7d --- /dev/null +++ b/example/widgetbook/pages/components/dialog_widgetbook.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget dialogUseCase(BuildContext context) { + final zeta = Zeta.of(context); + final title = context.knobs.string( + label: 'Dialog title', + initialValue: 'Dialog Title', + ); + final message = context.knobs.string( + label: 'Dialog message', + initialValue: + 'Lorem ipsum dolor sit amet, conse ctetur adipiscing elit, sed do eiusm od tempor incididunt ut labore et do lore magna aliqua.', + ); + final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); + final barrierDismissible = context.knobs.boolean(label: 'Barrier dismissible', initialValue: true); + final headerAlignment = context.knobs.list( + label: 'Header alignment', + options: ZetaDialogHeaderAlignment.values, + labelBuilder: (value) => value.name, + ); + return WidgetbookTestWidget( + widget: Padding( + padding: const EdgeInsets.all(ZetaSpacing.x5), + child: Center( + child: Column( + children: [ + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + ), + child: Text('Show dialog with one button'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + ), + child: Text('Show dialog with two buttons'), + ), + TextButton( + onPressed: () => showZetaDialog( + context, + useRootNavigator: false, + zeta: zeta, + rounded: rounded, + barrierDismissible: barrierDismissible, + headerAlignment: headerAlignment, + title: title, + icon: Icon( + ZetaIcons.warning_round, + color: zeta.colors.warning, + ), + message: message, + primaryButtonLabel: 'Confirm', + secondaryButtonLabel: 'Cancel', + tertiaryButtonLabel: 'Learn more', + onTertiaryButtonPressed: () {}, + ), + child: Text('Show dialog with three buttons'), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index c6c61700..be2087cb 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -12,6 +12,7 @@ class ZetaButton extends StatelessWidget { this.type = ZetaButtonType.primary, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }); @@ -21,6 +22,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.primary; @@ -30,6 +32,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.secondary; @@ -39,6 +42,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.positive; @@ -48,6 +52,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.negative; @@ -57,6 +62,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.outline; @@ -66,6 +72,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.outlineSubtle; @@ -75,6 +82,7 @@ class ZetaButton extends StatelessWidget { this.onPressed, this.size = ZetaWidgetSize.medium, this.borderType = ZetaWidgetBorder.rounded, + this.zeta, super.key, }) : type = ZetaButtonType.text; @@ -94,6 +102,10 @@ class ZetaButton extends StatelessWidget { /// Size of the button. Defaults to large. final ZetaWidgetSize size; + /// Sometimes we need to pass Zeta from outside, + /// like for example from [showZetaDialog] + final Zeta? zeta; + /// Creates a clone. ZetaButton copyWith({ String? label, @@ -109,13 +121,15 @@ class ZetaButton extends StatelessWidget { type: type ?? this.type, size: size ?? this.size, borderType: borderType ?? this.borderType, + zeta: zeta, key: key ?? this.key, ); } @override Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; + final zeta = this.zeta ?? Zeta.of(context); + final colors = zeta.colors; return ConstrainedBox( constraints: BoxConstraints(minHeight: _minConstraints, minWidth: _minConstraints), child: FilledButton( diff --git a/lib/src/components/dialog/dialog.dart b/lib/src/components/dialog/dialog.dart new file mode 100644 index 00000000..22068383 --- /dev/null +++ b/lib/src/components/dialog/dialog.dart @@ -0,0 +1,227 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zeta_flutter.dart'; + +/// [ZetaDialogHeaderAlignment] +enum ZetaDialogHeaderAlignment { + /// [left] + left, + + /// [center] + center, +} + +/// [showZetaDialog] +Future showZetaDialog( + BuildContext context, { + Zeta? zeta, + ZetaDialogHeaderAlignment headerAlignment = ZetaDialogHeaderAlignment.center, + Widget? icon, + String? title, + required String message, + String? primaryButtonLabel, + VoidCallback? onPrimaryButtonPressed, + String? secondaryButtonLabel, + VoidCallback? onSecondaryButtonPressed, + String? tertiaryButtonLabel, + VoidCallback? onTertiaryButtonPressed, + bool rounded = true, + bool barrierDismissible = true, + bool useRootNavigator = true, +}) => + showDialog( + context: context, + barrierDismissible: barrierDismissible, + useRootNavigator: useRootNavigator, + builder: (_) => _ZetaDialog( + zeta: zeta, + headerAlignment: headerAlignment, + icon: icon, + title: title, + message: message, + primaryButtonLabel: primaryButtonLabel, + onPrimaryButtonPressed: onPrimaryButtonPressed, + secondaryButtonLabel: secondaryButtonLabel, + onSecondaryButtonPressed: onSecondaryButtonPressed, + tertiaryButtonLabel: tertiaryButtonLabel, + onTertiaryButtonPressed: onTertiaryButtonPressed, + rounded: rounded, + ), + ); + +class _ZetaDialog extends StatelessWidget { + const _ZetaDialog({ + this.headerAlignment = ZetaDialogHeaderAlignment.center, + this.icon, + this.title, + required this.message, + this.primaryButtonLabel, + this.onPrimaryButtonPressed, + this.secondaryButtonLabel, + this.onSecondaryButtonPressed, + this.tertiaryButtonLabel, + this.onTertiaryButtonPressed, + this.rounded = true, + this.zeta, + }); + + final ZetaDialogHeaderAlignment headerAlignment; + final Widget? icon; + final String? title; + final String message; + final String? primaryButtonLabel; + final VoidCallback? onPrimaryButtonPressed; + final String? secondaryButtonLabel; + final VoidCallback? onSecondaryButtonPressed; + final String? tertiaryButtonLabel; + final VoidCallback? onTertiaryButtonPressed; + final bool rounded; + final Zeta? zeta; + + @override + Widget build(BuildContext context) { + final zeta = this.zeta ?? Zeta.of(context); + final primaryButton = primaryButtonLabel == null + ? null + : ZetaButton( + zeta: zeta, + label: primaryButtonLabel!, + onPressed: onPrimaryButtonPressed ?? () => Navigator.of(context).pop(true), + borderType: rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp, + ); + final secondaryButton = secondaryButtonLabel == null + ? null + : ZetaButton.outlineSubtle( + zeta: zeta, + label: secondaryButtonLabel!, + onPressed: onSecondaryButtonPressed ?? () => Navigator.of(context).pop(false), + borderType: rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp, + ); + final tertiaryButton = tertiaryButtonLabel == null + ? null + : TextButton( + onPressed: onTertiaryButtonPressed, + child: Text(tertiaryButtonLabel!), + ); + final hasButton = primaryButton != null || secondaryButton != null || tertiaryButton != null; + + return AlertDialog( + surfaceTintColor: zeta.colors.surfacePrimary, + shape: const RoundedRectangleBorder(borderRadius: ZetaRadius.large), + title: icon != null || title != null + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: switch (headerAlignment) { + ZetaDialogHeaderAlignment.left => CrossAxisAlignment.start, + ZetaDialogHeaderAlignment.center => CrossAxisAlignment.center, + }, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(bottom: ZetaSpacing.s), + child: icon, + ), + if (title != null) + Text( + title!, + textAlign: switch (headerAlignment) { + ZetaDialogHeaderAlignment.left => TextAlign.left, + ZetaDialogHeaderAlignment.center => TextAlign.center, + }, + ), + ], + ) + : null, + titlePadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.only( + left: ZetaSpacing.x10, + right: ZetaSpacing.x10, + top: ZetaSpacing.m, + ), + titleTextStyle: zetaTextTheme.headlineSmall?.copyWith( + color: zeta.colors.textDefault, + ), + content: Text(message), + contentPadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.only( + left: ZetaSpacing.x10, + right: ZetaSpacing.x10, + top: ZetaSpacing.s, + bottom: ZetaSpacing.m, + ), + contentTextStyle: context.deviceType == DeviceType.mobilePortrait + ? zetaTextTheme.bodySmall?.copyWith(color: zeta.colors.textDefault) + : zetaTextTheme.bodyMedium?.copyWith(color: zeta.colors.textDefault), + actions: [ + if (context.deviceType == DeviceType.mobilePortrait) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (hasButton) const SizedBox(height: ZetaSpacing.m), + if (tertiaryButton == null) + Row( + children: [ + if (secondaryButton != null) Expanded(child: secondaryButton), + if (primaryButton != null && secondaryButton != null) const SizedBox(width: ZetaSpacing.b), + if (primaryButton != null) Expanded(child: primaryButton), + ], + ) + else ...[ + if (primaryButton != null) primaryButton, + if (primaryButton != null && secondaryButton != null) const SizedBox(height: ZetaSpacing.s), + if (secondaryButton != null) secondaryButton, + if (primaryButton != null || secondaryButton != null) const SizedBox(height: ZetaSpacing.xs), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [tertiaryButton], + ), + ], + ], + ) + else + Row( + children: [ + if (tertiaryButton != null) tertiaryButton, + if (primaryButton != null || secondaryButton != null) ...[ + const SizedBox(width: ZetaSpacing.m), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (secondaryButton != null) secondaryButton, + if (primaryButton != null && secondaryButton != null) const SizedBox(width: ZetaSpacing.b), + if (primaryButton != null) primaryButton, + ], + ), + ), + ], + ], + ), + ], + actionsPadding: context.deviceType == DeviceType.mobilePortrait + ? null + : const EdgeInsets.symmetric( + horizontal: ZetaSpacing.x10, + vertical: ZetaSpacing.m, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('headerAlignment', headerAlignment)) + ..add(StringProperty('title', title)) + ..add(StringProperty('message', message)) + ..add(StringProperty('primaryButtonLabel', primaryButtonLabel)) + ..add(ObjectFlagProperty.has('onPrimaryButtonPressed', onPrimaryButtonPressed)) + ..add(StringProperty('secondaryButtonLabel', secondaryButtonLabel)) + ..add(ObjectFlagProperty.has('onSecondaryButtonPressed', onSecondaryButtonPressed)) + ..add(StringProperty('tertiaryButtonLabel', tertiaryButtonLabel)) + ..add(ObjectFlagProperty.has('onTertiaryButtonPressed', onTertiaryButtonPressed)) + ..add(DiagnosticsProperty('rounded', rounded)); + } +} diff --git a/lib/src/theme/tokens.dart b/lib/src/theme/tokens.dart index 5a31e659..caecce51 100644 --- a/lib/src/theme/tokens.dart +++ b/lib/src/theme/tokens.dart @@ -111,6 +111,9 @@ class ZetaRadius { /// Border radius used when rounded parameter is true; 8px radius. static const BorderRadius rounded = BorderRadius.all(Radius.circular(ZetaSpacing.xs)); + /// Large border radius; 16px radius. + static const BorderRadius large = BorderRadius.all(Radius.circular(ZetaSpacing.b)); + /// Wide border radius; 24px radius. static const BorderRadius wide = BorderRadius.all(Radius.circular(ZetaSpacing.m)); diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index bca4aef8..0ff913b1 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -24,6 +24,7 @@ export 'src/components/checkbox/checkbox.dart'; export 'src/components/chips/chip.dart'; export 'src/components/date_input/date_input.dart'; export 'src/components/dial_pad/dial_pad.dart'; +export 'src/components/dialog/dialog.dart'; export 'src/components/dropdown/dropdown.dart'; export 'src/components/list_item/list_item.dart'; export 'src/components/navigation bar/navigation_bar.dart';