diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 4a8905937..8ddc61604 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -22,10 +22,6 @@ class Application extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const ExampleWidget(), - FButton( - text: 'Hi', - onPress: () {}, - ), ], ), ), @@ -41,176 +37,39 @@ class ExampleWidget extends StatefulWidget { } class _ExampleWidgetState extends State { - late WidgetStatesController controller; - late TextEditingController textController; @override void initState() { super.initState(); - controller = WidgetStatesController(); - textController = TextEditingController()..addListener(() {}); - } - - Widget _dialog(BuildContext context) { - final style = context.theme.cardStyle; - final contentStyle = style.content; - final typography = context.theme.typography; - final effectivePadding = - MediaQuery.viewInsetsOf(context) + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0); - - final child = Align( - child: DefaultTextStyle( - style: context.theme.typography.toTextStyle( - fontSize: typography.base, - color: context.theme.colorScheme.foreground, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 280.0), - child: DecoratedBox( - decoration: style.decoration, - child: Padding( - padding: style.content.padding, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('Are you absolutely sure?', style: contentStyle.title.scale(typography)), - SizedBox( - height: 10, - ), - Text( - 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.', - style: contentStyle.subtitle.scale(typography), textAlign: TextAlign.center,), - SizedBox( - height: 18, - ), - LayoutBuilder( - builder: (context, constraints) { - // Get the width of the screen - final screenWidth = MediaQuery.sizeOf(context).width; - - // Calculate the combined width of the buttons - final buttonWidth = constraints.maxWidth; - - print(screenWidth); - print(buttonWidth); - - // If the combined width of the buttons exceeds the screen width, place them in a Column - if (true) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 7.0), - child: FButton(text: 'Continue', onPress: () {}), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 7.0), - child: FButton(design: FButtonVariant.outlined, text: 'Cancel', onPress: () {}), - ), - ], - ); - } else { - // Otherwise, place them in a Row - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 7.0), - child: FButton(design: FButtonVariant.outlined, text: 'Cancel', onPress: () {}), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 7.0), - child: FButton(text: 'Continue', onPress: () {}), - ), - ), - ], - ); - } - }, - ), - ], - ), - ), - )), - ), - ); - - return AnimatedPadding( - padding: effectivePadding, - duration: const Duration(milliseconds: 100), - curve: Curves.decelerate, - child: MediaQuery.removeViewInsets( - removeLeft: true, - removeTop: true, - removeRight: true, - removeBottom: true, - context: context, - child: child, - ), - ); } @override - Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showAdaptiveDialog( - context: context, - builder: (context) { - final style = context.theme.cardStyle; - return _dialog(context); - - // return AlertDialog() - // return Dialog(child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text('Are you absolutely sure?', style: contentStyle.title.scale(typography)), - // Text('This action cannot be undone. This will permanently delete your account and remove your data from our servers.', style: contentStyle.subtitle.scale(typography)), - // Row( - // children: [ - // FButton(design: FButtonVariant.secondary, text: 'Cancel', onPress: () {}), - // FButton(text: 'Continue', onPress: () {}), - // ], - // ) - // ], - // ),); - }, - ); - }); - return Padding( + Widget build(BuildContext context) => Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ - // FCard(title: 'Email'), - FTextField( - // enabled: false, - labelText: 'Email', - hintText: 'hannah@foruslabs.com', - helpText: 'This is your public display name.', - // errorText: 'Error', - // statesController: controller, - controller: textController, + FButton( + design: FButtonVariant.destructive, + text: 'Delete?', + onPress: () => showAdaptiveDialog( + context: context, + builder: (context) { + final style = context.theme.cardStyle; + return FDialog( + title: 'Are you absolutely sure?', + subtitle: 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.', + actions: [ + FButton(text: 'Continue', onPress: () {}), + FButton(design: FButtonVariant.outlined, text: 'Cancel', onPress: () { + Navigator.of(context).pop(); + }), + ], + ); + // return _dialog(context); + }, + ), ), - // const TextField( - // // enabled: false, - // decoration: InputDecoration( - // labelText: 'Material TextField', - // hintText: 'Email', - // errorText: 'Error text', - // ), - // ), ], ), ); - } - - @override - void dispose() { - super.dispose(); - controller.dispose(); - textController.dispose(); - } } diff --git a/forui/lib/src/theme/theme_data.dart b/forui/lib/src/theme/theme_data.dart index 33e3833d7..1a5719cd8 100644 --- a/forui/lib/src/theme/theme_data.dart +++ b/forui/lib/src/theme/theme_data.dart @@ -68,7 +68,7 @@ class FThemeData with Diagnosticable { style: style, ), cardStyle = FCardStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), - dialogStyle = FDialogStyle.inherit(colorScheme: colorScheme, typography: typography), + dialogStyle = FDialogStyle.inherit(style: style, colorScheme: colorScheme, typography: typography), headerStyle = FHeaderStyle.inherit(colorScheme: colorScheme, typography: typography), textFieldStyle = FTextFieldStyle.inherit(colorScheme: colorScheme, typography: typography, style: style), boxStyle = FBoxStyle.inherit(colorScheme: colorScheme), diff --git a/forui/lib/src/widgets/button/button.dart b/forui/lib/src/widgets/button/button.dart index dcc1d9ca8..28a4e96a3 100644 --- a/forui/lib/src/widgets/button/button.dart +++ b/forui/lib/src/widgets/button/button.dart @@ -7,13 +7,8 @@ import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; part 'button_content.dart'; - -part 'button_style.dart'; - part 'button_styles.dart'; -part 'button_content_style.dart'; - /// A button. class FButton extends StatelessWidget { /// The design. Defaults to [FBadgeVariant.primary]. @@ -120,3 +115,60 @@ class FButton extends StatelessWidget { ..add(DiagnosticsProperty('builder', builder)); } } + +/// The button design. Either a pre-defined [FButtonVariant], or a custom [FButtonStyle]. +sealed class FButtonDesign {} + +/// A pre-defined button variant. +enum FButtonVariant implements FButtonDesign { + /// A primary-styled button. + primary, + + /// A secondary-styled button. + secondary, + + /// An outlined button. + outlined, + + /// A destructive button. + destructive, +} + +/// Represents the theme data that is inherited by [FButtonStyle] and used by child [FButton]. +class FButtonStyle extends FButtonDesign with Diagnosticable{ + /// The content. + final FButtonContentStyle content; + + /// The box decoration for an enabled button. + final BoxDecoration enabledBoxDecoration; + + /// The box decoration for a disabled button. + final BoxDecoration disabledBoxDecoration; + + /// Creates a [FButtonStyle]. + FButtonStyle({ + required this.content, + required this.enabledBoxDecoration, + required this.disabledBoxDecoration, + }); + + /// Creates a copy of this [FButtonStyle] with the given properties replaced. + FButtonStyle copyWith({ + FButtonContentStyle? content, + BoxDecoration? enabledBoxDecoration, + BoxDecoration? disabledBoxDecoration, + }) => + FButtonStyle( + content: content ?? this.content, + enabledBoxDecoration: enabledBoxDecoration ?? this.enabledBoxDecoration, + disabledBoxDecoration: disabledBoxDecoration ?? this.disabledBoxDecoration, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties..add(DiagnosticsProperty('content', content)) + ..add(DiagnosticsProperty('enabledBoxDecoration', enabledBoxDecoration)) + ..add(DiagnosticsProperty('disabledBoxDecoration', disabledBoxDecoration)); + } +} diff --git a/forui/lib/src/widgets/button/button_content.dart b/forui/lib/src/widgets/button/button_content.dart index 1fe033807..27df54eb4 100644 --- a/forui/lib/src/widgets/button/button_content.dart +++ b/forui/lib/src/widgets/button/button_content.dart @@ -48,3 +48,60 @@ part of 'button.dart'; ..add(DiagnosticsProperty('icon', icon)); } } + +/// [FButtonContent]'s style. +class FButtonContentStyle with Diagnosticable { + /// The [TextStyle] when this button is enabled. + final TextStyle enabledText; + + /// The [TextStyle] when this button is disabled. + final TextStyle disabledText; + + /// The icon's color when this button is enabled. + final Color enabledIcon; + + /// The icon's color when this button is disabled. + final Color disabledIcon; + + /// The padding. + final EdgeInsets padding; + + /// Creates a [FButtonContentStyle]. + FButtonContentStyle({ + required this.enabledText, + required this.disabledText, + required this.enabledIcon, + required this.disabledIcon, + required this.padding, + }); + + /// Creates a [FButtonContentStyle] that inherits its properties from the given [foreground] and [disabledForeground]. + FButtonContentStyle.inherit({ required FTypography typography,required Color foreground, required Color disabledForeground}) + : padding = const EdgeInsets.symmetric( + horizontal: 20, + vertical: 15, + ), + enabledText = TextStyle( + fontSize: typography.base, + fontWeight: FontWeight.w600, + color: foreground, + ), + disabledText = TextStyle( + fontSize: typography.base, + fontWeight: FontWeight.w600, + color: disabledForeground, + ), + enabledIcon = foreground, + disabledIcon = disabledForeground; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('enabledText', enabledText)) + ..add(DiagnosticsProperty('disabledText', disabledText)) + ..add(DiagnosticsProperty('enabledIcon', enabledIcon)) + ..add(DiagnosticsProperty('disabledIcon', disabledIcon)) + ..add(DiagnosticsProperty('padding', padding)); + } +} diff --git a/forui/lib/src/widgets/button/button_content_style.dart b/forui/lib/src/widgets/button/button_content_style.dart deleted file mode 100644 index 85fa98468..000000000 --- a/forui/lib/src/widgets/button/button_content_style.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of 'button.dart'; - -/// [FButtonContent]'s style. -class FButtonContentStyle with Diagnosticable { - /// The [TextStyle] when this button is enabled. - final TextStyle enabledText; - - /// The [TextStyle] when this button is disabled. - final TextStyle disabledText; - - /// The icon's color when this button is enabled. - final Color enabledIcon; - - /// The icon's color when this button is disabled. - final Color disabledIcon; - - /// The padding. - final EdgeInsets padding; - - /// Creates a [FButtonContentStyle]. - FButtonContentStyle({ - required this.enabledText, - required this.disabledText, - required this.enabledIcon, - required this.disabledIcon, - required this.padding, - }); - - /// Creates a [FButtonContentStyle] that inherits its properties from the given [foreground] and [disabledForeground]. - FButtonContentStyle.inherit({ required FTypography typography,required Color foreground, required Color disabledForeground}) - : padding = const EdgeInsets.symmetric( - horizontal: 5, - vertical: 17, - ), - enabledText = TextStyle( - fontSize: typography.base, - fontWeight: FontWeight.w600, - color: foreground, - ), - disabledText = TextStyle( - fontSize: typography.base, - fontWeight: FontWeight.w600, - color: disabledForeground, - ), - enabledIcon = foreground, - disabledIcon = disabledForeground; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('enabledText', enabledText)) - ..add(DiagnosticsProperty('disabledText', disabledText)) - ..add(DiagnosticsProperty('enabledIcon', enabledIcon)) - ..add(DiagnosticsProperty('disabledIcon', disabledIcon)) - ..add(DiagnosticsProperty('padding', padding)); - } -} diff --git a/forui/lib/src/widgets/button/button_style.dart b/forui/lib/src/widgets/button/button_style.dart deleted file mode 100644 index ef852a08a..000000000 --- a/forui/lib/src/widgets/button/button_style.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of 'button.dart'; - -/// The button design. Either a pre-defined [FButtonVariant], or a custom [FButtonStyle]. -sealed class FButtonDesign {} - -/// A pre-defined button variant. -enum FButtonVariant implements FButtonDesign { - /// A primary-styled button. - primary, - - /// A secondary-styled button. - secondary, - - /// An outlined button. - outlined, - - /// A destructive button. - destructive, -} - -/// Represents the theme data that is inherited by [FButtonStyle] and used by child [FButton]. -class FButtonStyle extends FButtonDesign with Diagnosticable{ - /// The content. - final FButtonContentStyle content; - - /// The box decoration for an enabled button. - final BoxDecoration enabledBoxDecoration; - - /// The box decoration for a disabled button. - final BoxDecoration disabledBoxDecoration; - - /// Creates a [FButtonStyle]. - FButtonStyle({ - required this.content, - required this.enabledBoxDecoration, - required this.disabledBoxDecoration, - }); - - /// Creates a copy of this [FButtonStyle] with the given properties replaced. - FButtonStyle copyWith({ - FButtonContentStyle? content, - BoxDecoration? enabledBoxDecoration, - BoxDecoration? disabledBoxDecoration, - }) => - FButtonStyle( - content: content ?? this.content, - enabledBoxDecoration: enabledBoxDecoration ?? this.enabledBoxDecoration, - disabledBoxDecoration: disabledBoxDecoration ?? this.disabledBoxDecoration, - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties..add(DiagnosticsProperty('content', content)) - ..add(DiagnosticsProperty('enabledBoxDecoration', enabledBoxDecoration)) - ..add(DiagnosticsProperty('disabledBoxDecoration', disabledBoxDecoration)); - } -} diff --git a/forui/lib/src/widgets/dialog/dialog.dart b/forui/lib/src/widgets/dialog/dialog.dart index 33ac53b5e..79d0260ca 100644 --- a/forui/lib/src/widgets/dialog/dialog.dart +++ b/forui/lib/src/widgets/dialog/dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:meta/meta.dart'; +import 'package:sugar/collection.dart'; part 'dialog_content.dart'; @@ -30,6 +32,14 @@ class FDialog extends StatelessWidget { /// Defaults to [Curves.decelerate]. final Curve insetAnimationCurve; + /// The semantic label of the dialog used by accessibility frameworks to announce screen transitions when the dialog + /// is opened and closed. + /// + /// See also: + /// * [SemanticsConfiguration.namesRoute], for a description of how this + /// value is used. + final String? semanticLabel; + /// The builder. final Widget Function(BuildContext, FDialogStyle) builder; @@ -39,24 +49,27 @@ class FDialog extends StatelessWidget { this.style, this.insetAnimationDuration = const Duration(milliseconds: 100), this.insetAnimationCurve = Curves.decelerate, + String? semanticLabel, String? title, String? subtitle, FDialogAlignment alignment = FDialogAlignment.vertical, super.key, - }): builder = switch (alignment) { - FDialogAlignment.horizontal => (context, style) => FHorizontalDialogContent( - style: style.horizontal, - title: title, - subtitle: subtitle, - actions: actions, - ), - FDialogAlignment.vertical => (context, style) => FVerticalDialogContent( - style: style.vertical, - title: title, - subtitle: subtitle, - actions: actions, - ), - }; + }): + semanticLabel = semanticLabel ?? title ?? subtitle, + builder = switch (alignment) { + FDialogAlignment.horizontal => (context, style) => FHorizontalDialogContent( + style: style.horizontal, + title: title, + subtitle: subtitle, + actions: actions, + ), + FDialogAlignment.vertical => (context, style) => FVerticalDialogContent( + style: style.vertical, + title: title, + subtitle: subtitle, + actions: actions, + ), + }; /// Creates a [FDialog] with a custom builder. const FDialog.raw({ @@ -64,6 +77,7 @@ class FDialog extends StatelessWidget { this.style, this.insetAnimationDuration = const Duration(milliseconds: 100), this.insetAnimationCurve = Curves.decelerate, + this.semanticLabel, super.key, }); @@ -89,11 +103,20 @@ class FDialog extends StatelessWidget { fontSize: typography.base, color: context.theme.colorScheme.foreground, ), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 280.0), - child: DecoratedBox( - decoration: style.decoration, - child: builder(context, style), + child: Semantics( + scopesRoute: true, + explicitChildNodes: true, + namesRoute: true, + label: semanticLabel, + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 280.0, + maxWidth: 560.0, + ), + child: DecoratedBox( + decoration: style.decoration, + child: builder(context, style), + ), ), ), ), @@ -109,6 +132,7 @@ class FDialog extends StatelessWidget { ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('insetAnimationDuration', insetAnimationDuration)) ..add(DiagnosticsProperty('insetAnimationCurve', insetAnimationCurve)) + ..add(StringProperty('semanticLabel', semanticLabel)) ..add(DiagnosticsProperty('builder', builder)); } } @@ -136,13 +160,24 @@ final class FDialogStyle with Diagnosticable { }); /// Creates a [FDialogStyle] that inherits its properties from [colorScheme]. - FDialogStyle.inherit({required FColorScheme colorScheme, required FTypography typography}): + FDialogStyle.inherit({required FStyle style, required FColorScheme colorScheme, required FTypography typography}): decoration = BoxDecoration( + borderRadius: style.borderRadius, color: colorScheme.background, ), - insetPadding = const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), - horizontal = FDialogContentStyle.inherit(colorScheme: colorScheme, typography: typography), - vertical = FDialogContentStyle.inherit(colorScheme: colorScheme, typography: typography); + insetPadding = const EdgeInsets.symmetric(horizontal: 40, vertical: 24), + horizontal = FDialogContentStyle.inherit( + colorScheme: colorScheme, + typography: typography, + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), + actionPadding: 10, + ), + vertical = FDialogContentStyle.inherit( + colorScheme: colorScheme, + typography: typography, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 25), + actionPadding: 8, + ); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/forui/lib/src/widgets/dialog/dialog_content.dart b/forui/lib/src/widgets/dialog/dialog_content.dart index f6a2fa034..5c414fd13 100644 --- a/forui/lib/src/widgets/dialog/dialog_content.dart +++ b/forui/lib/src/widgets/dialog/dialog_content.dart @@ -23,32 +23,40 @@ part of 'dialog.dart'; @override Widget build(BuildContext context) { final typography = context.theme.typography; - return Padding( - padding: style.padding, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: alignment, - children: [ - if (title != null) - Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Text( - title!, - style: style.title.scale(typography), - textAlign: titleTextAlign, + return IntrinsicWidth( + child: Padding( + padding: style.padding, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: alignment, + children: [ + if (title != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Semantics( + container: true, + child: Text( + title!, + style: style.title.scale(typography), + textAlign: titleTextAlign, + ), + ), ), - ), - if (subtitle != null) - Padding( - padding: const EdgeInsets.only(bottom: 18), - child: Text( - subtitle!, - style: style.subtitle.scale(typography), - textAlign: subtitleTextAlign, + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Semantics( + container: true, + child: Text( + subtitle!, + style: style.subtitle.scale(typography), + textAlign: subtitleTextAlign, + ), + ), ), - ), - _actions(context), - ], + _actions(context), + ], + ), ), ); } @@ -83,17 +91,16 @@ part of 'dialog.dart'; @override Widget _actions(BuildContext context) => Row( - mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, - children: [ - for (final action in actions) - Expanded( - child: Padding( - padding: style.actionPadding, + children: separate( + [ + for (final action in actions) + IntrinsicWidth( child: action, ), - ), - ], + ], + by: [ SizedBox(width: style.actionPadding) ], + ), ); } @@ -113,13 +120,10 @@ part of 'dialog.dart'; @override Widget _actions(BuildContext context) => Column( mainAxisSize: MainAxisSize.min, - children: [ - for (final action in actions) - Padding( - padding: style.actionPadding, - child: action, - ), - ], + children: separate( + actions, + by: [ SizedBox(height: style.actionPadding) ], + ), ); } @@ -128,29 +132,32 @@ final class FDialogContentStyle with Diagnosticable { /// The padding surrounding the content. final EdgeInsets padding; - /// The padding surrounding each action. - final EdgeInsets actionPadding; - /// The title style. final TextStyle title; /// The subtitle style. final TextStyle subtitle; + + /// The padding between actions. + final double actionPadding; /// Creates a [FDialogContentStyle]. FDialogContentStyle({ + required this.padding, required this.title, required this.subtitle, - this.padding = const EdgeInsets.fromLTRB(16, 12, 16, 16), - this.actionPadding = const EdgeInsets.symmetric(vertical: 7.0), + required this.actionPadding, }); /// Creates a [FDialogContentStyle] that inherits its properties from [colorScheme] and [typography]. - FDialogContentStyle.inherit({required FColorScheme colorScheme, required FTypography typography}): - padding = const EdgeInsets.fromLTRB(16, 12, 16, 16), - actionPadding = const EdgeInsets.symmetric(vertical: 7.0), + FDialogContentStyle.inherit({ + required FColorScheme colorScheme, + required FTypography typography, + required this.padding, + required this.actionPadding, + }): title = TextStyle( - fontSize: typography.base, + fontSize: typography.lg, fontWeight: FontWeight.w600, color: colorScheme.foreground, ), @@ -164,8 +171,8 @@ final class FDialogContentStyle with Diagnosticable { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('padding', padding)) - ..add(DiagnosticsProperty('actionPadding', actionPadding)) ..add(DiagnosticsProperty('title', title)) - ..add(DiagnosticsProperty('subtitle', subtitle)); + ..add(DiagnosticsProperty('subtitle', subtitle)) + ..add(DoubleProperty('actionPadding', actionPadding)); } }