showWithCustomHeader static method

Future<int?> showWithCustomHeader({
  1. required BuildContext context,
  2. required String title,
  3. String? message,
  4. List<CNSheetItem> items = const [],
  5. List<CNSheetItemRow> itemRows = const [],
  6. List<CNSheetInlineActions> inlineActions = const [],
  7. List<CNSheetDetent> detents = const [CNSheetDetent.large],
  8. bool prefersGrabberVisible = true,
  9. bool isModal = true,
  10. bool prefersEdgeAttachedInCompactHeight = false,
  11. bool widthFollowsPreferredContentSizeWhenEdgeAttached = false,
  12. double? preferredCornerRadius,
  13. double? headerTitleSize,
  14. FontWeight? headerTitleWeight,
  15. Color? headerTitleColor,
  16. String headerTitleAlignment = 'left',
  17. String? subtitle,
  18. double? subtitleSize,
  19. Color? subtitleColor,
  20. double? headerHeight,
  21. Color? headerBackgroundColor,
  22. bool showHeaderDivider = true,
  23. Color? headerDividerColor,
  24. String closeButtonPosition = 'trailing',
  25. String closeButtonIcon = 'xmark',
  26. double? closeButtonSize,
  27. Color? closeButtonColor,
  28. Color? itemBackgroundColor,
  29. Color? itemTextColor,
  30. Color? itemTintColor,
  31. void onInlineActionSelected(
    1. int rowIndex,
    2. int actionIndex
    )?,
  32. void onItemSelected(
    1. int index
    )?,
  33. void onItemRowSelected(
    1. int rowIndex,
    2. int itemIndex
    )?,
})

Shows a native sheet with custom header (title + close button).

This is like the Apple Notes formatting sheet - it has a custom header bar with the title on the left and a close button (X) on the right.

Key differences from show():

  • Custom header with title and close button (like Notes app)
  • Title is displayed in the header bar, not in the content area
  • Close button allows manual dismissal
  • Still supports nonmodal behavior with isModal: false
  • Full control over header styling

Example:

await CNNativeSheet.showWithCustomHeader(
  context: context,
  title: 'Format',
  headerTitleSize: 20,
  headerTitleWeight: FontWeight.w600,
  headerHeight: 56,
  items: [
    CNSheetItem(title: 'Bold', icon: 'bold'),
    CNSheetItem(title: 'Italic', icon: 'italic'),
  ],
  detents: [CNSheetDetent.custom(280)],
  isModal: false, // Nonmodal - can interact with background
);

title - Title displayed in the header (required for custom header) message - Optional message below the header items - List of items to display detents - Heights at which the sheet can rest prefersGrabberVisible - Whether to show the grabber handle isModal - Whether the sheet blocks background interaction

Header Styling Options: headerTitleSize - Font size for the title (default: 20) headerTitleWeight - Font weight for the title (default: semibold/600) headerTitleColor - Color for the title (default: label color) headerTitleAlignment - Alignment of title: 'left', 'center' (default: 'left') subtitle - Optional subtitle/subheading displayed below the title subtitleSize - Font size for the subtitle (default: 13) subtitleColor - Color for the subtitle (default: secondary label) headerHeight - Height of the header bar (default: 56) headerBackgroundColor - Background color of the header (default: system background) showHeaderDivider - Whether to show divider below header (default: true) headerDividerColor - Color of the header divider (default: separator color) closeButtonPosition - Position of close button: 'leading' or 'trailing' (default: 'trailing') closeButtonIcon - SF Symbol name for close button (default: 'xmark') closeButtonSize - Size of the close button icon (default: 17) closeButtonColor - Color of the close button (default: label color) itemBackgroundColor - Background color for sheet item buttons (default: clear) itemTextColor - Text color for sheet item buttons (default: system label) itemTintColor - Tint color for icons in sheet item buttons (default: system tint) onInlineActionSelected - Callback invoked when an inline action is tapped, receives row and action indices onItemSelected - Callback invoked when a regular vertical item is tapped, receives the item index onItemRowSelected - Callback invoked when an item row item is tapped, receives row index and item index

Implementation

static Future<int?> showWithCustomHeader({
  required BuildContext context,
  required String title,
  String? message,
  List<CNSheetItem> items = const [],
  List<CNSheetItemRow> itemRows = const [],
  List<CNSheetInlineActions> inlineActions = const [],
  List<CNSheetDetent> detents = const [CNSheetDetent.large],
  bool prefersGrabberVisible = true,
  bool isModal = true,
  bool prefersEdgeAttachedInCompactHeight = false,
  bool widthFollowsPreferredContentSizeWhenEdgeAttached = false,
  double? preferredCornerRadius,
  // Header styling
  double? headerTitleSize,
  FontWeight? headerTitleWeight,
  Color? headerTitleColor,
  String headerTitleAlignment = 'left',
  String? subtitle,
  double? subtitleSize,
  Color? subtitleColor,
  double? headerHeight,
  Color? headerBackgroundColor,
  bool showHeaderDivider = true,
  Color? headerDividerColor,
  String closeButtonPosition = 'trailing',
  String closeButtonIcon = 'xmark',
  double? closeButtonSize,
  Color? closeButtonColor,
  // Item styling
  Color? itemBackgroundColor,
  Color? itemTextColor,
  Color? itemTintColor,
  // Callbacks
  void Function(int rowIndex, int actionIndex)? onInlineActionSelected,
  void Function(int index)? onItemSelected,
  void Function(int rowIndex, int itemIndex)? onItemRowSelected,
}) async {
  try {
    // Initialize handlers on first call
    _initializeHandlers();

    // Create a completer to wait for the native dismissal callback
    final completer = Completer<int?>();
    final sheetId = _generateSheetId();
    _pendingSheets[sheetId] = completer;

    // Serialize inline actions
    final inlineActionsList = <Map<String, dynamic>>[];
    for (final actionGroup in inlineActions) {
      final actionMaps = actionGroup.actions.map((action) {
        final map = <String, dynamic>{
          'label': action.label,
          'icon': action.icon,
          'enabled': action.enabled,
          'isToggled': action.isToggled,
          'dismissOnTap': action.dismissOnTap,
        };
        // Add backgroundColor if provided (convert to ARGB int)
        if (action.backgroundColor != null) {
          map['backgroundColor'] = action.backgroundColor!.value;
        }
        // Add styling properties if provided
        if (action.width != null) map['width'] = action.width;
        if (action.iconSize != null) map['iconSize'] = action.iconSize;
        if (action.labelSize != null) map['labelSize'] = action.labelSize;
        if (action.cornerRadius != null) map['cornerRadius'] = action.cornerRadius;
        if (action.iconLabelSpacing != null) map['iconLabelSpacing'] = action.iconLabelSpacing;

        return map;
      }).toList();

      // Add row-level styling properties
      final rowMap = <String, dynamic>{'actions': actionMaps};
      if (actionGroup.spacing != null) rowMap['spacing'] = actionGroup.spacing;
      if (actionGroup.horizontalPadding != null) rowMap['horizontalPadding'] = actionGroup.horizontalPadding;
      if (actionGroup.verticalPadding != null) rowMap['verticalPadding'] = actionGroup.verticalPadding;
      if (actionGroup.height != null) rowMap['height'] = actionGroup.height;

      inlineActionsList.add(rowMap);
    }

    // Set up callback handler if provided - merge with the existing onDismiss handler
    if (onInlineActionSelected != null || onItemSelected != null || onItemRowSelected != null) {
      _customChannel.setMethodCallHandler((call) async {
        if (call.method == 'onDismiss') {
          final args = call.arguments as Map;
          final selectedIndex = args['selectedIndex'] as int?;

          // Complete the pending sheet completer for onDismiss
          if (_pendingSheets.isNotEmpty) {
            final key = _pendingSheets.keys.first;
            final completer = _pendingSheets.remove(key);
            completer?.complete(selectedIndex == -1 ? null : selectedIndex);
          }
        } else if (call.method == 'onInlineActionSelected' && onInlineActionSelected != null) {
          final rowIndex = call.arguments['rowIndex'] as int;
          final actionIndex = call.arguments['actionIndex'] as int;
          onInlineActionSelected(rowIndex, actionIndex);
        } else if (call.method == 'onItemSelected' && onItemSelected != null) {
          final index = call.arguments['index'] as int;
          onItemSelected(index);
        } else if (call.method == 'onItemRowSelected' && onItemRowSelected != null) {
          final rowIndex = call.arguments['rowIndex'] as int;
          final itemIndex = call.arguments['itemIndex'] as int;
          onItemRowSelected(rowIndex, itemIndex);
        }
      });
    }

    // Serialize item rows
    final itemRowsList = itemRows.map((row) {
      final rowMap = <String, dynamic>{
        'items': row.items.map((item) => item.toMap()).toList(),
      };

      // Add row-level styling properties
      if (row.spacing != null) rowMap['spacing'] = row.spacing;
      if (row.height != null) rowMap['height'] = row.height;

      return rowMap;
    }).toList();

    // Call native method (result will be null from this call)
    await _customChannel.invokeMethod('showSheet', {
      'title': title,
      'message': message,
      'items': items.map((item) => item.toMap()).toList(),
      'itemRows': itemRowsList,
      'inlineActions': inlineActionsList,
      'detents': detents.map((d) => d.toMap()).toList(),
      'prefersGrabberVisible': prefersGrabberVisible,
      'isModal': isModal,
      'prefersEdgeAttachedInCompactHeight':
          prefersEdgeAttachedInCompactHeight,
      'widthFollowsPreferredContentSizeWhenEdgeAttached':
          widthFollowsPreferredContentSizeWhenEdgeAttached,
      'preferredCornerRadius': preferredCornerRadius,
      // Header styling parameters
      if (headerTitleSize != null) 'headerTitleSize': headerTitleSize,
      if (headerTitleWeight != null)
        'headerTitleWeight': _fontWeightToString(headerTitleWeight),
      if (headerTitleColor != null)
        'headerTitleColor': headerTitleColor.value,
      'headerTitleAlignment': headerTitleAlignment,
      if (subtitle != null) 'subtitle': subtitle,
      if (subtitleSize != null) 'subtitleSize': subtitleSize,
      if (subtitleColor != null) 'subtitleColor': subtitleColor.value,
      if (headerHeight != null) 'headerHeight': headerHeight,
      if (headerBackgroundColor != null)
        'headerBackgroundColor': headerBackgroundColor.value,
      'showHeaderDivider': showHeaderDivider,
      if (headerDividerColor != null)
        'headerDividerColor': headerDividerColor.value,
      'closeButtonPosition': closeButtonPosition,
      'closeButtonIcon': closeButtonIcon,
      if (closeButtonSize != null) 'closeButtonSize': closeButtonSize,
      if (closeButtonColor != null)
        'closeButtonColor': closeButtonColor.value,
      // Item styling parameters
      if (itemBackgroundColor != null)
        'itemBackgroundColor': itemBackgroundColor.value,
      if (itemTextColor != null) 'itemTextColor': itemTextColor.value,
      if (itemTintColor != null) 'itemTintColor': itemTintColor.value,
    });

    // Wait for the sheet dismissal callback to complete the completer
    return completer.future;
  } catch (e) {
    debugPrint('Error showing native custom header sheet: $e');
    return null;
  }
}