Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-voices): alert dialog #851

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions catalyst_voices/lib/widgets/avatars/voices_avatar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class VoicesAvatar extends StatelessWidget {
/// The size of the avatar, expressed as the radius (half the diameter).
final double radius;

/// The border around the widget.
final Border? border;

/// The callback called when the widget is tapped.
final VoidCallback? onTap;

Expand All @@ -32,15 +35,21 @@ class VoicesAvatar extends StatelessWidget {
this.backgroundColor,
this.padding = const EdgeInsets.all(8),
this.radius = 20,
this.border,
this.onTap,
});

@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: radius,
backgroundColor:
backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
return Container(
width: radius * 2,
height: radius * 2,
decoration: BoxDecoration(
color:
backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(radius),
border: border,
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
Expand Down
163 changes: 163 additions & 0 deletions catalyst_voices/lib/widgets/modals/voices_alert_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:flutter/material.dart';

/// An alert dialog similar to [AlertDialog]
/// but customized to the project needs.
///
/// On extra small screens (mobile) it will fill the whole screen width,
/// on larger screens it will take [_width] amount
/// of horizontal space and be centered.
///
/// The close (x) button will appear if the dialog [isDismissible].
class VoicesAlertDialog extends StatelessWidget {
static const double _width = 360;

/// The widget which appears at the top of the dialog next to the (x) button.
/// Usually a [Text] widget.
final Widget? title;

/// The widget which appears below the [title],
/// usually a [VoicesAvatar] widget.
final Widget? icon;

/// The widget appears below the [icon], is less prominent than the [title].
/// Usually a [Text] widget.
final Widget? subtitle;

/// The widget appears below the [subtitle], usually a [Text] widget,
/// can be multiline.
final Widget? content;

/// The list of widgets which appear at the bottom of the dialog,
/// usually [VoicesFilledButton] or [VoicesTextButton].
///
/// [buttons] are separated with 8px of padding between each other
/// so you don't need to add your own padding.
final List<Widget> buttons;

/// Whether to show a (x) close button.
final bool isDismissible;

const VoicesAlertDialog({
super.key,
this.title,
this.icon,
this.subtitle,
this.content,
this.buttons = const [],
this.isDismissible = true,
});

@override
Widget build(BuildContext context) {
final title = this.title;
final icon = this.icon;
final subtitle = this.subtitle;
final content = this.content;

return ResponsiveBuilder<double>(
xs: double.infinity,
other: _width,
builder: (context, width) {
return Dialog(
alignment: Alignment.center,
child: SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (title != null || isDismissible)
Row(
children: [
// if widget is dismissible then show an invisible
// close button to reserve space on this side of the
// row so that the title is centered
if (isDismissible)
const Visibility(
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: _CloseButton(),
),
Expanded(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.titleLarge!,
textAlign: TextAlign.center,
child: title ?? const SizedBox.shrink(),
),
),
if (isDismissible) const _CloseButton(),
],
),
if (icon != null)
Padding(
padding: const EdgeInsets.only(
top: 24,
left: 20,
right: 20,
),
child: Center(child: icon),
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 20,
right: 20,
),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.titleSmall!,
textAlign: TextAlign.center,
child: subtitle,
),
),
if (content != null)
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 20,
right: 20,
),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
textAlign: TextAlign.center,
child: content,
),
),
if (buttons.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
...buttons.separatedBy(const SizedBox(height: 8)),
],
),
),
],
),
),
),
);
},
);
}
}

class _CloseButton extends StatelessWidget {
const _CloseButton();

@override
Widget build(BuildContext context) {
return XButton(
onTap: () => Navigator.of(context).pop(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ class VoicesDesktopPanelsDialog extends StatelessWidget {
Expanded(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.colors.elevationsOnSurfaceNeutralLv1White,
),
child: right,
),
),
Expand Down
8 changes: 6 additions & 2 deletions catalyst_voices/lib/widgets/modals/voices_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import 'package:flutter/material.dart';
/// meant to be extended.
abstract final class VoicesDialog {
/// Encapsulates single entry point.
static Future<T?> show<T>(
BuildContext context, {
static Future<T?> show<T>({
required BuildContext context,
required WidgetBuilder builder,
RouteSettings? routeSettings,
bool barrierDismissible = true,
}) {
return showDialog<T>(
context: context,
builder: builder,
routeSettings: routeSettings,
barrierDismissible: barrierDismissible,
);
}
}
10 changes: 5 additions & 5 deletions catalyst_voices/lib/widgets/modals/voices_info_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
/// Call [VoicesDialog.show] with [VoicesDesktopInfoDialog] in order
/// to show it.
class VoicesDesktopInfoDialog extends StatelessWidget {
final String title;
final Widget title;

const VoicesDesktopInfoDialog({
super.key,
Expand All @@ -26,10 +26,10 @@ class VoicesDesktopInfoDialog extends StatelessWidget {
left: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge
?.copyWith(color: theme.colors.textOnPrimary),
DefaultTextStyle(
style: theme.textTheme.titleLarge!
.copyWith(color: theme.colors.textOnPrimary),
child: title,
),
],
),
Expand Down
1 change: 1 addition & 0 deletions catalyst_voices/lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export 'menu/voices_list_tile.dart';
export 'menu/voices_menu.dart';
export 'menu/voices_node_menu.dart';
export 'menu/voices_wallet_tile.dart';
export 'modals/voices_alert_dialog.dart';
export 'modals/voices_desktop_dialog.dart';
export 'modals/voices_dialog.dart';
export 'modals/voices_info_dialog.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ ThemeData _buildThemeData(
barrierColor: const Color(0x612A3D61),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.hardEdge,
backgroundColor: voicesColorScheme.onSurfaceNeutralOpaqueLv0,
backgroundColor: voicesColorScheme.elevationsOnSurfaceNeutralLv1White,
),
listTileTheme: ListTileThemeData(
shape: const StadiumBorder(),
Expand Down
40 changes: 21 additions & 19 deletions catalyst_voices/test/widgets/avatars/voices_avatar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ void main() {
),
);

// Verify if CircleAvatar is rendered with the correct default radius.
final circleAvatarFinder = find.byType(CircleAvatar);
expect(circleAvatarFinder, findsOneWidget);
// Verify if Container is rendered with the correct default radius.
final containerFinder = find.byType(Container);
expect(containerFinder, findsOneWidget);

final circleAvatarWidget =
tester.widget<CircleAvatar>(circleAvatarFinder);
expect(circleAvatarWidget.radius, 20);
final containerWidget = tester.widget<Container>(containerFinder);
expect(containerWidget.constraints?.maxWidth, 40);

// Verify the icon is rendered.
expect(find.byIcon(Icons.person), findsOneWidget);
Expand All @@ -40,11 +39,10 @@ void main() {
),
);

// Verify if CircleAvatar is rendered with the correct custom radius.
final circleAvatarFinder = find.byType(CircleAvatar);
final circleAvatarWidget =
tester.widget<CircleAvatar>(circleAvatarFinder);
expect(circleAvatarWidget.radius, 30);
// Verify if Container is rendered with the correct custom radius.
final containerFinder = find.byType(Container);
final containerWidget = tester.widget<Container>(containerFinder);
expect(containerWidget.constraints?.maxWidth, 60);

// Verify the Padding is applied correctly.
final paddingFinder = find.ancestor(
Expand Down Expand Up @@ -73,10 +71,12 @@ void main() {
);

// Verify the background color is correctly applied.
final circleAvatarFinder = find.byType(CircleAvatar);
final circleAvatarWidget =
tester.widget<CircleAvatar>(circleAvatarFinder);
expect(circleAvatarWidget.backgroundColor, backgroundColor);
final containerFinder = find.byType(Container);
final containerWidget = tester.widget<Container>(containerFinder);
expect(
(containerWidget.decoration! as BoxDecoration).color,
backgroundColor,
);

// Verify the foreground color is correctly applied to the icon.
final iconThemeFinder = find.ancestor(
Expand Down Expand Up @@ -132,10 +132,12 @@ void main() {
);

// Verify the background color is from the theme's primaryContainer.
final circleAvatarFinder = find.byType(CircleAvatar);
final circleAvatarWidget =
tester.widget<CircleAvatar>(circleAvatarFinder);
expect(circleAvatarWidget.backgroundColor, Colors.blueGrey);
final containerFinder = find.byType(Container);
final containerWidget = tester.widget<Container>(containerFinder);
expect(
(containerWidget.decoration! as BoxDecoration).color,
Colors.blueGrey,
);

// Verify the foreground color is from the theme's primary.
final iconThemeFinder = find.byType(IconTheme);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class VoicesAvatarExample extends StatelessWidget {
VoicesAvatar(
icon: VoicesAssets.icons.check.buildIcon(),
),
VoicesAvatar(
backgroundColor: Colors.transparent,
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
icon: VoicesAssets.icons.check.buildIcon(),
),
VoicesAvatar(
icon: const Text('A'),
onTap: () {},
Expand Down
Loading
Loading