@* Single global host component for IDialogService. Mounted once in MainLayout. Listens to DialogService.OnChange and renders the current dialog state. z-index ladder follows the same convention as ConfirmDialog/DiffDialog: Toast container 1090 > this modal 1055 > this backdrop 1040. *@ @implements IDisposable @inject IDialogService Service @inject IJSRuntime JS @if (Service is DialogService svc && svc.Current is { } state) { } @code { private ElementReference _modalRef; private ElementReference _promptInputRef; private string _promptValue = string.Empty; private DialogState? _lastSeenState; private DialogState? _focusedForState; protected override void OnInitialized() { // OnChange lives on the concrete DialogService — the interface stays // narrow (just ConfirmAsync / PromptAsync). DI hands us the concrete // instance, so a cast here is safe. if (Service is DialogService svc) svc.OnChange += OnServiceChanged; } public void Dispose() { if (Service is DialogService svc) svc.OnChange -= OnServiceChanged; } private void OnServiceChanged() { // Seed prompt input value when a new prompt dialog opens. if (Service is DialogService s && s.Current is { Kind: DialogKind.Prompt } promptState && !ReferenceEquals(promptState, _lastSeenState)) { _promptValue = promptState.PromptInitial; } _lastSeenState = (Service as DialogService)?.Current; InvokeAsync(StateHasChanged); } protected override async Task OnAfterRenderAsync(bool firstRender) { var current = (Service as DialogService)?.Current; if (current is not null) { try { await JS.InvokeVoidAsync("document.body.classList.add", "modal-open"); } catch { /* prerender: no JS — ignore */ } // Focus once per opened dialog. Without this guard, every input // keystroke triggers a re-render which would re-focus the modal // element and yank the caret off the prompt input. if (!ReferenceEquals(current, _focusedForState)) { _focusedForState = current; try { if (current.Kind == DialogKind.Prompt) await _promptInputRef.FocusAsync(); else await _modalRef.FocusAsync(); } catch { /* element not yet attached: ignore */ } } } else { _focusedForState = null; try { await JS.InvokeVoidAsync("document.body.classList.remove", "modal-open"); } catch { /* prerender: no JS — ignore */ } } } private void OnKeyDown(KeyboardEventArgs e) { if (e.Key == "Escape") { Cancel(); } } private void OnPromptInput(ChangeEventArgs e) { _promptValue = e.Value?.ToString() ?? string.Empty; } private void Cancel() { if (Service is not DialogService svc || svc.Current is null) return; var resolveValue = svc.Current.Kind == DialogKind.Confirm ? (object)false : (object?)null; _promptValue = string.Empty; svc.Resolve(resolveValue); } private void Confirm() { if (Service is not DialogService svc || svc.Current is null) return; var resolveValue = svc.Current.Kind == DialogKind.Confirm ? (object)true : (object?)_promptValue; _promptValue = string.Empty; svc.Resolve(resolveValue); } private static string ConfirmLabel(DialogState state) => state.Kind switch { DialogKind.Prompt => "Save", _ => state.Danger ? "Delete" : "Confirm", }; }