@namespace SufiChain.SufiBlazor.Components.Overlays @implements IAsyncDisposable @inject IJSRuntime JSRuntime @inject IStringLocalizer L
@if (ShowHeader) {
@if (Header != null) { @Header } else if (Title != null) {

@Title

} @if (ShowCloseButton) { }
}
@ChildContent
@if (Footer != null) {
@Footer
}
@code { private ElementReference _dialogRef; private readonly string _titleId = $"sb-dialog-title-{Guid.NewGuid():N}"; private bool _isOpen; private bool _hasRendered; private bool _pendingOpenSync; private bool _suppressNextNativeClose; /// /// Whether the dialog is open. /// [Parameter] public bool Open { get; set; } /// /// Callback when open state changes. /// [Parameter] public EventCallback OpenChanged { get; set; } /// /// Dialog title displayed in header. /// [Parameter] public string? Title { get; set; } /// /// Custom header content (replaces Title). /// [Parameter] public RenderFragment? Header { get; set; } /// /// Footer content for actions. /// [Parameter] public RenderFragment? Footer { get; set; } /// /// Dialog body content. /// [Parameter] public RenderFragment? ChildContent { get; set; } /// /// Dialog size preset (e.g. Lg). Used when Width/MaxWidth/Height/MaxHeight are not set; with Size alone the modal uses the preset dimensions. /// [Parameter] public SbDialogSize Size { get; set; } = SbDialogSize.Md; /// /// Optional. Override width. Value is applied as-is to CSS (e.g. "800px", "80%", "50vw"). When set, overrides Size for width. /// [Parameter] public string? Width { get; set; } /// /// Optional. Override max-width. Value is applied as-is to CSS (e.g. "800px", "80%", "1200px"). When set, overrides Size for max-width. /// [Parameter] public string? MaxWidth { get; set; } /// /// Optional. Override height. Value is applied as-is to CSS (e.g. "80vh", "600px", "80%"). /// [Parameter] public string? Height { get; set; } /// /// Optional. Override max-height. Value is applied as-is to CSS (e.g. "90vh", "800px", "80%"). /// [Parameter] public string? MaxHeight { get; set; } /// /// Whether to close on Escape key. /// [Parameter] public bool CloseOnEscape { get; set; } = true; /// /// Whether to close when clicking backdrop. Default is false so backdrop clicks do nothing. /// [Parameter] public bool CloseOnBackdropClick { get; set; } = false; /// /// Whether to show the close button. /// [Parameter] public bool ShowCloseButton { get; set; } = true; /// /// Optional callback when the header close (X) button is clicked. /// Runs before the standard close flow; the dialog still invokes OpenChanged(false). /// [Parameter] public EventCallback OnCloseButtonClick { get; set; } /// /// Callback when dialog closes with reason. /// [Parameter] public EventCallback OnClose { get; set; } /// /// When true, the dialog body uses overflow: visible so dropdowns (e.g. SbSelect) inside the modal /// are not clipped by the body. Use for modals that contain selects or other overflow menus. /// [Parameter] public bool AllowDropdownOverflow { get; set; } /// /// Additional CSS classes. /// [Parameter] public string? Class { get; set; } /// /// Inline styles. /// [Parameter] public string? Style { get; set; } /// /// Additional HTML attributes. /// [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } private bool ShowHeader => Header != null || Title != null || ShowCloseButton; private static string? GetDimensionValue(string? parameterValue, Dictionary? attrs, params string[] keys) { if (!string.IsNullOrWhiteSpace(parameterValue)) return parameterValue.Trim(); if (attrs == null) return null; foreach (var key in keys) { if (attrs.TryGetValue(key, out var v) && v is string s && !string.IsNullOrWhiteSpace(s)) return s.Trim(); } return null; } private bool HasDimensionOverrides { get { var w = GetDimensionValue(Width, AdditionalAttributes, "Width", "width"); var mw = GetDimensionValue(MaxWidth, AdditionalAttributes, "MaxWidth", "max-width", "maxwidth"); var h = GetDimensionValue(Height, AdditionalAttributes, "Height", "height"); var mh = GetDimensionValue(MaxHeight, AdditionalAttributes, "MaxHeight", "max-height", "maxheight"); return w != null || mw != null || h != null || mh != null; } } private IReadOnlyDictionary? FilteredAttributes { get { if (AdditionalAttributes == null || AdditionalAttributes.Count == 0) return AdditionalAttributes; var omit = new HashSet(StringComparer.OrdinalIgnoreCase) { "width", "maxwidth", "max-width", "height", "maxheight", "max-height" }; var filtered = new Dictionary(AdditionalAttributes); foreach (var key in AdditionalAttributes.Keys) { if (omit.Contains(key)) filtered.Remove(key); } return filtered.Count == AdditionalAttributes.Count ? AdditionalAttributes : filtered; } } private string EffectiveStyle { get { var parts = new List(); var w = GetDimensionValue(Width, AdditionalAttributes, "Width", "width"); if (w != null) parts.Add($"width: {w}"); var mw = GetDimensionValue(MaxWidth, AdditionalAttributes, "MaxWidth", "max-width", "maxwidth"); if (mw != null) parts.Add($"max-width: {mw}"); var h = GetDimensionValue(Height, AdditionalAttributes, "Height", "height"); if (h != null) parts.Add($"height: {h}"); var mh = GetDimensionValue(MaxHeight, AdditionalAttributes, "MaxHeight", "max-height", "maxheight"); if (mh != null) parts.Add($"max-height: {mh}"); if (!string.IsNullOrWhiteSpace(Style)) parts.Add(Style.Trim()); return parts.Count > 0 ? string.Join("; ", parts) : string.Empty; } } private string CssClass { get { var classes = new List { "sb-dialog" }; if (!HasDimensionOverrides) classes.Add($"sb-dialog--{Size.ToString().ToLowerInvariant()}"); if (AllowDropdownOverflow) classes.Add("sb-dialog--allow-dropdown-overflow"); if (!string.IsNullOrWhiteSpace(Class)) classes.Add(Class); return string.Join(" ", classes); } } protected override async Task OnParametersSetAsync() { if (Open != _isOpen) { _isOpen = Open; await SyncNativeOpenStateAsync(); } } protected override async Task OnAfterRenderAsync(bool firstRender) { _hasRendered = true; if (_pendingOpenSync) { await SyncNativeOpenStateAsync(); } } private async Task ShowAsync() { await JSRuntime.InvokeVoidAsync("SufiBlazor.dialog.showModal", _dialogRef); } private async Task HideAsync() { await JSRuntime.InvokeVoidAsync("SufiBlazor.dialog.close", _dialogRef); } private async Task SyncNativeOpenStateAsync() { if (!_hasRendered) { _pendingOpenSync = true; return; } _pendingOpenSync = false; if (_isOpen) { await ShowAsync(); } else { await HideAsync(); } } private async Task HandleCloseButtonClick() { if (OnCloseButtonClick.HasDelegate) { await OnCloseButtonClick.InvokeAsync(); } await CloseAsync(SbDialogCloseReason.CloseButton); } private async Task CloseAsync(SbDialogCloseReason reason) { _suppressNextNativeClose = true; _isOpen = false; await SyncNativeOpenStateAsync(); await OnClose.InvokeAsync(reason); await OpenChanged.InvokeAsync(false); } private async Task HandleKeyDown(KeyboardEventArgs args) { if (args.Key == "Escape" && CloseOnEscape) { await CloseAsync(SbDialogCloseReason.Escape); } } private async Task HandleCancel() { // Native dialog cancel event (Escape key) if (CloseOnEscape) { await CloseAsync(SbDialogCloseReason.Escape); } } private async Task HandleNativeClose() { if (_suppressNextNativeClose) { _suppressNextNativeClose = false; return; } if (!_isOpen) { return; } _isOpen = false; await OnClose.InvokeAsync(SbDialogCloseReason.CloseButton); await OpenChanged.InvokeAsync(false); } public async ValueTask DisposeAsync() { // Cleanup if needed } }