feat: implement modal (#7750)

This commit is contained in:
Richard Shiue 2025-04-15 17:02:08 +08:00 committed by GitHub
parent ed62f25086
commit 32afd5a194
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 404 additions and 6 deletions

View file

@ -0,0 +1,109 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
typedef SimpleAFDialogAction = (String, void Function(BuildContext)?);
/// A simple dialog with a title, content, and actions.
///
/// The primary button is a filled button and colored using theme or destructive
/// color depending on the [isDestructive] parameter. The secondary button is an
/// outlined button.
///
Future<void> showSimpleAFDialog({
required BuildContext context,
required String title,
required String content,
bool isDestructive = false,
required SimpleAFDialogAction primaryAction,
SimpleAFDialogAction? secondaryAction,
bool barrierDismissible = true,
}) {
final theme = AppFlowyTheme.of(context);
return showDialog(
context: context,
barrierColor: theme.surfaceColorScheme.overlay,
barrierDismissible: barrierDismissible,
builder: (_) {
return AFModal(
constraints: BoxConstraints(
maxWidth: AFModalDimension.S,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AFModalHeader(
leading: Text(
title,
style: theme.textStyle.heading.h4(
color: theme.textColorScheme.primary,
),
),
trailing: [
AFGhostButton.normal(
onTap: () => Navigator.of(context).pop(),
builder: (context, isHovering, disabled) {
return FlowySvg(
FlowySvgs.close_s,
size: Size.square(20),
);
},
),
],
),
Flexible(
child: ConstrainedBox(
// AFModalDimension.dialogHeight - header - footer
constraints: BoxConstraints(minHeight: 108.0),
child: AFModalBody(
child: Text(content),
),
),
),
AFModalFooter(
trailing: [
if (secondaryAction != null)
AFOutlinedButton.normal(
onTap: () {
secondaryAction.$2?.call(context);
Navigator.of(context).pop();
},
builder: (context, isHovering, disabled) {
return Text(secondaryAction.$1);
},
),
isDestructive
? AFFilledButton.destructive(
onTap: () {
primaryAction.$2?.call(context);
Navigator.of(context).pop();
},
builder: (context, isHovering, disabled) {
return Text(
primaryAction.$1,
style: TextStyle(
color: AppFlowyTheme.of(context)
.textColorScheme
.onFill,
),
);
},
)
: AFFilledButton.primary(
onTap: () {
primaryAction.$2?.call(context);
Navigator.of(context).pop();
},
builder: (context, isHovering, disabled) {
return Text(primaryAction.$1);
},
),
],
),
],
),
);
},
);
}

View file

@ -1,8 +1,10 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:appflowy_ui_example/src/buttons/buttons_page.dart';
import 'package:appflowy_ui_example/src/textfield/textfield_page.dart';
import 'package:flutter/material.dart';
import 'src/buttons/buttons_page.dart';
import 'src/modal/modal_page.dart';
import 'src/textfield/textfield_page.dart';
enum ThemeMode {
light,
dark,
@ -60,6 +62,7 @@ class _MyHomePageState extends State<MyHomePage> {
final tabs = [
Tab(text: 'Button'),
Tab(text: 'TextField'),
Tab(text: 'Modal'),
];
@override
@ -92,6 +95,7 @@ class _MyHomePageState extends State<MyHomePage> {
children: [
ButtonsPage(),
TextFieldPage(),
ModalPage(),
],
),
bottomNavigationBar: TabBar(

View file

@ -0,0 +1,153 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
class ModalPage extends StatefulWidget {
const ModalPage({super.key});
@override
State<ModalPage> createState() => _ModalPageState();
}
class _ModalPageState extends State<ModalPage> {
double width = AFModalDimension.M;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Center(
child: Container(
constraints: BoxConstraints(maxWidth: 600),
padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl),
child: Column(
spacing: theme.spacing.l,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
spacing: theme.spacing.m,
mainAxisSize: MainAxisSize.min,
children: [
AFGhostButton.normal(
onTap: () => setState(() => width = AFModalDimension.S),
builder: (context, isHovering, disabled) {
return Text(
'S',
style: TextStyle(
color: width == AFModalDimension.S
? theme.textColorScheme.theme
: theme.textColorScheme.primary,
),
);
},
),
AFGhostButton.normal(
onTap: () => setState(() => width = AFModalDimension.M),
builder: (context, isHovering, disabled) {
return Text(
'M',
style: TextStyle(
color: width == AFModalDimension.M
? theme.textColorScheme.theme
: theme.textColorScheme.primary,
),
);
},
),
AFGhostButton.normal(
onTap: () => setState(() => width = AFModalDimension.L),
builder: (context, isHovering, disabled) {
return Text(
'L',
style: TextStyle(
color: width == AFModalDimension.L
? theme.textColorScheme.theme
: theme.textColorScheme.primary,
),
);
},
),
],
),
AFFilledButton.primary(
builder: (context, isHovering, disabled) {
return Text(
'Show Modal',
style: TextStyle(
color: AppFlowyTheme.of(context).textColorScheme.onFill,
),
);
},
onTap: () {
showDialog(
context: context,
barrierColor: theme.surfaceColorScheme.overlay,
builder: (context) {
final theme = AppFlowyTheme.of(context);
return Center(
child: AFModal(
constraints: BoxConstraints(
maxWidth: width,
maxHeight: AFModalDimension.dialogHeight,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AFModalHeader(
leading: Text(
'Header',
style: theme.textStyle.heading.h4(
color: theme.textColorScheme.primary,
),
),
trailing: [
AFGhostButton.normal(
onTap: () => Navigator.of(context).pop(),
builder: (context, isHovering, disabled) {
return const Icon(Icons.close);
},
)
],
),
Expanded(
child: AFModalBody(
child: Text(
'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'),
),
),
AFModalFooter(
trailing: [
AFOutlinedButton.normal(
onTap: () => Navigator.of(context).pop(),
builder: (context, isHovering, disabled) {
return const Text('Cancel');
},
),
AFFilledButton.primary(
onTap: () => Navigator.of(context).pop(),
builder: (context, isHovering, disabled) {
return Text(
'Apply',
style: TextStyle(
color: AppFlowyTheme.of(context)
.textColorScheme
.onFill,
),
);
},
),
],
)
],
)),
);
},
);
},
),
],
),
),
);
}
}

View file

@ -144,10 +144,7 @@ class _AFBaseButtonState extends State<AFBaseButton> {
}
if (isFocused) {
return AppFlowyTheme.of(context)
.borderColorScheme
.themeThick
.withAlpha(128);
return theme.borderColorScheme.themeThick.withAlpha(128);
}
return theme.borderColorScheme.transparent;

View file

@ -1,2 +1,3 @@
export 'button/button.dart';
export 'modal/modal.dart';
export 'textfield/textfield.dart';

View file

@ -0,0 +1,9 @@
class AFModalDimension {
const AFModalDimension._();
static const double S = 400.0;
static const double M = 560.0;
static const double L = 720.0;
static const double dialogHeight = 200.0;
}

View file

@ -0,0 +1,125 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flutter/material.dart';
export 'dimension.dart';
class AFModal extends StatelessWidget {
const AFModal({
super.key,
this.constraints = const BoxConstraints(),
required this.child,
});
final BoxConstraints constraints;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Center(
child: Padding(
padding: EdgeInsets.all(theme.spacing.xl),
child: ConstrainedBox(
constraints: constraints,
child: DecoratedBox(
decoration: BoxDecoration(
boxShadow: theme.shadow.medium,
borderRadius: BorderRadius.circular(theme.borderRadius.xl),
color: theme.surfaceColorScheme.primary,
),
child: Material(
color: Colors.transparent,
child: child,
),
),
),
),
);
}
}
class AFModalHeader extends StatelessWidget {
const AFModalHeader({
super.key,
required this.leading,
this.trailing = const [],
});
final Widget leading;
final List<Widget> trailing;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Padding(
padding: EdgeInsets.only(
top: theme.spacing.xl,
left: theme.spacing.xxl,
right: theme.spacing.xxl,
),
child: Row(
spacing: theme.spacing.s,
children: [
Expanded(child: leading),
...trailing,
],
),
);
}
}
class AFModalFooter extends StatelessWidget {
const AFModalFooter({
super.key,
this.leading = const [],
this.trailing = const [],
});
final List<Widget> leading;
final List<Widget> trailing;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Padding(
padding: EdgeInsets.only(
bottom: theme.spacing.xl,
left: theme.spacing.xxl,
right: theme.spacing.xxl,
),
child: Row(
spacing: theme.spacing.l,
children: [
...leading,
Spacer(),
...trailing,
],
),
);
}
}
class AFModalBody extends StatelessWidget {
const AFModalBody({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Padding(
padding: EdgeInsets.symmetric(
vertical: theme.spacing.l,
horizontal: theme.spacing.xxl,
),
child: child,
);
}
}