feat(centralui): DialogHost ShowAsync<T> custom-content + focus trap/restore + backdrop hook (T33a)

This commit is contained in:
Joseph Doherty
2026-06-18 19:24:15 -04:00
parent c0aaba17ea
commit 4755ceee81
12 changed files with 438 additions and 23 deletions
@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Components;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
/// <summary>
/// Centralised dialog/modal service. Pages inject this service and call
/// <see cref="ConfirmAsync"/> or <see cref="PromptAsync"/> programmatically
/// instead of embedding per-page modal components. A single <c>DialogHost</c>
/// rendered in <c>MainLayout</c> displays the resulting dialog state.
/// <see cref="ConfirmAsync"/>, <see cref="PromptAsync"/>, or
/// <see cref="ShowAsync{TResult}"/> programmatically instead of embedding
/// per-page modal components. A single <c>DialogHost</c> rendered in
/// <c>MainLayout</c> displays the resulting dialog state.
/// </summary>
public interface IDialogService
{
@@ -31,4 +34,60 @@ public interface IDialogService
/// <param name="placeholder">Optional placeholder shown when the input is empty.</param>
/// <returns>A task that resolves to the entered string, or <c>null</c> if the user cancels.</returns>
Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null);
/// <summary>
/// Shows a modal hosting arbitrary custom body content and resolves to the
/// value the body closes with, or <c>default</c> (<c>null</c> for reference
/// types) if the user cancels (the body's own cancel path, the header close
/// button, or Escape). The body fragment receives a
/// <see cref="DialogContext{TResult}"/> and supplies its own action buttons —
/// the standard Cancel/Confirm footer is NOT rendered for this kind. Use this
/// to centralise modal chrome (backdrop, focus trap, focus restoration) while
/// keeping page-specific form content inside the body.
/// </summary>
/// <typeparam name="TResult">The type the body closes the dialog with.</typeparam>
/// <param name="title">Modal title text.</param>
/// <param name="body">Renders the modal body; receives the context whose
/// <c>Close</c>/<c>Cancel</c> methods resolve the returned task.</param>
/// <param name="size">Optional Bootstrap modal-dialog size modifier applied to
/// the <c>modal-dialog</c> element (e.g. <c>modal-lg</c>, <c>modal-sm</c>).
/// <c>null</c> leaves the default width.</param>
/// <returns>A task that resolves to the value passed to
/// <see cref="DialogContext{TResult}.Close"/>, or <c>default</c> on cancel.</returns>
Task<TResult?> ShowAsync<TResult>(string title, RenderFragment<DialogContext<TResult>> body, string? size = null);
}
/// <summary>
/// Handed to the body fragment of a <see cref="IDialogService.ShowAsync{TResult}"/>
/// dialog so the body can close itself with a typed result or cancel. Both
/// methods route back through the owning <see cref="DialogService"/> to complete
/// the awaited <c>ShowAsync</c> task and tear down the modal.
/// </summary>
/// <typeparam name="TResult">The type the dialog resolves with.</typeparam>
public sealed class DialogContext<TResult>
{
// Routes the close back to the owning service's Resolve(). Held as a typed
// delegate rather than a service reference so the context stays a small,
// self-contained closure handle and never widens the body's surface.
private readonly Action<object?> _resolve;
/// <summary>
/// Creates a context bound to the given resolve callback. The callback boxes
/// the result and forwards it to <see cref="DialogService.Resolve"/>.
/// </summary>
/// <param name="resolve">Invoked with the boxed result (<c>null</c> = cancel).</param>
internal DialogContext(Action<object?> resolve) => _resolve = resolve;
/// <summary>
/// Closes the dialog and resolves the awaited <c>ShowAsync</c> task with
/// <paramref name="result"/>.
/// </summary>
/// <param name="result">The value the caller awaits.</param>
public void Close(TResult result) => _resolve(result);
/// <summary>
/// Cancels the dialog and resolves the awaited <c>ShowAsync</c> task with
/// <c>default</c> (<c>null</c> for reference types).
/// </summary>
public void Cancel() => _resolve(null);
}