From 5248314f575b7508d603313fd50a1adb0bca90b3 Mon Sep 17 00:00:00 2001 From: albx Date: Mon, 28 Sep 2026 08:11:56 +0200 Subject: [PATCH 1/7] add dropdown item to toolbar (#99) --- .../Toolbar/BitToolbarDropdownItem.razor | 39 +++++ .../Toolbar/BitToolbarDropdownItem.razor.cs | 25 ++++ .../Components/Toolbar/BitToolbarItemBase.cs | 135 ++++++++++++++++++ .../Toolbar/BitToolbarTest.Rendering.razor | 110 ++++++++++++++ 4 files changed, 309 insertions(+) create mode 100644 src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor create mode 100644 src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor.cs create mode 100644 src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor new file mode 100644 index 0000000..748ace2 --- /dev/null +++ b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor @@ -0,0 +1,39 @@ +@namespace BitBlazor.Components + +@inherits BitToolbarItemBase + +
  • + + + + + + @ChildContent + + +
  • diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor.cs b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor.cs new file mode 100644 index 0000000..f75819c --- /dev/null +++ b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Components; + +/// +/// Represents a dropdown item that can be used within a component. +/// It provides a way to include dropdown functionality in a toolbar, allowing users to select from a list of options or actions. +/// +public partial class BitToolbarDropdownItem : BitToolbarItemBase +{ + /// + /// Gets or sets the content to be rendered inside the dropdown item. + /// + [Parameter] + [EditorRequired] + public RenderFragment ChildContent { get; set; } + + /// + /// Gets or sets the unique identifier for the dropdown item. + /// This ID is used to associate the item with its corresponding dropdown menu for accessibility purposes. + /// + [Parameter] + [EditorRequired] + public string Id { get; set; } = string.Empty; +} diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs new file mode 100644 index 0000000..9fde09b --- /dev/null +++ b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs @@ -0,0 +1,135 @@ +using BitBlazor.Core; +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Components; + +/// +/// Represents the base class for toolbar items that can be used within a component. +/// +public abstract class BitToolbarItemBase : ComponentBase +{ + [CascadingParameter] + protected BitToolbar Parent { get; set; } = default!; + + /// + /// Gets or sets the label for the toolbar item. + /// + [Parameter] + [EditorRequired] + public string Label { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the icon to be displayed for the toolbar item. + /// + [Parameter] + [EditorRequired] + public string IconName { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the toolbar item is active. When set to true, the item will be styled as active. + /// + [Parameter] + public bool Active { get; set; } + + /// + /// Gets or sets whether the toolbar item is disabled. When set to true, the item will be styled as disabled and will not respond to user interactions. + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Gets or sets the count to be displayed as a badge on the toolbar item. If set, a badge will be shown with the specified count. + /// + [Parameter] + public int? BadgeCount { get; set; } + + /// + /// Gets or sets the label for the badge on the toolbar item. This label can provide additional context for the badge count. + /// + [Parameter] + public string? BadgeLabel { get; set; } + + /// + /// Gets or sets additional attributes that do not match any of the explicitly defined parameters. + /// + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); + + /// + /// Gets a value indicating whether the toolbar item has a badge count greater than zero. This is used to determine if a badge should be displayed. + /// + protected bool HasBadgeNumber => BadgeCount.HasValue && BadgeCount.Value > 0; + + /// + /// Gets a value indicating whether the toolbar item has a non-empty badge label. This is used to determine if a badge label should be displayed. + /// + protected bool HasBadgeLabel => !string.IsNullOrWhiteSpace(BadgeLabel); + + /// + /// Gets a value indicating whether the toolbar item has either a badge count or a badge label. This is used to determine if any badge should be displayed. + /// + protected bool HasBadge => HasBadgeNumber || HasBadgeLabel; + + /// + /// Gets a value indicating whether the parent toolbar's size is set to the default size. This is used to apply specific styling based on the toolbar's size. + /// + protected bool IsToolbarSizeDefault => Parent.Size is ToolbarSize.Default; + + /// + protected override void OnInitialized() + { + if (Parent is null) + { + throw new InvalidOperationException($"This component must be used inside a BitToolbar component"); + } + } + + /// + protected override void OnParametersSet() + { + SetDisabled(); + } + + /// + /// Sets the "aria-disabled" attribute based on the Disabled parameter. + /// + protected virtual void SetDisabled() + { + if (Disabled) + { + AdditionalAttributes["aria-disabled"] = "true"; + } + else + { + AdditionalAttributes.Remove("aria-disabled"); + } + } + + /// + /// Computes the CSS classes for the label based on the parent toolbar's size. + /// + /// The computed CSS classes + protected virtual string ComputeLabelCssClass() + { + var builder = new CssClassBuilder(); + + var labelClass = Parent.Size switch + { + ToolbarSize.Medium or ToolbarSize.Small => "visually-hidden", + _ => "toolbar-label" + }; + + builder.Add(labelClass); + + return builder.Build(); + } + + protected RenderFragment RenderBadgeLabel() => + builder => + { + builder.OpenElement(0, "span"); + builder.AddAttribute(1, "class", "visually-hidden"); + builder.AddContent(2, BadgeLabel); + builder.CloseElement(); + }; +} diff --git a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor index b024f17..79a5c5b 100644 --- a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor +++ b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor @@ -558,4 +558,114 @@ Assert.True(component.Find("nav").ClassList.Contains("my-custom-toolbar")); } + + [Fact] + public void BitToolbar_Should_Render_Dropdown_Item_Correctly() + { + var component = Render( + @ + + Label 1 + Label 2 + + + Label 1 + Label 2 + + ); + + component.MarkupMatches( + @); + } + + [Fact] + public void BitToolbar_Should_Render_Dropdown_Item_With_Badge_Correctly() + { + var component = Render( + @ + + Label 1 + Label 2 + + + Label 1 + Label 2 + + ); + + component.MarkupMatches( + @); + } } From f4b07bf12c9004aae8b5293b4c4bd0baf472356b Mon Sep 17 00:00:00 2001 From: albx Date: Tue, 29 Sep 2026 19:25:18 +0200 Subject: [PATCH 2/7] #99 - add tests for toolbar dropdown --- .../Toolbar/BitToolbarDropdownItem.razor | 2 +- .../Components/Toolbar/BitToolbarItemBase.cs | 4 + .../Toolbar/BitToolbarTest.Behaviors.cs | 141 ++++++++++++++++++ .../Toolbar/BitToolbarTest.Rendering.razor | 73 +++++++++ 4 files changed, 219 insertions(+), 1 deletion(-) diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor index 748ace2..d4de6d7 100644 --- a/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor +++ b/src/BitBlazor/Components/Toolbar/BitToolbarDropdownItem.razor @@ -23,7 +23,7 @@ } - + @context.ActivatorLabel @if (Parent.Size is ToolbarSize.Default && HasBadgeLabel) { diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs index 9fde09b..f9f6930 100644 --- a/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs +++ b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs @@ -124,6 +124,10 @@ protected virtual string ComputeLabelCssClass() return builder.Build(); } + /// + /// Renders the badge label as a visually hidden span element for accessibility purposes. + /// + /// protected RenderFragment RenderBadgeLabel() => builder => { diff --git a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs index 943c7f2..771c656 100644 --- a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs +++ b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs @@ -224,4 +224,145 @@ public void BitToolbarDivider_Should_Throw_InvalidOperationException_When_Used_O Assert.Equal("BitToolbarDivider component must be used inside a BitToolbar component", exception.Message); } + + [Fact] + public void BitToolbarDropdownItem_Should_Open_On_Activator_Click() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.Click(); + + var menu = cut.Find("div.dropdown-menu"); + Assert.Contains("show", menu.ClassList); + Assert.Equal("true", button.GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Close_On_Activator_Click_Again() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.Click(); + button.Click(); + + var menu = cut.Find("div.dropdown-menu"); + Assert.False(menu.ClassList.Contains("show")); + Assert.Equal("false", button.GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Open_And_Focus_First_Item_On_ArrowDown_Key() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.KeyDown(new KeyboardEventArgs { Key = "ArrowDown" }); + + var menu = cut.Find("div.dropdown-menu"); + Assert.Contains("show", menu.ClassList); + Assert.Equal("true", button.GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Open_On_ArrowUp_Key() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.KeyDown(new KeyboardEventArgs { Key = "ArrowUp" }); + + var menu = cut.Find("div.dropdown-menu"); + Assert.Contains("show", menu.ClassList); + Assert.Equal("true", button.GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Toggle_Independently_When_Multiple_Present() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Images") + .Add(p => p.IconName, Icons.ItCamera) + .Add(p => p.Id, "dropdown2") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 2"))) + ); + + cut.FindAll("button")[0].Click(); + + var buttons = cut.FindAll("button"); + var menus = cut.FindAll("div.dropdown-menu"); + Assert.Contains("show", menus[0].ClassList); + Assert.False(menus[1].ClassList.Contains("show")); + Assert.Equal("true", buttons[0].GetAttribute("aria-expanded")); + Assert.Equal("false", buttons[1].GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Throw_InvalidOperationException_When_Used_Outside_BitToolbar() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var exception = Assert.Throws(() => + ctx.Render(parameters => parameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1")))); + + Assert.Equal("This component must be used inside a BitToolbar component", exception.Message); + } } diff --git a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor index 79a5c5b..e938f80 100644 --- a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor +++ b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Rendering.razor @@ -668,4 +668,77 @@ ); } + + [Theory] + [InlineData(ToolbarSize.Medium, "toolbar-medium")] + [InlineData(ToolbarSize.Small, "toolbar-small")] + public void BitToolbar_Should_Render_Dropdown_Item_In_Medium_And_Small_Toolbar_Correctly(ToolbarSize size, string sizeClass) + { + var component = Render( + @ + + Label 1 + Label 2 + + ); + + component.MarkupMatches( + @); + } + + [Theory] + [InlineData(ToolbarSize.Medium, "toolbar-medium")] + [InlineData(ToolbarSize.Small, "toolbar-small")] + public void BitToolbar_Should_Render_Dropdown_Item_With_Badge_In_Medium_And_Small_Toolbar_Correctly(ToolbarSize size, string sizeClass) + { + var component = Render( + @ + + Label 1 + Label 2 + + ); + + component.MarkupMatches( + @); + } } From 45aa11b779a1a0c9d28c44bf778e8a00c89823e2 Mon Sep 17 00:00:00 2001 From: albx Date: Tue, 29 Sep 2026 19:34:05 +0200 Subject: [PATCH 3/7] #99 - add stories and sample for toolbard dropdown support --- .../Components/Pages/Pratiche.razor | 14 ++++ .../Components/BitToolbar.stories.razor | 79 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Pages/Pratiche.razor b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Pages/Pratiche.razor index 14c9358..d619bcd 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Pages/Pratiche.razor +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Pages/Pratiche.razor @@ -31,6 +31,13 @@ Label="Stampa" Active="@(_activeToolbarItem == "stampa")" OnClick="StampaAsync" /> + + Esporta in PDF + Esporta in Excel + Esporta in CSV + + + + + + + + + + + + + + + + + + + + + From 37047e6b4e3241b1a917969a28baf5cfcb523a87 Mon Sep 17 00:00:00 2001 From: albx Date: Tue, 29 Sep 2026 19:45:10 +0200 Subject: [PATCH 4/7] #99 - reafactor BitToolbarItem to inherit from a base class --- .../Components/Toolbar/BitToolbarItem.razor | 6 +- .../Toolbar/BitToolbarItem.razor.cs | 101 +----------------- .../Components/Toolbar/BitToolbarItemBase.cs | 6 +- .../Toolbar/BitToolbarTest.Behaviors.cs | 4 +- 4 files changed, 11 insertions(+), 106 deletions(-) diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor b/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor index 6fbd32a..8c204a9 100644 --- a/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor +++ b/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor @@ -1,5 +1,7 @@ @namespace BitBlazor.Components +@inherits BitToolbarItemBase +
  • @if (HasBadge) @@ -27,7 +29,3 @@
  • - -@code { - private RenderFragment RenderBadgeLabel() => @@BadgeLabel; -} diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor.cs b/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor.cs index a288ef3..bc85934 100644 --- a/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor.cs +++ b/src/BitBlazor/Components/Toolbar/BitToolbarItem.razor.cs @@ -7,28 +7,11 @@ namespace BitBlazor.Components; /// /// Represents a toolbar item component that can be used within a component. /// -public partial class BitToolbarItem +public partial class BitToolbarItem : BitToolbarItemBase { - [CascadingParameter] - BitToolbar Parent { get; set; } = default!; - [Inject] private NavigationManager NavigationManager { get; set; } = default!; - /// - /// Gets or sets the label for the toolbar item. - /// - [Parameter] - [EditorRequired] - public string Label { get; set; } = string.Empty; - - /// - /// Gets or sets the name of the icon to be displayed for the toolbar item. - /// - [Parameter] - [EditorRequired] - public string IconName { get; set; } = string.Empty; - /// /// Gets or sets the URL that the toolbar item should link to. /// In SSR rendering, the browser follows this URL directly on click. @@ -39,30 +22,6 @@ public partial class BitToolbarItem [Parameter] public string? Href { get; set; } - /// - /// Gets or sets a value indicating whether the toolbar item is active. When set to true, the item will be styled as active. - /// - [Parameter] - public bool Active { get; set; } - - /// - /// Gets or sets whether the toolbar item is disabled. When set to true, the item will be styled as disabled and will not respond to user interactions. - /// - [Parameter] - public bool Disabled { get; set; } - - /// - /// Gets or sets the count to be displayed as a badge on the toolbar item. If set, a badge will be shown with the specified count. - /// - [Parameter] - public int? BadgeCount { get; set; } - - /// - /// Gets or sets the label for the badge on the toolbar item. This label can provide additional context for the badge count. - /// - [Parameter] - public string? BadgeLabel { get; set; } - /// /// Gets or sets the primary interactive callback, invoked when the toolbar item is clicked. /// When set, it takes precedence over navigation in interactive rendering. @@ -71,51 +30,10 @@ public partial class BitToolbarItem [Parameter] public EventCallback OnClick { get; set; } - /// - /// Gets or sets additional attributes that do not match any of the explicitly defined parameters. - /// - [Parameter(CaptureUnmatchedValues = true)] - public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); - - private bool HasBadgeNumber => BadgeCount.HasValue && BadgeCount.Value > 0; - - private bool HasBadgeLabel => !string.IsNullOrWhiteSpace(BadgeLabel); - - private bool HasBadge => HasBadgeNumber || HasBadgeLabel; - - private bool IsToolbarSizeDefault => Parent.Size is ToolbarSize.Default; - - /// - protected override void OnInitialized() - { - if (Parent is null) - { - throw new InvalidOperationException("BitToolbarItem component must be used inside a BitToolbar component"); - } - } - - /// - protected override void OnParametersSet() - { - SetDisabled(); - } - - private void SetDisabled() - { - if (Disabled) - { - AdditionalAttributes["aria-disabled"] = "true"; - } - else - { - AdditionalAttributes.Remove("aria-disabled"); - } - } - private string ComputeLinkCssClass() { var builder = new CssClassBuilder(); - + if (Active) { builder.Add("active"); @@ -129,21 +47,6 @@ private string ComputeLinkCssClass() return builder.Build(); } - private string ComputeLabelCssClass() - { - var builder = new CssClassBuilder(); - - var labelClass = Parent.Size switch - { - ToolbarSize.Medium or ToolbarSize.Small => "visually-hidden", - _ => "toolbar-label" - }; - builder.Add(labelClass); - - - return builder.Build(); - } - private async Task ClickAsync() { if (Disabled) diff --git a/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs index f9f6930..5f99422 100644 --- a/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs +++ b/src/BitBlazor/Components/Toolbar/BitToolbarItemBase.cs @@ -8,6 +8,10 @@ namespace BitBlazor.Components; /// public abstract class BitToolbarItemBase : ComponentBase { + /// + /// Gets or sets the parent component. + /// This property is set via cascading parameters and is used to access properties of the parent toolbar. + /// [CascadingParameter] protected BitToolbar Parent { get; set; } = default!; @@ -80,7 +84,7 @@ protected override void OnInitialized() { if (Parent is null) { - throw new InvalidOperationException($"This component must be used inside a BitToolbar component"); + throw new InvalidOperationException("A toolbar item must be only used inside a BitToolbar component"); } } diff --git a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs index 771c656..93f8171 100644 --- a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs +++ b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs @@ -210,7 +210,7 @@ public void BitToolbarItem_Should_Throw_InvalidOperationException_When_Used_Outs .Add(p => p.Label, "Item 1") .Add(p => p.IconName, Icons.ItComment))); - Assert.Equal("BitToolbarItem component must be used inside a BitToolbar component", exception.Message); + Assert.Equal("A toolbar item must be only used inside a BitToolbar component", exception.Message); } [Fact] @@ -363,6 +363,6 @@ public void BitToolbarDropdownItem_Should_Throw_InvalidOperationException_When_U .AddChildContent(dropdownItemParameters => dropdownItemParameters.AddChildContent("Label 1")))); - Assert.Equal("This component must be used inside a BitToolbar component", exception.Message); + Assert.Equal("A toolbar item must be only used inside a BitToolbar component", exception.Message); } } From 9890c1c0e2cd9135b2a681117381ffb9fbd2bd89 Mon Sep 17 00:00:00 2001 From: albx Date: Tue, 29 Sep 2026 19:52:28 +0200 Subject: [PATCH 5/7] #99 - update toolbar documentation --- docs/components/toolbar.md | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/components/toolbar.md b/docs/components/toolbar.md index eeae6f5..eff7db5 100644 --- a/docs/components/toolbar.md +++ b/docs/components/toolbar.md @@ -14,12 +14,17 @@ The Toolbar component provides a navigation bar for grouping and displaying icon Each `BitToolbarItem` can be used as a navigation link (via `Href`), a click handler (via `OnClick`), or both at the same time. When both are provided, `OnClick` takes precedence in interactive rendering while `Href` remains available for browser-native behaviors such as right-click → Open in new tab. In static SSR rendering, `Href` is the only navigation mechanism. +A `BitToolbarDropdownItem` renders a [`BitDropdown`](./dropdown.md) activator styled as a toolbar item, letting a single toolbar entry expand into a menu of `BitDropdownItem` choices. + +> **`BitToolbarDropdownItem` requires interactive rendering.** It renders a `BitDropdown` internally, and the dropdown's open/closed state is managed purely in C# (`@onclick` / `@onkeydown`) with no JavaScript fallback. Under static SSR the activator button cannot be toggled, so the menu can never open. Only use `BitToolbarDropdownItem` in pages or components running under an interactive render mode (Server, WebAssembly, or Auto). `BitToolbarItem` (with `Href` and/or `OnClick`) remains fully SSR-compatible. + ## Components | Component | Description | |-----------|-------------| | `BitToolbar` | Root container that renders a ` +/// +/// Rendered with , which requires an interactive render mode (Server, WebAssembly, or Auto) to open and close. +/// Under static SSR (no circuit/WASM runtime attached) the activator cannot be toggled, so the dropdown menu can never be opened. +/// public partial class BitToolbarDropdownItem : BitToolbarItemBase { /// @@ -22,4 +28,49 @@ public partial class BitToolbarDropdownItem : BitToolbarItemBase [Parameter] [EditorRequired] public string Id { get; set; } = string.Empty; + + private string ComputeActivatorCssClass() + { + var builder = new CssClassBuilder("btn", "btn-dropdown", "dropdown-toggle"); + + if (Disabled) + { + builder.Add("disabled"); + } + + return builder.Build(); + } + + // Merges the ActivatorContext's dropdown-managed attributes with the aria-disabled state set by SetDisabled(). + private IDictionary ComputeActivatorAttributes(ActivatorContext context) + { + var attributes = new Dictionary(context.Attributes); + + foreach (var attribute in AdditionalAttributes) + { + attributes[attribute.Key] = attribute.Value; + } + + return attributes; + } + + private void HandleActivatorClick(ActivatorContext context) + { + if (Disabled) + { + return; + } + + context.ToggleDropdown(); + } + + private async Task HandleActivatorKeyDownAsync(ActivatorContext context, KeyboardEventArgs args) + { + if (Disabled) + { + return; + } + + await context.HandleKeyDownAsync(args); + } } diff --git a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs index 93f8171..bdab4e5 100644 --- a/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs +++ b/tests/BitBlazor.Test/Components/Toolbar/BitToolbarTest.Behaviors.cs @@ -365,4 +365,75 @@ public void BitToolbarDropdownItem_Should_Throw_InvalidOperationException_When_U Assert.Equal("A toolbar item must be only used inside a BitToolbar component", exception.Message); } + + [Fact] + public void BitToolbarDropdownItem_Should_Render_Native_Disabled_State_On_Activator() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .Add(p => p.Disabled, true) + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + + Assert.True(button.HasAttribute("disabled")); + Assert.Equal("true", button.GetAttribute("aria-disabled")); + Assert.Contains("disabled", button.ClassList); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Not_Open_On_Activator_Click_When_Disabled() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .Add(p => p.Disabled, true) + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.Click(); + + var menu = cut.Find("div.dropdown-menu"); + Assert.False(menu.ClassList.Contains("show")); + Assert.Equal("false", button.GetAttribute("aria-expanded")); + } + + [Fact] + public void BitToolbarDropdownItem_Should_Not_Open_On_ArrowDown_Key_When_Disabled() + { + using var ctx = new BunitContext(); + ctx.SetRendererInfo(new RendererInfo("InteractiveServer", isInteractive: true)); + + var cut = ctx.Render(parameters => parameters + .AddChildContent(itemParameters => itemParameters + .Add(p => p.Label, "Messages") + .Add(p => p.IconName, Icons.ItComment) + .Add(p => p.Id, "dropdown1") + .Add(p => p.Disabled, true) + .AddChildContent(dropdownItemParameters => + dropdownItemParameters.AddChildContent("Label 1"))) + ); + + var button = cut.Find("button"); + button.KeyDown(new KeyboardEventArgs { Key = "ArrowDown" }); + + var menu = cut.Find("div.dropdown-menu"); + Assert.False(menu.ClassList.Contains("show")); + Assert.Equal("false", button.GetAttribute("aria-expanded")); + } } From 5fe955e11844937a383036a1df0e0a65d5a97de9 Mon Sep 17 00:00:00 2001 From: albx Date: Sat, 3 Oct 2026 14:37:50 +0200 Subject: [PATCH 7/7] #99 - fix documentation based on PR review --- docs/components/toolbar.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/components/toolbar.md b/docs/components/toolbar.md index eff7db5..f542f4e 100644 --- a/docs/components/toolbar.md +++ b/docs/components/toolbar.md @@ -48,7 +48,7 @@ A `BitToolbarDropdownItem` renders a [`BitDropdown`](./dropdown.md) activator st | `Active` | `bool` | ✗ | `false` | When `true`, applies the active style to the item | | `Disabled` | `bool` | ✗ | `false` | When `true`, disables the item and adds `aria-disabled="true"` | | `BadgeCount` | `int?` | ✗ | `null` | A numeric badge count shown on the item; hidden when `null` or `0` | -| `BadgeLabel` | `string?` | ✗ | `null` | A text label shown next to the badge; displayed in different positions depending on `Size` | +| `BadgeLabel` | `string?` | ✗ | `null` | A visually hidden accessible label that provides context for the badge count; its markup position varies with the toolbar `Size` | | `OnClick` | `EventCallback` | ✗ | - | Primary interactive callback invoked when the item is clicked. Takes precedence over `Href` navigation in interactive rendering. Not invoked during static (SSR) rendering — provide `Href` as a navigation fallback for SSR contexts. | | `Id` | `string?` | ✗ | `null` | Sets the `id` HTML attribute on the root element | | `CssClass` | `string?` | ✗ | `null` | Additional CSS classes to apply to the item | @@ -67,7 +67,7 @@ A `BitToolbarDropdownItem` renders a [`BitDropdown`](./dropdown.md) activator st | `Active` | `bool` | ✗ | `false` | When `true`, applies the active style to the item | | `Disabled` | `bool` | ✗ | `false` | When `true`, disables the item and adds `aria-disabled="true"` | | `BadgeCount` | `int?` | ✗ | `null` | A numeric badge count shown on the item; hidden when `null` or `0` | -| `BadgeLabel` | `string?` | ✗ | `null` | A text label shown next to the badge; displayed in different positions depending on `Size` | +| `BadgeLabel` | `string?` | ✗ | `null` | A visually hidden accessible label that provides context for the badge count; its markup position varies with the toolbar `Size` | | `AdditionalAttributes` | `IDictionary?` | ✗ | - | Additional HTML attributes forwarded to the activator button | ## Used Enumerations