From bc47fe7e4e82f2cea8e579c618ad6564c5da5fed Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Sat, 26 Sep 2026 14:21:03 +0200 Subject: [PATCH 01/11] fix(components): accept `Class` on every component that exposed only `ClassName` 30 CLI templates (Table and its parts, Select, Switch, Checkbox, RadioGroup, Tooltip, Toast, Popover, Dropdown, Slider, Separator, ScrollArea, Skeleton, the date/time pickers, Form, Menubar, Navbar, NavigationMenu, Pagination, Resizable) and 8 package components declared only `ClassName`, while the rest of the library uses `Class`. `Class="..."` on those components fell into AdditionalAttributes and could replace the component's own class attribute, dropping its base styling (reported against Skeleton). Each of them now declares `Class` and merges it after `ClassName`, which stays as a deprecated alias so existing markup keeps working. Not marked [Obsolete]: the warning would also fire inside consumers' generated files and break TreatWarningsAsErrors builds. ClassParameterTests fails if any template or package component declares `ClassName` without also declaring and rendering `Class`. Co-Authored-By: Claude Opus 5.5 --- ShellUI.Tests/ClassParameterTests.cs | 64 +++++++++++++++++++ src/ShellUI.Components/Components/Form.razor | 6 +- .../Components/Menubar.razor | 4 +- .../Components/MenubarItem.razor | 4 +- .../Components/NavItem.razor | 4 +- .../Components/Navbar.razor | 4 +- .../Components/NavigationMenu.razor | 4 +- .../Components/NavigationMenuItem.razor | 4 +- .../Components/Pagination.razor | 4 +- .../Templates/CheckboxTemplate.cs | 6 +- .../Templates/DatePickerTemplate.cs | 6 +- .../Templates/DateRangePickerTemplate.cs | 6 +- .../Templates/DropdownTemplate.cs | 6 +- .../Templates/FormTemplate.cs | 6 +- .../Templates/MenubarItemTemplate.cs | 6 +- .../Templates/MenubarTemplate.cs | 6 +- .../Templates/NavbarTemplate.cs | 6 +- .../Templates/NavigationMenuItemTemplate.cs | 8 ++- .../Templates/NavigationMenuTemplate.cs | 6 +- .../Templates/PaginationTemplate.cs | 6 +- .../Templates/PopoverTemplate.cs | 6 +- .../Templates/RadioGroupItemTemplate.cs | 6 +- .../Templates/RadioGroupTemplate.cs | 6 +- .../Templates/ResizableTemplate.cs | 6 +- .../Templates/ScrollAreaTemplate.cs | 6 +- .../Templates/SelectTemplate.cs | 6 +- .../Templates/SeparatorTemplate.cs | 6 +- .../Templates/SkeletonTemplate.cs | 6 +- .../Templates/SliderTemplate.cs | 6 +- .../Templates/SwitchTemplate.cs | 6 +- .../Templates/TableBodyTemplate.cs | 6 +- .../Templates/TableCellTemplate.cs | 6 +- .../Templates/TableHeadTemplate.cs | 6 +- .../Templates/TableHeaderTemplate.cs | 6 +- .../Templates/TableRowTemplate.cs | 6 +- .../Templates/TableTemplate.cs | 6 +- .../Templates/TimePickerTemplate.cs | 6 +- .../Templates/ToastTemplate.cs | 6 +- .../Templates/TooltipTemplate.cs | 6 +- 39 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 ShellUI.Tests/ClassParameterTests.cs diff --git a/ShellUI.Tests/ClassParameterTests.cs b/ShellUI.Tests/ClassParameterTests.cs new file mode 100644 index 0000000..8c1421f --- /dev/null +++ b/ShellUI.Tests/ClassParameterTests.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using ShellUI.Templates; +using Xunit; + +namespace ShellUI.Tests; + +/// `Class` is the library-wide parameter for extra CSS classes. Components that only +/// declared `ClassName` silently dropped `Class="..."`: Blazor routed it into +/// AdditionalAttributes, where it could replace the component's own class attribute +/// and strip its base styling (reported against Skeleton). `ClassName` remains as a +/// deprecated alias, but it must never be the only way in. +public class ClassParameterTests +{ + private static readonly Regex DeclaresClassName = new(@"public string\?? ClassName\b"); + private static readonly Regex DeclaresClass = new(@"public string\? Class \{"); + // Markup `@Class`, a Shell.Cn argument `, Class)` or a concatenation `+ Class)`. + private static readonly Regex RendersClass = new(@"@Class\b|[,+]\s*Class\)"); + + [Fact] + public void EveryTemplateWithClassName_AlsoAcceptsAndRendersClass() + { + var offenders = new List(); + foreach (var (name, _) in ComponentRegistry.Components) + { + var content = ComponentRegistry.GetComponentContent(name); + if (content is null || !DeclaresClassName.IsMatch(content)) continue; + + if (!DeclaresClass.IsMatch(content)) offenders.Add($"{name}: no `Class` parameter"); + else if (!RendersClass.IsMatch(content)) offenders.Add($"{name}: `Class` declared but never rendered"); + } + + Assert.True(offenders.Count == 0, + "Templates that expose ClassName must also accept and render Class:\n " + + string.Join("\n ", offenders)); + } + + [Fact] + public void EveryPackageComponentWithClassName_AlsoAcceptsAndRendersClass() + { + var offenders = new List(); + foreach (var path in Directory.GetFiles(GetComponentsDirectory(), "*.razor")) + { + var content = File.ReadAllText(path); + if (!DeclaresClassName.IsMatch(content)) continue; + + var file = Path.GetFileName(path); + if (!DeclaresClass.IsMatch(content)) offenders.Add($"{file}: no `Class` parameter"); + else if (!RendersClass.IsMatch(content)) offenders.Add($"{file}: `Class` declared but never rendered"); + } + + Assert.True(offenders.Count == 0, + "Package components that expose ClassName must also accept and render Class:\n " + + string.Join("\n ", offenders)); + } + + private static string GetComponentsDirectory([CallerFilePath] string thisFile = "") + { + var testDir = Path.GetDirectoryName(thisFile) ?? throw new InvalidOperationException("CallerFilePath is empty"); + return Path.GetFullPath(Path.Combine(testDir, "..", "src", "ShellUI.Components", "Components")); + } +} diff --git a/src/ShellUI.Components/Components/Form.razor b/src/ShellUI.Components/Components/Form.razor index 72022cc..c40432b 100644 --- a/src/ShellUI.Components/Components/Form.razor +++ b/src/ShellUI.Components/Components/Form.razor @@ -1,7 +1,7 @@ @namespace ShellUI.Components @using Microsoft.AspNetCore.Components.Forms -
+ @ChildContent
@@ -15,6 +15,10 @@ [Parameter] public EventCallback OnInvalidSubmit { get; set; } + [Parameter] + public string? Class { get; set; } + + // Deprecated alias for Class; kept so markup written against older ShellUI versions still applies. [Parameter] public string ClassName { get; set; } = "space-y-6"; diff --git a/src/ShellUI.Components/Components/Menubar.razor b/src/ShellUI.Components/Components/Menubar.razor index a6343e1..b69cbe0 100644 --- a/src/ShellUI.Components/Components/Menubar.razor +++ b/src/ShellUI.Components/Components/Menubar.razor @@ -1,11 +1,13 @@ @namespace ShellUI.Components -
+
@ChildContent
@code { [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? Class { get; set; } + // Deprecated alias for Class; kept so markup written against older ShellUI versions still applies. [Parameter] public string ClassName { get; set; } = ""; [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } diff --git a/src/ShellUI.Components/Components/MenubarItem.razor b/src/ShellUI.Components/Components/MenubarItem.razor index e19d8d7..b7beb3c 100644 --- a/src/ShellUI.Components/Components/MenubarItem.razor +++ b/src/ShellUI.Components/Components/MenubarItem.razor @@ -1,6 +1,6 @@ @namespace ShellUI.Components -
+
@@ -35,6 +35,10 @@ public class MenubarItemTemplate [Parameter] public bool Disabled { get; set; } + [Parameter] + public string? Class { get; set; } + + // Deprecated alias for Class; kept so markup written against older ShellUI versions still applies. [Parameter] public string ClassName { get; set; } = """"; diff --git a/src/ShellUI.Templates/Templates/MenubarTemplate.cs b/src/ShellUI.Templates/Templates/MenubarTemplate.cs index fc0ac55..87bcd41 100644 --- a/src/ShellUI.Templates/Templates/MenubarTemplate.cs +++ b/src/ShellUI.Templates/Templates/MenubarTemplate.cs @@ -18,7 +18,7 @@ public class MenubarTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI -
+
@ChildContent
@@ -26,6 +26,10 @@ public class MenubarTemplate [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] + public string? Class { get; set; } + + // Deprecated alias for Class; kept so markup written against older ShellUI versions still applies. [Parameter] public string ClassName { get; set; } = """"; diff --git a/src/ShellUI.Templates/Templates/NavbarTemplate.cs b/src/ShellUI.Templates/Templates/NavbarTemplate.cs index 86d0b3c..db89fa8 100644 --- a/src/ShellUI.Templates/Templates/NavbarTemplate.cs +++ b/src/ShellUI.Templates/Templates/NavbarTemplate.cs @@ -17,7 +17,7 @@ public static class NavbarTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI -