Skip to content

Commit a23cd58

Browse files
csharpfritzCopilot
andcommitted
feat(theming): Full Skins & Themes implementation (#369)
Wave 1 - Core Theme Fidelity: - ThemeMode enum (StyleSheetTheme/Theme) with dual-mode ApplyThemeSkin - Sub-component style theming (SubStyles on ControlSkin, SkinBuilder.SubStyle()) - 5 data controls override ApplyThemeSkin: GridView, DetailsView, FormView, DataGrid, DataList - Container-level EnableTheming propagation via ancestor chain walk - Runtime theme switching via ThemeProvider Mode parameter - Fix generic type name lookup (GridView1 -> GridView) for theme skin matching Wave 2 - Migration Accelerators: - .skin file parser (SkinFileParser) - reads Web Forms .skin files into ThemeConfiguration - JSON theme format (JsonThemeLoader) - load/save themes as JSON with custom converters - CSS file bundling - ThemeProvider renders <link> elements via HeadContent Wave 3 - Diagnostics: - ThemeDiagnostics with validation rules for unknown controls, sub-styles, empty skins - Runtime SkinID mismatch logging in BaseWebFormsComponent Tests: 120 theming tests (72 Wave 1 + 48 Wave 2), 2685 total tests passing Docs: themes-and-skins.md with migration guide, API reference, quick start Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f4a23d4 commit a23cd58

24 files changed

Lines changed: 4215 additions & 39 deletions

docs/themes-and-skins.md

Lines changed: 469 additions & 0 deletions
Large diffs are not rendered by default.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ plugins:
6464
nav:
6565
- Home: README.md
6666
- Component Health Dashboard: dashboard.md
67+
- Themes and Skins: themes-and-skins.md
6768
- Editor Controls:
6869
- AdRotator: EditorControls/AdRotator.md
6970
- BulletedList: EditorControls/BulletedList.md
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
@inherits Bunit.TestContext
2+
@using BlazorWebFormsComponents.Theming
3+
@using BlazorWebFormsComponents.Enums
4+
@using static BlazorWebFormsComponents.WebColor
5+
@using Moq
6+
7+
@code {
8+
9+
public ContainerPropagationTests()
10+
{
11+
Services.AddSingleton<Microsoft.AspNetCore.DataProtection.IDataProtectionProvider>(
12+
new Microsoft.AspNetCore.DataProtection.EphemeralDataProtectionProvider());
13+
Services.AddSingleton<Microsoft.AspNetCore.Routing.LinkGenerator>(new Mock<Microsoft.AspNetCore.Routing.LinkGenerator>().Object);
14+
Services.AddSingleton<Microsoft.AspNetCore.Http.IHttpContextAccessor>(new Mock<Microsoft.AspNetCore.Http.IHttpContextAccessor>().Object);
15+
}
16+
17+
/// <summary>
18+
/// EnableTheming=false on parent Panel blocks child theming (Issue #369 / WI-5).
19+
/// Child Button inside a Panel with EnableTheming=false should not receive theme.
20+
/// </summary>
21+
[Fact]
22+
public void EnableThemingFalse_OnParent_BlocksChildTheming()
23+
{
24+
var theme = new ThemeConfiguration()
25+
.ForControl("Button", skin => skin
26+
.Set(s => s.BackColor, Blue))
27+
.ForControl("Panel", skin => skin
28+
.Set(s => s.BackColor, Red));
29+
30+
var cut = Render(
31+
@<ThemeProvider Theme="theme">
32+
<Panel EnableTheming="false">
33+
<ChildContent>
34+
<Button Text="Child" />
35+
</ChildContent>
36+
</Panel>
37+
</ThemeProvider>
38+
);
39+
40+
// Panel should not be themed
41+
var div = cut.Find("div");
42+
div.HasAttribute("style").ShouldBeFalse();
43+
44+
// Button inside should NOT be themed (ancestor has EnableTheming=false)
45+
var input = cut.Find("input");
46+
input.HasAttribute("style").ShouldBeFalse();
47+
}
48+
49+
/// <summary>
50+
/// EnableTheming=false doesn't affect siblings.
51+
/// A Panel with EnableTheming=false next to a Button shouldn't block Button's theming.
52+
/// </summary>
53+
[Fact]
54+
public void EnableThemingFalse_DoesNotAffectSiblings()
55+
{
56+
var theme = new ThemeConfiguration()
57+
.ForControl("Button", skin => skin
58+
.Set(s => s.BackColor, Blue))
59+
.ForControl("Panel", skin => skin
60+
.Set(s => s.BackColor, Red));
61+
62+
var cut = Render(
63+
@<ThemeProvider Theme="theme">
64+
<Panel EnableTheming="false">
65+
<ChildContent>
66+
<text>Panel Content</text>
67+
</ChildContent>
68+
</Panel>
69+
<Button Text="Sibling" />
70+
</ThemeProvider>
71+
);
72+
73+
// Panel should not be themed
74+
var div = cut.Find("div");
75+
div.HasAttribute("style").ShouldBeFalse();
76+
77+
// Sibling Button SHOULD be themed (not affected by Panel's EnableTheming)
78+
var input = cut.Find("input");
79+
input.GetAttribute("style").ShouldContain("background-color:Blue");
80+
}
81+
82+
/// <summary>
83+
/// Nested containers propagate EnableTheming=false.
84+
/// Panel > Panel > Button chain should propagate the ancestor's EnableTheming=false.
85+
/// </summary>
86+
[Fact]
87+
public void EnableThemingFalse_PropagatesThroughNestedContainers()
88+
{
89+
var theme = new ThemeConfiguration()
90+
.ForControl("Button", skin => skin
91+
.Set(s => s.BackColor, Blue))
92+
.ForControl("Panel", skin => skin
93+
.Set(s => s.BackColor, Red));
94+
95+
var cut = Render(
96+
@<ThemeProvider Theme="theme">
97+
<Panel EnableTheming="false">
98+
<ChildContent>
99+
<Panel>
100+
<ChildContent>
101+
<Button Text="Nested" />
102+
</ChildContent>
103+
</Panel>
104+
</ChildContent>
105+
</Panel>
106+
</ThemeProvider>
107+
);
108+
109+
// Outer panel: no theme
110+
var outerDiv = cut.FindAll("div")[0];
111+
outerDiv.HasAttribute("style").ShouldBeFalse();
112+
113+
// Inner panel: no theme (ancestor blocked)
114+
var innerDiv = cut.FindAll("div")[1];
115+
innerDiv.HasAttribute("style").ShouldBeFalse();
116+
117+
// Button: no theme (ancestor chain blocked)
118+
var input = cut.Find("input");
119+
input.HasAttribute("style").ShouldBeFalse();
120+
}
121+
122+
/// <summary>
123+
/// EnableTheming=true (default) allows theming.
124+
/// Panel with default EnableTheming containing a Button should allow both to be themed.
125+
/// </summary>
126+
[Fact]
127+
public void EnableThemingTrue_AllowsTheming()
128+
{
129+
var theme = new ThemeConfiguration()
130+
.ForControl("Button", skin => skin
131+
.Set(s => s.BackColor, Blue))
132+
.ForControl("Panel", skin => skin
133+
.Set(s => s.BackColor, Red));
134+
135+
var cut = Render(
136+
@<ThemeProvider Theme="theme">
137+
<Panel>
138+
<ChildContent>
139+
<Button Text="Child" />
140+
</ChildContent>
141+
</Panel>
142+
</ThemeProvider>
143+
);
144+
145+
// Panel should be themed
146+
var div = cut.Find("div");
147+
div.GetAttribute("style").ShouldContain("background-color:Red");
148+
149+
// Button inside should be themed
150+
var input = cut.Find("input");
151+
input.GetAttribute("style").ShouldContain("background-color:Blue");
152+
}
153+
154+
/// <summary>
155+
/// EnableTheming on child can't override parent's false.
156+
/// Even if child has EnableTheming=true explicitly, parent's false takes precedence.
157+
/// </summary>
158+
[Fact]
159+
public void ChildEnableThemingTrue_CannotOverrideParentFalse()
160+
{
161+
var theme = new ThemeConfiguration()
162+
.ForControl("Button", skin => skin
163+
.Set(s => s.BackColor, Blue));
164+
165+
var cut = Render(
166+
@<ThemeProvider Theme="theme">
167+
<Panel EnableTheming="false">
168+
<ChildContent>
169+
<Button Text="Child" EnableTheming="true" />
170+
</ChildContent>
171+
</Panel>
172+
</ThemeProvider>
173+
);
174+
175+
// Button should NOT be themed (parent blocks it)
176+
var input = cut.Find("input");
177+
input.HasAttribute("style").ShouldBeFalse();
178+
}
179+
180+
/// <summary>
181+
/// Mixed container levels with theming control.
182+
/// Outer=true, Middle=false, Inner=true → Inner respects Middle's false.
183+
/// </summary>
184+
[Fact]
185+
public void MixedEnableTheming_MiddleFalse_BlocksInner()
186+
{
187+
var theme = new ThemeConfiguration()
188+
.ForControl("Button", skin => skin
189+
.Set(s => s.BackColor, Blue));
190+
191+
var cut = Render(
192+
@<ThemeProvider Theme="theme">
193+
<Panel EnableTheming="true">
194+
<ChildContent>
195+
<Panel EnableTheming="false">
196+
<ChildContent>
197+
<Panel EnableTheming="true">
198+
<ChildContent>
199+
<Button Text="Deep" />
200+
</ChildContent>
201+
</Panel>
202+
</ChildContent>
203+
</Panel>
204+
</ChildContent>
205+
</Panel>
206+
</ThemeProvider>
207+
);
208+
209+
// Button deep inside should NOT be themed (middle ancestor blocked)
210+
var input = cut.Find("input");
211+
input.HasAttribute("style").ShouldBeFalse();
212+
}
213+
214+
/// <summary>
215+
/// EnableTheming=false on the component itself takes precedence.
216+
/// Even if parent allows theming, component's own EnableTheming=false wins.
217+
/// </summary>
218+
[Fact]
219+
public void ComponentEnableThemingFalse_OverridesParentTrue()
220+
{
221+
var theme = new ThemeConfiguration()
222+
.ForControl("Button", skin => skin
223+
.Set(s => s.BackColor, Blue));
224+
225+
var cut = Render(
226+
@<ThemeProvider Theme="theme">
227+
<Panel>
228+
<ChildContent>
229+
<Button Text="NoTheme" EnableTheming="false" />
230+
</ChildContent>
231+
</Panel>
232+
</ThemeProvider>
233+
);
234+
235+
// Button should NOT be themed (its own EnableTheming=false)
236+
var input = cut.Find("input");
237+
input.HasAttribute("style").ShouldBeFalse();
238+
}
239+
240+
}

0 commit comments

Comments
 (0)