fix(secrets): ship the delete modal's own styles under collision-proof class names (0.2.3)
Fixes scadaproj#2. ConfirmDeleteModal used the bare `modal` class and the
Secrets.Ui RCL shipped no CSS at all, silently depending on the host having
no opinion about that selector. Bootstrap 5 does: `.modal { display: none }`
made the delete modal permanently invisible on every Bootstrap host
(ScadaBridge, OtOpcUa, HistorianGateway — confirmed live on the first two)
while every @onclick handler kept working. Only MxGateway, the lone
Bootstrap-free host, could ever have rendered it.
Fix, entirely inside the RCL so a package bump repairs every host with no
host-side changes:
- class vocabulary renamed to zb-secrets-modal / -backdrop / -card / -title
so no host framework selector can match the elements;
- the component emits its own <style> block alongside the markup. Scoped
.razor.css was deliberately NOT used: three of the four family hosts never
link a scoped-CSS bundle, so isolation CSS would silently fail to load —
the exact defect class being fixed. Theme tokens with fallbacks keep the
card legible even on a host without the kit stylesheet.
- data-testids unchanged, so existing Playwright gates and bUnit tests keep
their selectors.
New bUnit pins: no Bootstrap-reserved class name appears in the rendered
markup, and the component ships a style block that both positions the
overlay and gives it a display mode. Suite 182 pass / 0 fail / 0.2.3.
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.2.2</Version>
|
||||
<Version>0.2.3</Version>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -8,10 +8,34 @@
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="modal-backdrop-inpage" data-testid="delete-modal-backdrop"></div>
|
||||
<div class="modal" role="dialog" aria-modal="true" data-testid="delete-modal">
|
||||
<div class="modal-card">
|
||||
<h3 class="modal-title">Delete secret '@Name.Value'?</h3>
|
||||
@* The modal ships its own styles and uses zb-secrets-* class names (scadaproj#2): no host
|
||||
links a Secrets.Ui stylesheet, three of the four family hosts never link a scoped-CSS
|
||||
bundle at all, and host frameworks must not be able to match these elements — Bootstrap's
|
||||
bare `.modal { display: none }` rendered the previous markup permanently invisible on
|
||||
every Bootstrap host. Theme tokens with fallbacks keep it legible even without the kit. *@
|
||||
<style>
|
||||
.zb-secrets-modal-backdrop {
|
||||
position: fixed; inset: 0; z-index: 1080;
|
||||
background: rgba(15, 18, 20, 0.45);
|
||||
}
|
||||
.zb-secrets-modal {
|
||||
position: fixed; inset: 0; z-index: 1081;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--sans, system-ui, sans-serif);
|
||||
}
|
||||
.zb-secrets-modal-card {
|
||||
background: var(--card, #ffffff); color: var(--ink, #1a1d1f);
|
||||
border: 1px solid var(--rule-strong, #c9ced2);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
|
||||
padding: 1.25rem 1.5rem; max-width: 30rem; width: calc(100% - 2rem);
|
||||
}
|
||||
.zb-secrets-modal-title { margin: 0 0 0.5rem; font-size: 1.05rem; }
|
||||
.zb-secrets-modal-card .btn-row { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
||||
</style>
|
||||
<div class="zb-secrets-modal-backdrop" data-testid="delete-modal-backdrop"></div>
|
||||
<div class="zb-secrets-modal" role="dialog" aria-modal="true" data-testid="delete-modal">
|
||||
<div class="zb-secrets-modal-card">
|
||||
<h3 class="zb-secrets-modal-title">Delete secret '@Name.Value'?</h3>
|
||||
<p>This tombstones the secret so the removal can propagate. The action is audited.</p>
|
||||
|
||||
@if (_error is not null)
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ConfirmDeleteModalTests : TestContext
|
||||
.Add(c => c.OnDeleted, () => deleted = true));
|
||||
|
||||
// The confirmation is a rendered in-page modal element.
|
||||
Assert.NotEmpty(cut.FindAll(".modal"));
|
||||
Assert.NotEmpty(cut.FindAll(".zb-secrets-modal"));
|
||||
Assert.Contains("db/primary", cut.Markup);
|
||||
|
||||
cut.Find("[data-testid=confirm-delete]").Click();
|
||||
@@ -133,6 +133,59 @@ public class ConfirmDeleteModalTests : TestContext
|
||||
.Add(c => c.Visible, false)
|
||||
.Add(c => c.Name, name));
|
||||
|
||||
Assert.Empty(cut.FindAll(".modal"));
|
||||
Assert.Empty(cut.FindAll(".zb-secrets-modal"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// scadaproj#2 regression pins. The modal previously used the bare <c>modal</c> class and
|
||||
/// shipped no CSS, silently depending on the host having no opinion about it — Bootstrap 5's
|
||||
/// <c>.modal { display: none }</c> made the overlay permanently invisible on every
|
||||
/// Bootstrap host (ScadaBridge, OtOpcUa, HistorianGateway) while every handler kept working.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConfirmDeleteModal_UsesNoFrameworkReservedClassNames()
|
||||
{
|
||||
var cut = RenderVisibleModal();
|
||||
|
||||
// Bootstrap owns these selectors; any element carrying one is again at the mercy of
|
||||
// whatever stylesheet the host happens to link.
|
||||
string[] reserved = ["modal", "modal-title", "modal-backdrop", "modal-dialog", "modal-content"];
|
||||
foreach (string cls in reserved)
|
||||
{
|
||||
Assert.Empty(cut.FindAll($".{cls}"));
|
||||
}
|
||||
|
||||
// The elements themselves still render, under the component-scoped vocabulary.
|
||||
Assert.NotEmpty(cut.FindAll(".zb-secrets-modal"));
|
||||
Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-backdrop"));
|
||||
Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-card"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfirmDeleteModal_ShipsItsOwnStyles()
|
||||
{
|
||||
var cut = RenderVisibleModal();
|
||||
|
||||
// The RCL must carry the modal's CSS itself: no family host links a Secrets.Ui
|
||||
// stylesheet, and three of the four never link a scoped-CSS bundle at all — so any
|
||||
// styling delivered outside the component silently fails to load exactly like the
|
||||
// original defect. The inline block must both position the overlay and give it a
|
||||
// display mode, or a host reset/framework rule can hide it again.
|
||||
var style = cut.FindAll("style").SingleOrDefault();
|
||||
Assert.NotNull(style);
|
||||
Assert.Contains(".zb-secrets-modal", style.TextContent);
|
||||
Assert.Contains("position: fixed", style.TextContent);
|
||||
Assert.Contains("display: flex", style.TextContent);
|
||||
}
|
||||
|
||||
private IRenderedComponent<ConfirmDeleteModal> RenderVisibleModal()
|
||||
{
|
||||
Services.AddSingleton<ISecretStore>(new RecordingSecretStore());
|
||||
Services.AddSingleton<IAuditWriter>(new CapturingAuditWriter());
|
||||
this.AddTestAuthorization().SetAuthorized("carol");
|
||||
|
||||
return RenderComponent<ConfirmDeleteModal>(p => p
|
||||
.Add(c => c.Visible, true)
|
||||
.Add(c => c.Name, new SecretName("db/primary")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user