feat(centralui): DialogHost ShowAsync<T> custom-content + focus trap/restore + backdrop hook (T33a)
This commit is contained in:
@@ -8,14 +8,16 @@
|
||||
|
||||
@if (Service is DialogService svc && svc.Current is { } state)
|
||||
{
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
@* `sb-modal-backdrop` is a tokenized hook: a separate task attaches the
|
||||
var-driven background rule to it. No inline style here. *@
|
||||
<div class="modal-backdrop fade show sb-modal-backdrop"></div>
|
||||
<div @ref="_modalRef"
|
||||
class="modal fade show d-block"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@onkeydown="OnKeyDown">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-dialog modal-dialog-centered @state.Size" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@state.Title</h5>
|
||||
@@ -26,6 +28,10 @@
|
||||
{
|
||||
<p class="mb-0">@state.Body</p>
|
||||
}
|
||||
else if (state.Kind == DialogKind.Custom)
|
||||
{
|
||||
@state.Content
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label">@state.Body</label>
|
||||
@@ -36,14 +42,19 @@
|
||||
@oninput="OnPromptInput" />
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="Cancel">Cancel</button>
|
||||
<button type="button"
|
||||
class="btn @(state.Danger ? "btn-danger" : "btn-primary") btn-sm"
|
||||
@onclick="Confirm">
|
||||
@ConfirmLabel(state)
|
||||
</button>
|
||||
</div>
|
||||
@* Custom dialogs supply their own action buttons inside the body,
|
||||
so the standard footer is suppressed for that kind. *@
|
||||
@if (state.Kind != DialogKind.Custom)
|
||||
{
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="Cancel">Cancel</button>
|
||||
<button type="button"
|
||||
class="btn @(state.Danger ? "btn-danger" : "btn-primary") btn-sm"
|
||||
@onclick="Confirm">
|
||||
@ConfirmLabel(state)
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +106,13 @@
|
||||
if (!ReferenceEquals(current, _focusedForState))
|
||||
{
|
||||
_focusedForState = current;
|
||||
|
||||
// Stash the element that had focus when the dialog opened so it
|
||||
// can be restored on close. Must run BEFORE the modal steals
|
||||
// focus below, otherwise we'd capture a modal element.
|
||||
try { await JS.InvokeVoidAsync("sbDialog.captureActiveElement"); }
|
||||
catch { /* prerender: no JS — ignore */ }
|
||||
|
||||
try
|
||||
{
|
||||
if (current.Kind == DialogKind.Prompt)
|
||||
@@ -110,15 +128,28 @@
|
||||
_focusedForState = null;
|
||||
try { await JS.InvokeVoidAsync("document.body.classList.remove", "modal-open"); }
|
||||
catch { /* prerender: no JS — ignore */ }
|
||||
|
||||
// Return focus to whatever element triggered the dialog so keyboard
|
||||
// users are not dumped at the top of the document on close.
|
||||
try { await JS.InvokeVoidAsync("sbDialog.restoreActiveElement"); }
|
||||
catch { /* prerender: no JS — ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyboardEventArgs e)
|
||||
private async Task OnKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Escape")
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
else if (e.Key == "Tab")
|
||||
{
|
||||
// Trap Tab/Shift+Tab inside the modal so focus cannot escape to the
|
||||
// (inert) page behind the backdrop. The JS helper wraps focus at the
|
||||
// first/last focusable element and prevents the default move there.
|
||||
try { await JS.InvokeVoidAsync("sbDialog.focusTrap", _modalRef, e.ShiftKey); }
|
||||
catch { /* prerender / no JS: skip — native Tab order still applies */ }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPromptInput(ChangeEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user