Skip to content

Commit f0f1bf8

Browse files
rpodevnsvnbaaij
andauthored
[Menu] Add CheckedChanged EventCallback (fix 3390) (#3414)
* [Menu] Add CheckedChanged EventCallback (fix 3390) * [Menu] Add 2-way binding (fix 3390) * [Menu] Add CheckedChanged EventCallback (fix 3390) * [Menu] Add 2-way binding (fix 3390) * [Menu] fixed typos and changed properties to fields in sample --------- Co-authored-by: rpodevns <[email protected]> Co-authored-by: Vincent Baaij <[email protected]>
1 parent 29a33e9 commit f0f1bf8

File tree

9 files changed

+177
-11
lines changed

9 files changed

+177
-11
lines changed

examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6585,6 +6585,11 @@
65856585
Gets or sets the horizontal scaling mode.
65866586
</summary>
65876587
</member>
6588+
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentMenu.OnCheckedChanged">
6589+
<summary>
6590+
Raised when FluentMenuItem Checked changed.
6591+
</summary>
6592+
</member>
65886593
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenu.OnAfterRenderAsync(System.Boolean)">
65896594
<summary />
65906595
</member>
@@ -6669,8 +6674,10 @@
66696674
Event raised when the user click on this item.
66706675
</summary>
66716676
</member>
6672-
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenuItem.OnClickHandlerAsync(Microsoft.AspNetCore.Components.Web.MouseEventArgs)">
6673-
<summary />
6677+
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentMenuItem.CheckedChanged">
6678+
<summary>
6679+
Event raised for checkbox and radio menuitems
6680+
</summary>
66746681
</member>
66756682
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentMenuProvider.#ctor">
66766683
<summary />
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
2-
3-
1+

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="40">
3+
4+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">
5+
6+
<FluentLabel Typo="Typography.H5">Role=MenuItemCheckbox</FluentLabel>
7+
<FluentMenu Open="true" OnCheckedChanged="HandleCheckedChanged">
8+
9+
<FluentMenuItem Id="ck1" Role="MenuItemRole.MenuItemCheckbox" @bind-Checked=checkbox1>
10+
Checkbox MenuItem 1 is @(checkbox1 ? "Checked" : "Unchecked")
11+
</FluentMenuItem>
12+
13+
<FluentMenuItem Id="ck2" Role="MenuItemRole.MenuItemCheckbox" @bind-Checked=checkbox2>
14+
Checkbox MenuItem 2 is @(checkbox2 ? "Checked" : "Unchecked")
15+
</FluentMenuItem>
16+
17+
</FluentMenu>
18+
19+
<FluentButton Appearance="Appearance.Accent" OnClick="ToggleCheckboxItem2">Toggle checkbox item 2</FluentButton>
20+
21+
</FluentStack>
22+
23+
<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">
24+
25+
<FluentLabel Typo="Typography.H5">Role=MenuItemRadio</FluentLabel>
26+
<FluentMenu Open="true" OnCheckedChanged="HandleCheckedChanged">
27+
28+
<FluentMenuItem id="radio1" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio1>
29+
Radio 1 is @(radio1? "Checked" : "Unchecked" )
30+
</FluentMenuItem>
31+
32+
<FluentMenuItem Id="radio2" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio2>
33+
Radio 2 is @(radio2 ? "Checked" : "Unchecked")
34+
</FluentMenuItem>
35+
36+
<FluentMenuItem Id="radio3" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio3>
37+
Radio 3 is @(radio3 ? "Checked" : "Unchecked")
38+
</FluentMenuItem>
39+
40+
<FluentDivider/>
41+
42+
<FluentMenuItem id="radio4" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio4>
43+
Radio 4 is @(radio4 ? "Checked" : "Unchecked")
44+
</FluentMenuItem>
45+
46+
<FluentMenuItem Id="radio5" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio5>
47+
Radio 5 is @(radio5 ? "Checked" : "Unchecked")
48+
</FluentMenuItem>
49+
50+
<FluentMenuItem Id="radio6" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio6>
51+
Radio 6 is @(radio6 ? "Checked" : "Unchecked")
52+
</FluentMenuItem>
53+
54+
<FluentMenuItem Id="radio7" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio7>
55+
Radio 7 is @(radio7 ? "Checked" : "Unchecked")
56+
</FluentMenuItem>
57+
58+
<FluentMenuItem Id="radio8" Role="MenuItemRole.MenuItemRadio" @bind-Checked=radio8>
59+
Radio 8 is @(radio8 ? "Checked" : "Unchecked")
60+
</FluentMenuItem>
61+
62+
</FluentMenu>
63+
64+
<FluentButton Appearance="Appearance.Accent" OnClick="ToggleRadioItem6">Toggle radio item 6</FluentButton>
65+
66+
</FluentStack>
67+
68+
</FluentStack>
69+
70+
@code {
71+
72+
private bool checkbox1 = false;
73+
private bool checkbox2 = false;
74+
75+
private bool radio1 = false;
76+
private bool radio2 = false;
77+
private bool radio3 = false;
78+
private bool radio4 = false;
79+
private bool radio5 = false;
80+
private bool radio6 = false;
81+
private bool radio7 = false;
82+
private bool radio8 = false;
83+
84+
private void ToggleCheckboxItem2()
85+
{
86+
checkbox2 = !checkbox2;
87+
}
88+
89+
private void ToggleRadioItem6()
90+
{
91+
radio6 = !radio6;
92+
}
93+
94+
private void HandleCheckedChanged(FluentMenuItem item)
95+
{
96+
DemoLogger.WriteLine(@$"{item.Id} {(item.Checked ? "Checked" : "Unchecked")}");
97+
}
98+
}

examples/Demo/Shared/Pages/Menu/MenuPage.razor

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@
7575

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

78+
<DemoSection Title="Detect when checked and unchecked (2-way binding)" Component="@typeof(MenuCheckChanged)">
79+
<Description>
80+
<p>
81+
The <code>CheckedChanged</code> EventCallback is invoked when the <code>FluentMenuItem</code> Role is <code>MenuItemCheckbox</code> or <code>MenuItemRadio</code>.
82+
</p>
83+
</Description>
84+
</DemoSection>
85+
7886
<DemoSection Title="Nested" Component="@typeof(MenuNested)"></DemoSection>
7987

8088
<DemoSection Title="With icons" Component="@typeof(MenuIcon)">

src/Core/Components/Menu/FluentMenu.razor.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ public bool Open
173173
[Parameter]
174174
public AxisScalingMode? HorizontalScaling { get; set; }
175175

176+
/// <summary>
177+
/// Raised when FluentMenuItem Checked changed.
178+
/// </summary>
179+
[Parameter]
180+
public EventCallback<FluentMenuItem> OnCheckedChanged { get; set; }
181+
176182
protected override void OnInitialized()
177183
{
178184
if (Anchored && string.IsNullOrEmpty(Anchor))
@@ -199,9 +205,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
199205
{
200206
if (firstRender)
201207
{
208+
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration));
209+
202210
if (Trigger != MouseButton.None)
203211
{
204-
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration));
205212

206213
_dotNetHelper = DotNetObjectReference.Create(this);
207214

@@ -316,6 +323,16 @@ internal async Task SetOpenAsync(bool value)
316323
}
317324
}
318325

326+
internal async Task NotifyCheckedChangedAsync(FluentMenuItem fluentMenuItem)
327+
{
328+
await OnCheckedChanged.InvokeAsync(fluentMenuItem);
329+
}
330+
331+
internal async Task<bool> IsCheckedAsync (FluentMenuItem item)
332+
{
333+
return await _jsModule.InvokeAsync<bool>("isChecked", item.Id);
334+
}
335+
319336
/// <summary>
320337
/// Dispose this menu.
321338
/// </summary>

src/Core/Components/Menu/FluentMenu.razor.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Add Left click event to open the PowerMenu
1+
// Add Left click event to open the FluentMenu
22
export function addEventLeftClick(id, dotNetHelper) {
33
var item = document.getElementById(id);
44
if (!!item) {
@@ -8,7 +8,7 @@ export function addEventLeftClick(id, dotNetHelper) {
88
}
99
}
1010

11-
// Add Right click event to open the PowerMenu
11+
// Add Right click event to open the FluentMenu
1212
export function addEventRightClick(id, dotNetHelper) {
1313
var item = document.getElementById(id);
1414
if (!!item) {
@@ -18,4 +18,16 @@ export function addEventRightClick(id, dotNetHelper) {
1818
return false;
1919
}, false);
2020
}
21-
}
21+
}
22+
23+
// Must use an animation frame to ensure the DOM is fully updated before checking the element's
24+
// attributes to prevent stale or inconsistent reads.
25+
export function isChecked(menuItemId) {
26+
return new Promise(resolve => {
27+
requestAnimationFrame(() => {
28+
const menuItemElement = document.getElementById(menuItemId);
29+
if (!menuItemElement) return resolve(false);
30+
resolve(menuItemElement.hasAttribute("checked") || menuItemElement.getAttribute("aria-checked") === "true");
31+
});
32+
});
33+
}

src/Core/Components/Menu/FluentMenuItem.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@namespace Microsoft.FluentUI.AspNetCore.Components
1+
@namespace Microsoft.FluentUI.AspNetCore.Components
22
@inherits FluentComponentBase
33
<fluent-menu-item @ref=Element
44
class="@Class"
@@ -8,6 +8,7 @@
88
expanded=@Expanded
99
role="@GetRole()"
1010
checked="@Checked"
11+
@onchange="@OnChangeHandlerAsync"
1112
@onclick="@OnClickHandlerAsync"
1213
@attributes="AdditionalAttributes">
1314
@Label

src/Core/Components/Menu/FluentMenuItem.razor.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public partial class FluentMenuItem : FluentComponentBase, IDisposable
6666
[Parameter]
6767
public EventCallback<MouseEventArgs> OnClick { get; set; }
6868

69+
/// <summary>
70+
/// Event raised for checkbox and radio menuitems
71+
/// </summary>
72+
[Parameter]
73+
public EventCallback<bool> CheckedChanged { get; set; }
74+
6975
public FluentMenuItem()
7076
{
7177
Id = Identifier.NewId();
@@ -76,7 +82,6 @@ protected override void OnInitialized()
7682
Owner?.Register(this);
7783
}
7884

79-
/// <summary />
8085
protected async Task OnClickHandlerAsync(MouseEventArgs ev)
8186
{
8287
if (Disabled)
@@ -92,6 +97,25 @@ protected async Task OnClickHandlerAsync(MouseEventArgs ev)
9297
await OnClick.InvokeAsync(ev);
9398
}
9499

100+
protected async Task OnChangeHandlerAsync(ChangeEventArgs ev)
101+
{
102+
// fluent-menu-item v2 does not pass the checked state as a parameter when emitting
103+
// the change event so we need to capture the state from the html element using javascript.
104+
// The value is passed in v3 so javscript lookup won't be necessary.
105+
if (Owner != null && Role is MenuItemRole.MenuItemCheckbox or MenuItemRole.MenuItemRadio)
106+
{
107+
var isChecked = await Owner.IsCheckedAsync(this);
108+
Checked = isChecked;
109+
110+
await CheckedChanged.InvokeAsync(Checked);
111+
112+
if (Role == MenuItemRole.MenuItemCheckbox || (Role == MenuItemRole.MenuItemRadio && isChecked))
113+
{
114+
await Owner.NotifyCheckedChangedAsync(this);
115+
}
116+
}
117+
}
118+
95119
protected string? GetRole()
96120
{
97121
if (Role is not null)

tests/Core/_ToDo/MenuButton/FluentMenuButtonTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class FluentMenuButtonTests : TestBase
1010
public FluentMenuButtonTests()
1111
{
1212
TestContext.Services.AddSingleton(LibraryConfiguration.ForUnitTests);
13+
TestContext.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Menu/FluentMenu.razor.js");
1314
}
1415

1516
[Fact]

0 commit comments

Comments
 (0)