Files
plezy/lib/widgets/bottom_sheet_header.dart
T
edde746 096e285ab0 fix(ui): center sheet back button hover circle on the arrow
Hovering the back button in bottom sheet headers drew the circular
highlight 12px right of the arrow: the 48px hit target was positioned
from the stack's padded origin while the glyph sits flush at the
leading content edge. Move the horizontal padding onto the header row
and position the target so the InkResponse box centers on the glyph.
2026-08-17 01:41:46 +02:00

142 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../i18n/strings.g.dart';
import 'overlay_sheet.dart';
/// A reusable header widget for bottom sheets
/// Provides consistent styling with title, optional leading widget, optional action, and close button
class BottomSheetHeader extends StatelessWidget {
/// The title text to display
final String title;
/// Optional leading widget (e.g., icon or back button)
/// Takes precedence over [icon] and [onBack]
final Widget? leading;
/// Optional action widget (e.g., clear button)
final Widget? action;
/// Optional callback when close button is pressed
/// Defaults to closing the nearest hosted sheet, with modal-route fallback.
final VoidCallback? onClose;
/// Optional icon to display as leading widget
/// Only used if [leading] and [onBack] are null
final IconData? icon;
/// Optional color for the icon
/// Only used when [icon] is provided
final Color? iconColor;
/// Optional callback for back button
/// When provided, displays a back button as the leading widget
/// Takes precedence over [icon]
final VoidCallback? onBack;
/// Optional text style for the title
final TextStyle? titleStyle;
/// Optional text color for the title
/// Only used if [titleStyle] is null
final Color? titleColor;
/// Whether to show the bottom border
/// Defaults to true
final bool showBorder;
/// Optional focus node for the close button
final FocusNode? closeFocusNode;
const BottomSheetHeader({
super.key,
required this.title,
this.leading,
this.action,
this.onClose,
this.icon,
this.iconColor,
this.onBack,
this.titleStyle,
this.titleColor,
this.showBorder = true,
this.closeFocusNode,
});
@override
Widget build(BuildContext context) {
final usesBackButton = leading == null && onBack != null;
Widget? resolvedLeading;
if (leading != null) {
resolvedLeading = leading;
} else if (onBack != null) {
resolvedLeading = SizedBox(
width: 24,
height: kMinInteractiveDimension,
child: Align(
alignment: Alignment.centerLeft,
child: ExcludeSemantics(child: AppIcon(Symbols.arrow_back_rounded, fill: 1, color: iconColor)),
),
);
} else if (icon != null) {
resolvedLeading = AppIcon(icon!, fill: 1, color: iconColor);
}
final effectiveTitleStyle = titleStyle ?? TextStyle(fontSize: 18, fontWeight: .bold, color: titleColor);
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: showBorder
? BoxDecoration(
border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)),
)
: null,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
if (resolvedLeading != null) ...[resolvedLeading, const SizedBox(width: 8)],
Expanded(child: Text(title, style: effectiveTitleStyle)),
?action,
ExcludeFocusTraversal(
child: IconButton(
focusNode: closeFocusNode,
tooltip: t.common.close,
icon: AppIcon(Symbols.close_rounded, fill: 1, color: iconColor),
onPressed: onClose ?? () => OverlaySheetController.closeAdaptive(context),
),
),
],
),
),
if (usesBackButton)
PositionedDirectional(
// Center the hit target (and thus the circular hover/press
// highlight of the InkResponse) on the 24px arrow glyph, which
// sits at the row's leading edge inside the 16px padding.
start: 16 + 12 - kMinInteractiveDimension / 2,
top: 0,
bottom: 0,
width: kMinInteractiveDimension,
child: ExcludeFocusTraversal(
child: Semantics(
label: MaterialLocalizations.of(context).backButtonTooltip,
button: true,
child: InkResponse(
onTap: onBack,
radius: kMinInteractiveDimension / 2,
child: const SizedBox.expand(),
),
),
),
),
],
),
);
}
}