dismissBottomSheet static method

Future<void> dismissBottomSheet({
  1. String? id,
  2. VoidCallback? onDismissed,
})

Dismisses only the bottom sheet, preserving snackbars and dialogs

If an id is provided, only dismisses if the active bottom sheet matches that ID. This provides extra safety to ensure you're dismissing the intended modal.

Parameters:

  • id: Optional. If provided, only dismiss if the bottom sheet's ID matches.
  • onDismissed: Optional callback executed after the bottom sheet is dismissed.

Example:

// Dismiss current bottom sheet (less safe)
Modal.dismissBottomSheet();

// Dismiss specific bottom sheet by ID (recommended)
Modal.dismissBottomSheet(id: 'settings_sheet');

Implementation

static Future<void> dismissBottomSheet(
    {String? id, VoidCallback? onDismissed}) async {
  if (Modal.isSheetActive && !Modal.isSheetDismissing) {
    // CAPTURE the ID of the bottom sheet we intend to dismiss at the START
    final targetSheetId = _sheetController.state?.uniqueId;
    final sheetId = _sheetController.state?.id;
    if (targetSheetId == null) {
      if (_showDebugPrints) {
        debugPrint(
            'Modal.dismissBottomSheet: WARNING - No bottom sheet ID found, aborting');
      }
      return;
    }

    // If an ID was provided, verify it matches before dismissing
    if (id != null && id != sheetId && id != targetSheetId) {
      if (_showDebugPrints) {
        debugPrint(
            'Modal.dismissBottomSheet: ID mismatch. Requested=$id, Active=$sheetId. Aborting.');
      }
      return;
    }

    if (_showDebugPrints) {
      debugPrint(
          'Modal.dismissBottomSheet: targeting sheet ID=$targetSheetId');
    }
    Modal.isDismissing = true;
    _sheetDismissingNotifier.state = true;

    // (intentionally not capturing dismissed type here) bottom sheet cleanup
    // is handled deterministically below when we refresh the controller.

    // Reset blur animation state - BUT ONLY if no other blur-enabled modal remains active
    // If a dialog with blur is still showing, we must preserve its blur
    final dialogNeedsBlur = Modal.isDialogActive &&
        (_dialogController.state?.shouldBlurBackground ?? false);
    if (!dialogNeedsBlur) {
      _blurAnimationStateNotifier.state = 0.0;
    }

    // Animate background out (if no other modals need it)
    if (!Modal.isDialogActive) {
      _backgroundAnimationTimer?.cancel();
      const animSteps = 10;
      const totalDuration = 300;
      final stepDuration = Duration(milliseconds: totalDuration ~/ animSteps);
      double startValue = _backgroundLayerAnimationNotifier.state;
      double step = startValue / animSteps;
      int currentStep = 0;

      _backgroundAnimationTimer = Timer.periodic(stepDuration, (timer) {
        currentStep++;
        if (currentStep > animSteps) {
          timer.cancel();
          _backgroundAnimationTimer = null;
          _backgroundLayerAnimationNotifier.state = 0.0;
        } else {
          _backgroundLayerAnimationNotifier.state =
              startValue - (step * currentStep);
        }
      });
    }

    // Animate out
    _dismissModalAnimationController.state = true;

    await Future.delayed(0.4.sec, () {
      // VALIDATE: Check if the sheet ID still matches what we intended to dismiss
      final currentSheetId = _sheetController.state?.uniqueId;
      if (currentSheetId != null && currentSheetId != targetSheetId) {
        if (_showDebugPrints) {
          debugPrint(
              'Modal.dismissBottomSheet: WARNING - Sheet ID changed during animation! '
              'Target=$targetSheetId, Current=$currentSheetId. Aborting cleanup.');
        }
        Modal.isDismissing = false;
        _sheetDismissingNotifier.state = false;
        return;
      }

      if (_showDebugPrints) {
        debugPrint('Modal.dismissBottomSheet: cleanup for ID=$targetSheetId');
      }
      _resetHeightCallback?.call();
      _modalSheetHeightNotifier.state = 0.0;
      _modalDragOffsetNotifier.state = 0.0;

      // Run callbacks after cleanup
      if (_showDebugPrints) {
        debugPrint('Modal.dismissBottomSheet: running callbacks');
      }
      onDismissed?.call();
      _sheetController.state?.onDismissed?.call();

      final currentQueue = _snackbarQueueNotifier.state;
      if (_showDebugPrints) {
        debugPrint(
            'Modal.dismissBottomSheet: snackbar queue has ${currentQueue.keys.length} positions');
      }

      // IMPORTANT: Do NOT clear the snackbar queue when dismissing a bottom sheet.
      // Snackbars are independent modals and should remain visible.
      // Only update the active modal controller to point to a remaining snackbar if any.
      if (currentQueue.isNotEmpty) {
        if (_showDebugPrints) {
          debugPrint(
              'Modal.dismissBottomSheet: snackbar queue not empty, preserving snackbars');
        }
        // Find the first position with snackbars and make it the active modal
        Alignment? positionWithContent;
        for (final position in currentQueue.keys) {
          if (currentQueue[position]!.isNotEmpty) {
            positionWithContent = position;
            break;
          }
        }
        if (positionWithContent != null) {
          final snackbarToShow = currentQueue[positionWithContent]!.first;
          // Only update if the snackbar content has actually changed
          if (_snackbarController.state?.id != snackbarToShow.id) {
            _snackbarController.state = snackbarToShow;
            Modal.controller.state = snackbarToShow;
          }
          // Ensure the newly activated snackbar is not marked as dismissing
          _setSnackbarDismissing(snackbarToShow.uniqueId, false);
          if (Modal.dismissModalAnimationController.state != false) {
            Modal.dismissModalAnimationController.state = false;
          }
        }
      }

      _sheetController.refresh();

      // Unregister from modal registry only if the ID exists
      if (_modalRegistry.state.containsKey(targetSheetId)) {
        final updatedRegistry =
            Map<String, ModalType>.from(_modalRegistry.state);
        updatedRegistry.remove(targetSheetId);
        _modalRegistry.state = updatedRegistry;
      }

      _sheetDismissingNotifier.state = false;

      // IMPORTANT: Only clear active modal controller if NO other modals are active
      if (currentQueue.isEmpty && !Modal.isDialogActive) {
        _snackbarController.refresh();
        _activeModalController.refresh();
      } else if (currentQueue.isEmpty && Modal.isDialogActive) {
        // Dialog is still active - make it the active modal
        _snackbarController.refresh();
        Modal.controller.state = _dialogController.state;
      }

      // Reset dismiss animation controller if no other modals active
      if (!Modal.isDialogActive && !Modal.isSnackbarActive) {
        if (_dismissModalAnimationController.state != false) {
          _dismissModalAnimationController.state = false;
        }
      }

      Modal.isDismissing = false;
      HapticFeedback.lightImpact();
    });
  }
}