Files
buzz/mobile/lib/shared/widgets/modal_presentation.dart
klopez4212 75a4efd0ca fix(mobile): unify sheet sections and bottom spacing (#8081)
## Summary
Use rounded, grouped rows consistently across mobile sheets and menus,
including Members and list pickers. Keep sheet content above the system
gesture area with 4dp of extra bottom padding on Android and iOS.

### Related issue
No duplicate found; related member-picker work: #4051.

### Testing
- Mobile analysis, formatting, file-size checks, and full mobile test
suite pass.
- Android build installed and launched on Pixel 10.
- Before/after Members previews use sample data (attached below).
- Full `just ci` stopped during Rust compilation because local disk
space ran out.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 16:20:01 +01:00

222 lines
7.1 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../theme/theme.dart';
import 'buzz_sheet_header.dart';
import 'buzz_titled_sheet_layout.dart';
import 'concentric_sheet_surface.dart';
/// Shared motion for occasional modal UI.
///
/// The strong ease-out makes entrances respond immediately, while the shorter
/// exit keeps dismissals from feeling sluggish.
const buzzModalAnimationStyle = AnimationStyle(
curve: Cubic(0.23, 1, 0.32, 1),
duration: Duration(milliseconds: 280),
reverseCurve: Cubic(0.77, 0, 0.175, 1),
reverseDuration: Duration(milliseconds: 220),
);
/// Shows a bottom sheet with Buzz's shared motion and sheet chrome.
///
/// Sheets include the shared close control by default. On iOS, the surface
/// uses native concentric corners when available and paints a requested drag
/// handle inside the shared header so its spacing is consistent on every
/// platform.
Future<T?> showBuzzModalBottomSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
String? title,
Color? backgroundColor,
String? barrierLabel,
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
Color? barrierColor,
bool isScrollControlled = false,
double scrollControlDisabledMaxHeightRatio = 9.0 / 16.0,
bool useRootNavigator = false,
bool isDismissible = true,
bool enableDrag = true,
bool? showDragHandle,
bool showCloseButton = true,
bool useSafeArea = false,
RouteSettings? routeSettings,
AnimationController? transitionAnimationController,
Offset? anchorPoint,
AnimationStyle? sheetAnimationStyle,
bool? requestFocus,
}) {
final isIos = defaultTargetPlatform == TargetPlatform.iOS;
final theme = Theme.of(context);
// Explicitly colored sheets (for example the dark Huddle drawer) own their
// complete surface treatment. Standard utility sheets use the quieter page
// canvas and promote their controls onto the base app surface.
final sheetTheme = backgroundColor == null
? utilitySurfaceThemeData(theme)
: theme;
final surfaceColor =
backgroundColor ??
sheetTheme.bottomSheetTheme.modalBackgroundColor ??
sheetTheme.bottomSheetTheme.backgroundColor ??
sheetTheme.colorScheme.surface;
final reduceMotion = MediaQuery.disableAnimationsOf(context);
return showModalBottomSheet<T>(
context: context,
builder: (sheetContext) => Theme(
data: sheetTheme,
child: Builder(
builder: (themedContext) => ConcentricSheetSurface(
enabled: isIos,
color: surfaceColor,
child: _SheetContent(
title: title,
showCloseButton: showCloseButton,
showDragHandle: showDragHandle == true,
surfaceColor: surfaceColor,
// Keep the entire scroll viewport above gesture navigation. The
// inner builder sees a consumed bottom inset, so sheet-local safe
// areas and scroll padding do not reserve that system inset twice.
child: SafeArea(
top: false,
left: false,
right: false,
child: Padding(
padding: const EdgeInsets.only(bottom: Grid.half),
child: Builder(builder: builder),
),
),
),
),
),
),
backgroundColor: isIos || title != null ? Colors.transparent : surfaceColor,
barrierLabel: barrierLabel,
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
barrierColor: barrierColor,
isScrollControlled: isScrollControlled,
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
useRootNavigator: useRootNavigator,
isDismissible: isDismissible,
enableDrag: enableDrag,
// The shared header owns the handle so Android does not reserve a second
// handle band above the title row and iOS keeps it inside its native inset.
showDragHandle: false,
useSafeArea: useSafeArea,
routeSettings: routeSettings,
transitionAnimationController: transitionAnimationController,
anchorPoint: anchorPoint,
sheetAnimationStyle: reduceMotion
? AnimationStyle.noAnimation
: (sheetAnimationStyle ?? buzzModalAnimationStyle),
requestFocus: requestFocus,
);
}
class _SheetContent extends StatelessWidget {
const _SheetContent({
required this.child,
required this.title,
required this.showCloseButton,
required this.showDragHandle,
required this.surfaceColor,
});
final Widget child;
final String? title;
final bool showCloseButton;
final bool showDragHandle;
final Color surfaceColor;
@override
Widget build(BuildContext context) {
if (title == null || !showCloseButton) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showCloseButton)
BuzzSheetHeader(title: title, showDragHandle: showDragHandle)
else if (showDragHandle)
const Padding(
padding: EdgeInsets.only(top: Grid.xxs, bottom: Grid.xs),
child: _StandaloneSheetDragHandle(),
),
Flexible(child: child),
],
);
}
return BuzzTitledSheetLayout(
title: title!,
showDragHandle: showDragHandle,
surfaceColor: surfaceColor,
child: child,
);
}
}
class _StandaloneSheetDragHandle extends StatelessWidget {
const _StandaloneSheetDragHandle();
@override
Widget build(BuildContext context) {
return Semantics(
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
container: true,
button: true,
onTap: () => Navigator.of(context).pop(),
child: Container(
key: const ValueKey('buzz-sheet-drag-handle'),
width: 32,
height: 4,
decoration: BoxDecoration(
color: context.colors.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(Radii.full),
),
),
);
}
}
/// Shows a dialog with Buzz's shared motion, respecting reduced-motion settings.
Future<T?> showBuzzDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
bool barrierDismissible = true,
Color? barrierColor,
String? barrierLabel,
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings? routeSettings,
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
bool fullscreenDialog = false,
bool? requestFocus,
AnimationStyle? animationStyle,
}) {
final reduceMotion = MediaQuery.disableAnimationsOf(context);
return showDialog<T>(
context: context,
builder: builder,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
useSafeArea: useSafeArea,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
anchorPoint: anchorPoint,
traversalEdgeBehavior: traversalEdgeBehavior,
fullscreenDialog: fullscreenDialog,
requestFocus: requestFocus,
animationStyle: reduceMotion
? AnimationStyle.noAnimation
: (animationStyle ?? buzzModalAnimationStyle),
);
}