Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6585,6 +6585,11 @@
Gets or sets the horizontal scaling mode.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentMenu.OnCheckedChanged">
<summary>
Raised when FluentMenuItem Checked changed.
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenu.OnAfterRenderAsync(System.Boolean)">
<summary />
</member>
Expand Down Expand Up @@ -6669,8 +6674,10 @@
Event raised when the user click on this item.
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenuItem.OnClickHandlerAsync(Microsoft.AspNetCore.Components.Web.MouseEventArgs)">
<summary />
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentMenuItem.CheckedChanged">
<summary>
Event raised for checkbox and radio menuitems
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenuProvider.#ctor">
<summary />
Expand Down
4 changes: 1 addition & 3 deletions examples/Demo/Shared/Pages/Lab/IssueTester.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
ο»Ώ


ο»Ώ
98 changes: 98 additions & 0 deletions examples/Demo/Shared/Pages/Menu/Examples/MenuCheckChanged.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
ο»Ώ
<FluentStack Orientation="Orientation.Vertical" VerticalGap="40">

<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">

<FluentLabel Typo="Typography.H5">Role=MenuItemCheckbox</FluentLabel>
<FluentMenu Open="true" OnCheckedChanged="HandleCheckedChanged">

<FluentMenuItem Id="ck1" Role="MenuItemRole.MenuItemCheckbox" @bind-Checked=checkbox1>
Checkbox MenuItem 1 is @(checkbox1 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="ck2" Role="MenuItemRole.MenuItemCheckbox" @bind-Checked=checkbox2>
Checkbox MenuItem 2 is @(checkbox2 ? "Checked" : "Unchecked")
</FluentMenuItem>

</FluentMenu>

<FluentButton Appearance="Appearance.Accent" OnClick="ToggleCheckboxItem2">Toggle checkbox item 2</FluentButton>

</FluentStack>

<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">

<FluentLabel Typo="Typography.H5">Role=MenuItemRadio</FluentLabel>
<FluentMenu Open="true" OnCheckedChanged="HandleCheckedChanged">

<FluentMenuItem id="radio1" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio1>
Radio 1 is @(radio1? "Checked" : "Unchecked" )
</FluentMenuItem>

<FluentMenuItem Id="radio2" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio2>
Radio 2 is @(radio2 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="radio3" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio3>
Radio 3 is @(radio3 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentDivider/>

<FluentMenuItem id="radio4" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio4>
Radio 4 is @(radio4 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="radio5" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio5>
Radio 5 is @(radio5 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="radio6" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio6>
Radio 6 is @(radio6 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="radio7" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio7>
Radio 7 is @(radio7 ? "Checked" : "Unchecked")
</FluentMenuItem>

<FluentMenuItem Id="radio8" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio8>
Radio 8 is @(radio8 ? "Checked" : "Unchecked")
</FluentMenuItem>

</FluentMenu>

<FluentButton Appearance="Appearance.Accent" OnClick="ToggleRadioItem6">Toggle radio item 6</FluentButton>

</FluentStack>

</FluentStack>

@code {

private bool checkbox1 = false;
private bool checkbox2 = false;

private bool radio1 { get; set; } = false;
private bool radio2 { get; set; } = false;
private bool radio3 { get; set; } = false;
private bool radio4 { get; set; } = false;
private bool radio5 { get; set; } = false;
private bool radio6 { get; set; } = false;
private bool radio7 { get; set; } = false;
private bool radio8 { get; set; } = false;

private void ToggleCheckboxItem2()
{
checkbox2 = !checkbox2;
}

private void ToggleRadioItem6()
{
radio6 = !radio6;
}

private void HandleCheckedChanged(FluentMenuItem item)
{
DemoLogger.WriteLine(@$"{item.Id} {(item.Checked ? "Checked" : "Unchecked")}");
}
}
8 changes: 8 additions & 0 deletions examples/Demo/Shared/Pages/Menu/MenuPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@

<DemoSection Title="With radio buttons and checkboxes" Component="@typeof(MenuCheckRadio)"></DemoSection>

<DemoSection Title="Detect when checked and unchecked" Component="@typeof(MenuCheckChanged)">
<Description>
<p>
The <code>CheckedChanged</code> EventCallback is invoked when the <code>FluentMenuItem</code> Role is <code>MenuItemCheckbox</code> or <code>MenuItemRadio</code>.
</p>
</Description>
</DemoSection>

<DemoSection Title="Nested" Component="@typeof(MenuNested)"></DemoSection>

<DemoSection Title="With icons" Component="@typeof(MenuIcon)">
Expand Down
19 changes: 18 additions & 1 deletion src/Core/Components/Menu/FluentMenu.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ public bool Open
[Parameter]
public AxisScalingMode? HorizontalScaling { get; set; }

/// <summary>
/// Raised when FluentMenuItem Checked changed.
/// </summary>
[Parameter]
public EventCallback<FluentMenuItem> OnCheckedChanged { get; set; }

protected override void OnInitialized()
{
if (Anchored && string.IsNullOrEmpty(Anchor))
Expand All @@ -199,9 +205,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration));

if (Trigger != MouseButton.None)
{
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration));

_dotNetHelper = DotNetObjectReference.Create(this);

Expand Down Expand Up @@ -316,6 +323,16 @@ internal async Task SetOpenAsync(bool value)
}
}

internal async Task NotifyCheckedChangedAsync(FluentMenuItem fluentMenuItem)
{
await OnCheckedChanged.InvokeAsync(fluentMenuItem);
}

internal async Task<bool> IsCheckedAsync (FluentMenuItem item)
{
return await _jsModule.InvokeAsync<bool>("isChecked", item.Id);
}

/// <summary>
/// Dispose this menu.
/// </summary>
Expand Down
14 changes: 12 additions & 2 deletions src/Core/Components/Menu/FluentMenu.razor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ο»Ώ// Add Left click event to open the PowerMenu
// Add Left click event to open the PowerMenu
export function addEventLeftClick(id, dotNetHelper) {
var item = document.getElementById(id);
if (!!item) {
Expand All @@ -18,4 +18,14 @@ export function addEventRightClick(id, dotNetHelper) {
return false;
}, false);
}
}
}

export function isChecked(menuItemId) {
return new Promise(resolve => {
requestAnimationFrame(() => {
const menuItemElement = document.getElementById(menuItemId);
if (!menuItemElement) return resolve(false);
resolve(menuItemElement.hasAttribute("checked") || menuItemElement.getAttribute("aria-checked") === "true");
});
});
}
3 changes: 2 additions & 1 deletion src/Core/Components/Menu/FluentMenuItem.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
ο»Ώ@namespace Microsoft.FluentUI.AspNetCore.Components
@inherits FluentComponentBase
<fluent-menu-item @ref=Element
class="@Class"
Expand All @@ -8,6 +8,7 @@
expanded=@Expanded
role="@GetRole()"
checked="@Checked"
@onchange="@OnChangeHandlerAsync"
@onclick="@OnClickHandlerAsync"
@attributes="AdditionalAttributes">
@Label
Expand Down
26 changes: 25 additions & 1 deletion src/Core/Components/Menu/FluentMenuItem.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public partial class FluentMenuItem : FluentComponentBase, IDisposable
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }

/// <summary>
/// Event raised for checkbox and radio menuitems
/// </summary>
[Parameter]
public EventCallback<bool> CheckedChanged { get; set; }

public FluentMenuItem()
{
Id = Identifier.NewId();
Expand All @@ -76,7 +82,6 @@ protected override void OnInitialized()
Owner?.Register(this);
}

/// <summary />
protected async Task OnClickHandlerAsync(MouseEventArgs ev)
{
if (Disabled)
Expand All @@ -92,6 +97,25 @@ protected async Task OnClickHandlerAsync(MouseEventArgs ev)
await OnClick.InvokeAsync(ev);
}

protected async Task OnChangeHandlerAsync(ChangeEventArgs ev)
{
// fluent-menu-item v2 does not pass the checked state as a parameter when emitting
// the change event so we need to capture the state from the html element using javascript.
// The value is passed in v3 so javscript lookup won't be necessary.
if (Owner != null && Role is MenuItemRole.MenuItemCheckbox or MenuItemRole.MenuItemRadio)
{
var isChecked = await Owner.IsCheckedAsync(this);
Checked = isChecked;

await CheckedChanged.InvokeAsync(Checked);

if (Role == MenuItemRole.MenuItemCheckbox || (Role == MenuItemRole.MenuItemRadio && isChecked))
{
await Owner.NotifyCheckedChangedAsync(this);
}
}
}

protected string? GetRole()
{
if (Role is not null)
Expand Down
1 change: 1 addition & 0 deletions tests/Core/_ToDo/MenuButton/FluentMenuButtonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class FluentMenuButtonTests : TestBase
public FluentMenuButtonTests()
{
TestContext.Services.AddSingleton(LibraryConfiguration.ForUnitTests);
TestContext.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Menu/FluentMenu.razor.js");
}

[Fact]
Expand Down