feat(centralui): DialogHost ShowAsync<T> custom-content + focus trap/restore + backdrop hook (T33a)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
|
||||
/// <summary>
|
||||
@@ -54,6 +56,41 @@ public class DialogService : IDialogService
|
||||
return Project(tcs.Task, static r => r as string);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TResult?> ShowAsync<TResult>(string title, RenderFragment<DialogContext<TResult>> body, string? size = null)
|
||||
{
|
||||
EnsureNoActiveDialog();
|
||||
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_tcs = tcs;
|
||||
|
||||
// The context routes Close/Cancel back through the same boxed Resolve()
|
||||
// path the host uses for confirm/prompt — Close boxes the typed value,
|
||||
// Cancel passes null. Capturing Resolve (not a service reference) keeps
|
||||
// the body's surface narrow and the resolution path uniform.
|
||||
var context = new DialogContext<TResult>(Resolve);
|
||||
|
||||
// Materialise the caller's RenderFragment<DialogContext<TResult>> into a
|
||||
// plain RenderFragment bound to this context, so DialogState stays
|
||||
// generic-free and the host can render it without knowing TResult.
|
||||
var content = body(context);
|
||||
|
||||
Current = new DialogState(
|
||||
title,
|
||||
DialogKind.Custom,
|
||||
Body: string.Empty,
|
||||
Danger: false,
|
||||
PromptInitial: string.Empty,
|
||||
Placeholder: null,
|
||||
Content: content,
|
||||
Size: size);
|
||||
OnChange?.Invoke();
|
||||
|
||||
// Project the boxed result to TResult?: a Close value unboxes to the
|
||||
// typed result; a Cancel (null) yields default(TResult?). The cast is
|
||||
// safe because only Close (with a TResult) ever boxes a non-null value.
|
||||
return Project(tcs.Task, static r => r is TResult typed ? typed : default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits the host's result and projects it to the caller's type. The
|
||||
/// <c>await</c> here resumes on the caller's captured context (the renderer
|
||||
@@ -66,11 +103,14 @@ public class DialogService : IDialogService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the host component when the user dismisses or confirms the
|
||||
/// dialog. <paramref name="result"/> must be a <c>bool</c> for confirms
|
||||
/// and a <c>string?</c> for prompts (null = cancel).
|
||||
/// Called by the host component (or a custom dialog's
|
||||
/// <see cref="DialogContext{TResult}"/>) when the user dismisses or confirms
|
||||
/// the dialog. <paramref name="result"/> must be a <c>bool</c> for confirms,
|
||||
/// a <c>string?</c> for prompts, or the boxed <c>TResult</c> (null = cancel)
|
||||
/// for custom dialogs.
|
||||
/// </summary>
|
||||
/// <param name="result">The user's response: a <c>bool</c> for confirms or a <c>string?</c> for prompts.</param>
|
||||
/// <param name="result">The user's response: a <c>bool</c> for confirms, a
|
||||
/// <c>string?</c> for prompts, or the boxed custom result (null = cancel).</param>
|
||||
internal void Resolve(object? result)
|
||||
{
|
||||
var tcs = _tcs;
|
||||
@@ -92,24 +132,33 @@ public class DialogService : IDialogService
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a dialog's display state, exposed read-only on
|
||||
/// <see cref="DialogService.Current"/> for the host component to render.
|
||||
/// <see cref="DialogService.Current"/> for the host component to render. New
|
||||
/// fields are appended as optional positional parameters so existing
|
||||
/// confirm/prompt construct calls keep compiling unchanged.
|
||||
/// </summary>
|
||||
/// <param name="Title">Modal title text.</param>
|
||||
/// <param name="Kind">Discriminates between confirm and prompt rendering.</param>
|
||||
/// <param name="Body">For confirm: the message; for prompt: the input label.</param>
|
||||
/// <param name="Kind">Discriminates between confirm, prompt, and custom rendering.</param>
|
||||
/// <param name="Body">For confirm: the message; for prompt: the input label; ignored for custom.</param>
|
||||
/// <param name="Danger">When true, the confirm button uses danger styling.</param>
|
||||
/// <param name="PromptInitial">Initial value for prompt-kind dialogs.</param>
|
||||
/// <param name="Placeholder">Placeholder shown when the prompt input is empty.</param>
|
||||
/// <param name="Content">For <see cref="DialogKind.Custom"/>: the body fragment to
|
||||
/// render inside <c>.modal-body</c>; <c>null</c> for confirm/prompt.</param>
|
||||
/// <param name="Size">Optional Bootstrap modal-dialog size modifier class applied
|
||||
/// to the <c>modal-dialog</c> element (e.g. <c>modal-lg</c>); <c>null</c> = default.</param>
|
||||
public record DialogState(
|
||||
string Title,
|
||||
DialogKind Kind,
|
||||
string Body,
|
||||
bool Danger,
|
||||
string PromptInitial,
|
||||
string? Placeholder);
|
||||
string? Placeholder,
|
||||
RenderFragment? Content = null,
|
||||
string? Size = null);
|
||||
|
||||
public enum DialogKind
|
||||
{
|
||||
Confirm,
|
||||
Prompt
|
||||
Prompt,
|
||||
Custom
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user