Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.
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
27 changes: 26 additions & 1 deletion example/lib/src/storybook/stories/primitives/modal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ class ModalStory extends StatelessWidget {

@override
Widget build(BuildContext context) {
final alignmentKnob = context.knobs.nullable.options(
label: "alignment",
description: "Alignment for MoonModal.",
enabled: false,
initial: AlignmentDirectional.center,
options: const [
Option(label: "center", value: AlignmentDirectional.center),
Option(label: "centerStart", value: AlignmentDirectional.centerStart),
Option(label: "centerEnd", value: AlignmentDirectional.centerEnd),
Option(label: "topCenter", value: AlignmentDirectional.topCenter),
Option(label: "topStart", value: AlignmentDirectional.topStart),
Option(label: "topEnd", value: AlignmentDirectional.topEnd),
Option(label: "bottomCenter", value: AlignmentDirectional.bottomCenter),
Option(label: "bottomStart", value: AlignmentDirectional.bottomStart),
Option(label: "bottomEnd", value: AlignmentDirectional.bottomEnd),
],
);

final textColorKnob = context.knobs.nullable.options(
label: "Text color",
description: "MoonColors variants for MoonModal text.",
Expand Down Expand Up @@ -51,15 +69,22 @@ class ModalStory extends StatelessWidget {
max: 32,
);

final barrierDismissibleKnob = context.knobs.boolean(
label: "barrierDismissible",
description: "Modal barrier is dismissible via barrier taps.",
initial: true,
);

Future<void> modalBuilder(BuildContext context) {
return showMoonModal<void>(
context: context,
useRootNavigator: false,
barrierColor: barrierColor,
barrierDismissible: barrierDismissibleKnob,
builder: (BuildContext context) {
return Directionality(
textDirection: Directionality.of(context),
child: MoonModal(
alignment: alignmentKnob,
backgroundColor: backgroundColor,
borderRadius: borderRadiusKnob != null
? BorderRadius.circular(borderRadiusKnob.toDouble())
Expand Down
183 changes: 46 additions & 137 deletions lib/src/widgets/modal/modal.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import 'package:flutter/material.dart';

import 'package:mix/mix.dart';
import 'package:moon_core/moon_core.dart';

import 'package:moon_design/src/theme/theme.dart';
import 'package:moon_design/src/theme/tokens/borders.dart';
import 'package:moon_design/src/theme/tokens/transitions.dart';
import 'package:moon_design/src/theme/tokens/typography/typography.dart';
import 'package:moon_design/src/utils/extensions.dart';
import 'package:moon_design/src/utils/squircle/squircle_border.dart';

import 'package:moon_tokens/moon_tokens.dart';

/// Displays a modal overlay over the app's current content, incorporating
/// entrance and exit animations, modal barrier color, and modal barrier
/// behavior, enabling dialog dismissal via barrier taps. Intended for use in
/// behavior, enabling modal dismissal via barrier taps. Intended for use in
/// conjunction with [MoonModal].
Future<T?> showMoonModal<T>({
bool barrierDismissible = true,
Expand All @@ -28,115 +26,32 @@ Future<T?> showMoonModal<T>({
required BuildContext context,
required WidgetBuilder builder,
}) {
assert(!barrierDismissible || barrierLabel != null);
assert(_debugIsActive(context));

final CapturedThemes themes = InheritedTheme.capture(
from: context,
to: Navigator.of(context, rootNavigator: useRootNavigator).context,
);

final Color effectiveBarrierColor = barrierColor ??
context.moonTheme?.modalTheme.colors.barrierColor ??
MoonColors.light.zeno;
final Color effectiveBarrierColor = barrierColor ?? MoonColors.light.zeno;

final Duration effectiveTransitionDuration = transitionDuration ??
context.moonTheme?.modalTheme.properties.transitionDuration ??
MoonTransitions.transitions.defaultTransitionDuration;

final Curve effectiveTransitionCurve = transitionCurve ??
context.moonTheme?.modalTheme.properties.transitionCurve ??
MoonTransitions.transitions.defaultTransitionCurve;

return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
MoonModalRoute<T>(
context: context,
builder: builder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: effectiveBarrierColor,
transitionDuration: effectiveTransitionDuration,
transitionCurve: effectiveTransitionCurve,
useSafeArea: useSafeArea,
settings: routeSettings,
anchorPoint: anchorPoint,
themes: themes,
),
final Curve effectiveTransitionCurve =
transitionCurve ?? MoonTransitions.transitions.defaultTransitionCurve;

return showMoonRawModal(
context: context,
builder: builder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: effectiveBarrierColor,
transitionDuration: effectiveTransitionDuration,
transitionCurve: effectiveTransitionCurve,
useSafeArea: useSafeArea,
routeSettings: routeSettings,
anchorPoint: anchorPoint,
);
}

bool _debugIsActive(BuildContext context) {
if (context is Element && !context.debugIsActive) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('This BuildContext is no longer valid.'),
ErrorDescription(
'The showMoonModal function context parameter is a BuildContext that is '
'no longer valid.',
),
ErrorHint(
'This can commonly occur when the showMoonModal function is called after '
'awaiting a Future. In this situation the BuildContext might refer to a '
'widget that has already been disposed during the await. Consider using '
'a parent context instead.',
),
]);
}

return true;
}

class MoonModalRoute<T> extends RawDialogRoute<T> {
/// A Moon Design modal route with entrance and exit animations, modal barrier
/// color, and modal barrier behavior that allows dismissing the modal when
/// tapped on the barrier.
MoonModalRoute({
super.anchorPoint,
required super.barrierColor,
super.barrierDismissible,
String? barrierLabel,
super.settings,
CapturedThemes? themes,
required super.transitionDuration,
required Curve transitionCurve,
bool useSafeArea = true,
required BuildContext context,
required WidgetBuilder builder,
}) : super(
barrierLabel: barrierLabel ??
MaterialLocalizations.of(context).modalBarrierDismissLabel,
pageBuilder: (
BuildContext buildContext,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final Widget pageChild = Builder(builder: builder);

Widget modal = themes?.wrap(pageChild) ?? pageChild;

if (useSafeArea) modal = SafeArea(child: modal);

return modal;
},
transitionBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return RepaintBoundary(
child: FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: transitionCurve,
),
child: child,
),
);
},
);
}

class MoonModal extends StatelessWidget {
/// The alignment of the modal.
final AlignmentGeometry? alignment;

/// The border radius of the modal.
final BorderRadiusGeometry? borderRadius;

Expand All @@ -155,6 +70,7 @@ class MoonModal extends StatelessWidget {
/// Creates a Moon Design modal.
const MoonModal({
super.key,
this.alignment,
this.borderRadius,
this.backgroundColor,
this.decoration,
Expand All @@ -164,46 +80,39 @@ class MoonModal extends StatelessWidget {

@override
Widget build(BuildContext context) {
final BorderRadiusGeometry effectiveBorderRadius = borderRadius ??
context.moonTheme?.modalTheme.properties.borderRadius ??
MoonBorders.borders.surfaceSm;
final BorderRadiusGeometry effectiveBorderRadius =
borderRadius ?? MoonBorders.borders.surfaceSm;

final Color effectiveBackgroundColor = backgroundColor ??
context.moonTheme?.modalTheme.colors.backgroundColor ??
MoonColors.light.goku;
final Color effectiveBackgroundColor =
backgroundColor ?? MoonColors.light.goku;

final Color effectiveTextColor =
context.moonTheme?.modalTheme.colors.textColor ??
MoonColors.light.textPrimary;
final Color effectiveTextColor = MoonColors.light.textPrimary;

final Color effectiveIconColor =
context.moonTheme?.modalTheme.colors.iconColor ??
MoonColors.light.iconPrimary;
final Color effectiveIconColor = MoonColors.light.iconPrimary;

final TextStyle effectiveTextStyle =
context.moonTheme?.modalTheme.properties.textStyle ??
MoonTypography.typography.body.textDefault;
MoonTypography.typography.body.textDefault;

final TextStyle resolvedTextStyle =
effectiveTextStyle.copyWith(color: effectiveTextColor);

final Style modalStyle = Style(
decorationToAttribute(
decoration ??
ShapeDecorationWithPremultipliedAlpha(
color: effectiveBackgroundColor,
shape: MoonBorder(borderRadius: effectiveBorderRadius),
),
),
$with.align(alignment: alignment ?? Alignment.center),
$with.iconTheme.data.color(effectiveIconColor),
$with.defaultTextStyle.style.as(resolvedTextStyle),
);
return Semantics(
label: semanticLabel,
child: IconTheme(
data: IconThemeData(color: effectiveIconColor),
child: DefaultTextStyle(
style: effectiveTextStyle.copyWith(color: effectiveTextColor),
child: Center(
child: Container(
decoration: decoration ??
ShapeDecorationWithPremultipliedAlpha(
color: effectiveBackgroundColor,
shape: MoonSquircleBorder(
borderRadius:
effectiveBorderRadius.squircleBorderRadius(context),
),
),
child: child,
),
),
),
child: Box(
style: modalStyle,
child: child,
),
);
}
Expand Down