diff --git a/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor b/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor
index 793eb668..d02f190a 100644
--- a/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor
+++ b/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor
@@ -3,6 +3,7 @@
Editor Controls
@@ -51,7 +53,7 @@
Navigation Controls
diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/BulletedList/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/BulletedList/Index.razor
new file mode 100644
index 00000000..29ba7d14
--- /dev/null
+++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/BulletedList/Index.razor
@@ -0,0 +1,236 @@
+@page "/ControlSamples/BulletedList"
+@using BlazorWebFormsComponents.Enums
+
+BulletedList Component Samples
+
+Basic BulletedList with Static Items (Text Mode)
+
+<BulletedList TItem="object" StaticItems="basicItems" />
+
+@@code {
+ private ListItemCollection basicItems = new() {
+ new ListItem("First item", "1"),
+ new ListItem("Second item", "2"),
+ new ListItem("Third item", "3")
+ };
+}
+
+
+
+Bullet Style: Disc (Default for unordered)
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.Disc" />
+
+
+
+Bullet Style: Circle
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.Circle" />
+
+
+
+Bullet Style: Square
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.Square" />
+
+
+
+Bullet Style: Numbered
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.Numbered" />
+
+
+
+Bullet Style: Lower Alpha (a, b, c...)
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.LowerAlpha" />
+
+
+
+Bullet Style: Upper Alpha (A, B, C...)
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.UpperAlpha" />
+
+
+
+Bullet Style: Lower Roman (i, ii, iii...)
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.LowerRoman" />
+
+
+
+Bullet Style: Upper Roman (I, II, III...)
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.UpperRoman" />
+
+
+
+Numbered List Starting at 5
+
+<BulletedList TItem="object" StaticItems="bulletItems" BulletStyle="BulletStyle.Numbered" FirstBulletNumber="5" />
+
+
+
+Display Mode: HyperLink
+
+<BulletedList TItem="object" StaticItems="linkItems" DisplayMode="BulletedListDisplayMode.HyperLink" />
+
+@@code {
+ private ListItemCollection linkItems = new() {
+ new ListItem("Microsoft", "https://microsoft.com"),
+ new ListItem("GitHub", "https://github.com"),
+ new ListItem("Azure", "https://azure.com")
+ };
+}
+
+
+
+Display Mode: HyperLink with Target="_blank"
+
+<BulletedList TItem="object" StaticItems="linkItems" DisplayMode="BulletedListDisplayMode.HyperLink" Target="_blank" />
+
+
+
+Display Mode: LinkButton (Clickable)
+
+@clickMessage
+<BulletedList TItem="object"
+ StaticItems="clickItems"
+ DisplayMode="BulletedListDisplayMode.LinkButton"
+ OnClick="HandleItemClick" />
+
+@@code {
+ private void HandleItemClick(BulletedListEventArgs e) {
+ clickMessage = $"You clicked item at index {e.Index}";
+ }
+}
+
+
+
+Data-Bound BulletedList
+
+<BulletedList TItem="Product"
+ Items="products"
+ DataTextField="Name"
+ DataValueField="Url"
+ DisplayMode="BulletedListDisplayMode.HyperLink" />
+
+@@code {
+ private List<Product> products = new() {
+ new Product { Name = "Laptop", Url = "/products/laptop" },
+ new Product { Name = "Phone", Url = "/products/phone" }
+ };
+}
+
+
+
+With Styling
+
+<BulletedList TItem="object"
+ StaticItems="bulletItems"
+ CssClass="styled-list"
+ BackColor="new WebColor(System.Drawing.Color.LightYellow)"
+ ForeColor="new WebColor(System.Drawing.Color.DarkGreen)"
+ BorderStyle="BorderStyle.Dashed"
+ BorderWidth="Unit.Pixel(2)"
+ Width="Unit.Pixel(300)" />
+
+
+
+Disabled Item in List
+
+<BulletedList TItem="object"
+ StaticItems="disabledItems"
+ DisplayMode="BulletedListDisplayMode.HyperLink" />
+
+@@code {
+ private ListItemCollection disabledItems = new() {
+ new ListItem("Enabled Link", "https://example.com"),
+ new ListItem("Disabled Link", "https://blocked.com") { Enabled = false },
+ new ListItem("Another Enabled", "https://another.com")
+ };
+}
+
+
+
+Entire List Disabled
+
+(Entire BulletedList is disabled - items render as text)
+<BulletedList TItem="object"
+ StaticItems="linkItems"
+ DisplayMode="BulletedListDisplayMode.LinkButton"
+ Enabled="false" />
+
+@code {
+ private string clickMessage = "Click an item above to see the result.";
+
+ private ListItemCollection basicItems = new()
+ {
+ new ListItem("First item", "1"),
+ new ListItem("Second item", "2"),
+ new ListItem("Third item", "3")
+ };
+
+ private ListItemCollection bulletItems = new()
+ {
+ new ListItem("Item One", "1"),
+ new ListItem("Item Two", "2"),
+ new ListItem("Item Three", "3"),
+ new ListItem("Item Four", "4")
+ };
+
+ private ListItemCollection linkItems = new()
+ {
+ new ListItem("Microsoft", "https://microsoft.com"),
+ new ListItem("GitHub", "https://github.com"),
+ new ListItem("Azure", "https://azure.com")
+ };
+
+ private ListItemCollection clickItems = new()
+ {
+ new ListItem("Option A", "a"),
+ new ListItem("Option B", "b"),
+ new ListItem("Option C", "c")
+ };
+
+ private ListItemCollection disabledItems = new()
+ {
+ new ListItem("Enabled Link", "https://example.com"),
+ new ListItem("Disabled Link (renders as text)", "https://blocked.com") { Enabled = false },
+ new ListItem("Another Enabled Link", "https://another.com")
+ };
+
+ private List products = new()
+ {
+ new Product { Name = "Laptop", Url = "/products/laptop" },
+ new Product { Name = "Phone", Url = "/products/phone" },
+ new Product { Name = "Tablet", Url = "/products/tablet" }
+ };
+
+ private void HandleItemClick(BulletedListEventArgs e)
+ {
+ clickMessage = $"You clicked item at index {e.Index}: {clickItems[e.Index].Text}";
+ }
+
+ public class Product
+ {
+ public string Name { get; set; } = "";
+ public string Url { get; set; } = "";
+ }
+}
diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/DataList/RepeatColumns.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/DataList/RepeatColumns.razor
index 942c217c..02aaca79 100644
--- a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/DataList/RepeatColumns.razor
+++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/DataList/RepeatColumns.razor
@@ -1,4 +1,4 @@
-@page "/ControlSamples/DataList/RepeatColumns"
+@page "/ControlSamples/DataList/RepeatColumns"
DataList Repeat Layout Sample
@@ -44,7 +44,7 @@
runat="server"
EnableViewState="false"
RepeatDirection="Vertical"
- RepeatLayout="Table"
+ RepeatLayout="BlazorWebFormsComponents.Enums.RepeatLayout.Table"
RepeatColumns="4"
GridLines="Both"
Context="Item"
@@ -61,7 +61,7 @@
runat="server"
EnableViewState="false"
RepeatDirection="Vertical"
- RepeatLayout="Table"
+ RepeatLayout="BlazorWebFormsComponents.Enums.RepeatLayout.Table"
RepeatColumns="3"
GridLines="Both"
Context="Item"
@@ -78,7 +78,7 @@
runat="server"
EnableViewState="false"
RepeatDirection="Horizontal"
- RepeatLayout="Table"
+ RepeatLayout="BlazorWebFormsComponents.Enums.RepeatLayout.Table"
RepeatColumns="3"
GridLines="Both"
Context="Item"
@@ -95,7 +95,7 @@
runat="server"
EnableViewState="false"
RepeatDirection="Horizontal"
- RepeatLayout="Table"
+ RepeatLayout="BlazorWebFormsComponents.Enums.RepeatLayout.Table"
GridLines="Both"
Context="Item"
ItemType="SharedSampleObjects.Models.Widget">
diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/SiteMapPath/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/SiteMapPath/Index.razor
new file mode 100644
index 00000000..49af564f
--- /dev/null
+++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/SiteMapPath/Index.razor
@@ -0,0 +1,128 @@
+@page "/ControlSamples/SiteMapPath"
+
+SiteMapPath Sample
+
+SiteMapPath - Breadcrumb Navigation
+
+The SiteMapPath component displays a breadcrumb trail showing the current page's location in the site hierarchy.
+
+Basic Breadcrumb
+Select a page to see the breadcrumb path:
+
+
+ Current Page:
+
+ Home
+ Products
+ Electronics
+ Phones
+ Clothing
+
+
+
+
+
+
+
+Custom Separator
+Using a custom path separator:
+
+
+
+
+
+Current Node as Link
+Making the current page clickable:
+
+
+
+
+
+Reverse Path Direction
+Showing current page first:
+
+
+
+
+
+Limited Parent Levels
+Only showing 1 parent level:
+
+
+
+
+
+Custom Templates
+Using custom templates for nodes and separators:
+
+
+
+@code {
+ private string CurrentUrl = "/products/electronics/phones";
+ private SiteMapNode SiteMap;
+
+ protected override void OnInitialized()
+ {
+ SiteMap = BuildSiteMap();
+ }
+
+ private void OnPageChanged(ChangeEventArgs e)
+ {
+ CurrentUrl = e.Value?.ToString() ?? "/";
+ }
+
+ private static SiteMapNode BuildSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/", "Return to home page");
+
+ var products = new SiteMapNode("Products", "/products", "Browse our product catalog");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics", "Electronic devices");
+ var phones = new SiteMapNode("Phones", "/products/electronics/phones", "Mobile phones");
+ var clothing = new SiteMapNode("Clothing", "/products/clothing", "Apparel and accessories");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+ products.AddChild(clothing);
+ electronics.AddChild(phones);
+
+ return root;
+ }
+}
diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Table/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Table/Index.razor
new file mode 100644
index 00000000..27411ceb
--- /dev/null
+++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Table/Index.razor
@@ -0,0 +1,162 @@
+@page "/ControlSamples/Table"
+@using BlazorWebFormsComponents.Enums
+@using TableComponent = BlazorWebFormsComponents.Table
+@using TableRowComponent = BlazorWebFormsComponents.TableRow
+@using TableCellComponent = BlazorWebFormsComponents.TableCell
+@using TableHeaderCellComponent = BlazorWebFormsComponents.TableHeaderCell
+
+Table Component Samples
+
+Basic Table
+
+
+ Header 1
+ Header 2
+ Header 3
+
+
+ Cell 1
+ Cell 2
+ Cell 3
+
+
+ Cell 4
+ Cell 5
+ Cell 6
+
+
+<Table>
+ <TableRow>
+ <TableHeaderCell>Header 1</TableHeaderCell>
+ <TableHeaderCell>Header 2</TableHeaderCell>
+ </TableRow>
+ <TableRow>
+ <TableCell>Cell 1</TableCell>
+ <TableCell>Cell 2</TableCell>
+ </TableRow>
+</Table>
+
+
+
+Table with Caption
+
+
+ Product
+ Price
+
+
+ Widget
+ $10.00
+
+
+ Gadget
+ $25.00
+
+
+<Table Caption="Product List">
+ ...
+</Table>
+
+
+
+Table with Grid Lines
+
+
+ Name
+ Age
+ City
+
+
+ Alice
+ 30
+ New York
+
+
+ Bob
+ 25
+ Los Angeles
+
+
+<Table GridLines="GridLines.Both" CellPadding="5">
+ ...
+</Table>
+
+
+
+Cell Spanning
+
+
+ Full Width Header
+
+
+ Spans 2 Rows
+ Cell 1
+ Cell 2
+
+
+ Cell 3
+ Cell 4
+
+
+<TableHeaderCell ColumnSpan="3">Full Width Header</TableHeaderCell>
+<TableCell RowSpan="2">Spans 2 Rows</TableCell>
+
+
+
+Table with Styling
+
+
+ Column A
+ Column B
+
+
+ Centered
+ Top Aligned
+
+
+ Alternating
+ Row Color
+
+
+<Table BackColor="new WebColor(System.Drawing.Color.WhiteSmoke)"
+ BorderStyle="BorderStyle.Solid"
+ CellPadding="10">
+ <TableRow BackColor="new WebColor(System.Drawing.Color.LightBlue)">
+ <TableHeaderCell>Column A</TableHeaderCell>
+ </TableRow>
+ <TableRow>
+ <TableCell HorizontalAlign="HorizontalAlign.Center">Centered</TableCell>
+ </TableRow>
+</Table>
+
+
+
+Accessible Table with Header Scope
+
+
+ Product
+ Q1
+ Q2
+
+
+ Widgets
+ 100
+ 150
+
+
+ Gadgets
+ 200
+ 175
+
+
+<TableHeaderCell Scope="TableHeaderScope.Column">Product</TableHeaderCell>
+<TableHeaderCell Scope="TableHeaderScope.Row">Widgets</TableHeaderCell>
+
+@code {
+}
+
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/BulletStyle.razor b/src/BlazorWebFormsComponents.Test/BulletedList/BulletStyle.razor
new file mode 100644
index 00000000..b0499386
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/BulletStyle.razor
@@ -0,0 +1,202 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+
+@code {
+
+ [Fact]
+ public void BulletedList_DefaultStyle_RendersUnorderedList()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1"),
+ new ListItem("Item 2", "2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void BulletedList_DiscStyle_RendersWithDiscListStyleType()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+ ul.GetAttribute("style").ShouldContain("list-style-type: disc");
+ }
+
+ [Fact]
+ public void BulletedList_CircleStyle_RendersWithCircleListStyleType()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.GetAttribute("style").ShouldContain("list-style-type: circle");
+ }
+
+ [Fact]
+ public void BulletedList_SquareStyle_RendersWithSquareListStyleType()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.GetAttribute("style").ShouldContain("list-style-type: square");
+ }
+
+ [Fact]
+ public void BulletedList_NumberedStyle_RendersOrderedList()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1"),
+ new ListItem("Item 2", "2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.ShouldNotBeNull();
+ ol.GetAttribute("type").ShouldBe("1");
+ }
+
+ [Fact]
+ public void BulletedList_LowerAlphaStyle_RendersOrderedListWithLowerAlpha()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.ShouldNotBeNull();
+ ol.GetAttribute("type").ShouldBe("a");
+ }
+
+ [Fact]
+ public void BulletedList_UpperAlphaStyle_RendersOrderedListWithUpperAlpha()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.GetAttribute("type").ShouldBe("A");
+ }
+
+ [Fact]
+ public void BulletedList_LowerRomanStyle_RendersOrderedListWithLowerRoman()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.GetAttribute("type").ShouldBe("i");
+ }
+
+ [Fact]
+ public void BulletedList_UpperRomanStyle_RendersOrderedListWithUpperRoman()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.GetAttribute("type").ShouldBe("I");
+ }
+
+ [Fact]
+ public void BulletedList_CustomImageStyle_RendersWithListStyleImage()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+ ul.GetAttribute("style").ShouldContain("list-style-image: url('/images/bullet.png')");
+ }
+
+ [Fact]
+ public void BulletedList_FirstBulletNumber_SetsStartAttribute()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1"),
+ new ListItem("Item 2", "2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.GetAttribute("start").ShouldBe("5");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/ClickEvents.razor b/src/BlazorWebFormsComponents.Test/BulletedList/ClickEvents.razor
new file mode 100644
index 00000000..f7606192
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/ClickEvents.razor
@@ -0,0 +1,120 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public async Task BulletedList_LinkButtonClick_InvokesOnClickEvent()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 0", "value0"),
+ new ListItem("Item 1", "value1"),
+ new ListItem("Item 2", "value2")
+ };
+
+ int clickedIndex = -1;
+ var cut = Render(@ );
+
+ // Act
+ var links = cut.FindAll("a");
+ await links[1].ClickAsync(new Microsoft.AspNetCore.Components.Web.MouseEventArgs());
+
+ // Assert
+ clickedIndex.ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task BulletedList_LinkButtonClick_FirstItem_ReturnsIndexZero()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("First", "first"),
+ new ListItem("Second", "second")
+ };
+
+ int clickedIndex = -1;
+ var cut = Render(@ );
+
+ // Act
+ var links = cut.FindAll("a");
+ await links[0].ClickAsync(new Microsoft.AspNetCore.Components.Web.MouseEventArgs());
+
+ // Assert
+ clickedIndex.ShouldBe(0);
+ }
+
+ [Fact]
+ public async Task BulletedList_LinkButtonClick_LastItem_ReturnsCorrectIndex()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 0", "0"),
+ new ListItem("Item 1", "1"),
+ new ListItem("Item 2", "2"),
+ new ListItem("Item 3", "3")
+ };
+
+ int clickedIndex = -1;
+ var cut = Render(@ );
+
+ // Act
+ var links = cut.FindAll("a");
+ await links[3].ClickAsync(new Microsoft.AspNetCore.Components.Web.MouseEventArgs());
+
+ // Assert
+ clickedIndex.ShouldBe(3);
+ }
+
+ [Fact]
+ public void BulletedList_TextDisplayMode_NoClickHandler_NoLinks()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ int clickedIndex = -1;
+ var cut = Render(@ );
+
+ // Assert - Text mode should not render links even with OnClick handler
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void BulletedList_HyperLinkDisplayMode_NoClickEvents()
+ {
+ // HyperLink mode uses actual href navigation, not click events
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Link", "http://example.com")
+ };
+
+ var cut = Render(@ );
+
+ // Assert - Links should have actual href, not javascript:void(0)
+ var link = cut.Find("a");
+ link.GetAttribute("href").ShouldBe("http://example.com");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/DataBinding.razor b/src/BlazorWebFormsComponents.Test/BulletedList/DataBinding.razor
new file mode 100644
index 00000000..6bf741c8
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/DataBinding.razor
@@ -0,0 +1,125 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ public class Product
+ {
+ public string Name { get; set; }
+ public string Url { get; set; }
+ }
+
+ [Fact]
+ public void BulletedList_DataBinding_RendersItemsFromDataSource()
+ {
+ // Arrange
+ var products = new List
+ {
+ new Product { Name = "Product A", Url = "/products/a" },
+ new Product { Name = "Product B", Url = "/products/b" },
+ new Product { Name = "Product C", Url = "/products/c" }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(3);
+
+ var spans = cut.FindAll("li span");
+ spans[0].TextContent.ShouldBe("Product A");
+ spans[1].TextContent.ShouldBe("Product B");
+ spans[2].TextContent.ShouldBe("Product C");
+ }
+
+ [Fact]
+ public void BulletedList_DataBinding_WithHyperLinkMode_UsesValueFieldAsHref()
+ {
+ // Arrange
+ var products = new List
+ {
+ new Product { Name = "Product A", Url = "/products/a" },
+ new Product { Name = "Product B", Url = "/products/b" }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(2);
+ links[0].GetAttribute("href").ShouldBe("/products/a");
+ links[0].TextContent.ShouldBe("Product A");
+ links[1].GetAttribute("href").ShouldBe("/products/b");
+ links[1].TextContent.ShouldBe("Product B");
+ }
+
+ [Fact]
+ public void BulletedList_CombinedStaticAndDataBound_RendersAll()
+ {
+ // Arrange
+ var staticItems = new ListItemCollection
+ {
+ new ListItem("Static Item 1", "static1"),
+ new ListItem("Static Item 2", "static2")
+ };
+
+ var products = new List
+ {
+ new Product { Name = "Dynamic Item 1", Url = "dynamic1" },
+ new Product { Name = "Dynamic Item 2", Url = "dynamic2" }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(4);
+
+ var spans = cut.FindAll("li span");
+ spans[0].TextContent.ShouldBe("Static Item 1");
+ spans[1].TextContent.ShouldBe("Static Item 2");
+ spans[2].TextContent.ShouldBe("Dynamic Item 1");
+ spans[3].TextContent.ShouldBe("Dynamic Item 2");
+ }
+
+ [Fact]
+ public void BulletedList_EmptyDataSource_RendersEmptyList()
+ {
+ // Arrange
+ var products = new List();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void BulletedList_NullItems_RendersStaticItemsOnly()
+ {
+ // Arrange
+ var staticItems = new ListItemCollection
+ {
+ new ListItem("Static Only", "static")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(1);
+
+ var span = cut.Find("li span");
+ span.TextContent.ShouldBe("Static Only");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/DisplayMode.razor b/src/BlazorWebFormsComponents.Test/BulletedList/DisplayMode.razor
new file mode 100644
index 00000000..b2e79feb
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/DisplayMode.razor
@@ -0,0 +1,155 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void BulletedList_TextDisplayMode_RendersSpans()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "http://example.com/1"),
+ new ListItem("Item 2", "http://example.com/2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var spans = cut.FindAll("li span");
+ spans.Count.ShouldBe(2);
+ spans[0].TextContent.ShouldBe("Item 1");
+ spans[1].TextContent.ShouldBe("Item 2");
+
+ // Should not have any links
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void BulletedList_HyperLinkDisplayMode_RendersAnchors()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Link 1", "http://example.com/1"),
+ new ListItem("Link 2", "http://example.com/2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(2);
+ links[0].GetAttribute("href").ShouldBe("http://example.com/1");
+ links[0].TextContent.ShouldBe("Link 1");
+ links[1].GetAttribute("href").ShouldBe("http://example.com/2");
+ links[1].TextContent.ShouldBe("Link 2");
+ }
+
+ [Fact]
+ public void BulletedList_HyperLinkDisplayMode_WithTarget_SetsTargetAttribute()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Link 1", "http://example.com/1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var link = cut.Find("a");
+ link.GetAttribute("target").ShouldBe("_blank");
+ }
+
+ [Fact]
+ public void BulletedList_LinkButtonDisplayMode_RendersClickableLinks()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Click Me", "value1"),
+ new ListItem("Click Me Too", "value2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(2);
+ links[0].GetAttribute("href").ShouldBe("javascript:void(0)");
+ links[0].TextContent.ShouldBe("Click Me");
+ }
+
+ [Fact]
+ public void BulletedList_HyperLinkDisplayMode_DisabledItem_RendersSpan()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Enabled", "http://example.com/1") { Enabled = true },
+ new ListItem("Disabled", "http://example.com/2") { Enabled = false }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(1);
+ links[0].TextContent.ShouldBe("Enabled");
+
+ var spans = cut.FindAll("li span");
+ spans.Count.ShouldBe(1);
+ spans[0].TextContent.ShouldBe("Disabled");
+ }
+
+ [Fact]
+ public void BulletedList_LinkButtonDisplayMode_DisabledItem_RendersSpan()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Enabled", "1") { Enabled = true },
+ new ListItem("Disabled", "2") { Enabled = false }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(1);
+
+ var spans = cut.FindAll("li span");
+ spans.Count.ShouldBe(1);
+ spans[0].TextContent.ShouldBe("Disabled");
+ }
+
+ [Fact]
+ public void BulletedList_DisabledControl_RendersAllAsSpans()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "http://example.com/1"),
+ new ListItem("Item 2", "http://example.com/2")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(0);
+
+ var spans = cut.FindAll("li span");
+ spans.Count.ShouldBe(2);
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/StaticItems.razor b/src/BlazorWebFormsComponents.Test/BulletedList/StaticItems.razor
new file mode 100644
index 00000000..6260d682
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/StaticItems.razor
@@ -0,0 +1,100 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void BulletedList_WithStaticItems_RendersListItems()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Option 1", "1"),
+ new ListItem("Option 2", "2"),
+ new ListItem("Option 3", "3")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(3);
+
+ var spans = cut.FindAll("li span");
+ spans[0].TextContent.ShouldBe("Option 1");
+ spans[1].TextContent.ShouldBe("Option 2");
+ spans[2].TextContent.ShouldBe("Option 3");
+ }
+
+ [Fact]
+ public void BulletedList_EmptyItems_RendersEmptyList()
+ {
+ // Arrange
+ var items = new ListItemCollection();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.ShouldNotBeNull();
+
+ var listItems = cut.FindAll("li");
+ listItems.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void BulletedList_ClientID_RendersIdAttribute()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.GetAttribute("id").ShouldNotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void BulletedList_NotVisible_RendersNothing()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void BulletedList_ItemWithTextOnly_UsesToStringForValue()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem { Text = "Text Only" }
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var span = cut.Find("li span");
+ span.TextContent.ShouldBe("Text Only");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/BulletedList/Style.razor b/src/BlazorWebFormsComponents.Test/BulletedList/Style.razor
new file mode 100644
index 00000000..1a4e4490
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/BulletedList/Style.razor
@@ -0,0 +1,132 @@
+@using BulletStyleEnum = BlazorWebFormsComponents.Enums.BulletStyle
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void BulletedList_CssClass_AppliedToList()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ ul.GetAttribute("class").ShouldBe("my-custom-class");
+ }
+
+ [Fact]
+ public void BulletedList_BackColor_AppliedToStyle()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ var style = ul.GetAttribute("style");
+ style.ShouldContain("background-color");
+ }
+
+ [Fact]
+ public void BulletedList_ForeColor_AppliedToStyle()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ var style = ul.GetAttribute("style");
+ style.ShouldContain("color");
+ }
+
+ [Fact]
+ public void BulletedList_BorderStyle_AppliedToStyle()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ var style = ul.GetAttribute("style");
+ style.ShouldContain("border");
+ }
+
+ [Fact]
+ public void BulletedList_Width_AppliedToStyle()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ var style = ul.GetAttribute("style");
+ style.ShouldContain("width");
+ style.ShouldContain("200px");
+ }
+
+ [Fact]
+ public void BulletedList_Height_AppliedToStyle()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ul = cut.Find("ul");
+ var style = ul.GetAttribute("style");
+ style.ShouldContain("height");
+ style.ShouldContain("100px");
+ }
+
+ [Fact]
+ public void BulletedList_OrderedList_StyleApplied()
+ {
+ // Arrange
+ var items = new ListItemCollection
+ {
+ new ListItem("Item 1", "1")
+ };
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var ol = cut.Find("ol");
+ ol.GetAttribute("class").ShouldBe("styled-list");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/AlternatingTemplate.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/AlternatingTemplate.razor
index 189635b8..1d0c1b57 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/AlternatingTemplate.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/AlternatingTemplate.razor
@@ -4,7 +4,7 @@
[Fact]
public void DataList_TableLayout_AlternatingTemplate_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
== @Item.Name ==
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Caption.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Caption.razor
index 565ca8b7..820fc8be 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Caption.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Caption.razor
@@ -5,7 +5,7 @@
[Fact]
public void DataList_TableLayout_Caption_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ComplexStyle.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ComplexStyle.razor
index f144cb08..e8a117bc 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ComplexStyle.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ComplexStyle.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_ComplexStyle_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/DataBind.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/DataBind.razor
index bc269cd0..fcd4de1a 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/DataBind.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/DataBind.razor
@@ -13,7 +13,7 @@
[Fact]
public void DataList_TableLayout_DataBind_EventsTriggered()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FontStyle.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FontStyle.razor
index e2bde2c3..77e377ee 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FontStyle.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FontStyle.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_FontStyle_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleClass.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleClass.razor
index 19b43f00..e0d452ee 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleClass.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleClass.razor
@@ -4,7 +4,7 @@
[Fact]
public void DataList_TableLayout_FooterStyleClass_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleEmpty.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleEmpty.razor
index 4a2db78e..898041ce 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleEmpty.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleEmpty.razor
@@ -5,7 +5,7 @@
[Fact]
public void DataList_TableLayout_FooterStyleEmpty_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
FooterTemplate
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleStyle.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleStyle.razor
index 293403b9..a3c28c20 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleStyle.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterStyleStyle.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_FooterStyleStyle_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterTemplate.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterTemplate.razor
index 7f5d21df..8f7a0318 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterTemplate.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/FooterTemplate.razor
@@ -3,7 +3,7 @@
[Fact]
public void DataList_TableLayout_FooterTemplate_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
@Item.Name
FooterTemplate
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/GridLines.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/GridLines.razor
index 93e47077..6a590de1 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/GridLines.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/GridLines.razor
@@ -4,7 +4,7 @@
[Fact]
public void DataList_TableLayout_GridLines_Both_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
@@ -15,7 +15,7 @@
[Fact]
public void DataList_TableLayout_GridLines_Rows_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
@@ -26,7 +26,7 @@
[Fact]
public void DataList_TableLayout_GridLines_Cols_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleCss.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleCss.razor
index 84f49dc8..a5ef3973 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleCss.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleCss.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyleCss_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
@@ -23,7 +23,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyleCss_Null_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleFont.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleFont.razor
index cc0652f8..910f3257 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleFont.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleFont.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyleFont_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleTest.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleTest.razor
index 3e56b8e0..64dac191 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleTest.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleTest.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyle_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
@@ -27,7 +27,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyle_Empty_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleWrap.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleWrap.razor
index 0ff9194f..75a1bd08 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleWrap.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/HeaderStyleWrap.razor
@@ -6,7 +6,7 @@
[Fact]
public void DataList_TableLayout_HeaderStyleWrap_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/InlineHeaderStyle.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/InlineHeaderStyle.razor
index 406df2fc..fdfbc587 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/InlineHeaderStyle.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/InlineHeaderStyle.razor
@@ -10,7 +10,7 @@
HeaderStyle-BorderColor="White"
HeaderStyle-BorderStyle="Solid"
HeaderStyle-BorderWidth="2"
- RepeatLayout="Table"
+ RepeatLayout="Enums.RepeatLayout.Table"
Context="Item">
My Widget List
@Item.Name
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ItemStyleTest.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ItemStyleTest.razor
index 1645b4fd..ff2b26bc 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ItemStyleTest.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ItemStyleTest.razor
@@ -4,7 +4,7 @@
[Fact]
public void DataList_TableLayout_ItemStyle_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsHorizontal.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsHorizontal.razor
index 1a61e1cd..29af0d5a 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsHorizontal.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsHorizontal.razor
@@ -7,7 +7,7 @@
ItemType="Widget"
Caption="DataListCaption"
RepeatDirection="Horizontal"
- RepeatLayout="Table"
+ RepeatLayout="Enums.RepeatLayout.Table"
RepeatColumns="4"
Context="Item">
@Item.Id
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsVertical.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsVertical.razor
index 90011c42..a1e2576b 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsVertical.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/RepeatColumnsVertical.razor
@@ -6,7 +6,7 @@
var cut = Render(@
@Item.Id
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SeparatorTemplate.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SeparatorTemplate.razor
index 83fe5c21..18a83e3e 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SeparatorTemplate.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SeparatorTemplate.razor
@@ -3,7 +3,7 @@
[Fact]
public void DataList_TableLayout_SeparatorTemplate_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
====
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ShowHeaderFooter.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ShowHeaderFooter.razor
index 664842eb..32e19951 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ShowHeaderFooter.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/ShowHeaderFooter.razor
@@ -7,7 +7,7 @@
ItemType="Widget"
ShowHeader="true"
ShowFooter="true"
- RepeatLayout="Table"
+ RepeatLayout="Enums.RepeatLayout.Table"
Context="Item">
My Widget List
@Item.Name
@@ -26,7 +26,7 @@
ItemType="Widget"
ShowHeader="false"
ShowFooter="false"
- RepeatLayout="Table"
+ RepeatLayout="Enums.RepeatLayout.Table"
Context="Item">
My Widget List
@Item.Name
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Simple.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Simple.razor
index f26b55c8..6b7387ae 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Simple.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Simple.razor
@@ -3,7 +3,7 @@
[Fact]
public void DataList_TableLayout_Simple_RendersCorrectly()
{
- var cut = Render(@
+ var cut = Render(@
My Widget List
@Item.Name
);
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleAccessibleHeaders.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleAccessibleHeaders.razor
index 8712ef42..e83d644d 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleAccessibleHeaders.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleAccessibleHeaders.razor
@@ -5,7 +5,7 @@
{
var cut = Render(@
My Widget List
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleStyle.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleStyle.razor
index c9d9b0c7..ae14ac3a 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleStyle.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/SimpleStyle.razor
@@ -5,7 +5,7 @@
{
var cut = Render(@
My Widget List
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tabindex.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tabindex.razor
index aa59f723..9f2c7dda 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tabindex.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tabindex.razor
@@ -5,7 +5,7 @@
{
var cut = Render(@
My Widget List
diff --git a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tooltip.razor b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tooltip.razor
index 4d3a3b29..41ee1a76 100644
--- a/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tooltip.razor
+++ b/src/BlazorWebFormsComponents.Test/DataList/TableLayout/Tooltip.razor
@@ -5,7 +5,7 @@
{
var cut = Render(@
My Widget List
diff --git a/src/BlazorWebFormsComponents.Test/DataPager/BasicRendering.razor b/src/BlazorWebFormsComponents.Test/DataPager/BasicRendering.razor
new file mode 100644
index 00000000..050a85cd
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/DataPager/BasicRendering.razor
@@ -0,0 +1,74 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ [Fact]
+ public void Renders_Nothing_When_Single_Page()
+ {
+ // Arrange & Act - 5 items with page size 10 = 1 page
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Renders_Nothing_When_No_Items()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Renders_Pager_When_Multiple_Pages()
+ {
+ // Arrange & Act - 25 items with page size 10 = 3 pages
+ var cut = Render(@ );
+
+ // Assert
+ cut.Find("div").ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void Renders_With_ID_Attribute()
+ {
+ // Arrange & Act
+ var cut = Render(@);
+
+ // Assert
+ cut.Find("div").Id.ShouldBe("myPager");
+ }
+
+ [Fact]
+ public void Default_PageIndex_Is_Zero()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert - First page (1) should be current
+ cut.Find(".aspNetCurrentPage").TextContent.ShouldBe("1");
+ }
+
+ [Fact]
+ public void Calculates_TotalPages_Correctly()
+ {
+ // Arrange & Act - 25 items / 10 per page = 3 pages
+ var cut = Render(@ );
+
+ // Assert - Should show pages 1, 2, 3
+ var pageLinks = cut.FindAll("a, .aspNetCurrentPage");
+ pageLinks.Count.ShouldBeGreaterThanOrEqualTo(3);
+ }
+
+ [Fact]
+ public void Respects_PageIndex_Parameter()
+ {
+ // Arrange & Act - Start on page 2 (index 1)
+ var cut = Render(@ );
+
+ // Assert - Page 2 should be current
+ cut.Find(".aspNetCurrentPage").TextContent.ShouldBe("2");
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/DataPager/DisabledStates.razor b/src/BlazorWebFormsComponents.Test/DataPager/DisabledStates.razor
new file mode 100644
index 00000000..b96130bf
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/DataPager/DisabledStates.razor
@@ -0,0 +1,79 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ [Fact]
+ public void Previous_Disabled_On_First_Page()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var prevLink = cut.FindAll("a").First(a => a.TextContent == "Previous");
+ prevLink.ClassList.ShouldContain("aspNetDisabled");
+ }
+
+ [Fact]
+ public void Next_Disabled_On_Last_Page()
+ {
+ // Arrange & Act - Page 5 (index 4) is last page
+ var cut = Render(@ );
+
+ // Assert
+ var nextLink = cut.FindAll("a").First(a => a.TextContent == "Next");
+ nextLink.ClassList.ShouldContain("aspNetDisabled");
+ }
+
+ [Fact]
+ public void First_Disabled_On_First_Page()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var firstLink = cut.FindAll("a").First(a => a.TextContent == "First");
+ firstLink.ClassList.ShouldContain("aspNetDisabled");
+ }
+
+ [Fact]
+ public void Last_Disabled_On_Last_Page()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var lastLink = cut.FindAll("a").First(a => a.TextContent == "Last");
+ lastLink.ClassList.ShouldContain("aspNetDisabled");
+ }
+
+ [Fact]
+ public void Current_Page_Is_Not_A_Link()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert - Page 3 (current) should be a span, not a link
+ var currentPage = cut.Find(".aspNetCurrentPage");
+ currentPage.TagName.ShouldBe("SPAN");
+ currentPage.TextContent.ShouldBe("3");
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/DataPager/Navigation.razor b/src/BlazorWebFormsComponents.Test/DataPager/Navigation.razor
new file mode 100644
index 00000000..d0086d73
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/DataPager/Navigation.razor
@@ -0,0 +1,110 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private int _currentPage = 0;
+
+ private void OnPageChanged(int newPage)
+ {
+ _currentPage = newPage;
+ }
+
+ [Fact]
+ public void Click_Next_Advances_Page()
+ {
+ // Arrange
+ _currentPage = 0;
+ var cut = Render(@ );
+
+ // Act - Click Next
+ var nextLink = cut.FindAll("a").First(a => a.TextContent == "Next");
+ nextLink.Click();
+
+ // Assert
+ _currentPage.ShouldBe(1);
+ }
+
+ [Fact]
+ public void Click_Previous_Goes_Back()
+ {
+ // Arrange - Start on page 2
+ _currentPage = 1;
+ var cut = Render(@ );
+
+ // Act - Click Previous
+ var prevLink = cut.FindAll("a").First(a => a.TextContent == "Previous");
+ prevLink.Click();
+
+ // Assert
+ _currentPage.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Click_Page_Number_Navigates_To_Page()
+ {
+ // Arrange
+ _currentPage = 0;
+ var cut = Render(@ );
+
+ // Act - Click page 3
+ var page3Link = cut.FindAll("a").First(a => a.TextContent == "3");
+ page3Link.Click();
+
+ // Assert
+ _currentPage.ShouldBe(2); // Zero-based index
+ }
+
+ [Fact]
+ public void Click_First_Goes_To_First_Page()
+ {
+ // Arrange - Start on page 3
+ _currentPage = 2;
+ var cut = Render(@ );
+
+ // Act - Click First
+ var firstLink = cut.FindAll("a").First(a => a.TextContent == "First");
+ firstLink.Click();
+
+ // Assert
+ _currentPage.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Click_Last_Goes_To_Last_Page()
+ {
+ // Arrange
+ _currentPage = 0;
+ var cut = Render(@ );
+
+ // Act - Click Last
+ var lastLink = cut.FindAll("a").First(a => a.TextContent == "Last");
+ lastLink.Click();
+
+ // Assert
+ _currentPage.ShouldBe(4); // 5 pages (0-4)
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/DataPager/PageButtonCount.razor b/src/BlazorWebFormsComponents.Test/DataPager/PageButtonCount.razor
new file mode 100644
index 00000000..cec7ba25
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/DataPager/PageButtonCount.razor
@@ -0,0 +1,100 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ [Fact]
+ public void Default_PageButtonCount_Is_5()
+ {
+ // Arrange & Act - 100 items = 10 pages, but only 5 buttons shown
+ var cut = Render(@ );
+
+ // Assert - Should show exactly 5 page numbers
+ var pageElements = cut.FindAll(".aspNetCurrentPage, a")
+ .Where(e => int.TryParse(e.TextContent, out _))
+ .ToList();
+ pageElements.Count.ShouldBe(5);
+ }
+
+ [Fact]
+ public void PageButtonCount_Limits_Visible_Buttons()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var pageElements = cut.FindAll(".aspNetCurrentPage, a")
+ .Where(e => int.TryParse(e.TextContent, out _))
+ .ToList();
+ pageElements.Count.ShouldBe(3);
+ }
+
+ [Fact]
+ public void Page_Range_Centers_On_Current_Page()
+ {
+ // Arrange & Act - Start on page 5 (index 4)
+ var cut = Render(@ );
+
+ // Assert - Should show pages 3, 4, 5, 6, 7 (centered on 5)
+ var pageElements = cut.FindAll(".aspNetCurrentPage, a")
+ .Where(e => int.TryParse(e.TextContent, out _))
+ .Select(e => int.Parse(e.TextContent))
+ .ToList();
+
+ pageElements.ShouldContain(5); // Current page
+ pageElements.Min().ShouldBeGreaterThanOrEqualTo(3);
+ pageElements.Max().ShouldBeLessThanOrEqualTo(7);
+ }
+
+ [Fact]
+ public void Page_Range_Adjusts_At_Beginning()
+ {
+ // Arrange & Act - Start on page 1 (index 0)
+ var cut = Render(@ );
+
+ // Assert - Should show pages 1, 2, 3, 4, 5
+ var pageElements = cut.FindAll(".aspNetCurrentPage, a")
+ .Where(e => int.TryParse(e.TextContent, out _))
+ .Select(e => int.Parse(e.TextContent))
+ .ToList();
+
+ pageElements.First().ShouldBe(1);
+ pageElements.Last().ShouldBe(5);
+ }
+
+ [Fact]
+ public void Page_Range_Adjusts_At_End()
+ {
+ // Arrange & Act - Start on last page (index 9)
+ var cut = Render(@ );
+
+ // Assert - Should show pages 6, 7, 8, 9, 10
+ var pageElements = cut.FindAll(".aspNetCurrentPage, a")
+ .Where(e => int.TryParse(e.TextContent, out _))
+ .Select(e => int.Parse(e.TextContent))
+ .ToList();
+
+ pageElements.Last().ShouldBe(10);
+ pageElements.First().ShouldBe(6);
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/DataPager/PagerModes.razor b/src/BlazorWebFormsComponents.Test/DataPager/PagerModes.razor
new file mode 100644
index 00000000..34112e8b
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/DataPager/PagerModes.razor
@@ -0,0 +1,74 @@
+@inherits BlazorWebFormsTestContext
+@using BlazorWebFormsComponents.Enums
+
+@code {
+ [Fact]
+ public void Default_Mode_Shows_Numeric_Buttons()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert - Should show page numbers, not Previous/Next
+ cut.FindAll(".aspNetCurrentPage, a").Any(e => e.TextContent == "1").ShouldBeTrue();
+ cut.FindAll("a").Any(e => e.TextContent == "Previous").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void NextPrevious_Mode_Shows_Navigation_Buttons()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.FindAll("a").Any(e => e.TextContent == "Previous").ShouldBeTrue();
+ cut.FindAll("a").Any(e => e.TextContent == "Next").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void NextPreviousFirstLast_Mode_Shows_All_Navigation()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Any(e => e.TextContent == "First").ShouldBeTrue();
+ links.Any(e => e.TextContent == "Previous").ShouldBeTrue();
+ links.Any(e => e.TextContent == "Next").ShouldBeTrue();
+ links.Any(e => e.TextContent == "Last").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void NumericFirstLast_Mode_Shows_Numbers_And_FirstLast()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Any(e => e.TextContent == "First").ShouldBeTrue();
+ links.Any(e => e.TextContent == "Last").ShouldBeTrue();
+ cut.FindAll(".aspNetCurrentPage, a").Any(e => e.TextContent == "1").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Custom_Button_Text()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Any(e => e.TextContent == "<<").ShouldBeTrue();
+ links.Any(e => e.TextContent == "<").ShouldBeTrue();
+ links.Any(e => e.TextContent == ">").ShouldBeTrue();
+ links.Any(e => e.TextContent == ">>").ShouldBeTrue();
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/BasicRendering.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/BasicRendering.razor
new file mode 100644
index 00000000..d86a7c8a
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/BasicRendering.razor
@@ -0,0 +1,100 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateTestSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/");
+ var products = new SiteMapNode("Products", "/products", "Our product catalog");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics", "Electronic products");
+ var phones = new SiteMapNode("Phones", "/products/electronics/phones", "Mobile phones");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+ electronics.AddChild(phones);
+
+ return root;
+ }
+
+ [Fact]
+ public void Renders_Empty_When_No_SiteMapProvider()
+ {
+ // Arrange & Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Renders_Empty_When_No_CurrentUrl()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Renders_Breadcrumb_Path_For_Current_Url()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.MarkupMatches(@
+ Home
+ >
+ Products
+ >
+ Electronics
+ );
+ }
+
+ [Fact]
+ public void Renders_Root_Only_When_At_Root()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var span = cut.Find("span");
+ span.InnerHtml.ShouldContain("Home");
+ cut.FindAll("a").Count.ShouldBe(0); // Current node is not a link by default
+ }
+
+ [Fact]
+ public void Uses_Default_PathSeparator()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldContain(" > ");
+ }
+
+ [Fact]
+ public void Renders_With_ID_Attribute()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Find("span").Id.ShouldBe("myBreadcrumb");
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/CurrentNode.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/CurrentNode.razor
new file mode 100644
index 00000000..4fd6073f
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/CurrentNode.razor
@@ -0,0 +1,82 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateTestSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/", "Return to home page");
+ var products = new SiteMapNode("Products", "/products", "Browse our products");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics", "Electronic devices");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+
+ return root;
+ }
+
+ [Fact]
+ public void CurrentNode_Is_Not_A_Link_By_Default()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Should have 1 link (Home) and 1 span (Products)
+ cut.FindAll("a").Count.ShouldBe(1);
+ var spans = cut.FindAll("span > span");
+ spans.Any(s => s.TextContent == "Products").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void RenderCurrentNodeAsLink_Makes_Current_Node_A_Link()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Both nodes should be links
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(2);
+ links[1].TextContent.ShouldBe("Products");
+ }
+
+ [Fact]
+ public void ShowToolTips_Shows_Description_As_Title()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var link = cut.Find("a");
+ link.GetAttribute("title").ShouldBe("Return to home page");
+ }
+
+ [Fact]
+ public void ShowToolTips_False_Hides_Tooltips()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var link = cut.Find("a");
+ link.GetAttribute("title").ShouldBeNull();
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/ParentLevels.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/ParentLevels.razor
new file mode 100644
index 00000000..b68641c5
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/ParentLevels.razor
@@ -0,0 +1,73 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateDeepSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/");
+ var products = new SiteMapNode("Products", "/products");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics");
+ var phones = new SiteMapNode("Phones", "/products/electronics/phones");
+ var iphone = new SiteMapNode("iPhone", "/products/electronics/phones/iphone");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+ electronics.AddChild(phones);
+ phones.AddChild(iphone);
+
+ return root;
+ }
+
+ [Fact]
+ public void ParentLevelsDisplayed_Minus1_Shows_All()
+ {
+ // Arrange
+ var siteMap = CreateDeepSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Should show all 5 nodes (4 links + 1 current node span + separator spans)
+ cut.FindAll("a").Count.ShouldBe(4);
+ }
+
+ [Fact]
+ public void ParentLevelsDisplayed_2_Shows_Limited_Path()
+ {
+ // Arrange
+ var siteMap = CreateDeepSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Should show Electronics, Phones, iPhone (2 parents + current)
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(2); // Electronics and Phones
+
+ // First link should be Electronics (not Home or Products)
+ links[0].TextContent.ShouldBe("Electronics");
+ }
+
+ [Fact]
+ public void ParentLevelsDisplayed_0_Shows_Only_Current()
+ {
+ // Arrange
+ var siteMap = CreateDeepSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Should show only current node
+ cut.FindAll("a").Count.ShouldBe(0);
+ var spans = cut.FindAll("span > span");
+ spans.Any(s => s.TextContent == "iPhone").ShouldBeTrue();
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/PathDirection.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/PathDirection.razor
new file mode 100644
index 00000000..1ff6b489
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/PathDirection.razor
@@ -0,0 +1,72 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateTestSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/");
+ var products = new SiteMapNode("Products", "/products");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics");
+ var phones = new SiteMapNode("Phones", "/products/electronics/phones");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+ electronics.AddChild(phones);
+
+ return root;
+ }
+
+ [Fact]
+ public void Default_PathDirection_Is_RootToCurrent()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - First link should be Home (root)
+ var links = cut.FindAll("a");
+ links[0].TextContent.ShouldBe("Home");
+ links[1].TextContent.ShouldBe("Products");
+ }
+
+ [Fact]
+ public void CurrentToRoot_Reverses_Path_Order()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert - Current node should be first (as span), then links in reverse order
+ var spans = cut.FindAll("span > span");
+ var firstSpan = spans[0];
+ firstSpan.TextContent.ShouldBe("Electronics");
+
+ var links = cut.FindAll("a");
+ links[0].TextContent.ShouldBe("Products");
+ links[1].TextContent.ShouldBe("Home");
+ }
+
+ [Fact]
+ public void CurrentToRoot_With_Deep_Path()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ var links = cut.FindAll("a");
+ links.Count.ShouldBe(3); // Products, Electronics, Home are all links
+ links[2].TextContent.ShouldBe("Home"); // Last link should be Home
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/PathSeparator.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/PathSeparator.razor
new file mode 100644
index 00000000..08297876
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/PathSeparator.razor
@@ -0,0 +1,69 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateTestSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/");
+ var products = new SiteMapNode("Products", "/products");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+
+ return root;
+ }
+
+ [Fact]
+ public void Custom_PathSeparator_String()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldContain(" / ");
+ cut.Markup.ShouldNotContain(" > ");
+ }
+
+ [Fact]
+ public void Custom_PathSeparator_Arrow()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Markup.ShouldContain("→");
+ }
+
+ [Fact]
+ public void PathSeparatorTemplate_Overrides_String()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@
+
+ |
+
+ );
+
+ // Assert
+ cut.Markup.ShouldContain("separator-icon");
+ cut.Markup.ShouldNotContain(" > ");
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/SiteMapPath/Templates.razor b/src/BlazorWebFormsComponents.Test/SiteMapPath/Templates.razor
new file mode 100644
index 00000000..91949874
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/SiteMapPath/Templates.razor
@@ -0,0 +1,105 @@
+@inherits BlazorWebFormsTestContext
+
+@code {
+ private SiteMapNode CreateTestSiteMap()
+ {
+ var root = new SiteMapNode("Home", "/");
+ var products = new SiteMapNode("Products", "/products");
+ var electronics = new SiteMapNode("Electronics", "/products/electronics");
+
+ root.AddChild(products);
+ products.AddChild(electronics);
+
+ return root;
+ }
+
+ [Fact]
+ public void NodeTemplate_Customizes_Regular_Nodes()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@
+
+ [@node.Title]
+
+ );
+
+ // Assert
+ var customNodes = cut.FindAll("a.custom-node");
+ customNodes.Count.ShouldBe(1); // Only Products (not root, not current)
+ customNodes[0].TextContent.ShouldBe("[Products]");
+ }
+
+ [Fact]
+ public void RootNodeTemplate_Customizes_Root()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@
+
+ 🏠 @node.Title
+
+ );
+
+ // Assert
+ var rootNode = cut.Find("a.root-node");
+ rootNode.TextContent.ShouldContain("🏠");
+ rootNode.TextContent.ShouldContain("Home");
+ }
+
+ [Fact]
+ public void CurrentNodeTemplate_Customizes_Current()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@
+
+ 📍 @node.Title
+
+ );
+
+ // Assert
+ var currentNode = cut.Find("strong.current-node");
+ currentNode.TextContent.ShouldContain("📍");
+ currentNode.TextContent.ShouldContain("Electronics");
+ }
+
+ [Fact]
+ public void All_Templates_Work_Together()
+ {
+ // Arrange
+ var siteMap = CreateTestSiteMap();
+
+ // Act
+ var cut = Render(@
+
+ 🏠
+
+
+ 📁 @node.Title
+
+
+ 📍 @node.Title
+
+ );
+
+ // Assert
+ cut.Find("a.root").TextContent.ShouldContain("🏠");
+ cut.Find("a.node").TextContent.ShouldContain("📁 Products");
+ cut.Find("span.current").TextContent.ShouldContain("📍 Electronics");
+ }
+}
diff --git a/src/BlazorWebFormsComponents.Test/Table/Accessibility.razor b/src/BlazorWebFormsComponents.Test/Table/Accessibility.razor
new file mode 100644
index 00000000..884e90df
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/Table/Accessibility.razor
@@ -0,0 +1,84 @@
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void TableHeaderCell_RendersThElement()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var th = cut.Find("th");
+ th.ShouldNotBeNull();
+ th.TextContent.ShouldBe("Header");
+ }
+
+ [Fact]
+ public void TableHeaderCell_ScopeColumn_RendersScopeAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var th = cut.Find("th");
+ th.GetAttribute("scope").ShouldBe("col");
+ }
+
+ [Fact]
+ public void TableHeaderCell_ScopeRow_RendersScopeAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var th = cut.Find("th");
+ th.GetAttribute("scope").ShouldBe("row");
+ }
+
+ [Fact]
+ public void TableHeaderCell_WithAbbr_RendersAbbrAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var th = cut.Find("th");
+ th.GetAttribute("abbr").ShouldBe("Qty");
+ }
+
+ [Fact]
+ public void TableCell_AssociatedHeaderCellID_RendersHeadersAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var td = cut.Find("td");
+ td.GetAttribute("headers").ShouldBe("header1");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/Table/BasicRendering.razor b/src/BlazorWebFormsComponents.Test/Table/BasicRendering.razor
new file mode 100644
index 00000000..85439e3b
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/Table/BasicRendering.razor
@@ -0,0 +1,88 @@
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void Table_BasicRendering_RendersTableElement()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.ShouldNotBeNull();
+
+ var rows = cut.FindAll("tr");
+ rows.Count.ShouldBe(1);
+
+ var cells = cut.FindAll("td");
+ cells.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void Table_WithCaption_RendersCaptionElement()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var caption = cut.Find("caption");
+ caption.ShouldNotBeNull();
+ caption.TextContent.ShouldBe("My Table");
+ }
+
+ [Fact]
+ public void Table_NotVisible_RendersNothing()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ cut.Markup.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Table_WithCssClass_AppliesClass()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("class").ShouldBe("my-table");
+ }
+
+ [Fact]
+ public void Table_CellPaddingAndSpacing_RendersAttributes()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("cellpadding").ShouldBe("5");
+ table.GetAttribute("cellspacing").ShouldBe("2");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/Table/CellSpanning.razor b/src/BlazorWebFormsComponents.Test/Table/CellSpanning.razor
new file mode 100644
index 00000000..f9072a77
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/Table/CellSpanning.razor
@@ -0,0 +1,68 @@
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void TableCell_ColumnSpan_RendersColspanAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ cell.GetAttribute("colspan").ShouldBe("3");
+ }
+
+ [Fact]
+ public void TableCell_RowSpan_RendersRowspanAttribute()
+ {
+ // Act
+ var cut = Render(@
+
+ Spanning Cell
+ Cell 2
+
+
);
+
+ // Assert
+ var cells = cut.FindAll("td");
+ cells[0].GetAttribute("rowspan").ShouldBe("2");
+ }
+
+ [Fact]
+ public void TableCell_BothSpans_RendersBothAttributes()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ cell.GetAttribute("colspan").ShouldBe("2");
+ cell.GetAttribute("rowspan").ShouldBe("3");
+ }
+
+ [Fact]
+ public void TableCell_ZeroSpan_DoesNotRenderAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ cell.GetAttribute("colspan").ShouldBeNull();
+ cell.GetAttribute("rowspan").ShouldBeNull();
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/Table/GridLines.razor b/src/BlazorWebFormsComponents.Test/Table/GridLines.razor
new file mode 100644
index 00000000..12d394f2
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/Table/GridLines.razor
@@ -0,0 +1,67 @@
+@using GridLinesEnum = BlazorWebFormsComponents.Enums.GridLines
+
+@code {
+
+ [Fact]
+ public void Table_GridLinesHorizontal_RendersRulesRows()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("rules").ShouldBe("rows");
+ table.GetAttribute("border").ShouldBe("1");
+ }
+
+ [Fact]
+ public void Table_GridLinesVertical_RendersRulesCols()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("rules").ShouldBe("cols");
+ }
+
+ [Fact]
+ public void Table_GridLinesBoth_RendersRulesAll()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("rules").ShouldBe("all");
+ }
+
+ [Fact]
+ public void Table_GridLinesNone_NoRulesAttribute()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ table.GetAttribute("rules").ShouldBeNull();
+ table.GetAttribute("border").ShouldBeNull();
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents.Test/Table/Styling.razor b/src/BlazorWebFormsComponents.Test/Table/Styling.razor
new file mode 100644
index 00000000..bbb4722a
--- /dev/null
+++ b/src/BlazorWebFormsComponents.Test/Table/Styling.razor
@@ -0,0 +1,101 @@
+@using BlazorWebFormsComponents.Enums
+
+@code {
+
+ [Fact]
+ public void Table_WithBackColor_RendersStyle()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var table = cut.Find("table");
+ var style = table.GetAttribute("style");
+ style.ShouldContain("background-color");
+ }
+
+ [Fact]
+ public void TableRow_WithBackColor_RendersStyle()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var row = cut.Find("tr");
+ var style = row.GetAttribute("style");
+ style.ShouldContain("background-color");
+ }
+
+ [Fact]
+ public void TableCell_WithBackColor_RendersStyle()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ var style = cell.GetAttribute("style");
+ style.ShouldContain("background-color");
+ }
+
+ [Fact]
+ public void TableCell_NoWrap_RendersWhiteSpaceNowrap()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ var style = cell.GetAttribute("style");
+ style.ShouldContain("white-space: nowrap");
+ }
+
+ [Fact]
+ public void TableCell_HorizontalAlignCenter_RendersTextAlignCenter()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ var style = cell.GetAttribute("style");
+ style.ShouldContain("text-align: center");
+ }
+
+ [Fact]
+ public void TableCell_VerticalAlignTop_RendersVerticalAlignTop()
+ {
+ // Act
+ var cut = Render(@);
+
+ // Assert
+ var cell = cut.Find("td");
+ var style = cell.GetAttribute("style");
+ style.ShouldContain("vertical-align: top");
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents/BulletedList.razor b/src/BlazorWebFormsComponents/BulletedList.razor
new file mode 100644
index 00000000..343027a4
--- /dev/null
+++ b/src/BlazorWebFormsComponents/BulletedList.razor
@@ -0,0 +1,65 @@
+@using BlazorWebFormsComponents.DataBinding
+@using BlazorWebFormsComponents.Enums
+@typeparam TItem
+@inherits DataBoundComponent
+
+@if (Visible)
+{
+ var allItems = GetItems().ToList();
+
+ @if (IsOrderedList)
+ {
+
+ @RenderListItems(allItems)
+
+ }
+ else
+ {
+
+ @RenderListItems(allItems)
+
+ }
+}
+
+@code {
+ private RenderFragment RenderListItems(List items) => __builder =>
+ {
+ for (var i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ var index = i;
+ var isEnabled = Enabled && item.Enabled;
+
+
+ @switch (DisplayMode)
+ {
+ case BulletedListDisplayMode.Text:
+ @item.Text
+ break;
+
+ case BulletedListDisplayMode.HyperLink:
+ @if (isEnabled && !string.IsNullOrEmpty(item.Value))
+ {
+ @item.Text
+ }
+ else
+ {
+ @item.Text
+ }
+ break;
+
+ case BulletedListDisplayMode.LinkButton:
+ @if (isEnabled)
+ {
+ HandleItemClick(index)" @onclick:preventDefault>@item.Text
+ }
+ else
+ {
+ @item.Text
+ }
+ break;
+ }
+
+ }
+ };
+}
diff --git a/src/BlazorWebFormsComponents/BulletedList.razor.cs b/src/BlazorWebFormsComponents/BulletedList.razor.cs
new file mode 100644
index 00000000..8d355869
--- /dev/null
+++ b/src/BlazorWebFormsComponents/BulletedList.razor.cs
@@ -0,0 +1,222 @@
+using BlazorComponentUtilities;
+using BlazorWebFormsComponents.DataBinding;
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a list control that displays items as a bulleted or numbered list.
+ /// Emulates the ASP.NET Web Forms BulletedList control.
+ ///
+ /// The type of items in the data source.
+ public partial class BulletedList : DataBoundComponent, IStyle
+ {
+ private readonly string _baseId = Guid.NewGuid().ToString("N").Substring(0, 8);
+
+ ///
+ /// Gets or sets the bullet style of the list.
+ ///
+ [Parameter]
+ public BulletStyle BulletStyle { get; set; } = BulletStyle.NotSet;
+
+ ///
+ /// Gets or sets the URL of the custom bullet image.
+ /// Only used when BulletStyle is set to CustomImage.
+ ///
+ [Parameter]
+ public string BulletImageUrl { get; set; }
+
+ ///
+ /// Gets or sets the display mode of the list items.
+ ///
+ [Parameter]
+ public BulletedListDisplayMode DisplayMode { get; set; } = BulletedListDisplayMode.Text;
+
+ ///
+ /// Gets or sets the starting number for a numbered list.
+ ///
+ [Parameter]
+ public int FirstBulletNumber { get; set; } = 1;
+
+ ///
+ /// Gets or sets the collection of static list items.
+ ///
+ [Parameter]
+ public ListItemCollection StaticItems { get; set; } = new();
+
+ ///
+ /// Gets or sets the field of the data source that provides the text content of the list items.
+ ///
+ [Parameter]
+ public string DataTextField { get; set; }
+
+ ///
+ /// Gets or sets the field of the data source that provides the value of each list item.
+ ///
+ [Parameter]
+ public string DataValueField { get; set; }
+
+ ///
+ /// Gets or sets the target window or frame for hyperlinks when DisplayMode is HyperLink.
+ ///
+ [Parameter]
+ public string Target { get; set; }
+
+ ///
+ /// Gets or sets the event callback that is invoked when an item is clicked in LinkButton mode.
+ ///
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ // IStyle implementation
+ [Parameter]
+ public WebColor BackColor { get; set; }
+
+ [Parameter]
+ public WebColor BorderColor { get; set; }
+
+ [Parameter]
+ public BorderStyle BorderStyle { get; set; }
+
+ [Parameter]
+ public Unit BorderWidth { get; set; }
+
+ [Parameter]
+ public string CssClass { get; set; }
+
+ [Parameter]
+ public FontInfo Font { get; set; } = new FontInfo();
+
+ [Parameter]
+ public WebColor ForeColor { get; set; }
+
+ [Parameter]
+ public Unit Height { get; set; }
+
+ [Parameter]
+ public Unit Width { get; set; }
+
+ ///
+ /// Gets the computed style string for the component.
+ ///
+ protected string Style => this.ToStyle().NullIfEmpty();
+
+ ///
+ /// Gets a value indicating whether the bullet style renders as an ordered list.
+ ///
+ protected bool IsOrderedList => BulletStyle switch
+ {
+ BulletStyle.Numbered => true,
+ BulletStyle.LowerAlpha => true,
+ BulletStyle.UpperAlpha => true,
+ BulletStyle.LowerRoman => true,
+ BulletStyle.UpperRoman => true,
+ _ => false
+ };
+
+ ///
+ /// Gets the HTML list-style-type value for the current bullet style.
+ ///
+ protected string ListStyleType => BulletStyle switch
+ {
+ BulletStyle.Disc => "disc",
+ BulletStyle.Circle => "circle",
+ BulletStyle.Square => "square",
+ BulletStyle.Numbered => "decimal",
+ BulletStyle.LowerAlpha => "lower-alpha",
+ BulletStyle.UpperAlpha => "upper-alpha",
+ BulletStyle.LowerRoman => "lower-roman",
+ BulletStyle.UpperRoman => "upper-roman",
+ BulletStyle.CustomImage => null,
+ _ => null
+ };
+
+ ///
+ /// Gets the HTML type attribute value for ordered lists.
+ ///
+ protected string OrderedListType => BulletStyle switch
+ {
+ BulletStyle.Numbered => "1",
+ BulletStyle.LowerAlpha => "a",
+ BulletStyle.UpperAlpha => "A",
+ BulletStyle.LowerRoman => "i",
+ BulletStyle.UpperRoman => "I",
+ _ => null
+ };
+
+ ///
+ /// Gets the combined style string including list-style customization.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style ?? string.Empty;
+
+ if (BulletStyle == BulletStyle.CustomImage && !string.IsNullOrEmpty(BulletImageUrl))
+ {
+ var imageStyle = $"list-style-image: url('{BulletImageUrl}');";
+ return string.IsNullOrEmpty(baseStyle) ? imageStyle : $"{baseStyle} {imageStyle}";
+ }
+
+ if (ListStyleType != null)
+ {
+ var typeStyle = $"list-style-type: {ListStyleType};";
+ return string.IsNullOrEmpty(baseStyle) ? typeStyle : $"{baseStyle} {typeStyle}";
+ }
+
+ return string.IsNullOrEmpty(baseStyle) ? null : baseStyle;
+ }
+ }
+
+ ///
+ /// Handles the click event for an item in LinkButton mode.
+ ///
+ protected async Task HandleItemClick(int index)
+ {
+ if (DisplayMode == BulletedListDisplayMode.LinkButton && Enabled)
+ {
+ await OnClick.InvokeAsync(new BulletedListEventArgs(index));
+ }
+ }
+
+ ///
+ /// Gets all items from both static and data-bound sources.
+ ///
+ protected IEnumerable GetItems()
+ {
+ // Return static items first
+ foreach (var item in StaticItems)
+ {
+ yield return item;
+ }
+
+ // Then data-bound items
+ if (Items != null)
+ {
+ foreach (var dataItem in Items)
+ {
+ yield return new ListItem
+ {
+ Text = GetPropertyValue(dataItem, DataTextField),
+ Value = GetPropertyValue(dataItem, DataValueField)
+ };
+ }
+ }
+ }
+
+ private string GetPropertyValue(TItem item, string propertyName)
+ {
+ if (string.IsNullOrEmpty(propertyName))
+ return item?.ToString() ?? string.Empty;
+
+ var prop = typeof(TItem).GetProperty(propertyName);
+ return prop?.GetValue(item)?.ToString() ?? string.Empty;
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/BulletedListEventArgs.cs b/src/BlazorWebFormsComponents/BulletedListEventArgs.cs
new file mode 100644
index 00000000..a155ea10
--- /dev/null
+++ b/src/BlazorWebFormsComponents/BulletedListEventArgs.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Provides data for the Click event of a BulletedList control.
+ ///
+ public class BulletedListEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the BulletedListEventArgs class.
+ ///
+ /// The zero-based index of the clicked item.
+ public BulletedListEventArgs(int index)
+ {
+ Index = index;
+ }
+
+ ///
+ /// Gets the zero-based index of the item that was clicked in the BulletedList control.
+ ///
+ public int Index { get; }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/DataPager.razor b/src/BlazorWebFormsComponents/DataPager.razor
new file mode 100644
index 00000000..a7278f16
--- /dev/null
+++ b/src/BlazorWebFormsComponents/DataPager.razor
@@ -0,0 +1,75 @@
+@inherits BaseStyledComponent
+
+@if (TotalPages > 1)
+{
+
+ @if (ChildContent != null)
+ {
+ @ChildContent
+ }
+ else
+ {
+ @* First Button *@
+ @if (ShouldShowFirstLast)
+ {
+
@FirstPageText
+
+ }
+
+ @* Previous Button *@
+ @if (ShouldShowPreviousNext)
+ {
+
@PreviousPageText
+
+ }
+
+ @* Numeric Page Buttons *@
+ @if (ShouldShowNumeric)
+ {
+ @foreach (var page in GetPageRange())
+ {
+ @if (page == PageIndex)
+ {
+
@(page + 1)
+ }
+ else
+ {
+
GoToPage(page)"
+ @onclick:preventDefault="true">@(page + 1)
+ }
+
+ }
+ }
+
+ @* Next Button *@
+ @if (ShouldShowPreviousNext)
+ {
+
@NextPageText
+
+ }
+
+ @* Last Button *@
+ @if (ShouldShowFirstLast)
+ {
+
@LastPageText
+ }
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents/DataPager.razor.cs b/src/BlazorWebFormsComponents/DataPager.razor.cs
new file mode 100644
index 00000000..8923fb54
--- /dev/null
+++ b/src/BlazorWebFormsComponents/DataPager.razor.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Provides paging functionality for data-bound controls like ListView.
+ ///
+ public partial class DataPager : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the total number of rows in the data source.
+ ///
+ [Parameter]
+ public int TotalRowCount { get; set; }
+
+ ///
+ /// Gets or sets the number of records to display on each page.
+ ///
+ [Parameter]
+ public int PageSize { get; set; } = 10;
+
+ ///
+ /// Gets or sets the current page index (zero-based).
+ ///
+ [Parameter]
+ public int PageIndex { get; set; } = 0;
+
+ ///
+ /// Gets or sets the callback for when PageIndex changes.
+ ///
+ [Parameter]
+ public EventCallback PageIndexChanged { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of page buttons to display.
+ ///
+ [Parameter]
+ public int PageButtonCount { get; set; } = 5;
+
+ ///
+ /// Gets or sets the type of pager buttons to display.
+ ///
+ [Parameter]
+ public PagerButtons Mode { get; set; } = PagerButtons.Numeric;
+
+ ///
+ /// Gets or sets the text for the First page button.
+ ///
+ [Parameter]
+ public string FirstPageText { get; set; } = "First";
+
+ ///
+ /// Gets or sets the text for the Previous page button.
+ ///
+ [Parameter]
+ public string PreviousPageText { get; set; } = "Previous";
+
+ ///
+ /// Gets or sets the text for the Next page button.
+ ///
+ [Parameter]
+ public string NextPageText { get; set; } = "Next";
+
+ ///
+ /// Gets or sets the text for the Last page button.
+ ///
+ [Parameter]
+ public string LastPageText { get; set; } = "Last";
+
+ ///
+ /// Gets or sets whether to show the First and Last buttons.
+ ///
+ [Parameter]
+ public bool ShowFirstLastButtons { get; set; } = true;
+
+ ///
+ /// Gets or sets whether to show the Previous and Next buttons.
+ ///
+ [Parameter]
+ public bool ShowPreviousNextButtons { get; set; } = true;
+
+ ///
+ /// Gets or sets whether to show numeric page buttons.
+ ///
+ [Parameter]
+ public bool ShowNumericButtons { get; set; } = true;
+
+ ///
+ /// Event raised when the page is about to change.
+ ///
+ [Parameter]
+ public EventCallback OnPageIndexChanging { get; set; }
+
+ ///
+ /// Event raised after the page has changed.
+ ///
+ [Parameter]
+ public EventCallback OnPageIndexChanged { get; set; }
+
+ ///
+ /// Gets or sets custom child content (for TemplatePagerField support).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets the total number of pages.
+ ///
+ protected int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalRowCount / PageSize) : 0;
+
+ ///
+ /// Gets whether the current page is the first page.
+ ///
+ protected bool IsFirstPage => PageIndex <= 0;
+
+ ///
+ /// Gets whether the current page is the last page.
+ ///
+ protected bool IsLastPage => PageIndex >= TotalPages - 1;
+
+ ///
+ /// Gets the start row index for the current page.
+ ///
+ public int StartRowIndex => PageIndex * PageSize;
+
+ ///
+ /// Gets the maximum rows for the current page.
+ ///
+ public int MaximumRows => PageSize;
+
+ ///
+ /// Gets the range of page numbers to display.
+ ///
+ protected IEnumerable GetPageRange()
+ {
+ if (TotalPages <= 0)
+ return Enumerable.Empty();
+
+ // Calculate the start and end of the page button range
+ var halfButtons = PageButtonCount / 2;
+ var startPage = Math.Max(0, PageIndex - halfButtons);
+ var endPage = Math.Min(TotalPages - 1, startPage + PageButtonCount - 1);
+
+ // Adjust start if we're near the end
+ if (endPage - startPage < PageButtonCount - 1)
+ {
+ startPage = Math.Max(0, endPage - PageButtonCount + 1);
+ }
+
+ return Enumerable.Range(startPage, endPage - startPage + 1);
+ }
+
+ ///
+ /// Navigates to the specified page.
+ ///
+ protected async void GoToPage(int newPageIndex)
+ {
+ if (newPageIndex < 0 || newPageIndex >= TotalPages || newPageIndex == PageIndex)
+ return;
+
+ var args = new PageChangedEventArgs(newPageIndex, PageIndex, TotalPages, newPageIndex * PageSize);
+
+ // Raise changing event (allows cancellation)
+ await OnPageIndexChanging.InvokeAsync(args);
+
+ if (args.Cancel)
+ return;
+
+ // Update page index
+ PageIndex = args.NewPageIndex;
+ await PageIndexChanged.InvokeAsync(PageIndex);
+
+ // Raise changed event
+ await OnPageIndexChanged.InvokeAsync(args);
+
+ StateHasChanged();
+ }
+
+ ///
+ /// Goes to the first page.
+ ///
+ protected void GoToFirstPage() => GoToPage(0);
+
+ ///
+ /// Goes to the previous page.
+ ///
+ protected void GoToPreviousPage() => GoToPage(PageIndex - 1);
+
+ ///
+ /// Goes to the next page.
+ ///
+ protected void GoToNextPage() => GoToPage(PageIndex + 1);
+
+ ///
+ /// Goes to the last page.
+ ///
+ protected void GoToLastPage() => GoToPage(TotalPages - 1);
+
+ ///
+ /// Determines if First/Last buttons should be shown based on Mode.
+ ///
+ protected bool ShouldShowFirstLast =>
+ ShowFirstLastButtons &&
+ (Mode == PagerButtons.NextPreviousFirstLast || Mode == PagerButtons.NumericFirstLast);
+
+ ///
+ /// Determines if Previous/Next buttons should be shown based on Mode.
+ ///
+ protected bool ShouldShowPreviousNext =>
+ ShowPreviousNextButtons &&
+ (Mode == PagerButtons.NextPrevious || Mode == PagerButtons.NextPreviousFirstLast);
+
+ ///
+ /// Determines if numeric buttons should be shown based on Mode.
+ ///
+ protected bool ShouldShowNumeric =>
+ ShowNumericButtons &&
+ (Mode == PagerButtons.Numeric || Mode == PagerButtons.NumericFirstLast);
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/BulletStyle.cs b/src/BlazorWebFormsComponents/Enums/BulletStyle.cs
new file mode 100644
index 00000000..f933449f
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/BulletStyle.cs
@@ -0,0 +1,58 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the bullet style of a BulletedList control.
+ ///
+ public enum BulletStyle
+ {
+ ///
+ /// The bullet style is not set.
+ ///
+ NotSet = 0,
+
+ ///
+ /// The bullet is a filled circle.
+ ///
+ Disc = 1,
+
+ ///
+ /// The bullet is an empty circle.
+ ///
+ Circle = 2,
+
+ ///
+ /// The bullet is a filled square.
+ ///
+ Square = 3,
+
+ ///
+ /// The bullet is a number (1, 2, 3, ...).
+ ///
+ Numbered = 4,
+
+ ///
+ /// The bullet is a lowercase letter (a, b, c, ...).
+ ///
+ LowerAlpha = 5,
+
+ ///
+ /// The bullet is an uppercase letter (A, B, C, ...).
+ ///
+ UpperAlpha = 6,
+
+ ///
+ /// The bullet is a lowercase Roman numeral (i, ii, iii, ...).
+ ///
+ LowerRoman = 7,
+
+ ///
+ /// The bullet is an uppercase Roman numeral (I, II, III, ...).
+ ///
+ UpperRoman = 8,
+
+ ///
+ /// The bullet is a custom image specified by the BulletImageUrl property.
+ ///
+ CustomImage = 9
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/BulletedListDisplayMode.cs b/src/BlazorWebFormsComponents/Enums/BulletedListDisplayMode.cs
new file mode 100644
index 00000000..47450a40
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/BulletedListDisplayMode.cs
@@ -0,0 +1,23 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the display mode of a BulletedList control.
+ ///
+ public enum BulletedListDisplayMode
+ {
+ ///
+ /// The list items are displayed as plain text.
+ ///
+ Text = 0,
+
+ ///
+ /// The list items are displayed as hyperlinks that navigate to a URL.
+ ///
+ HyperLink = 1,
+
+ ///
+ /// The list items are displayed as link buttons that post back to the server.
+ ///
+ LinkButton = 2
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/GridLines.cs b/src/BlazorWebFormsComponents/Enums/GridLines.cs
new file mode 100644
index 00000000..1b3e6b33
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/GridLines.cs
@@ -0,0 +1,28 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies which table borders are displayed.
+ ///
+ public enum GridLines
+ {
+ ///
+ /// No grid lines are displayed.
+ ///
+ None = 0,
+
+ ///
+ /// Only horizontal grid lines are displayed.
+ ///
+ Horizontal = 1,
+
+ ///
+ /// Only vertical grid lines are displayed.
+ ///
+ Vertical = 2,
+
+ ///
+ /// Both horizontal and vertical grid lines are displayed.
+ ///
+ Both = 3
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/PagerButtons.cs b/src/BlazorWebFormsComponents/Enums/PagerButtons.cs
new file mode 100644
index 00000000..7a8311c6
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/PagerButtons.cs
@@ -0,0 +1,28 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the position of the first and last buttons in a DataPager control.
+ ///
+ public enum PagerButtons
+ {
+ ///
+ /// Display Next and Previous buttons.
+ ///
+ NextPrevious = 0,
+
+ ///
+ /// Display numeric page buttons.
+ ///
+ Numeric = 1,
+
+ ///
+ /// Display Next, Previous, First, and Last buttons.
+ ///
+ NextPreviousFirstLast = 2,
+
+ ///
+ /// Display numeric page buttons with First and Last buttons.
+ ///
+ NumericFirstLast = 3
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/PathDirection.cs b/src/BlazorWebFormsComponents/Enums/PathDirection.cs
new file mode 100644
index 00000000..8dadc58f
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/PathDirection.cs
@@ -0,0 +1,18 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the direction in which a SiteMapPath control renders navigation nodes.
+ ///
+ public enum PathDirection
+ {
+ ///
+ /// Navigation nodes are rendered from root to current node (left to right).
+ ///
+ RootToCurrent = 0,
+
+ ///
+ /// Navigation nodes are rendered from current node to root (right to left).
+ ///
+ CurrentToRoot = 1
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/TableCaptionAlign.cs b/src/BlazorWebFormsComponents/Enums/TableCaptionAlign.cs
new file mode 100644
index 00000000..baae34ea
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/TableCaptionAlign.cs
@@ -0,0 +1,33 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the alignment of the caption in a Table control.
+ ///
+ public enum TableCaptionAlign
+ {
+ ///
+ /// The caption alignment is not set.
+ ///
+ NotSet = 0,
+
+ ///
+ /// The caption is aligned at the top of the table.
+ ///
+ Top = 1,
+
+ ///
+ /// The caption is aligned at the bottom of the table.
+ ///
+ Bottom = 2,
+
+ ///
+ /// The caption is aligned at the left of the table.
+ ///
+ Left = 3,
+
+ ///
+ /// The caption is aligned at the right of the table.
+ ///
+ Right = 4
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/TableHeaderScope.cs b/src/BlazorWebFormsComponents/Enums/TableHeaderScope.cs
new file mode 100644
index 00000000..8bf1b95a
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/TableHeaderScope.cs
@@ -0,0 +1,23 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the scope of a table header cell.
+ ///
+ public enum TableHeaderScope
+ {
+ ///
+ /// The scope is not set.
+ ///
+ NotSet = 0,
+
+ ///
+ /// The header applies to a row.
+ ///
+ Row = 1,
+
+ ///
+ /// The header applies to a column.
+ ///
+ Column = 2
+ }
+}
diff --git a/src/BlazorWebFormsComponents/Enums/TableRowSection.cs b/src/BlazorWebFormsComponents/Enums/TableRowSection.cs
new file mode 100644
index 00000000..82b85aec
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Enums/TableRowSection.cs
@@ -0,0 +1,23 @@
+namespace BlazorWebFormsComponents.Enums
+{
+ ///
+ /// Specifies the section of the table where a row belongs.
+ ///
+ public enum TableRowSection
+ {
+ ///
+ /// The row is in the table header (thead).
+ ///
+ TableHeader = 0,
+
+ ///
+ /// The row is in the table body (tbody).
+ ///
+ TableBody = 1,
+
+ ///
+ /// The row is in the table footer (tfoot).
+ ///
+ TableFooter = 2
+ }
+}
diff --git a/src/BlazorWebFormsComponents/PageChangedEventArgs.cs b/src/BlazorWebFormsComponents/PageChangedEventArgs.cs
new file mode 100644
index 00000000..3e6d527d
--- /dev/null
+++ b/src/BlazorWebFormsComponents/PageChangedEventArgs.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Provides data for the PageIndexChanging and PageIndexChanged events of pageable controls.
+ ///
+ public class PageChangedEventArgs : EventArgs
+ {
+ ///
+ /// Gets or sets the index of the new page.
+ ///
+ public int NewPageIndex { get; set; }
+
+ ///
+ /// Gets the index of the previous page.
+ ///
+ public int OldPageIndex { get; }
+
+ ///
+ /// Gets the total number of pages.
+ ///
+ public int TotalPages { get; }
+
+ ///
+ /// Gets the index of the first row on the new page.
+ ///
+ public int StartRowIndex { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the event should be canceled.
+ ///
+ public bool Cancel { get; set; }
+
+ ///
+ /// Creates a new instance of PageChangedEventArgs.
+ ///
+ public PageChangedEventArgs(int newPageIndex, int oldPageIndex, int totalPages, int startRowIndex)
+ {
+ NewPageIndex = newPageIndex;
+ OldPageIndex = oldPageIndex;
+ TotalPages = totalPages;
+ StartRowIndex = startRowIndex;
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/SiteMapNode.cs b/src/BlazorWebFormsComponents/SiteMapNode.cs
new file mode 100644
index 00000000..adb569f6
--- /dev/null
+++ b/src/BlazorWebFormsComponents/SiteMapNode.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a node in a site navigation hierarchy used by SiteMapPath.
+ ///
+ public class SiteMapNode
+ {
+ ///
+ /// Gets or sets the title of the node displayed in the breadcrumb.
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets the URL that the node links to.
+ ///
+ public string Url { get; set; }
+
+ ///
+ /// Gets or sets the description used for tooltips.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Gets or sets the parent node. Null for root nodes.
+ ///
+ public SiteMapNode ParentNode { get; set; }
+
+ ///
+ /// Gets or sets the child nodes.
+ ///
+ public List ChildNodes { get; set; } = new List();
+
+ ///
+ /// Gets a value indicating whether this node is a root node.
+ ///
+ public bool IsRootNode => ParentNode == null;
+
+ ///
+ /// Creates an empty SiteMapNode.
+ ///
+ public SiteMapNode() { }
+
+ ///
+ /// Creates a SiteMapNode with specified title and URL.
+ ///
+ /// The title to display.
+ /// The URL to navigate to.
+ public SiteMapNode(string title, string url)
+ {
+ Title = title;
+ Url = url;
+ }
+
+ ///
+ /// Creates a SiteMapNode with specified title, URL, and description.
+ ///
+ /// The title to display.
+ /// The URL to navigate to.
+ /// The description for tooltips.
+ public SiteMapNode(string title, string url, string description)
+ {
+ Title = title;
+ Url = url;
+ Description = description;
+ }
+
+ ///
+ /// Adds a child node and sets its parent to this node.
+ ///
+ /// The child node to add.
+ public void AddChild(SiteMapNode child)
+ {
+ child.ParentNode = this;
+ ChildNodes.Add(child);
+ }
+
+ ///
+ /// Finds a node by URL in this node's hierarchy (including this node and all descendants).
+ ///
+ /// The URL to search for.
+ /// The matching node or null if not found.
+ public SiteMapNode FindByUrl(string url)
+ {
+ if (MatchesUrl(url))
+ return this;
+
+ foreach (var child in ChildNodes)
+ {
+ var found = child.FindByUrl(url);
+ if (found != null)
+ return found;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Determines whether this node's URL matches the specified URL.
+ /// Handles relative and absolute URL comparisons.
+ ///
+ /// The URL to compare.
+ /// True if URLs match; otherwise false.
+ public bool MatchesUrl(string url)
+ {
+ if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(url))
+ return false;
+
+ // Normalize URLs for comparison
+ var thisUrl = NormalizeUrl(Url);
+ var otherUrl = NormalizeUrl(url);
+
+ return string.Equals(thisUrl, otherUrl, System.StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string NormalizeUrl(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ return url;
+
+ // Remove leading ~/ for ASP.NET style paths
+ if (url.StartsWith("~/"))
+ url = url.Substring(1);
+
+ // Ensure leading slash
+ if (!url.StartsWith("/"))
+ url = "/" + url;
+
+ // Remove trailing slash (except for root)
+ if (url.Length > 1 && url.EndsWith("/"))
+ url = url.TrimEnd('/');
+
+ // Remove query string for matching
+ var queryIndex = url.IndexOf('?');
+ if (queryIndex >= 0)
+ url = url.Substring(0, queryIndex);
+
+ return url;
+ }
+
+ ///
+ /// Gets the path from root to this node as a list.
+ ///
+ /// List of nodes from root to this node.
+ public List GetPathFromRoot()
+ {
+ var path = new List();
+ var current = this;
+
+ while (current != null)
+ {
+ path.Insert(0, current);
+ current = current.ParentNode;
+ }
+
+ return path;
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/SiteMapPath.razor b/src/BlazorWebFormsComponents/SiteMapPath.razor
new file mode 100644
index 00000000..cc03e5e2
--- /dev/null
+++ b/src/BlazorWebFormsComponents/SiteMapPath.razor
@@ -0,0 +1,55 @@
+@inherits BaseStyledComponent
+
+@if (NavigationPath.Count > 0)
+{
+
+ @for (int i = 0; i < NavigationPath.Count; i++)
+ {
+ var node = NavigationPath[i];
+ var isFirst = i == 0;
+ var isCurrent = IsCurrentNode(node);
+ var isRoot = IsRootNode(node);
+
+ @* Render separator before all nodes except the first *@
+ @if (!isFirst)
+ {
+
+ @if (PathSeparatorTemplate != null)
+ {
+ @PathSeparatorTemplate
+ }
+ else
+ {
+ @PathSeparator
+ }
+
+ }
+
+ @* Render the node *@
+ @if (isCurrent && CurrentNodeTemplate != null)
+ {
+ @CurrentNodeTemplate(node)
+ }
+ else if (isRoot && RootNodeTemplate != null)
+ {
+ @RootNodeTemplate(node)
+ }
+ else if (!isCurrent && !isRoot && NodeTemplate != null)
+ {
+ @NodeTemplate(node)
+ }
+ else
+ {
+ @* Default rendering *@
+ @if (isCurrent && !RenderCurrentNodeAsLink)
+ {
+ @node.Title
+ }
+ else
+ {
+ @node.Title
+ }
+ }
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents/SiteMapPath.razor.cs b/src/BlazorWebFormsComponents/SiteMapPath.razor.cs
new file mode 100644
index 00000000..2e6381c6
--- /dev/null
+++ b/src/BlazorWebFormsComponents/SiteMapPath.razor.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BlazorWebFormsComponents.Enums;
+
+using Microsoft.AspNetCore.Components;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Displays a breadcrumb navigation path showing the current page's location within a site hierarchy.
+ ///
+ public partial class SiteMapPath : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the root node of the site map hierarchy.
+ ///
+ [Parameter]
+ public SiteMapNode SiteMapProvider { get; set; }
+
+ ///
+ /// Gets or sets the separator string displayed between path nodes.
+ /// Default is " > ".
+ ///
+ [Parameter]
+ public string PathSeparator { get; set; } = " > ";
+
+ ///
+ /// Gets or sets a custom template for rendering the path separator.
+ /// When set, PathSeparator string is ignored.
+ ///
+ [Parameter]
+ public RenderFragment PathSeparatorTemplate { get; set; }
+
+ ///
+ /// Gets or sets the direction of path rendering.
+ ///
+ [Parameter]
+ public PathDirection PathDirection { get; set; } = PathDirection.RootToCurrent;
+
+ ///
+ /// Gets or sets whether the current node is rendered as a hyperlink.
+ /// Default is false.
+ ///
+ [Parameter]
+ public bool RenderCurrentNodeAsLink { get; set; } = false;
+
+ ///
+ /// Gets or sets whether tooltips are shown on navigation nodes.
+ /// Default is true.
+ ///
+ [Parameter]
+ public bool ShowToolTips { get; set; } = true;
+
+ ///
+ /// Gets or sets the number of parent levels to display.
+ /// -1 means display all parent levels. Default is -1.
+ ///
+ [Parameter]
+ public int ParentLevelsDisplayed { get; set; } = -1;
+
+ ///
+ /// Gets or sets the template for rendering the current (active) node.
+ ///
+ [Parameter]
+ public RenderFragment CurrentNodeTemplate { get; set; }
+
+ ///
+ /// Gets or sets the template for rendering regular path nodes.
+ ///
+ [Parameter]
+ public RenderFragment NodeTemplate { get; set; }
+
+ ///
+ /// Gets or sets the template for rendering the root node.
+ ///
+ [Parameter]
+ public RenderFragment RootNodeTemplate { get; set; }
+
+ ///
+ /// Gets or sets the current URL to match against site map nodes.
+ /// If not set, attempts to detect from NavigationManager.
+ ///
+ [Parameter]
+ public string CurrentUrl { get; set; }
+
+ ///
+ /// Gets or sets custom child content (not typically used).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ // Style properties for different node types
+ private CurrentNodeStyle _currentNodeStyle = new CurrentNodeStyle();
+ private NodeStyle _nodeStyle = new NodeStyle();
+ private RootNodeStyle _rootNodeStyle = new RootNodeStyle();
+ private PathSeparatorStyle _pathSeparatorStyle = new PathSeparatorStyle();
+
+ ///
+ /// Gets or sets the style for the current node.
+ ///
+ public CurrentNodeStyle CurrentNodeStyle
+ {
+ get => _currentNodeStyle;
+ set
+ {
+ _currentNodeStyle = value;
+ StateHasChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the style for regular path nodes.
+ ///
+ public NodeStyle NodeStyle
+ {
+ get => _nodeStyle;
+ set
+ {
+ _nodeStyle = value;
+ StateHasChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the style for the root node.
+ ///
+ public RootNodeStyle RootNodeStyle
+ {
+ get => _rootNodeStyle;
+ set
+ {
+ _rootNodeStyle = value;
+ StateHasChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the style for the path separator.
+ ///
+ public PathSeparatorStyle PathSeparatorStyle
+ {
+ get => _pathSeparatorStyle;
+ set
+ {
+ _pathSeparatorStyle = value;
+ StateHasChanged();
+ }
+ }
+
+ ///
+ /// Gets the navigation path from root to current node.
+ ///
+ protected List NavigationPath { get; private set; } = new List();
+
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+ BuildNavigationPath();
+ }
+
+ private void BuildNavigationPath()
+ {
+ NavigationPath.Clear();
+
+ if (SiteMapProvider == null)
+ return;
+
+ // Determine current URL
+ var url = CurrentUrl;
+ if (string.IsNullOrEmpty(url))
+ {
+ // If no URL provided, the path will be empty
+ // In a real scenario, inject NavigationManager to get current URI
+ return;
+ }
+
+ // Find the current node in the site map
+ var currentNode = SiteMapProvider.FindByUrl(url);
+ if (currentNode == null)
+ return;
+
+ // Build path from root to current
+ var fullPath = currentNode.GetPathFromRoot();
+
+ // Apply ParentLevelsDisplayed limit
+ if (ParentLevelsDisplayed >= 0 && fullPath.Count > ParentLevelsDisplayed + 1)
+ {
+ // Keep root + last N parents + current
+ var startIndex = fullPath.Count - ParentLevelsDisplayed - 1;
+ fullPath = fullPath.Skip(startIndex).ToList();
+ }
+
+ // Apply path direction
+ if (PathDirection == PathDirection.CurrentToRoot)
+ {
+ fullPath.Reverse();
+ }
+
+ NavigationPath = fullPath;
+ }
+
+ ///
+ /// Determines if the given node is the current (last) node in the path.
+ ///
+ protected bool IsCurrentNode(SiteMapNode node)
+ {
+ if (NavigationPath.Count == 0)
+ return false;
+
+ var lastNode = PathDirection == PathDirection.RootToCurrent
+ ? NavigationPath.Last()
+ : NavigationPath.First();
+
+ return ReferenceEquals(node, lastNode);
+ }
+
+ ///
+ /// Determines if the given node is the root (first) node in the path.
+ ///
+ protected bool IsRootNode(SiteMapNode node)
+ {
+ if (NavigationPath.Count == 0)
+ return false;
+
+ var firstNode = PathDirection == PathDirection.RootToCurrent
+ ? NavigationPath.First()
+ : NavigationPath.Last();
+
+ return ReferenceEquals(node, firstNode);
+ }
+
+ ///
+ /// Gets the tooltip text for a node.
+ ///
+ protected string GetTooltip(SiteMapNode node)
+ {
+ if (!ShowToolTips)
+ return null;
+
+ return !string.IsNullOrEmpty(node.Description) ? node.Description : node.Title;
+ }
+
+ ///
+ /// Gets the inline style for a node based on its position.
+ ///
+ protected string GetNodeStyle(SiteMapNode node)
+ {
+ Style style;
+ if (IsCurrentNode(node))
+ style = _currentNodeStyle;
+ else if (IsRootNode(node))
+ style = _rootNodeStyle;
+ else
+ style = _nodeStyle;
+
+ if (style == null)
+ return null;
+
+ var result = style.ToString();
+ return string.IsNullOrEmpty(result) ? null : result;
+ }
+
+ ///
+ /// Gets the inline style for the path separator.
+ ///
+ protected string GetSeparatorStyle()
+ {
+ if (_pathSeparatorStyle == null)
+ return null;
+
+ var result = _pathSeparatorStyle.ToString();
+ return string.IsNullOrEmpty(result) ? null : result;
+ }
+ }
+
+ // Style component classes for SiteMapPath - implement IStyle for ToStyle() extension
+ public class CurrentNodeStyle : Style { }
+ public class NodeStyle : Style { }
+ public class RootNodeStyle : Style { }
+ public class PathSeparatorStyle : Style { }
+}
diff --git a/src/BlazorWebFormsComponents/Table.razor b/src/BlazorWebFormsComponents/Table.razor
new file mode 100644
index 00000000..174b9fcf
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Table.razor
@@ -0,0 +1,20 @@
+@using BlazorWebFormsComponents.Enums
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @if (!string.IsNullOrEmpty(Caption))
+ {
+ @Caption
+ }
+ @ChildContent
+
+}
diff --git a/src/BlazorWebFormsComponents/Table.razor.cs b/src/BlazorWebFormsComponents/Table.razor.cs
new file mode 100644
index 00000000..a6139084
--- /dev/null
+++ b/src/BlazorWebFormsComponents/Table.razor.cs
@@ -0,0 +1,138 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents an HTML table control.
+ /// Emulates the ASP.NET Web Forms Table control.
+ ///
+ public partial class Table : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the table (rows).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the caption text for the table.
+ ///
+ [Parameter]
+ public string Caption { get; set; }
+
+ ///
+ /// Gets or sets the alignment of the caption.
+ ///
+ [Parameter]
+ public TableCaptionAlign CaptionAlign { get; set; } = TableCaptionAlign.NotSet;
+
+ ///
+ /// Gets or sets the cell padding in pixels.
+ ///
+ [Parameter]
+ public int CellPadding { get; set; } = -1;
+
+ ///
+ /// Gets or sets the cell spacing in pixels.
+ ///
+ [Parameter]
+ public int CellSpacing { get; set; } = -1;
+
+ ///
+ /// Gets or sets which grid lines are displayed.
+ ///
+ [Parameter]
+ public GridLines GridLines { get; set; } = GridLines.None;
+
+ ///
+ /// Gets or sets the horizontal alignment of the table.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the background image URL for the table.
+ ///
+ [Parameter]
+ public string BackImageUrl { get; set; }
+
+ ///
+ /// Gets the cellpadding attribute value, or null if not set.
+ ///
+ protected int? CellPaddingValue => CellPadding >= 0 ? CellPadding : null;
+
+ ///
+ /// Gets the cellspacing attribute value, or null if not set.
+ ///
+ protected int? CellSpacingValue => CellSpacing >= 0 ? CellSpacing : null;
+
+ ///
+ /// Gets the caption-side CSS value.
+ ///
+ protected string CaptionSideStyle => CaptionAlign switch
+ {
+ TableCaptionAlign.Top => "caption-side: top",
+ TableCaptionAlign.Bottom => "caption-side: bottom",
+ _ => null
+ };
+
+ ///
+ /// Gets the rules attribute for grid lines (deprecated HTML, but matches Web Forms output).
+ ///
+ protected string RulesAttribute => GridLines switch
+ {
+ GridLines.Horizontal => "rows",
+ GridLines.Vertical => "cols",
+ GridLines.Both => "all",
+ _ => null
+ };
+
+ ///
+ /// Gets the border attribute for the table.
+ ///
+ protected int? BorderAttribute => GridLines != GridLines.None ? 1 : null;
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ var baseStyle = Style;
+ if (!string.IsNullOrEmpty(baseStyle))
+ styles.Add(baseStyle);
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var marginStyle = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "margin-right: auto",
+ HorizontalAlign.Center => "margin-left: auto; margin-right: auto",
+ HorizontalAlign.Right => "margin-left: auto",
+ _ => null
+ };
+ if (marginStyle != null)
+ styles.Add(marginStyle);
+ }
+
+ if (!string.IsNullOrEmpty(BackImageUrl))
+ {
+ styles.Add($"background-image: url('{BackImageUrl}')");
+ }
+
+ // Modern CSS for grid lines (in addition to deprecated attributes for compatibility)
+ if (GridLines != GridLines.None)
+ {
+ styles.Add("border-collapse: collapse");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/TableCell.razor b/src/BlazorWebFormsComponents/TableCell.razor
new file mode 100644
index 00000000..54732ce5
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableCell.razor
@@ -0,0 +1,21 @@
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @if (ChildContent != null)
+ {
+ @ChildContent
+ }
+ else if (!string.IsNullOrEmpty(Text))
+ {
+ @Text
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents/TableCell.razor.cs b/src/BlazorWebFormsComponents/TableCell.razor.cs
new file mode 100644
index 00000000..6b2042cf
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableCell.razor.cs
@@ -0,0 +1,136 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a cell in a Table control.
+ /// Emulates the ASP.NET Web Forms TableCell control.
+ ///
+ public partial class TableCell : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the cell.
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the number of columns the cell spans.
+ ///
+ [Parameter]
+ public int ColumnSpan { get; set; } = 0;
+
+ ///
+ /// Gets or sets the number of rows the cell spans.
+ ///
+ [Parameter]
+ public int RowSpan { get; set; } = 0;
+
+ ///
+ /// Gets or sets the horizontal alignment of the cell content.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the vertical alignment of the cell content.
+ ///
+ [Parameter]
+ public VerticalAlign VerticalAlign { get; set; } = VerticalAlign.NotSet;
+
+ ///
+ /// Gets or sets a value indicating whether the cell content wraps.
+ ///
+ [Parameter]
+ public bool Wrap { get; set; } = true;
+
+ ///
+ /// Gets or sets the ID of the associated header cell for accessibility.
+ ///
+ [Parameter]
+ public string AssociatedHeaderCellID { get; set; }
+
+ ///
+ /// Gets or sets the text content of the cell.
+ ///
+ [Parameter]
+ public string Text { get; set; }
+
+ ///
+ /// Gets the colspan attribute value, or null if not set.
+ ///
+ protected int? ColSpanValue => ColumnSpan > 0 ? ColumnSpan : null;
+
+ ///
+ /// Gets the rowspan attribute value, or null if not set.
+ ///
+ protected int? RowSpanValue => RowSpan > 0 ? RowSpan : null;
+
+ ///
+ /// Gets the alignment style for the cell.
+ ///
+ protected string AlignmentStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var align = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "left",
+ HorizontalAlign.Center => "center",
+ HorizontalAlign.Right => "right",
+ HorizontalAlign.Justify => "justify",
+ _ => null
+ };
+ if (align != null)
+ styles.Add($"text-align: {align}");
+ }
+
+ if (VerticalAlign != VerticalAlign.NotSet)
+ {
+ var valign = VerticalAlign switch
+ {
+ VerticalAlign.Top => "top",
+ VerticalAlign.Middle => "middle",
+ VerticalAlign.Bottom => "bottom",
+ _ => null
+ };
+ if (valign != null)
+ styles.Add($"vertical-align: {valign}");
+ }
+
+ if (!Wrap)
+ {
+ styles.Add("white-space: nowrap");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style;
+ var alignStyle = AlignmentStyle;
+
+ if (!string.IsNullOrEmpty(baseStyle) && !string.IsNullOrEmpty(alignStyle))
+ return $"{baseStyle}; {alignStyle}";
+ if (!string.IsNullOrEmpty(baseStyle))
+ return baseStyle;
+ if (!string.IsNullOrEmpty(alignStyle))
+ return alignStyle;
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/TableFooterRow.razor b/src/BlazorWebFormsComponents/TableFooterRow.razor
new file mode 100644
index 00000000..932aceab
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableFooterRow.razor
@@ -0,0 +1,12 @@
+@using BlazorWebFormsComponents.Enums
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @ChildContent
+
+}
diff --git a/src/BlazorWebFormsComponents/TableFooterRow.razor.cs b/src/BlazorWebFormsComponents/TableFooterRow.razor.cs
new file mode 100644
index 00000000..55a90e3f
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableFooterRow.razor.cs
@@ -0,0 +1,96 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a footer row in a Table control.
+ /// Emulates the ASP.NET Web Forms TableFooterRow control.
+ ///
+ public partial class TableFooterRow : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the row (cells).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets the section of the table where this row belongs (always TableFooter).
+ ///
+ public TableRowSection TableSection => TableRowSection.TableFooter;
+
+ ///
+ /// Gets or sets the horizontal alignment of the row content.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the vertical alignment of the row content.
+ ///
+ [Parameter]
+ public VerticalAlign VerticalAlign { get; set; } = VerticalAlign.NotSet;
+
+ ///
+ /// Gets the alignment style for the row.
+ ///
+ protected string AlignmentStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var align = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "left",
+ HorizontalAlign.Center => "center",
+ HorizontalAlign.Right => "right",
+ HorizontalAlign.Justify => "justify",
+ _ => null
+ };
+ if (align != null)
+ styles.Add($"text-align: {align}");
+ }
+
+ if (VerticalAlign != VerticalAlign.NotSet)
+ {
+ var valign = VerticalAlign switch
+ {
+ VerticalAlign.Top => "top",
+ VerticalAlign.Middle => "middle",
+ VerticalAlign.Bottom => "bottom",
+ _ => null
+ };
+ if (valign != null)
+ styles.Add($"vertical-align: {valign}");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style;
+ var alignStyle = AlignmentStyle;
+
+ if (!string.IsNullOrEmpty(baseStyle) && !string.IsNullOrEmpty(alignStyle))
+ return $"{baseStyle}; {alignStyle}";
+ if (!string.IsNullOrEmpty(baseStyle))
+ return baseStyle;
+ if (!string.IsNullOrEmpty(alignStyle))
+ return alignStyle;
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/TableHeaderCell.razor b/src/BlazorWebFormsComponents/TableHeaderCell.razor
new file mode 100644
index 00000000..0229a68d
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableHeaderCell.razor
@@ -0,0 +1,23 @@
+@using BlazorWebFormsComponents.Enums
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @if (ChildContent != null)
+ {
+ @ChildContent
+ }
+ else if (!string.IsNullOrEmpty(Text))
+ {
+ @Text
+ }
+
+}
diff --git a/src/BlazorWebFormsComponents/TableHeaderCell.razor.cs b/src/BlazorWebFormsComponents/TableHeaderCell.razor.cs
new file mode 100644
index 00000000..b965d14a
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableHeaderCell.razor.cs
@@ -0,0 +1,152 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a header cell in a Table control.
+ /// Emulates the ASP.NET Web Forms TableHeaderCell control.
+ ///
+ public partial class TableHeaderCell : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the header cell.
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the number of columns the cell spans.
+ ///
+ [Parameter]
+ public int ColumnSpan { get; set; } = 0;
+
+ ///
+ /// Gets or sets the number of rows the cell spans.
+ ///
+ [Parameter]
+ public int RowSpan { get; set; } = 0;
+
+ ///
+ /// Gets or sets the horizontal alignment of the cell content.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the vertical alignment of the cell content.
+ ///
+ [Parameter]
+ public VerticalAlign VerticalAlign { get; set; } = VerticalAlign.NotSet;
+
+ ///
+ /// Gets or sets a value indicating whether the cell content wraps.
+ ///
+ [Parameter]
+ public bool Wrap { get; set; } = true;
+
+ ///
+ /// Gets or sets the scope of the header cell (row or column).
+ ///
+ [Parameter]
+ public TableHeaderScope Scope { get; set; } = TableHeaderScope.NotSet;
+
+ ///
+ /// Gets or sets the abbreviated text for the header cell.
+ ///
+ [Parameter]
+ public string AbbreviatedText { get; set; }
+
+ ///
+ /// Gets or sets the text content of the header cell.
+ ///
+ [Parameter]
+ public string Text { get; set; }
+
+ ///
+ /// Gets the colspan attribute value, or null if not set.
+ ///
+ protected int? ColSpanValue => ColumnSpan > 0 ? ColumnSpan : null;
+
+ ///
+ /// Gets the rowspan attribute value, or null if not set.
+ ///
+ protected int? RowSpanValue => RowSpan > 0 ? RowSpan : null;
+
+ ///
+ /// Gets the scope attribute value, or null if not set.
+ ///
+ protected string ScopeValue => Scope switch
+ {
+ TableHeaderScope.Row => "row",
+ TableHeaderScope.Column => "col",
+ _ => null
+ };
+
+ ///
+ /// Gets the alignment style for the cell.
+ ///
+ protected string AlignmentStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var align = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "left",
+ HorizontalAlign.Center => "center",
+ HorizontalAlign.Right => "right",
+ HorizontalAlign.Justify => "justify",
+ _ => null
+ };
+ if (align != null)
+ styles.Add($"text-align: {align}");
+ }
+
+ if (VerticalAlign != VerticalAlign.NotSet)
+ {
+ var valign = VerticalAlign switch
+ {
+ VerticalAlign.Top => "top",
+ VerticalAlign.Middle => "middle",
+ VerticalAlign.Bottom => "bottom",
+ _ => null
+ };
+ if (valign != null)
+ styles.Add($"vertical-align: {valign}");
+ }
+
+ if (!Wrap)
+ {
+ styles.Add("white-space: nowrap");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style;
+ var alignStyle = AlignmentStyle;
+
+ if (!string.IsNullOrEmpty(baseStyle) && !string.IsNullOrEmpty(alignStyle))
+ return $"{baseStyle}; {alignStyle}";
+ if (!string.IsNullOrEmpty(baseStyle))
+ return baseStyle;
+ if (!string.IsNullOrEmpty(alignStyle))
+ return alignStyle;
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/TableHeaderRow.razor b/src/BlazorWebFormsComponents/TableHeaderRow.razor
new file mode 100644
index 00000000..932aceab
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableHeaderRow.razor
@@ -0,0 +1,12 @@
+@using BlazorWebFormsComponents.Enums
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @ChildContent
+
+}
diff --git a/src/BlazorWebFormsComponents/TableHeaderRow.razor.cs b/src/BlazorWebFormsComponents/TableHeaderRow.razor.cs
new file mode 100644
index 00000000..0475ed9b
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableHeaderRow.razor.cs
@@ -0,0 +1,96 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a header row in a Table control.
+ /// Emulates the ASP.NET Web Forms TableHeaderRow control.
+ ///
+ public partial class TableHeaderRow : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the row (cells).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets the section of the table where this row belongs (always TableHeader).
+ ///
+ public TableRowSection TableSection => TableRowSection.TableHeader;
+
+ ///
+ /// Gets or sets the horizontal alignment of the row content.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the vertical alignment of the row content.
+ ///
+ [Parameter]
+ public VerticalAlign VerticalAlign { get; set; } = VerticalAlign.NotSet;
+
+ ///
+ /// Gets the alignment style for the row.
+ ///
+ protected string AlignmentStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var align = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "left",
+ HorizontalAlign.Center => "center",
+ HorizontalAlign.Right => "right",
+ HorizontalAlign.Justify => "justify",
+ _ => null
+ };
+ if (align != null)
+ styles.Add($"text-align: {align}");
+ }
+
+ if (VerticalAlign != VerticalAlign.NotSet)
+ {
+ var valign = VerticalAlign switch
+ {
+ VerticalAlign.Top => "top",
+ VerticalAlign.Middle => "middle",
+ VerticalAlign.Bottom => "bottom",
+ _ => null
+ };
+ if (valign != null)
+ styles.Add($"vertical-align: {valign}");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style;
+ var alignStyle = AlignmentStyle;
+
+ if (!string.IsNullOrEmpty(baseStyle) && !string.IsNullOrEmpty(alignStyle))
+ return $"{baseStyle}; {alignStyle}";
+ if (!string.IsNullOrEmpty(baseStyle))
+ return baseStyle;
+ if (!string.IsNullOrEmpty(alignStyle))
+ return alignStyle;
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/BlazorWebFormsComponents/TableRow.razor b/src/BlazorWebFormsComponents/TableRow.razor
new file mode 100644
index 00000000..932aceab
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableRow.razor
@@ -0,0 +1,12 @@
+@using BlazorWebFormsComponents.Enums
+@inherits BaseStyledComponent
+
+@if (Visible)
+{
+
+ @ChildContent
+
+}
diff --git a/src/BlazorWebFormsComponents/TableRow.razor.cs b/src/BlazorWebFormsComponents/TableRow.razor.cs
new file mode 100644
index 00000000..52160161
--- /dev/null
+++ b/src/BlazorWebFormsComponents/TableRow.razor.cs
@@ -0,0 +1,97 @@
+using BlazorWebFormsComponents.Enums;
+using Microsoft.AspNetCore.Components;
+using System.Collections.Generic;
+
+namespace BlazorWebFormsComponents
+{
+ ///
+ /// Represents a row in a Table control.
+ /// Emulates the ASP.NET Web Forms TableRow control.
+ ///
+ public partial class TableRow : BaseStyledComponent
+ {
+ ///
+ /// Gets or sets the child content of the row (cells).
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the section of the table where this row belongs.
+ ///
+ [Parameter]
+ public TableRowSection TableSection { get; set; } = TableRowSection.TableBody;
+
+ ///
+ /// Gets or sets the horizontal alignment of the row content.
+ ///
+ [Parameter]
+ public HorizontalAlign HorizontalAlign { get; set; } = HorizontalAlign.NotSet;
+
+ ///
+ /// Gets or sets the vertical alignment of the row content.
+ ///
+ [Parameter]
+ public VerticalAlign VerticalAlign { get; set; } = VerticalAlign.NotSet;
+
+ ///
+ /// Gets the alignment style for the row.
+ ///
+ protected string AlignmentStyle
+ {
+ get
+ {
+ var styles = new List();
+
+ if (HorizontalAlign != HorizontalAlign.NotSet)
+ {
+ var align = HorizontalAlign switch
+ {
+ HorizontalAlign.Left => "left",
+ HorizontalAlign.Center => "center",
+ HorizontalAlign.Right => "right",
+ HorizontalAlign.Justify => "justify",
+ _ => null
+ };
+ if (align != null)
+ styles.Add($"text-align: {align}");
+ }
+
+ if (VerticalAlign != VerticalAlign.NotSet)
+ {
+ var valign = VerticalAlign switch
+ {
+ VerticalAlign.Top => "top",
+ VerticalAlign.Middle => "middle",
+ VerticalAlign.Bottom => "bottom",
+ _ => null
+ };
+ if (valign != null)
+ styles.Add($"vertical-align: {valign}");
+ }
+
+ return styles.Count > 0 ? string.Join("; ", styles) : null;
+ }
+ }
+
+ ///
+ /// Gets the combined style string.
+ ///
+ protected string CombinedStyle
+ {
+ get
+ {
+ var baseStyle = Style;
+ var alignStyle = AlignmentStyle;
+
+ if (!string.IsNullOrEmpty(baseStyle) && !string.IsNullOrEmpty(alignStyle))
+ return $"{baseStyle}; {alignStyle}";
+ if (!string.IsNullOrEmpty(baseStyle))
+ return baseStyle;
+ if (!string.IsNullOrEmpty(alignStyle))
+ return alignStyle;
+ return null;
+ }
+ }
+ }
+}
diff --git a/status.md b/status.md
index 39e9f909..208cd7dc 100644
--- a/status.md
+++ b/status.md
@@ -2,12 +2,12 @@
| Category | Completed | In Progress | Not Started | Total |
|----------|-----------|-------------|-------------|-------|
-| Editor Controls | 15 | 0 | 12 | 27 |
-| Data Controls | 6 | 0 | 2 | 8 |
+| Editor Controls | 17 | 0 | 10 | 27 |
+| Data Controls | 7 | 0 | 2 | 9 |
| Validation Controls | 7 | 0 | 0 | 7 |
-| Navigation Controls | 2 | 0 | 1 | 3 |
+| Navigation Controls | 3 | 0 | 0 | 3 |
| Login Controls | 4 | 0 | 3 | 7 |
-| **TOTAL** | **34** | **0** | **18** | **52** |
+| **TOTAL** | **38** | **0** | **15** | **53** |
---
@@ -30,7 +30,7 @@
| Literal | ✅ Complete | Documented |
| RadioButton | ✅ Complete | Documented, tested, sample page exists |
| TextBox | ✅ Complete | Documented, tested, sample page exists |
-| BulletedList | 🔴 Not Started | List control |
+| BulletedList | ✅ Complete | Documented, tested (41 tests), sample page exists |
| Calendar | 🔴 Not Started | Complex date picker |
| CheckBoxList | ✅ Complete | Documented, tested (26 tests) |
| FileUpload | 🔴 Not Started | Consider Blazor InputFile |
@@ -42,11 +42,11 @@
| PlaceHolder | ✅ Complete | Documented, tested - renders no wrapper element |
| RadioButtonList | ✅ Complete | Documented, tested (30 tests) |
| Substitution | 🔴 Not Started | Cache substitution - may not apply |
-| Table | 🔴 Not Started | HTML table wrapper |
+| Table | ✅ Complete | Includes TableRow, TableCell, TableHeaderCell, TableHeaderRow, TableFooterRow |
| View | 🔴 Not Started | Used with MultiView |
| Xml | 🔴 Not Started | XML display/transform |
-### 🟡 Data Controls (6/8 - 75% Complete)
+### 🟡 Data Controls (7/9 - 78% Complete)
| Component | Status | Notes |
|-----------|--------|-------|
@@ -57,7 +57,7 @@
| ListView | ✅ Complete | Documented |
| Repeater | ✅ Complete | Documented |
| Chart | 🔴 Not Started | Consider deferring - very high complexity |
-| DataPager | 🔴 Not Started | Paging for ListView |
+| DataPager | ✅ Complete | Documented in DataPager.md |
| DetailsView | 🔴 Not Started | Single-record display/edit |
### ✅ Validation Controls (7/7 - 100% Complete)
@@ -73,13 +73,13 @@
| RequiredFieldValidator | ✅ Complete | Documented |
| ValidationSummary | ✅ Complete | Documented |
-### 🟡 Navigation Controls (2/3 - 67% Complete)
+### ✅ Navigation Controls (3/3 - 100% Complete)
| Component | Status | Notes |
|-----------|--------|-------|
| Menu | ✅ Complete | Documented, tested, sample pages exist |
+| SiteMapPath | ✅ Complete | Documented, tested (23 tests), sample page exists |
| TreeView | ✅ Complete | Documented in TreeView.md |
-| SiteMapPath | 🔴 Not Started | Listed in README |
### 🟡 Login Controls (4/7 - 57% Complete)
@@ -129,8 +129,8 @@
| Component | Complexity | Est. Hours (Manual) | Est. Hours (with Copilot) |
|-----------|------------|---------------------|---------------------------|
| ~~**Menu**~~ | ~~Medium-High~~ | ~~12-16~~ | ~~6-8~~ | ✅ Complete |
-| **SiteMapPath** | Medium | 8-10 | 4-5 |
-| **DataPager** | Medium | 8-12 | 4-6 |
+| ~~**SiteMapPath**~~ | ~~Medium~~ | ~~8-10~~ | ~~4-5~~ | ✅ Complete |
+| ~~**DataPager**~~ | ~~Medium~~ | ~~8-12~~ | ~~4-6~~ | ✅ Complete |
| **DetailsView** | High | 16-24 | 8-12 |
#### Login Controls
@@ -205,8 +205,8 @@
### Phase 3: Navigation & Data
10. ~~**Menu**~~ - ✅ Complete
-11. **SiteMapPath** - Breadcrumb navigation
-12. **DataPager** - Paging for ListView
+11. ~~**SiteMapPath**~~ - ✅ Complete (Breadcrumb navigation, 23 tests)
+12. ~~**DataPager**~~ - ✅ Complete (Paging for ListView)
13. **DetailsView** - Single-record display
### Phase 4: Login Controls