diff --git a/README.md b/README.md index 797cbd4f..805f5ff1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ There are a significant number of controls in ASP.NET Web Forms, and we will foc - Editor Controls - [AdRotator](docs/EditorControls/AdRotator.md) - - BulletedList + - [BulletedList](docs/EditorControls/BulletedList.md) - [Button](docs/EditorControls/Button.md) - Calendar - [CheckBox](docs/EditorControls/CheckBox.md) @@ -45,8 +45,7 @@ There are a significant number of controls in ASP.NET Web Forms, and we will foc - [PlaceHolder](docs/EditorControls/PlaceHolder.md) - [RadioButton](docs/EditorControls/RadioButton.md) - [RadioButtonList](docs/EditorControls/RadioButtonList.md) - - Substitution - - Table + - [Table](docs/EditorControls/Table.md) - [TextBox](docs/EditorControls/TextBox.md) - View - Xml @@ -54,7 +53,7 @@ There are a significant number of controls in ASP.NET Web Forms, and we will foc - Chart(?) - [DataGrid](docs/DataControls/DataGrid.md) - [DataList](docs/DataControls/DataList.md) - - DataPager + - [DataPager](docs/DataControls/DataPager.md) - DetailsView - [FormView](docs/DataControls/FormView.md) - [GridView](docs/DataControls/GridView.md) @@ -70,7 +69,7 @@ There are a significant number of controls in ASP.NET Web Forms, and we will foc - Navigation Controls - [HyperLink](docs/NavigationControls/HyperLink.md) - [Menu](docs/NavigationControls/Menu.md) - - SiteMapPath + - [SiteMapPath](docs/NavigationControls/SiteMapPath.md) - [TreeView](docs/NavigationControls/TreeView.md) - Login Controls - ChangePassword diff --git a/docs/DataControls/DataPager.md b/docs/DataControls/DataPager.md new file mode 100644 index 00000000..08811104 --- /dev/null +++ b/docs/DataControls/DataPager.md @@ -0,0 +1,196 @@ +# DataPager + +The DataPager component provides paging functionality for data-bound controls. It displays navigation UI (page numbers, Previous/Next buttons, First/Last buttons) to navigate through large data sets. + +Original Microsoft implementation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.datapager?view=netframework-4.8 + +## Features Supported in Blazor + +- **TotalRowCount** - Total number of items in the data source +- **PageSize** - Number of items per page (default: 10) +- **PageIndex** - Current page (zero-based) +- **PageButtonCount** - Number of numeric page buttons to show (default: 5) +- **Mode** - PagerButtons enum (Numeric, NextPrevious, NextPreviousFirstLast, NumericFirstLast) +- **Custom button text** - FirstPageText, PreviousPageText, NextPageText, LastPageText +- **Events** - OnPageIndexChanging (cancellable), OnPageIndexChanged +- **StartRowIndex** / **MaximumRows** - Properties for data slicing + +### Blazor Notes + +Unlike Web Forms, which links DataPager to ListView via `PagedControlID`, the Blazor implementation uses two-way binding. You manage the page index in your component and pass it to both the DataPager and your data query. + +The DataPager provides `StartRowIndex` and `MaximumRows` properties that you can use to slice your data: + +```csharp +var pagedData = allData.Skip(dataPager.StartRowIndex).Take(dataPager.MaximumRows); +``` + +## Web Forms Features NOT Supported + +- **PagedControlID** - Not supported; use two-way binding instead +- **Fields collection** - Not supported; use Mode property for common layouts +- **QueryStringField** - Not supported; implement URL-based paging separately + +## Web Forms Declarative Syntax + +```html + + + + + + +``` + +## Blazor Syntax + +```razor + +``` + +## Usage Notes + +1. **Manage state externally** - The DataPager doesn't fetch data; you handle paging logic +2. **Use @bind-PageIndex** - For two-way binding of the current page +3. **Subscribe to events** - Use OnPageIndexChanging to cancel navigation or OnPageIndexChanged for side effects +4. **Slice your data** - Use `Skip()` and `Take()` based on StartRowIndex and MaximumRows + +## Examples + +### Basic Usage with ListView + +```razor +@* Data paging with ListView *@ + + +
@product.Name - @product.Price.ToString("C")
+
+
+ + + +@code { + private List AllProducts = GetAllProducts(); + private int CurrentPage = 0; + + private IEnumerable PagedProducts => + AllProducts.Skip(CurrentPage * 10).Take(10); +} +``` + +### Next/Previous Navigation + +```razor +@* Simple Previous/Next buttons *@ + +``` + +### Full Navigation with Numbers + +```razor +@* All navigation options *@ + +``` + +### Handling Page Changes + +```razor + + +@code { + private async Task HandlePageChanging(PageChangedEventArgs args) + { + // Cancel if there are unsaved changes + if (HasUnsavedChanges) + { + args.Cancel = true; + ShowWarning("Please save your changes first."); + } + } + + private async Task HandlePageChanged(PageChangedEventArgs args) + { + // Log or track page navigation + await LogPageView(args.NewPageIndex); + } +} +``` + +### Custom Styling + +```razor + + + +``` + +## See Also + +- [ListView](../DataControls/ListView.md) +- [GridView](../DataControls/GridView.md) +- [Repeater](../DataControls/Repeater.md) diff --git a/docs/EditorControls/BulletedList.md b/docs/EditorControls/BulletedList.md new file mode 100644 index 00000000..2e7cc22a --- /dev/null +++ b/docs/EditorControls/BulletedList.md @@ -0,0 +1,313 @@ +# BulletedList + +The BulletedList component renders items as a bulleted or numbered list. It supports multiple bullet styles, display modes (text, hyperlink, or clickable link buttons), and both static items and data-bound scenarios. + +Original Web Forms documentation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.bulletedlist?view=netframework-4.8 + +## Blazor Features Supported + +- Static items via `StaticItems` parameter with `ListItemCollection` +- Data binding via `Items` parameter with `DataTextField` and `DataValueField` +- Multiple bullet styles: Disc, Circle, Square, Numbered, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman, CustomImage +- Display modes: Text (default), HyperLink, LinkButton +- `FirstBulletNumber` for starting ordered lists at a specific number +- `BulletImageUrl` for custom bullet images +- `Target` attribute for hyperlinks (e.g., "_blank") +- `OnClick` event handler for LinkButton mode +- Disabled state via `Enabled` parameter (affects entire control or individual items) +- Style attributes (BackColor, ForeColor, Font, BorderStyle, etc.) and CssClass formatting + +## WebForms Features Not Supported + +- **AutoPostBack** - Not supported in Blazor; use `OnClick` event for LinkButton mode +- **DataSourceID** - Not supported; bind directly to collections via `Items` parameter + +## WebForms Syntax + +```html + + + + + + +``` + +## Blazor Syntax + +### Static Items (Text Mode) + +```razor + + +@code { + private ListItemCollection items = new() + { + new ListItem("First item", "1"), + new ListItem("Second item", "2"), + new ListItem("Third item", "3") + }; +} +``` + +### Data Binding + +```razor + + +@code { + 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" } + }; + + public class Product + { + public string Name { get; set; } + public string Url { get; set; } + } +} +``` + +### HyperLink Mode with Target + +```razor + + +@code { + private ListItemCollection links = new() + { + new ListItem("Microsoft", "https://microsoft.com"), + new ListItem("GitHub", "https://github.com"), + new ListItem("Azure", "https://azure.com") + }; +} +``` + +### LinkButton Mode with Click Handler + +```razor + + +

@message

+ +@code { + private string message = ""; + + private ListItemCollection items = new() + { + new ListItem("Option A", "a"), + new ListItem("Option B", "b"), + new ListItem("Option C", "c") + }; + + private void HandleItemClick(BulletedListEventArgs e) + { + message = $"You clicked item at index {e.Index}"; + } +} +``` + +### Numbered List Starting at 5 + +```razor + +``` + +### Custom Bullet Image + +```razor + +``` + +### With Styling + +```razor + +``` + +## Bullet Styles + +| BulletStyle | List Type | CSS/HTML | +|-------------|-----------|----------| +| `NotSet` | Unordered | Browser default | +| `Disc` | Unordered | Filled circle (●) | +| `Circle` | Unordered | Empty circle (○) | +| `Square` | Unordered | Filled square (■) | +| `Numbered` | Ordered | 1, 2, 3... | +| `LowerAlpha` | Ordered | a, b, c... | +| `UpperAlpha` | Ordered | A, B, C... | +| `LowerRoman` | Ordered | i, ii, iii... | +| `UpperRoman` | Ordered | I, II, III... | +| `CustomImage` | Unordered | Custom image via `BulletImageUrl` | + +## Display Modes + +| DisplayMode | Description | Rendered As | +|-------------|-------------|-------------| +| `Text` | Plain text items (default) | `` | +| `HyperLink` | Navigable links using Value as URL | `` | +| `LinkButton` | Clickable items that fire OnClick event | `` | + +## HTML Output + +### Unordered List (Disc Style) + +```html +
    +
  • First item
  • +
  • Second item
  • +
+``` + +### Ordered List (Numbered) + +```html +
    +
  1. First item
  2. +
  3. Second item
  4. +
+``` + +### HyperLink Mode + +```html +
+``` + +### LinkButton Mode + +```html + +``` + +## Key Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `StaticItems` | `ListItemCollection` | empty | Static list items | +| `Items` | `IEnumerable` | null | Data-bound items | +| `DataTextField` | `string` | null | Property for display text | +| `DataValueField` | `string` | null | Property for value (used as URL in HyperLink mode) | +| `BulletStyle` | `BulletStyle` | NotSet | Style of bullets or numbers | +| `BulletImageUrl` | `string` | null | URL of custom bullet image | +| `DisplayMode` | `BulletedListDisplayMode` | Text | How items are rendered | +| `FirstBulletNumber` | `int` | 1 | Starting number for ordered lists | +| `Target` | `string` | null | Target window for hyperlinks | +| `OnClick` | `EventCallback` | - | Click handler for LinkButton mode | + +## Key Differences from Web Forms + +1. **Type Parameter**: Blazor BulletedList requires a `TItem` type parameter for data binding +2. **Property Names**: Use `StaticItems` for the item collection (not `Items`), as `Items` is reserved for data-bound scenarios +3. **Event Handling**: Uses `OnClick` with `EventCallback` instead of server-side postback +4. **Enum References**: Use `BulletStyle.Numbered` and `BulletedListDisplayMode.HyperLink` syntax +5. **No AutoPostBack**: Blazor's event model is immediate; events fire without postback + +## BulletedListEventArgs + +The `OnClick` event provides a `BulletedListEventArgs` object with: + +| Property | Type | Description | +|----------|------|-------------| +| `Index` | `int` | Zero-based index of the clicked item | + +```csharp +private void HandleClick(BulletedListEventArgs e) +{ + var clickedIndex = e.Index; + var clickedItem = items[clickedIndex]; + Console.WriteLine($"Clicked: {clickedItem.Text}"); +} +``` + +## Migration Notes + +When migrating from Web Forms to Blazor: + +1. Remove the `asp:` prefix and `runat="server"` attribute +2. Add the `TItem="object"` type parameter (or your specific data type) +3. Remove `` tags and define items in code-behind as `ListItemCollection` +4. For LinkButton mode, add an `OnClick` handler +5. Update enum references to Blazor syntax (e.g., `BulletStyle.Numbered`) + +### Before (Web Forms): + +```html + + + + + +``` + +### After (Blazor): + +```razor + + +@code { + private ListItemCollection links = new() + { + new ListItem("Microsoft", "https://microsoft.com"), + new ListItem("GitHub", "https://github.com"), + new ListItem("Azure", "https://azure.com") + }; +} +``` + +## See Also + +- [CheckBoxList](CheckBoxList.md) - Multi-select checkbox group +- [RadioButtonList](RadioButtonList.md) - Single-select radio button group +- [ListBox](ListBox.md) - Multi-select list control diff --git a/docs/EditorControls/Table.md b/docs/EditorControls/Table.md new file mode 100644 index 00000000..45c489e9 --- /dev/null +++ b/docs/EditorControls/Table.md @@ -0,0 +1,270 @@ +# Table + +The Table component provides a container for table rows and cells, rendering as an HTML `` element. It includes related components: TableRow, TableCell, TableHeaderCell, TableHeaderRow, and TableFooterRow. + +Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.table?view=netframework-4.8 + +## Features Supported in Blazor + +### Table Component + +- **Caption** - Table caption text +- **CaptionAlign** - Position of caption (Top, Bottom) +- **CellPadding** - Padding within cells in pixels +- **CellSpacing** - Spacing between cells in pixels +- **GridLines** - Display of grid lines (None, Horizontal, Vertical, Both) +- **HorizontalAlign** - Table alignment (Left, Center, Right) +- **BackImageUrl** - Background image for the table +- All style properties (CssClass, BackColor, etc.) + +### TableRow Component + +- **TableSection** - Where the row belongs (TableHeader, TableBody, TableFooter) +- **HorizontalAlign** - Horizontal alignment of row content +- **VerticalAlign** - Vertical alignment of row content +- All style properties + +### TableCell Component + +- **ColumnSpan** - Number of columns the cell spans +- **RowSpan** - Number of rows the cell spans +- **HorizontalAlign** / **VerticalAlign** - Content alignment +- **Wrap** - Whether content wraps (default: true) +- **Text** - Text content of the cell +- **AssociatedHeaderCellID** - Accessibility association with header + +### TableHeaderCell Component + +- All TableCell properties plus: +- **Scope** - Header scope for accessibility (Row, Column) +- **AbbreviatedText** - Abbreviated header text for accessibility + +### Specialized Row Components + +- **TableHeaderRow** - Renders in `` section +- **TableFooterRow** - Renders in `` section + +## Web Forms Features NOT Supported + +- **Rows collection** - Use declarative child content instead +- **Programmatic row generation** - Build rows in Blazor code + +## Web Forms Declarative Syntax + +```html + + + Name + Price + + + Widget + $10.00 + + + Total: $10.00 + + +``` + +## Blazor Syntax + +### Basic Table + +```razor +
+ + Cell 1 + Cell 2 + + + Cell 3 + Cell 4 + +
+``` + +### Table with Header and Footer + +```razor + + + Product + Quantity + Price + + + Widget A + 50 + $25.00 + + + Widget B + 30 + $15.00 + + + Total + $1,700.00 + +
+``` + +### Cell Spanning + +```razor + + + + Header spanning 2 columns + + + + Spans 2 rows + Row 1 + + + Row 2 + +
+``` + +### Accessible Table with Scope + +```razor + + + + Q1 + Q2 + + + North + $100 + $150 + + + South + $200 + $175 + +
+``` + +### Styled Table + +```razor + + + Name + Status + + + Item 1 + Active + +
+``` + +## HTML Output + +**Blazor:** +```razor + + + Header + + + Data + +
+``` + +**Rendered HTML:** +```html + + + + + + + + + + + + +
Data
Header
Data
+``` + +## GridLines Property Values + +| Value | HTML Output | +|-------|-------------| +| `GridLines.None` | No border or rules | +| `GridLines.Horizontal` | `rules="rows"` | +| `GridLines.Vertical` | `rules="cols"` | +| `GridLines.Both` | `rules="all" border="1"` | + +## Migration Notes + +When migrating from Web Forms to Blazor: + +1. Remove the `asp:` prefix and `runat="server"` attribute +2. Use enum types with full qualification (e.g., `GridLines.Both`) +3. Replace programmatic row generation with Blazor `@foreach` loops +4. The component uses standard ``, ``, `` sections + +### Before (Web Forms): +```html + + + Data + + +``` + +### After (Blazor): +```razor + + + Data + +
+``` + +### Dynamic Rows + +```razor + + + Name + Value + + @foreach (var item in Items) + { + + @item.Name + @item.Value + + } +
+``` + +## See Also + +- [GridView](../DataControls/GridView.md) - Data-bound grid control +- [DataList](../DataControls/DataList.md) - Data-bound repeating control +- [Panel](Panel.md) - Container control diff --git a/docs/NavigationControls/SiteMapPath.md b/docs/NavigationControls/SiteMapPath.md new file mode 100644 index 00000000..cf515a58 --- /dev/null +++ b/docs/NavigationControls/SiteMapPath.md @@ -0,0 +1,183 @@ +# SiteMapPath + +The SiteMapPath component displays a breadcrumb navigation path showing the current page's location within a site hierarchy. It helps users understand where they are in your application and provides quick navigation back to parent pages. + +Original Microsoft implementation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.sitemappath?view=netframework-4.8 + +## Features Supported in Blazor + +- **PathSeparator** - Custom separator string between nodes (default: " > ") +- **PathSeparatorTemplate** - Custom template for rendering separators +- **PathDirection** - RootToCurrent or CurrentToRoot ordering +- **RenderCurrentNodeAsLink** - Whether the current page is clickable +- **ShowToolTips** - Display node descriptions as tooltips +- **ParentLevelsDisplayed** - Limit breadcrumb depth (-1 for all) +- **CurrentNodeTemplate** - Custom template for current page node +- **NodeTemplate** - Custom template for ancestor nodes +- **RootNodeTemplate** - Custom template for the root/home node +- **Style properties** - CurrentNodeStyle, NodeStyle, RootNodeStyle, PathSeparatorStyle + +### Blazor Notes + +Unlike Web Forms, which uses a `Web.sitemap` XML file with `SiteMapDataSource`, the Blazor implementation requires you to provide the site hierarchy programmatically via the `SiteMapProvider` property. This gives you more flexibility to build navigation hierarchies dynamically from databases, configuration, or other sources. + +The `SiteMapNode` class provides a simple way to build hierarchies: + +```csharp +var root = new SiteMapNode("Home", "/"); +var products = new SiteMapNode("Products", "/products", "Browse our products"); +root.AddChild(products); +``` + +## Web Forms Features NOT Supported + +- **SiteMapDataSource** - Not supported; provide a `SiteMapNode` hierarchy directly +- **Web.sitemap XML file** - Not supported; build the hierarchy in code +- **Provider** - The `SiteMapProvider` property accepts a `SiteMapNode` root, not a provider name +- **SkipLinkText** - Accessibility skip link not implemented +- **ItemDataBound event** - Not supported; use templates for customization + +## Web Forms Declarative Syntax + +```html + +``` + +## Blazor Syntax + +```razor + +``` + +## Usage Notes + +1. **Build your site map in code** - Create a `SiteMapNode` hierarchy that represents your site structure +2. **Provide the current URL** - Set `CurrentUrl` to match against the site map nodes +3. **URL matching is flexible** - The component normalizes URLs, handling leading slashes and query strings +4. **Templates override default rendering** - When you provide a template, you're responsible for all markup including links + +## Examples + +### Basic Usage + +```razor +@* Basic breadcrumb navigation *@ + + +@code { + private string CurrentUrl = "/products/electronics"; + + private SiteMapNode SiteMap = BuildSiteMap(); + + private static SiteMapNode BuildSiteMap() + { + var root = new SiteMapNode("Home", "/"); + var products = new SiteMapNode("Products", "/products", "Browse our catalog"); + var electronics = new SiteMapNode("Electronics", "/products/electronics"); + + root.AddChild(products); + products.AddChild(electronics); + + return root; + } +} +``` + +### Custom Separator + +```razor +@* Using a custom separator *@ + + +@* Using a template for the separator *@ + + + + + +``` + +### Current Node as Link + +```razor +@* Make the current page clickable *@ + +``` + +### Limiting Parent Levels + +```razor +@* Only show 2 parent levels *@ + +@* Renders: Electronics > Phones > iPhone (skips Home and Products) *@ +``` + +### Custom Templates + +```razor +@* Full template customization *@ + + + 🏠 @node.Title + + + @node.Title + + + 📍 @node.Title + + + / + + +``` + +### Reverse Path Direction + +```razor +@* Show current page first, then parents *@ + +@* Renders: Electronics > Products > Home *@ +``` + +## See Also + +- [Menu](Menu.md) - Hierarchical navigation menu +- [TreeView](TreeView.md) - Tree-style navigation +- [Migration Guide](../Migration/readme.md) diff --git a/mkdocs.yml b/mkdocs.yml index 6038cb78..fb0ac6c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Home: README.md - Editor Controls: - AdRotator: EditorControls/AdRotator.md + - BulletedList: EditorControls/BulletedList.md - Button: EditorControls/Button.md - CheckBox: EditorControls/CheckBox.md - CheckBoxList: EditorControls/CheckBoxList.md @@ -79,10 +80,12 @@ nav: - PlaceHolder: EditorControls/PlaceHolder.md - RadioButton: EditorControls/RadioButton.md - RadioButtonList: EditorControls/RadioButtonList.md + - Table: EditorControls/Table.md - TextBox: EditorControls/TextBox.md - Data Controls: - DataGrid: DataControls/DataGrid.md - DataList: DataControls/DataList.md + - DataPager: DataControls/DataPager.md - FormView: DataControls/FormView.md - GridView: DataControls/GridView.md - ListView: DataControls/ListView.md @@ -97,6 +100,7 @@ nav: - Navigation Controls: - HyperLink: NavigationControls/HyperLink.md - Menu: NavigationControls/Menu.md + - SiteMapPath: NavigationControls/SiteMapPath.md - TreeView: NavigationControls/TreeView.md - Login Controls: - Login: LoginControls/Login.md diff --git a/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs b/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs index 03a580a6..eb589f4b 100644 --- a/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs +++ b/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs @@ -14,6 +14,7 @@ public ControlSampleTests(PlaywrightFixture fixture) // Editor Controls [Theory] + [InlineData("/ControlSamples/BulletedList")] [InlineData("/ControlSamples/Button")] [InlineData("/ControlSamples/CheckBox")] [InlineData("/ControlSamples/CheckBox/Events")] @@ -26,6 +27,7 @@ public ControlSampleTests(PlaywrightFixture fixture) [InlineData("/ControlSamples/PlaceHolder")] [InlineData("/ControlSamples/RadioButton")] [InlineData("/ControlSamples/RadioButtonList")] + [InlineData("/ControlSamples/Table")] [InlineData("/ControlSamples/TextBox")] public async Task EditorControl_Loads_WithoutErrors(string path) { @@ -99,6 +101,7 @@ public async Task ListView_ItemDataBound_DisplaysItemProperties() // Navigation Controls [Theory] + [InlineData("/ControlSamples/SiteMapPath")] [InlineData("/ControlSamples/TreeView")] public async Task NavigationControl_Loads_WithoutErrors(string path) { diff --git a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor index f2727418..27a594ad 100644 --- a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor +++ b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor @@ -18,6 +18,7 @@ + @@ -38,6 +39,7 @@ + @@ -99,6 +101,8 @@ + + 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:

+ +
+ + +
+ +
+
+ +
+
+ +
+ +

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:

+ +
+
+ + + 🏠 @node.Title + + + @node.Title + + + 📍 @node.Title + + + + + +
+
+ +@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(@ + + Header + +
); + + // Assert + var th = cut.Find("th"); + th.ShouldNotBeNull(); + th.TextContent.ShouldBe("Header"); + } + + [Fact] + public void TableHeaderCell_ScopeColumn_RendersScopeAttribute() + { + // Act + var cut = Render(@ + + Header + +
); + + // Assert + var th = cut.Find("th"); + th.GetAttribute("scope").ShouldBe("col"); + } + + [Fact] + public void TableHeaderCell_ScopeRow_RendersScopeAttribute() + { + // Act + var cut = Render(@ + + Header + +
); + + // Assert + var th = cut.Find("th"); + th.GetAttribute("scope").ShouldBe("row"); + } + + [Fact] + public void TableHeaderCell_WithAbbr_RendersAbbrAttribute() + { + // Act + var cut = Render(@ + + Quantity + +
); + + // Assert + var th = cut.Find("th"); + th.GetAttribute("abbr").ShouldBe("Qty"); + } + + [Fact] + public void TableCell_AssociatedHeaderCellID_RendersHeadersAttribute() + { + // Act + var cut = Render(@ + + Header + + + Data + +
); + + // 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(@ + + Cell 1 + Cell 2 + +
); + + // 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(@ + + Cell + +
); + + // Assert + var caption = cut.Find("caption"); + caption.ShouldNotBeNull(); + caption.TextContent.ShouldBe("My Table"); + } + + [Fact] + public void Table_NotVisible_RendersNothing() + { + // Act + var cut = Render(@ + + Cell + +
); + + // Assert + cut.Markup.ShouldBeEmpty(); + } + + [Fact] + public void Table_WithCssClass_AppliesClass() + { + // Act + var cut = Render(@ + + Cell + +
); + + // Assert + var table = cut.Find("table"); + table.GetAttribute("class").ShouldBe("my-table"); + } + + [Fact] + public void Table_CellPaddingAndSpacing_RendersAttributes() + { + // Act + var cut = Render(@ + + Cell + +
); + + // 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(@ + + Spanning Cell + +
); + + // 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(@ + + Spanning Cell + +
); + + // 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(@ + + Cell + +
); + + // 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(@ + + Cell + +
); + + // 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(@ + + Cell + +
); + + // Assert + var table = cut.Find("table"); + table.GetAttribute("rules").ShouldBe("cols"); + } + + [Fact] + public void Table_GridLinesBoth_RendersRulesAll() + { + // Act + var cut = Render(@ + + Cell + +
); + + // Assert + var table = cut.Find("table"); + table.GetAttribute("rules").ShouldBe("all"); + } + + [Fact] + public void Table_GridLinesNone_NoRulesAttribute() + { + // Act + var cut = Render(@ + + Cell + +
); + + // 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(@ + + Cell + +
); + + // 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(@ + + Cell + +
); + + // 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(@ + + Cell + +
); + + // 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(@ + + No Wrap Cell + +
); + + // 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(@ + + Centered + +
); + + // 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(@ + + Top Aligned + +
); + + // 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) + { + @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 + { + @(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)) + { + + } + @ChildContent +
    @Caption
    +} 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