mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-18 20:05:05 -04:00
feat: implement modal (#7750)
This commit is contained in:
parent
ed62f25086
commit
32afd5a194
7 changed files with 404 additions and 6 deletions
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export 'button/button.dart';
|
||||
export 'modal/modal.dart';
|
||||
export 'textfield/textfield.dart';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue