From 3124fbe56c82f49a9735ded44584e4945ffeef9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:11:02 +0000 Subject: [PATCH 1/6] Initial plan From 915ad0878dea5229b2dcc9b7f883d2425e34e180 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:31:50 +0000 Subject: [PATCH 2/6] Add ImageMap component with HotSpot classes Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --- src/BlazorWebFormsComponents/CircleHotSpot.cs | 35 +++++++ .../Enums/HotSpotMode.cs | 23 +++++ src/BlazorWebFormsComponents/HotSpot.cs | 57 +++++++++++ src/BlazorWebFormsComponents/ImageMap.razor | 60 ++++++++++++ .../ImageMap.razor.cs | 96 +++++++++++++++++++ .../ImageMapEventArgs.cs | 24 +++++ .../PolygonHotSpot.cs | 25 +++++ .../RectangleHotSpot.cs | 40 ++++++++ 8 files changed, 360 insertions(+) create mode 100644 src/BlazorWebFormsComponents/CircleHotSpot.cs create mode 100644 src/BlazorWebFormsComponents/Enums/HotSpotMode.cs create mode 100644 src/BlazorWebFormsComponents/HotSpot.cs create mode 100644 src/BlazorWebFormsComponents/ImageMap.razor create mode 100644 src/BlazorWebFormsComponents/ImageMap.razor.cs create mode 100644 src/BlazorWebFormsComponents/ImageMapEventArgs.cs create mode 100644 src/BlazorWebFormsComponents/PolygonHotSpot.cs create mode 100644 src/BlazorWebFormsComponents/RectangleHotSpot.cs diff --git a/src/BlazorWebFormsComponents/CircleHotSpot.cs b/src/BlazorWebFormsComponents/CircleHotSpot.cs new file mode 100644 index 00000000..fcd3464e --- /dev/null +++ b/src/BlazorWebFormsComponents/CircleHotSpot.cs @@ -0,0 +1,35 @@ +namespace BlazorWebFormsComponents +{ + /// + /// Defines a circular hot spot region in an ImageMap control. + /// + public class CircleHotSpot : HotSpot + { + /// + /// Gets or sets the x-coordinate of the center of the circular region defined by this CircleHotSpot object. + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of the center of the circular region defined by this CircleHotSpot object. + /// + public int Y { get; set; } + + /// + /// Gets or sets the distance from the center to the edge of the circular region defined by this CircleHotSpot object. + /// + public int Radius { get; set; } + + /// + /// Gets the shape type for this hot spot. + /// + /// "circle" + public override string GetShapeType() => "circle"; + + /// + /// Gets the coordinates for this circular hot spot. + /// + /// A comma-separated string of coordinates in format: x,y,radius + public override string GetCoordinates() => $"{X},{Y},{Radius}"; + } +} diff --git a/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs b/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs new file mode 100644 index 00000000..c1b13c98 --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs @@ -0,0 +1,23 @@ +namespace BlazorWebFormsComponents.Enums +{ + /// + /// Specifies the behaviors of a HotSpot object in an ImageMap control when the HotSpot is clicked. + /// + public enum HotSpotMode + { + /// + /// The HotSpot does not have any behavior. + /// + Inactive, + + /// + /// The HotSpot navigates to a URL. + /// + Navigate, + + /// + /// The HotSpot generates a postback to the server. + /// + PostBack + } +} diff --git a/src/BlazorWebFormsComponents/HotSpot.cs b/src/BlazorWebFormsComponents/HotSpot.cs new file mode 100644 index 00000000..e9fe5dcb --- /dev/null +++ b/src/BlazorWebFormsComponents/HotSpot.cs @@ -0,0 +1,57 @@ +using BlazorWebFormsComponents.Enums; + +namespace BlazorWebFormsComponents +{ + /// + /// Implements the basic functionality common to all hot spot shapes. + /// + public abstract class HotSpot + { + /// + /// Gets or sets the alternate text to display for a HotSpot object in an ImageMap control when the image is unavailable or renders to a browser that does not support images. + /// + public string AlternateText { get; set; } = string.Empty; + + /// + /// Gets or sets the behavior of a HotSpot object in an ImageMap control when the HotSpot is clicked. + /// + public HotSpotMode HotSpotMode { get; set; } = HotSpotMode.Navigate; + + /// + /// Gets or sets the URL to navigate to when a HotSpot object is clicked. + /// + public string NavigateUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the HotSpot object to pass in the event data when the HotSpot is clicked. + /// + public string PostBackValue { get; set; } = string.Empty; + + /// + /// Gets or sets the target window or frame in which to display the Web page content linked to when the HotSpot object is clicked. + /// + public string Target { get; set; } = string.Empty; + + /// + /// Gets or sets the tab index of the HotSpot object. + /// + public short TabIndex { get; set; } + + /// + /// Gets or sets the AccessKey that allows you to quickly navigate to the HotSpot object. + /// + public string AccessKey { get; set; } = string.Empty; + + /// + /// Gets the shape type for this hot spot. + /// + /// The shape type (rect, circle, or poly) + public abstract string GetShapeType(); + + /// + /// Gets the coordinates for this hot spot as a comma-separated string. + /// + /// The coordinates string + public abstract string GetCoordinates(); + } +} diff --git a/src/BlazorWebFormsComponents/ImageMap.razor b/src/BlazorWebFormsComponents/ImageMap.razor new file mode 100644 index 00000000..4cfd8bf8 --- /dev/null +++ b/src/BlazorWebFormsComponents/ImageMap.razor @@ -0,0 +1,60 @@ +@inherits BaseWebFormsComponent + +@if (Visible) +{ + kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + + + @foreach (var hotSpot in HotSpots) + { + var effectiveMode = hotSpot.HotSpotMode != Enums.HotSpotMode.Navigate ? hotSpot.HotSpotMode : HotSpotMode; + var href = effectiveMode == Enums.HotSpotMode.Navigate ? hotSpot.NavigateUrl : "#"; + var target = !string.IsNullOrEmpty(hotSpot.Target) ? hotSpot.Target : Target; + + @if (effectiveMode == Enums.HotSpotMode.Inactive) + { + @hotSpot.AlternateText + } + else if (effectiveMode == Enums.HotSpotMode.Navigate) + { + @hotSpot.AlternateText 0 ? hotSpot.TabIndex.ToString() : null }, + { "accesskey", !string.IsNullOrEmpty(hotSpot.AccessKey) ? hotSpot.AccessKey : null } + }.Where(kvp => kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + } + else if (effectiveMode == Enums.HotSpotMode.PostBack) + { + @hotSpot.AlternateText 0 ? hotSpot.TabIndex.ToString() : null }, + { "accesskey", !string.IsNullOrEmpty(hotSpot.AccessKey) ? hotSpot.AccessKey : null } + }.Where(kvp => kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + } + } + +} + +@code { + +} diff --git a/src/BlazorWebFormsComponents/ImageMap.razor.cs b/src/BlazorWebFormsComponents/ImageMap.razor.cs new file mode 100644 index 00000000..558dcef4 --- /dev/null +++ b/src/BlazorWebFormsComponents/ImageMap.razor.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using BlazorWebFormsComponents.Enums; +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + /// + /// Creates an image map control that displays an image with defined clickable hot spot regions. + /// + public partial class ImageMap : BaseWebFormsComponent, IImageComponent + { + /// + /// Gets or sets the alternate text to display in the ImageMap control when the image is unavailable. + /// + [Parameter] + public string AlternateText { get; set; } = string.Empty; + + /// + /// Gets or sets the URL to a detailed description of the image in the ImageMap control. + /// + [Parameter] + public string DescriptionUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the alignment of the Image control in relation to other elements on the Web page. + /// + [Parameter] + public ImageAlign ImageAlign { get; set; } = ImageAlign.NotSet; + + /// + /// Gets or sets the URL to the image to display in the ImageMap control. + /// + [Parameter] + public string ImageUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the ToolTip text for the ImageMap control. + /// + [Parameter] + public string ToolTip { get; set; } = string.Empty; + + /// + /// Gets or sets the default behavior for the HotSpot objects in the ImageMap control when the HotSpot objects are clicked. + /// + [Parameter] + public HotSpotMode HotSpotMode { get; set; } = HotSpotMode.Navigate; + + /// + /// Gets or sets the target window or frame in which to display the Web page content linked to when a HotSpot object in an ImageMap control is clicked. + /// + [Parameter] + public string Target { get; set; } = string.Empty; + + /// + /// Gets the collection of HotSpot objects defined in the ImageMap control. + /// + [Parameter] + public List HotSpots { get; set; } = new List(); + + /// + /// Occurs when a HotSpot object in an ImageMap control is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// Gets or sets a value that indicates whether the ImageMap control generates an empty alternate text attribute. + /// + [Parameter] + public bool GenerateEmptyAlternateText { get; set; } + + private string _mapId = string.Empty; + + protected override void OnInitialized() + { + base.OnInitialized(); + // Generate a unique map ID + _mapId = $"ImageMap_{GetHashCode()}"; + } + + /// + /// Handles the click event for a hot spot. + /// + /// The hot spot that was clicked + protected async void HandleHotSpotClick(HotSpot hotSpot) + { + if (hotSpot.HotSpotMode == HotSpotMode.PostBack || + (hotSpot.HotSpotMode == HotSpotMode.Navigate && HotSpotMode == HotSpotMode.PostBack)) + { + var eventArgs = new ImageMapEventArgs(hotSpot.PostBackValue); + await OnClick.InvokeAsync(eventArgs); + } + } + } +} diff --git a/src/BlazorWebFormsComponents/ImageMapEventArgs.cs b/src/BlazorWebFormsComponents/ImageMapEventArgs.cs new file mode 100644 index 00000000..dcb1d4a0 --- /dev/null +++ b/src/BlazorWebFormsComponents/ImageMapEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace BlazorWebFormsComponents +{ + /// + /// Provides data for the Click event of an ImageMap control. + /// + public class ImageMapEventArgs : EventArgs + { + /// + /// Gets the PostBackValue associated with the HotSpot object in the ImageMap control that was clicked. + /// + public string PostBackValue { get; } + + /// + /// Initializes a new instance of the ImageMapEventArgs class. + /// + /// The string assigned to the PostBackValue property of the HotSpot object that was clicked. + public ImageMapEventArgs(string postBackValue) + { + PostBackValue = postBackValue; + } + } +} diff --git a/src/BlazorWebFormsComponents/PolygonHotSpot.cs b/src/BlazorWebFormsComponents/PolygonHotSpot.cs new file mode 100644 index 00000000..4f672795 --- /dev/null +++ b/src/BlazorWebFormsComponents/PolygonHotSpot.cs @@ -0,0 +1,25 @@ +namespace BlazorWebFormsComponents +{ + /// + /// Defines a polygon-shaped hot spot region in an ImageMap control. + /// + public class PolygonHotSpot : HotSpot + { + /// + /// Gets or sets a string of coordinates that represents the vertexes of a PolygonHotSpot object. + /// + public string Coordinates { get; set; } = string.Empty; + + /// + /// Gets the shape type for this hot spot. + /// + /// "poly" + public override string GetShapeType() => "poly"; + + /// + /// Gets the coordinates for this polygonal hot spot. + /// + /// A comma-separated string of x,y coordinate pairs + public override string GetCoordinates() => Coordinates; + } +} diff --git a/src/BlazorWebFormsComponents/RectangleHotSpot.cs b/src/BlazorWebFormsComponents/RectangleHotSpot.cs new file mode 100644 index 00000000..2409a299 --- /dev/null +++ b/src/BlazorWebFormsComponents/RectangleHotSpot.cs @@ -0,0 +1,40 @@ +namespace BlazorWebFormsComponents +{ + /// + /// Defines a rectangular hot spot region in an ImageMap control. + /// + public class RectangleHotSpot : HotSpot + { + /// + /// Gets or sets the x-coordinate of the left side of the rectangular region defined by this RectangleHotSpot object. + /// + public int Left { get; set; } + + /// + /// Gets or sets the y-coordinate of the top of the rectangular region defined by this RectangleHotSpot object. + /// + public int Top { get; set; } + + /// + /// Gets or sets the x-coordinate of the right side of the rectangular region defined by this RectangleHotSpot object. + /// + public int Right { get; set; } + + /// + /// Gets or sets the y-coordinate of the bottom of the rectangular region defined by this RectangleHotSpot object. + /// + public int Bottom { get; set; } + + /// + /// Gets the shape type for this hot spot. + /// + /// "rect" + public override string GetShapeType() => "rect"; + + /// + /// Gets the coordinates for this rectangular hot spot. + /// + /// A comma-separated string of coordinates in format: left,top,right,bottom + public override string GetCoordinates() => $"{Left},{Top},{Right},{Bottom}"; + } +} From e52e4142cde8ed51ed0186784a021ca396af4ad0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:43:46 +0000 Subject: [PATCH 3/6] Add ImageMap component tests - all 23 tests passing Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --- .../ImageMap/CircleHotSpot.razor | 53 +++++++++ .../ImageMap/InactiveMode.razor | 58 +++++++++ .../ImageMap/MixedHotSpots.razor | 41 +++++++ .../ImageMap/NavigateMode.razor | 65 ++++++++++ .../ImageMap/PolygonHotSpot.razor | 39 ++++++ .../ImageMap/PostBackMode.razor | 90 ++++++++++++++ .../ImageMap/Properties.razor | 111 ++++++++++++++++++ .../ImageMap/RectangleHotSpot.razor | 66 +++++++++++ .../ImageMap/Visible.razor | 42 +++++++ src/BlazorWebFormsComponents/ImageMap.razor | 69 ++++++++--- 10 files changed, 618 insertions(+), 16 deletions(-) create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/CircleHotSpot.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/InactiveMode.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/MixedHotSpots.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/NavigateMode.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/PolygonHotSpot.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/PostBackMode.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/Properties.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/RectangleHotSpot.razor create mode 100644 src/BlazorWebFormsComponents.Test/ImageMap/Visible.razor diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/CircleHotSpot.razor b/src/BlazorWebFormsComponents.Test/ImageMap/CircleHotSpot.razor new file mode 100644 index 00000000..b4916f60 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/CircleHotSpot.razor @@ -0,0 +1,53 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_CircleHotSpot_RendersCorrectly() + { + var hotSpots = new List(); + var circle = new BlazorWebFormsComponents.CircleHotSpot(); + circle.X = 100; + circle.Y = 100; + circle.Radius = 50; + circle.AlternateText = "Circle Area"; + circle.NavigateUrl = "/page1.html"; + hotSpots.Add(circle); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("shape").ShouldBe("circle"); + area.GetAttribute("coords").ShouldBe("100,100,50"); + area.GetAttribute("alt").ShouldBe("Circle Area"); + area.GetAttribute("href").ShouldBe("/page1.html"); + } + + [Fact] + public void ImageMap_MultipleCircleHotSpots_RendersAll() + { + var hotSpots = new List(); + + var circle1 = new BlazorWebFormsComponents.CircleHotSpot(); + circle1.X = 50; + circle1.Y = 50; + circle1.Radius = 25; + circle1.AlternateText = "Circle 1"; + circle1.NavigateUrl = "/page1.html"; + hotSpots.Add(circle1); + + var circle2 = new BlazorWebFormsComponents.CircleHotSpot(); + circle2.X = 150; + circle2.Y = 150; + circle2.Radius = 35; + circle2.AlternateText = "Circle 2"; + circle2.NavigateUrl = "/page2.html"; + hotSpots.Add(circle2); + + var cut = Render(@); + + var areas = cut.FindAll("area"); + areas.Count.ShouldBe(2); + areas[0].GetAttribute("coords").ShouldBe("50,50,25"); + areas[1].GetAttribute("coords").ShouldBe("150,150,35"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/InactiveMode.razor b/src/BlazorWebFormsComponents.Test/ImageMap/InactiveMode.razor new file mode 100644 index 00000000..b578151c --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/InactiveMode.razor @@ -0,0 +1,58 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_InactiveMode_RendersNoHrefAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Inactive Area"; + rect.HotSpotMode = HotSpotMode.Inactive; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("shape").ShouldBe("rect"); + area.GetAttribute("nohref").ShouldBe("nohref"); + area.HasAttribute("href").ShouldBeFalse(); + } + + [Fact] + public void ImageMap_InactiveMode_MultipleHotSpots_AllInactive() + { + var hotSpots = new List(); + + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 50; + rect.Bottom = 50; + rect.AlternateText = "Area 1"; + rect.HotSpotMode = HotSpotMode.Inactive; + hotSpots.Add(rect); + + var circle = new BlazorWebFormsComponents.CircleHotSpot(); + circle.X = 100; + circle.Y = 100; + circle.Radius = 30; + circle.AlternateText = "Area 2"; + circle.HotSpotMode = HotSpotMode.Inactive; + hotSpots.Add(circle); + + var cut = Render(@); + + var areas = cut.FindAll("area"); + areas.Count.ShouldBe(2); + + foreach (var area in areas) + { + area.GetAttribute("nohref").ShouldBe("nohref"); + area.HasAttribute("href").ShouldBeFalse(); + } + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/MixedHotSpots.razor b/src/BlazorWebFormsComponents.Test/ImageMap/MixedHotSpots.razor new file mode 100644 index 00000000..e174b5cf --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/MixedHotSpots.razor @@ -0,0 +1,41 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_MixedHotSpots_RendersAll() + { + var hotSpots = new List(); + + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Rectangle"; + rect.NavigateUrl = "/rect.html"; + hotSpots.Add(rect); + + var circle = new BlazorWebFormsComponents.CircleHotSpot(); + circle.X = 200; + circle.Y = 50; + circle.Radius = 40; + circle.AlternateText = "Circle"; + circle.NavigateUrl = "/circle.html"; + hotSpots.Add(circle); + + var polygon = new BlazorWebFormsComponents.PolygonHotSpot(); + polygon.Coordinates = "300,10,350,50,320,90"; + polygon.AlternateText = "Triangle"; + polygon.NavigateUrl = "/triangle.html"; + hotSpots.Add(polygon); + + var cut = Render(@); + + var areas = cut.FindAll("area"); + areas.Count.ShouldBe(3); + + areas[0].GetAttribute("shape").ShouldBe("rect"); + areas[1].GetAttribute("shape").ShouldBe("circle"); + areas[2].GetAttribute("shape").ShouldBe("poly"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/NavigateMode.razor b/src/BlazorWebFormsComponents.Test/ImageMap/NavigateMode.razor new file mode 100644 index 00000000..2f368158 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/NavigateMode.razor @@ -0,0 +1,65 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_NavigateMode_RendersHref() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + rect.NavigateUrl = "/page1.html"; + rect.HotSpotMode = HotSpotMode.Navigate; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("href").ShouldBe("/page1.html"); + area.GetAttribute("alt").ShouldBe("Area 1"); + } + + [Fact] + public void ImageMap_NavigateMode_WithTarget_RendersTargetAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + rect.NavigateUrl = "/page1.html"; + rect.HotSpotMode = HotSpotMode.Navigate; + rect.Target = "_blank"; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("target").ShouldBe("_blank"); + } + + [Fact] + public void ImageMap_NavigateMode_DefaultTarget_UsesImageMapTarget() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + rect.NavigateUrl = "/page1.html"; + rect.HotSpotMode = HotSpotMode.Navigate; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("target").ShouldBe("_self"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/PolygonHotSpot.razor b/src/BlazorWebFormsComponents.Test/ImageMap/PolygonHotSpot.razor new file mode 100644 index 00000000..806660a1 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/PolygonHotSpot.razor @@ -0,0 +1,39 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_PolygonHotSpot_RendersCorrectly() + { + var hotSpots = new List(); + var polygon = new BlazorWebFormsComponents.PolygonHotSpot(); + polygon.Coordinates = "10,10,50,10,50,50,10,50"; + polygon.AlternateText = "Polygon Area"; + polygon.NavigateUrl = "/page1.html"; + hotSpots.Add(polygon); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("shape").ShouldBe("poly"); + area.GetAttribute("coords").ShouldBe("10,10,50,10,50,50,10,50"); + area.GetAttribute("alt").ShouldBe("Polygon Area"); + area.GetAttribute("href").ShouldBe("/page1.html"); + } + + [Fact] + public void ImageMap_ComplexPolygonHotSpot_RendersCorrectly() + { + var hotSpots = new List(); + var polygon = new BlazorWebFormsComponents.PolygonHotSpot(); + polygon.Coordinates = "100,50,120,80,110,120,80,120,70,80,90,50"; + polygon.AlternateText = "Star Shape"; + polygon.NavigateUrl = "/star.html"; + hotSpots.Add(polygon); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("shape").ShouldBe("poly"); + area.GetAttribute("coords").ShouldBe("100,50,120,80,110,120,80,120,70,80,90,50"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/PostBackMode.razor b/src/BlazorWebFormsComponents.Test/ImageMap/PostBackMode.razor new file mode 100644 index 00000000..162601a7 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/PostBackMode.razor @@ -0,0 +1,90 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_PostBackMode_InvokesClickEvent() + { + var clickedValue = string.Empty; + + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + rect.PostBackValue = "Area1Clicked"; + rect.HotSpotMode = HotSpotMode.PostBack; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.Click(); + + clickedValue.ShouldBe("Area1Clicked"); + } + + [Fact] + public void ImageMap_PostBackMode_MultipleHotSpots_InvokesCorrectEvent() + { + var clickedValue = string.Empty; + + var hotSpots = new List(); + + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 50; + rect.Bottom = 50; + rect.AlternateText = "Area 1"; + rect.PostBackValue = "Area1"; + rect.HotSpotMode = HotSpotMode.PostBack; + hotSpots.Add(rect); + + var circle = new BlazorWebFormsComponents.CircleHotSpot(); + circle.X = 100; + circle.Y = 100; + circle.Radius = 30; + circle.AlternateText = "Area 2"; + circle.PostBackValue = "Area2"; + circle.HotSpotMode = HotSpotMode.PostBack; + hotSpots.Add(circle); + + var cut = Render(@); + + // Click first area + var areas1 = cut.FindAll("area"); + areas1[0].Click(); + clickedValue.ShouldBe("Area1"); + + // Re-find areas after render and click second area + var areas2 = cut.FindAll("area"); + areas2[1].Click(); + clickedValue.ShouldBe("Area2"); + } + + [Fact] + public void ImageMap_PostBackMode_RendersHrefHash() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + rect.PostBackValue = "Area1"; + rect.HotSpotMode = HotSpotMode.PostBack; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("href").ShouldBe("#"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/Properties.razor b/src/BlazorWebFormsComponents.Test/ImageMap/Properties.razor new file mode 100644 index 00000000..be786e61 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/Properties.razor @@ -0,0 +1,111 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_WithAlternateText_RendersAltAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("alt").ShouldBe("Site Map"); + } + + [Fact] + public void ImageMap_WithToolTip_RendersTitleAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("title").ShouldBe("Click on the map"); + } + + [Fact] + public void ImageMap_WithDescriptionUrl_RendersLongDescAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("longdesc").ShouldBe("/descriptions/map.html"); + } + + [Fact] + public void ImageMap_GenerateEmptyAlternateText_RendersEmptyAlt() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("alt").ShouldBe(""); + } + + [Fact] + public void ImageMap_WithClientID_RendersIdAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("id").ShouldBe("myMap"); + } + + [Fact] + public void ImageMap_WithImageAlign_RendersAlignAttribute() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + var img = cut.Find("img"); + img.GetAttribute("align").ShouldBe("left"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/RectangleHotSpot.razor b/src/BlazorWebFormsComponents.Test/ImageMap/RectangleHotSpot.razor new file mode 100644 index 00000000..d70d569a --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/RectangleHotSpot.razor @@ -0,0 +1,66 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_RectangleHotSpot_RendersCorrectly() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 10; + rect.Top = 20; + rect.Right = 110; + rect.Bottom = 120; + rect.AlternateText = "Rectangle Area"; + rect.NavigateUrl = "/page1.html"; + hotSpots.Add(rect); + + var cut = Render(@); + + var area = cut.Find("area"); + area.GetAttribute("shape").ShouldBe("rect"); + area.GetAttribute("coords").ShouldBe("10,20,110,120"); + area.GetAttribute("alt").ShouldBe("Rectangle Area"); + area.GetAttribute("href").ShouldBe("/page1.html"); + } + + [Fact] + public void ImageMap_MultipleRectangleHotSpots_RendersAll() + { + var hotSpots = new List(); + + var rect1 = new BlazorWebFormsComponents.RectangleHotSpot(); + rect1.Left = 0; + rect1.Top = 0; + rect1.Right = 50; + rect1.Bottom = 50; + rect1.AlternateText = "Area 1"; + rect1.NavigateUrl = "/page1.html"; + hotSpots.Add(rect1); + + var rect2 = new BlazorWebFormsComponents.RectangleHotSpot(); + rect2.Left = 60; + rect2.Top = 60; + rect2.Right = 110; + rect2.Bottom = 110; + rect2.AlternateText = "Area 2"; + rect2.NavigateUrl = "/page2.html"; + hotSpots.Add(rect2); + + var rect3 = new BlazorWebFormsComponents.RectangleHotSpot(); + rect3.Left = 120; + rect3.Top = 120; + rect3.Right = 170; + rect3.Bottom = 170; + rect3.AlternateText = "Area 3"; + rect3.NavigateUrl = "/page3.html"; + hotSpots.Add(rect3); + + var cut = Render(@); + + var areas = cut.FindAll("area"); + areas.Count.ShouldBe(3); + areas[0].GetAttribute("coords").ShouldBe("0,0,50,50"); + areas[1].GetAttribute("coords").ShouldBe("60,60,110,110"); + areas[2].GetAttribute("coords").ShouldBe("120,120,170,170"); + } +} diff --git a/src/BlazorWebFormsComponents.Test/ImageMap/Visible.razor b/src/BlazorWebFormsComponents.Test/ImageMap/Visible.razor new file mode 100644 index 00000000..7c43f2c9 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ImageMap/Visible.razor @@ -0,0 +1,42 @@ +@using BlazorWebFormsComponents.Enums + +@code { + [Fact] + public void ImageMap_Visible_True_RendersImageAndMap() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + // Should render an img element + cut.Find("img").GetAttribute("src").ShouldBe("/images/map.jpg"); + + // Should render a map element + var map = cut.Find("map"); + map.ShouldNotBeNull(); + } + + [Fact] + public void ImageMap_Visible_False_RendersNothing() + { + var hotSpots = new List(); + var rect = new BlazorWebFormsComponents.RectangleHotSpot(); + rect.Left = 0; + rect.Top = 0; + rect.Right = 100; + rect.Bottom = 100; + rect.AlternateText = "Area 1"; + hotSpots.Add(rect); + + var cut = Render(@); + + cut.Markup.Trim().ShouldBeEmpty(); + } +} diff --git a/src/BlazorWebFormsComponents/ImageMap.razor b/src/BlazorWebFormsComponents/ImageMap.razor index 4cfd8bf8..d0430a02 100644 --- a/src/BlazorWebFormsComponents/ImageMap.razor +++ b/src/BlazorWebFormsComponents/ImageMap.razor @@ -1,16 +1,15 @@ +@using System.Text @inherits BaseWebFormsComponent @if (Visible) { kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + alt="@GetAltText()" + id="@GetId()" + title="@GetTitle()" + longdesc="@GetLongDesc()" + align="@GetAlign()" /> @foreach (var hotSpot in HotSpots) @@ -32,11 +31,9 @@ coords="@hotSpot.GetCoordinates()" href="@href" alt="@hotSpot.AlternateText" - @attributes="@(new Dictionary { - { "target", !string.IsNullOrEmpty(target) ? target : null }, - { "tabindex", hotSpot.TabIndex > 0 ? hotSpot.TabIndex.ToString() : null }, - { "accesskey", !string.IsNullOrEmpty(hotSpot.AccessKey) ? hotSpot.AccessKey : null } - }.Where(kvp => kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + target="@GetTargetOrNull(target)" + tabindex="@GetTabIndexOrNull(hotSpot.TabIndex)" + accesskey="@GetAccessKeyOrNull(hotSpot.AccessKey)" /> } else if (effectiveMode == Enums.HotSpotMode.PostBack) { @@ -46,15 +43,55 @@ alt="@hotSpot.AlternateText" @onclick="@(() => HandleHotSpotClick(hotSpot))" @onclick:preventDefault="true" - @attributes="@(new Dictionary { - { "tabindex", hotSpot.TabIndex > 0 ? hotSpot.TabIndex.ToString() : null }, - { "accesskey", !string.IsNullOrEmpty(hotSpot.AccessKey) ? hotSpot.AccessKey : null } - }.Where(kvp => kvp.Value != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))" /> + tabindex="@GetTabIndexOrNull(hotSpot.TabIndex)" + accesskey="@GetAccessKeyOrNull(hotSpot.AccessKey)" /> } } } @code { + private string GetAltText() + { + if (!string.IsNullOrEmpty(AlternateText)) + return AlternateText; + if (GenerateEmptyAlternateText) + return ""; + return null; + } + private string GetId() + { + return !string.IsNullOrEmpty(ClientID) ? ClientID : null; + } + + private string GetTitle() + { + return !string.IsNullOrEmpty(ToolTip) ? ToolTip : null; + } + + private string GetLongDesc() + { + return !string.IsNullOrEmpty(DescriptionUrl) ? DescriptionUrl : null; + } + + private string GetAlign() + { + return ImageAlign != Enums.ImageAlign.NotSet ? ImageAlign.ToString().ToLower() : null; + } + + private string GetTargetOrNull(string target) + { + return !string.IsNullOrEmpty(target) ? target : null; + } + + private string GetTabIndexOrNull(short tabIndex) + { + return tabIndex > 0 ? tabIndex.ToString() : null; + } + + private string GetAccessKeyOrNull(string accessKey) + { + return !string.IsNullOrEmpty(accessKey) ? accessKey : null; + } } From 447e25db36b97fcc96e1e916987976688c17c225 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:47:37 +0000 Subject: [PATCH 4/6] Add ImageMap documentation and update project files Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --- README.md | 2 +- docs/EditorControls/ImageMap.md | 330 ++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + status.md | 14 +- 4 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 docs/EditorControls/ImageMap.md diff --git a/README.md b/README.md index e9f41144..f923154b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ There are a significant number of controls in ASP.NET Web Forms, and we will foc - [HiddenField](docs/EditorControls/HiddenField.md) - [Image](docs/EditorControls/Image.md) - [ImageButton](docs/EditorControls/ImageButton.md) - - ImageMap + - [ImageMap](docs/EditorControls/ImageMap.md) - [Label](docs/EditorControls/Label.md) - [LinkButton](docs/EditorControls/LinkButton.md) - [ListBox](docs/EditorControls/ListBox.md) diff --git a/docs/EditorControls/ImageMap.md b/docs/EditorControls/ImageMap.md new file mode 100644 index 00000000..21920589 --- /dev/null +++ b/docs/EditorControls/ImageMap.md @@ -0,0 +1,330 @@ +# ImageMap + +The **ImageMap** component enables you to create interactive images with clickable regions (hot spots) that can navigate to URLs or trigger server-side events. This is useful for creating image-based navigation, interactive diagrams, or clickable floor plans. + +Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.imagemap?view=netframework-4.8 + +## Features Supported in Blazor + +- **HotSpot Types** - Supports three types of clickable regions: + - `RectangleHotSpot` - Rectangular regions defined by left, top, right, bottom coordinates + - `CircleHotSpot` - Circular regions defined by center point (X, Y) and radius + - `PolygonHotSpot` - Irregular regions defined by a series of coordinate pairs +- **HotSpot Modes** - Three interaction behaviors: + - `Navigate` - Redirects to a URL when clicked + - `PostBack` - Triggers a server-side Click event with PostBackValue + - `Inactive` - No action (displays as non-clickable region) +- **ImageUrl** - Path to the image to display +- **AlternateText** - Alt text for accessibility +- **ToolTip** - Hover tooltip text +- **DescriptionUrl** - URL to detailed image description for accessibility +- **GenerateEmptyAlternateText** - Creates empty alt attribute for decorative images +- **ImageAlign** - Alignment relative to other page elements +- **Target** - Default target window for navigation (_blank, _self, etc.) +- **OnClick** - Event raised when a PostBack mode HotSpot is clicked +- **Visible** - Controls component visibility + +### Blazor Notes + +- Each HotSpot can override the ImageMap's default HotSpotMode +- HotSpots can have individual Target values that override the ImageMap's default +- The component renders standard HTML `` and `` elements with `` tags +- PostBack mode uses Blazor's event handling rather than actual postbacks +- Navigation mode generates standard hyperlinks that work without JavaScript + +## Web Forms Features NOT Supported + +- **AccessKey** - Not implemented; use standard HTML attribute if needed +- **TabIndex** - Not implemented on ImageMap itself; HotSpots support TabIndex +- **Style Properties** - BackColor, ForeColor, BorderColor, CssClass, Height, Width not yet implemented +- **EnableTheming/SkinID** - ASP.NET theming not available in Blazor +- **ViewState** - Not needed; Blazor manages state differently +- **Lifecycle Events** - OnDataBinding, OnInit, etc. not supported; use Blazor lifecycle methods + +## Web Forms Declarative Syntax + +```aspx + + + + + + + + +``` + +## Blazor Syntax + +```razor + + + +@code { + private List hotSpotList = new() + { + new RectangleHotSpot + { + Left = 10, Top = 10, Right = 110, Bottom = 60, + AlternateText = "Home", + NavigateUrl = "/", + HotSpotMode = HotSpotMode.Navigate + }, + new CircleHotSpot + { + X = 200, Y = 35, Radius = 25, + AlternateText = "Products", + PostBackValue = "Products", + HotSpotMode = HotSpotMode.PostBack + }, + new PolygonHotSpot + { + Coordinates = "300,10,350,10,325,50", + AlternateText = "Contact", + NavigateUrl = "/contact" + } + }; + + private void HandleMapClick(ImageMapEventArgs e) + { + // Handle postback - e.PostBackValue contains the clicked HotSpot's value + var clickedArea = e.PostBackValue; + } +} +``` + +## Usage Notes + +### HotSpot Mode Precedence + +When a HotSpot doesn't specify its own `HotSpotMode`, it inherits from the ImageMap's default mode. This allows you to set a common behavior for all regions while overriding specific ones: + +```razor + + +@code { + private List regions = new() + { + // This uses Navigate mode (inherited from ImageMap) + new RectangleHotSpot { ..., NavigateUrl = "/page1" }, + + // This overrides to use PostBack mode + new CircleHotSpot + { + ..., + HotSpotMode = HotSpotMode.PostBack, + PostBackValue = "SpecialAction" + } + }; +} +``` + +### Coordinate Systems + +- **RectangleHotSpot**: Coordinates are in pixels from top-left corner of image + - Left/Top = top-left corner, Right/Bottom = bottom-right corner +- **CircleHotSpot**: X,Y is center point, Radius is distance from center +- **PolygonHotSpot**: Comma-separated pairs of X,Y coordinates defining vertices + +### Accessibility Considerations + +Always provide meaningful `AlternateText` for each HotSpot to ensure screen readers can describe the clickable regions: + +```csharp +new RectangleHotSpot +{ + Left = 0, Top = 0, Right = 100, Bottom = 50, + AlternateText = "Navigate to Products page", // Descriptive text + NavigateUrl = "/products" +} +``` + +## Examples + +### Basic Navigation Map + +```razor +@page "/image-map-demo" + +

Site Navigation

+ + + +@code { + private List navigationRegions = new() + { + new RectangleHotSpot + { + Left = 20, Top = 50, Right = 180, Bottom = 120, + AlternateText = "Home Page", + NavigateUrl = "/" + }, + new RectangleHotSpot + { + Left = 200, Top = 50, Right = 360, Bottom = 120, + AlternateText = "Products", + NavigateUrl = "/products" + }, + new RectangleHotSpot + { + Left = 380, Top = 50, Right = 540, Bottom = 120, + AlternateText = "Contact Us", + NavigateUrl = "/contact" + } + }; +} +``` + +### Interactive Diagram with PostBack + +```razor +@page "/floor-plan" + +

Office Floor Plan

+

Click on a room to see details

+ + + +@if (!string.IsNullOrEmpty(selectedRoom)) +{ +
+

@selectedRoom

+

@roomDetails

+
+} + +@code { + private string selectedRoom = ""; + private string roomDetails = ""; + + private List roomRegions = new() + { + new PolygonHotSpot + { + Coordinates = "50,50,150,50,150,150,50,150", + AlternateText = "Conference Room A", + PostBackValue = "ConfA" + }, + new CircleHotSpot + { + X = 300, Y = 100, Radius = 40, + AlternateText = "Break Room", + PostBackValue = "Break" + } + }; + + private void ShowRoomDetails(ImageMapEventArgs e) + { + selectedRoom = e.PostBackValue switch + { + "ConfA" => "Conference Room A", + "Break" => "Break Room", + _ => "Unknown Room" + }; + + roomDetails = e.PostBackValue switch + { + "ConfA" => "Capacity: 12 people. Equipped with projector and whiteboard.", + "Break" => "Kitchen facilities, coffee maker, and seating for 8.", + _ => "" + }; + } +} +``` + +### Mixed Mode Map + +```razor + + +@code { + private List mixedRegions = new() + { + // External link - opens in new window + new RectangleHotSpot + { + Left = 10, Top = 10, Right = 100, Bottom = 60, + AlternateText = "Documentation", + NavigateUrl = "https://docs.example.com", + Target = "_blank", + HotSpotMode = HotSpotMode.Navigate + }, + + // Server action - triggers event + new CircleHotSpot + { + X = 150, Y = 35, Radius = 25, + AlternateText = "Download", + PostBackValue = "StartDownload", + HotSpotMode = HotSpotMode.PostBack + }, + + // Inactive region - informational only + new PolygonHotSpot + { + Coordinates = "200,10,250,10,225,50", + AlternateText = "Coming Soon", + HotSpotMode = HotSpotMode.Inactive + } + }; + + private void HandleAction(ImageMapEventArgs e) + { + if (e.PostBackValue == "StartDownload") + { + // Initiate download logic + } + } +} +``` + +## Migration Tips + +1. **Convert Declarative HotSpots to Collection**: In Web Forms, HotSpots are declared as child elements. In Blazor, create a List in your @code block. + +2. **Event Handler Signature**: Web Forms uses `ImageMapEventHandler` with sender and `ImageMapEventArgs`. Blazor simplifies this - you only need the `ImageMapEventArgs` parameter. + +3. **Coordinate Validation**: Consider validating HotSpot coordinates are within image bounds to prevent rendering issues. + +4. **Dynamic HotSpots**: In Blazor, you can easily add/remove HotSpots dynamically by modifying the List and calling `StateHasChanged()`. + +## See Also + +- [Image](Image.md) - Display static images +- [ImageButton](ImageButton.md) - Clickable image that acts as a button +- [HyperLink](HyperLink.md) - Text or image hyperlinks diff --git a/mkdocs.yml b/mkdocs.yml index fb0ac6c9..0c72ca45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - HiddenField: EditorControls/HiddenField.md - Image: EditorControls/Image.md - ImageButton: EditorControls/ImageButton.md + - ImageMap: EditorControls/ImageMap.md - Label: EditorControls/Label.md - LinkButton: EditorControls/LinkButton.md - ListBox: EditorControls/ListBox.md diff --git a/status.md b/status.md index 208cd7dc..b5d55381 100644 --- a/status.md +++ b/status.md @@ -2,18 +2,18 @@ | Category | Completed | In Progress | Not Started | Total | |----------|-----------|-------------|-------------|-------| -| Editor Controls | 17 | 0 | 10 | 27 | +| Editor Controls | 18 | 0 | 9 | 27 | | Data Controls | 7 | 0 | 2 | 9 | | Validation Controls | 7 | 0 | 0 | 7 | | Navigation Controls | 3 | 0 | 0 | 3 | | Login Controls | 4 | 0 | 3 | 7 | -| **TOTAL** | **38** | **0** | **15** | **53** | +| **TOTAL** | **39** | **0** | **14** | **53** | --- ## Detailed Component Breakdown -### 🟡 Editor Controls (15/27 - 56% Complete) +### 🟡 Editor Controls (18/27 - 67% Complete) | Component | Status | Notes | |-----------|--------|-------| @@ -34,7 +34,7 @@ | Calendar | 🔴 Not Started | Complex date picker | | CheckBoxList | ✅ Complete | Documented, tested (26 tests) | | FileUpload | 🔴 Not Started | Consider Blazor InputFile | -| ImageMap | 🔴 Not Started | Clickable image regions | +| ImageMap | ✅ Complete | Documented, tested (23 tests) | | ListBox | ✅ Complete | Documented, tested, supports single/multi-select | | Localize | 🔴 Not Started | Localization control | | MultiView | 🔴 Not Started | Tab container | @@ -143,12 +143,12 @@ #### Lower Priority / Consider Deferring | Component | Complexity | Notes | |-----------|------------|-------| -| **BulletedList** | Low | Simple HTML list | +| ~~**BulletedList**~~ | ~~Low~~ | ~~Simple HTML list~~ | ✅ Complete | | **Calendar** | High | Complex date picker | | **FileUpload** | Medium | Blazor has InputFile | -| **ImageMap** | Medium | Clickable regions | +| ~~**ImageMap**~~ | ~~Medium~~ | ~~Clickable regions~~ | ✅ Complete | | **MultiView/View** | Medium | Tab-like container | -| **Table** | Low | HTML table wrapper | +| ~~**Table**~~ | ~~Low~~ | ~~HTML table wrapper~~ | ✅ Complete | | **Localize** | Low | Localization | | **Xml** | Medium | XML transform | | **Substitution** | N/A | Cache-related, may not apply | From de0647ceeb55cb7a1b1142496b82b287e8a8a783 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:50:36 +0000 Subject: [PATCH 5/6] Fix code review issues: use unique counter for map IDs, return Task from event handler, fix mode inheritance logic Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --- src/BlazorWebFormsComponents/Enums/HotSpotMode.cs | 5 +++++ src/BlazorWebFormsComponents/HotSpot.cs | 2 +- src/BlazorWebFormsComponents/ImageMap.razor | 2 +- src/BlazorWebFormsComponents/ImageMap.razor.cs | 13 ++++++++----- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs b/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs index c1b13c98..18570dbd 100644 --- a/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs +++ b/src/BlazorWebFormsComponents/Enums/HotSpotMode.cs @@ -5,6 +5,11 @@ namespace BlazorWebFormsComponents.Enums /// public enum HotSpotMode { + /// + /// The HotSpot mode is not set and inherits from the parent ImageMap control. + /// + NotSet, + /// /// The HotSpot does not have any behavior. /// diff --git a/src/BlazorWebFormsComponents/HotSpot.cs b/src/BlazorWebFormsComponents/HotSpot.cs index e9fe5dcb..8ba745b1 100644 --- a/src/BlazorWebFormsComponents/HotSpot.cs +++ b/src/BlazorWebFormsComponents/HotSpot.cs @@ -15,7 +15,7 @@ public abstract class HotSpot /// /// Gets or sets the behavior of a HotSpot object in an ImageMap control when the HotSpot is clicked. /// - public HotSpotMode HotSpotMode { get; set; } = HotSpotMode.Navigate; + public HotSpotMode HotSpotMode { get; set; } = HotSpotMode.NotSet; /// /// Gets or sets the URL to navigate to when a HotSpot object is clicked. diff --git a/src/BlazorWebFormsComponents/ImageMap.razor b/src/BlazorWebFormsComponents/ImageMap.razor index d0430a02..4839322e 100644 --- a/src/BlazorWebFormsComponents/ImageMap.razor +++ b/src/BlazorWebFormsComponents/ImageMap.razor @@ -14,7 +14,7 @@ @foreach (var hotSpot in HotSpots) { - var effectiveMode = hotSpot.HotSpotMode != Enums.HotSpotMode.Navigate ? hotSpot.HotSpotMode : HotSpotMode; + var effectiveMode = hotSpot.HotSpotMode != Enums.HotSpotMode.NotSet ? hotSpot.HotSpotMode : HotSpotMode; var href = effectiveMode == Enums.HotSpotMode.Navigate ? hotSpot.NavigateUrl : "#"; var target = !string.IsNullOrEmpty(hotSpot.Target) ? hotSpot.Target : Target; diff --git a/src/BlazorWebFormsComponents/ImageMap.razor.cs b/src/BlazorWebFormsComponents/ImageMap.razor.cs index 558dcef4..f162e7a0 100644 --- a/src/BlazorWebFormsComponents/ImageMap.razor.cs +++ b/src/BlazorWebFormsComponents/ImageMap.razor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using BlazorWebFormsComponents.Enums; using BlazorWebFormsComponents.Interfaces; using Microsoft.AspNetCore.Components; @@ -70,23 +71,25 @@ public partial class ImageMap : BaseWebFormsComponent, IImageComponent [Parameter] public bool GenerateEmptyAlternateText { get; set; } + private static int _mapIdCounter = 0; private string _mapId = string.Empty; protected override void OnInitialized() { base.OnInitialized(); - // Generate a unique map ID - _mapId = $"ImageMap_{GetHashCode()}"; + // Generate a unique map ID using a counter + _mapId = $"ImageMap_{System.Threading.Interlocked.Increment(ref _mapIdCounter)}"; } /// /// Handles the click event for a hot spot. /// /// The hot spot that was clicked - protected async void HandleHotSpotClick(HotSpot hotSpot) + protected async Task HandleHotSpotClick(HotSpot hotSpot) { - if (hotSpot.HotSpotMode == HotSpotMode.PostBack || - (hotSpot.HotSpotMode == HotSpotMode.Navigate && HotSpotMode == HotSpotMode.PostBack)) + var effectiveMode = hotSpot.HotSpotMode != HotSpotMode.NotSet ? hotSpot.HotSpotMode : HotSpotMode; + + if (effectiveMode == HotSpotMode.PostBack) { var eventArgs = new ImageMapEventArgs(hotSpot.PostBackValue); await OnClick.InvokeAsync(eventArgs); From 6d0ff9ac0be42d5a7a2ab2e9841d1ab8f39c2318 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 10 Feb 2026 11:27:04 -0500 Subject: [PATCH 6/6] fix: ImageMap use BaseStyledComponent and instance-based IDs (#337) - Change base class from BaseWebFormsComponent to BaseStyledComponent for CssClass/Style support - Replace static _mapIdCounter with Guid-based instance ID to avoid cross-test leaks - Add Enabled property propagation to area elements (disabled renders nohref) - Add class and style attributes to img element - ImageAlign rendering via .ToString().ToLower() already matches Web Forms output --- src/BlazorWebFormsComponents/ImageMap.razor | 12 ++++++++++-- src/BlazorWebFormsComponents/ImageMap.razor.cs | 8 +++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/BlazorWebFormsComponents/ImageMap.razor b/src/BlazorWebFormsComponents/ImageMap.razor index 4839322e..ebce4662 100644 --- a/src/BlazorWebFormsComponents/ImageMap.razor +++ b/src/BlazorWebFormsComponents/ImageMap.razor @@ -1,5 +1,5 @@ @using System.Text -@inherits BaseWebFormsComponent +@inherits BaseStyledComponent @if (Visible) { @@ -7,6 +7,8 @@ usemap="#@_mapId" alt="@GetAltText()" id="@GetId()" + class="@GetCssClassOrNull()" + style="@Style" title="@GetTitle()" longdesc="@GetLongDesc()" align="@GetAlign()" /> @@ -17,8 +19,9 @@ var effectiveMode = hotSpot.HotSpotMode != Enums.HotSpotMode.NotSet ? hotSpot.HotSpotMode : HotSpotMode; var href = effectiveMode == Enums.HotSpotMode.Navigate ? hotSpot.NavigateUrl : "#"; var target = !string.IsNullOrEmpty(hotSpot.Target) ? hotSpot.Target : Target; + var isDisabled = !Enabled; - @if (effectiveMode == Enums.HotSpotMode.Inactive) + @if (isDisabled || effectiveMode == Enums.HotSpotMode.Inactive) { /// Creates an image map control that displays an image with defined clickable hot spot regions. /// - public partial class ImageMap : BaseWebFormsComponent, IImageComponent + public partial class ImageMap : BaseStyledComponent, IImageComponent { /// /// Gets or sets the alternate text to display in the ImageMap control when the image is unavailable. @@ -71,14 +72,11 @@ public partial class ImageMap : BaseWebFormsComponent, IImageComponent [Parameter] public bool GenerateEmptyAlternateText { get; set; } - private static int _mapIdCounter = 0; - private string _mapId = string.Empty; + private readonly string _mapId = $"ImageMap_{Guid.NewGuid():N}"; protected override void OnInitialized() { base.OnInitialized(); - // Generate a unique map ID using a counter - _mapId = $"ImageMap_{System.Threading.Interlocked.Increment(ref _mapIdCounter)}"; } ///