diff --git a/ShellUI.Tests/ClassParameterTests.cs b/ShellUI.Tests/ClassParameterTests.cs index cf633f3..3b714fc 100644 --- a/ShellUI.Tests/ClassParameterTests.cs +++ b/ShellUI.Tests/ClassParameterTests.cs @@ -11,7 +11,7 @@ public class ClassParameterTests { private static readonly Regex DeclaresClassName = new(@"public string\?? ClassName\b"); private static readonly Regex DeclaresClass = new(@"public string\? Class \{"); - private static readonly Regex RendersClass = new(@"@Class\b|[,+]\s*Class\)"); + private static readonly Regex RendersClass = new(@"@Class\b|[,+]\s*Class\s*\)"); [Fact] public void EveryTemplateWithClassName_AlsoAcceptsAndRendersClass() diff --git a/ShellUI.Tests/PackageBehaviorTests.cs b/ShellUI.Tests/PackageBehaviorTests.cs index 4e92919..49b24a8 100644 --- a/ShellUI.Tests/PackageBehaviorTests.cs +++ b/ShellUI.Tests/PackageBehaviorTests.cs @@ -116,6 +116,24 @@ public void LegacySheet_TrapsFocusAndReleasesItWhenEscapeCloses() ctx.JSInterop.VerifyInvoke("ShellUI.releaseFocus"); } + [Fact] + public void RadioGroup_SelectsTheClickedItemButNotADisabledOne() + { + using var ctx = new BunitContext(); + var changes = new List(); + + var group = ctx.Render(p => p + .Add(c => c.ValueChanged, EventCallback.Factory.Create(this, v => changes.Add(v))) + .AddChildContent(i => i.Add(c => c.Value, "a").AddChildContent("A")) + .AddChildContent(i => i.Add(c => c.Value, "b").Add(c => c.Disabled, true).AddChildContent("B"))); + + var buttons = group.FindAll("button"); + buttons[1].Click(); + buttons[0].Click(); + + Assert.Equal(new[] { "a" }, changes); + } + [Fact] public void TimePicker_ShowsASetValue() { diff --git a/ShellUI.Tests/TemplateSyncTests.cs b/ShellUI.Tests/TemplateSyncTests.cs index a54226a..b179a86 100644 --- a/ShellUI.Tests/TemplateSyncTests.cs +++ b/ShellUI.Tests/TemplateSyncTests.cs @@ -136,13 +136,29 @@ public class TemplateSyncTests [InlineData("sheet-content", "SheetContent.razor")] [InlineData("drawer-content", "DrawerContent.razor")] [InlineData("sidebar-menu-button", "SidebarMenuButton.razor")] + [InlineData("table", "Table.razor")] + [InlineData("table-body", "TableBody.razor")] + [InlineData("table-cell", "TableCell.razor")] + [InlineData("table-head", "TableHead.razor")] + [InlineData("table-header", "TableHeader.razor")] + [InlineData("table-row", "TableRow.razor")] + [InlineData("separator", "Separator.razor")] + [InlineData("slider", "Slider.razor")] + [InlineData("navbar", "Navbar.razor")] + [InlineData("checkbox", "Checkbox.razor")] + [InlineData("switch", "Switch.razor")] + [InlineData("tooltip", "Tooltip.razor")] + [InlineData("date-range-picker", "DateRangePicker.razor")] + [InlineData("radio-group", "RadioGroup.razor")] + [InlineData("radio-group-item", "RadioGroupItem.razor")] + [InlineData("empty-state", "EmptyState.razor")] public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string razorFileName) { if (AllowedDrift.ContainsKey(templateName)) return; - var liveContent = File.ReadAllText(GetLiveRazorPath(razorFileName)); - var templateContent = ComponentRegistry.GetComponentContent(templateName) - ?? throw new InvalidOperationException($"Template '{templateName}' not found in registry"); + var liveContent = WithoutClassNameAlias(File.ReadAllText(GetLiveRazorPath(razorFileName))); + var templateContent = WithoutClassNameAlias(ComponentRegistry.GetComponentContent(templateName) + ?? throw new InvalidOperationException($"Template '{templateName}' not found in registry")); var liveCode = ExtractCodeBlock(liveContent) ?? throw new InvalidOperationException($"Live {razorFileName} has no @code block"); @@ -168,11 +184,27 @@ public void TemplateCodeBlock_MatchesLiveLibrary(string templateName, string raz [InlineData("drawer-content", "DrawerContent.razor")] [InlineData("sidebar-menu-button", "SidebarMenuButton.razor")] [InlineData("theme-toggle", "ThemeToggle.razor")] + [InlineData("table", "Table.razor")] + [InlineData("table-body", "TableBody.razor")] + [InlineData("table-cell", "TableCell.razor")] + [InlineData("table-head", "TableHead.razor")] + [InlineData("table-header", "TableHeader.razor")] + [InlineData("table-row", "TableRow.razor")] + [InlineData("separator", "Separator.razor")] + [InlineData("slider", "Slider.razor")] + [InlineData("navbar", "Navbar.razor")] + [InlineData("checkbox", "Checkbox.razor")] + [InlineData("switch", "Switch.razor")] + [InlineData("tooltip", "Tooltip.razor")] + [InlineData("date-range-picker", "DateRangePicker.razor")] + [InlineData("radio-group", "RadioGroup.razor")] + [InlineData("radio-group-item", "RadioGroupItem.razor")] + [InlineData("empty-state", "EmptyState.razor")] public void TemplateMarkup_MatchesLiveLibrary(string templateName, string razorFileName) { - var liveContent = File.ReadAllText(GetLiveRazorPath(razorFileName)); - var templateContent = ComponentRegistry.GetComponentContent(templateName) - ?? throw new InvalidOperationException($"Template '{templateName}' not found in registry"); + var liveContent = WithoutClassNameAlias(File.ReadAllText(GetLiveRazorPath(razorFileName))); + var templateContent = WithoutClassNameAlias(ComponentRegistry.GetComponentContent(templateName) + ?? throw new InvalidOperationException($"Template '{templateName}' not found in registry")); var normalizedLive = Normalize(ExtractMarkup(liveContent)); var normalizedTemplate = Normalize(ExtractMarkup(templateContent)); @@ -202,6 +234,13 @@ private static string GetLiveRazorPath(string razorFileName, [CallerFilePath] st return Path.Combine(repoRoot, "src", "ShellUI.Components", "Components", razorFileName); } + // CLI templates keep the deprecated ClassName alias for existing projects; the package may not have it. + private static string WithoutClassNameAlias(string content) + { + var withoutParameter = Regex.Replace(content, @"^[ \t]*\[Parameter\]\s*public string ClassName \{ get; set; \} = ""\s*"";[ \t]*\r?\n", string.Empty, RegexOptions.Multiline); + return Regex.Replace(withoutParameter, @"\bClassName,\s*", string.Empty); + } + private static string Normalize(string code) { var withoutBlock = Regex.Replace(code, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline); diff --git a/src/ShellUI.Templates/Templates/CheckboxTemplate.cs b/src/ShellUI.Templates/Templates/CheckboxTemplate.cs index 2562e57..c6bc54c 100644 --- a/src/ShellUI.Templates/Templates/CheckboxTemplate.cs +++ b/src/ShellUI.Templates/Templates/CheckboxTemplate.cs @@ -23,7 +23,7 @@ public static class CheckboxTemplate aria-checked=""@Checked"" @onclick=""HandleClick"" disabled=""@Disabled"" - class=""peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 @(Checked ? ""bg-primary text-primary-foreground"" : ""bg-background"") @ClassName @Class"" + class=""@ComputedClass"" @attributes=""AdditionalAttributes""> @if (Checked) { @@ -34,25 +34,21 @@ public static class CheckboxTemplate @code { - [Parameter] - public bool Checked { get; set; } - - [Parameter] - public EventCallback CheckedChanged { get; set; } - - [Parameter] - public bool Disabled { get; set; } - - [Parameter] - public string? Class { get; set; } - + [Parameter] public bool Checked { get; set; } + [Parameter] public EventCallback CheckedChanged { get; set; } + [Parameter] public bool Disabled { get; set; } + [Parameter] public string? Class { get; set; } // Deprecated: use Class. - [Parameter] - public string ClassName { get; set; } = """"; - + [Parameter] public string ClassName { get; set; } = """"; [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + private string ComputedClass => Shell.Cn( + ""peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"", + Checked ? ""bg-primary text-primary-foreground"" : ""bg-background"", + ClassName, Class + ); + private async Task HandleClick() { if (!Disabled) diff --git a/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs b/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs index 2940337..a66e29a 100644 --- a/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs +++ b/src/ShellUI.Templates/Templates/DateRangePickerTemplate.cs @@ -20,11 +20,11 @@ @using Microsoft.JSInterop @implements IAsyncDisposable @inject IJSRuntime JS -
+
-@code { - [Parameter] - public DateTime? StartDate { get; set; } - - [Parameter] - public EventCallback StartDateChanged { get; set; } - - [Parameter] - public DateTime? EndDate { get; set; } - - [Parameter] - public EventCallback EndDateChanged { get; set; } - - [Parameter] - public string Placeholder { get; set; } = ""Pick a date range""; - - [Parameter] - public bool AllowClear { get; set; } = true; - - [Parameter] - public bool Disabled { get; set; } +@if (_isOpen) +{ +
+} +@code { + [Parameter] public DateTime? StartDate { get; set; } + [Parameter] public EventCallback StartDateChanged { get; set; } + [Parameter] public DateTime? EndDate { get; set; } + [Parameter] public EventCallback EndDateChanged { get; set; } + [Parameter] public string Placeholder { get; set; } = ""Pick a date range""; + [Parameter] public bool AllowClear { get; set; } = true; + [Parameter] public bool Disabled { get; set; } /// Close the calendar when the page scrolls; set false to keep it open. - [Parameter] - public bool CloseOnScroll { get; set; } - - [Parameter] - public string? Class { get; set; } - + [Parameter] public bool CloseOnScroll { get; set; } + [Parameter] public string? Class { get; set; } // Deprecated: use Class. - [Parameter] - public string ClassName { get; set; } = """"; - + [Parameter] public string ClassName { get; set; } = """"; [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } @@ -253,13 +238,19 @@ private async Task ClearRange() var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); var lastDay = firstDay.AddMonths(1).AddDays(-1); var startDayOfWeek = (int)firstDay.DayOfWeek; - + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); return days; } + private async Task CloseBackdrop() + { + _isOpen = false; + await UnregisterDismissAsync(); + } + public async ValueTask DisposeAsync() { await UnregisterDismissAsync(); diff --git a/src/ShellUI.Templates/Templates/EmptyStateTemplate.cs b/src/ShellUI.Templates/Templates/EmptyStateTemplate.cs index 1d30fa6..4bd4446 100644 --- a/src/ShellUI.Templates/Templates/EmptyStateTemplate.cs +++ b/src/ShellUI.Templates/Templates/EmptyStateTemplate.cs @@ -16,10 +16,10 @@ public static class EmptyStateTemplate }; public const string Content = """ -
+
@if (!string.IsNullOrEmpty(Icon)) { -
+
@((MarkupString)Icon)
} @@ -55,7 +55,9 @@ public static class EmptyStateTemplate [Parameter] public string? Description { get; set; } [Parameter] public RenderFragment? Action { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] public string Class { get; set; } = string.Empty; + [Parameter] public string? Class { get; set; } + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary? AdditionalAttributes { get; set; } } """; } diff --git a/src/ShellUI.Templates/Templates/NavbarTemplate.cs b/src/ShellUI.Templates/Templates/NavbarTemplate.cs index f570b42..d541fe5 100644 --- a/src/ShellUI.Templates/Templates/NavbarTemplate.cs +++ b/src/ShellUI.Templates/Templates/NavbarTemplate.cs @@ -17,23 +17,17 @@ public static class NavbarTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI -