diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
index c847541d17..3c32e73e8a 100644
--- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
+++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
@@ -7559,6 +7559,55 @@
+
+
+ Gets or sets the maximum value.
+
+
+
+
+ Gets or sets the icon drawing and fill color.
+ Value comes from the enumeration. Defaults to Accent.
+
+
+
+
+ Gets or sets the icon drawing and fill color to a custom value.
+ Needs to be formatted as an HTML hex color string (#rrggbb or #rgb) or CSS variable.
+ ⚠️ Only available when Color is set to Color.Custom.
+
+
+
+
+ The icon width.
+
+
+
+
+ The icon to display when the rating value is greater than or equal to the item's value.
+
+
+
+
+ The icon to display when the rating value is less than the item's value.
+
+
+
+
+ Gets or sets a value that whether to allow clear when click again.
+
+
+
+
+ Fires when hovered value changes. Value will be null if no rating item is hovered.
+
+
+
+
+
+
+
+
diff --git a/examples/Demo/Shared/Pages/Rating/Examples/RatingDefault.razor b/examples/Demo/Shared/Pages/Rating/Examples/RatingDefault.razor
new file mode 100644
index 0000000000..e29cc8747d
--- /dev/null
+++ b/examples/Demo/Shared/Pages/Rating/Examples/RatingDefault.razor
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/Demo/Shared/Pages/Rating/Examples/RatingEvent.razor b/examples/Demo/Shared/Pages/Rating/Examples/RatingEvent.razor
new file mode 100644
index 0000000000..2decd1a47d
--- /dev/null
+++ b/examples/Demo/Shared/Pages/Rating/Examples/RatingEvent.razor
@@ -0,0 +1,38 @@
+
+
+
+@code
+{
+ bool _readOnly;
+ bool _disabled;
+ bool _allowReset;
+ int _maxValue = 10;
+ int _value = 2;
+ Color _iconColor = Color.Accent;
+
+ Icon _iconFilled = new Icons.Filled.Size20.Star();
+ Icon _iconOutline = new Icons.Regular.Size20.Star();
+ List _icons = ["Star", "Heart", "Alert", "PersonCircle"];
+
+ private void SelectedOptionChanged(string name) => SetIcon(_icons.IndexOf(name));
+
+ private void SetIcon(int index)
+ {
+ _iconFilled = new Icon[]
+ { new Icons.Filled.Size20.Star(),
+ new Icons.Filled.Size20.Heart(),
+ new Icons.Filled.Size20.Alert(),
+ new Icons.Filled.Size20.PersonCircle(),
+ }[index];
+
+ _iconOutline = new Icon[]
+ { new Icons.Regular.Size20.Star(),
+ new Icons.Regular.Size20.Heart(),
+ new Icons.Regular.Size20.Alert(),
+ new Icons.Regular.Size20.PersonCircle(),
+ }[index];
+ }
+
+ protected override void OnParametersSet() => SetIcon(0);
+}
diff --git a/examples/Demo/Shared/Pages/Rating/RatingPage.razor b/examples/Demo/Shared/Pages/Rating/RatingPage.razor
new file mode 100644
index 0000000000..ee4dd289e0
--- /dev/null
+++ b/examples/Demo/Shared/Pages/Rating/RatingPage.razor
@@ -0,0 +1,30 @@
+@page "/Rating"
+@using FluentUI.Demo.Shared.Pages.Rating.Examples
+
+@App.PageTitle("Rating")
+
+
Rating
+
+
+ A <FluentRating> component allows users to provide a rating for a particular item.
+ <FluentRating> allows customers to determine a sense of value of a good or a service.
+ By default, the rating is selected out of 5 stars, but the number and symbol used can be customized.
+
+
+
Examples
+
+
+
+
+
+
+
Accessibility
+
+ You can use the arrow keys to increase or decrease the value. Pressing Shift+arrow changes the value to 0 or the maximum value.
+
+
+
+
+
Documentation
+
+
diff --git a/examples/Demo/Shared/Shared/DemoNavProvider.cs b/examples/Demo/Shared/Shared/DemoNavProvider.cs
index b2443934f8..4cba6dae4e 100644
--- a/examples/Demo/Shared/Shared/DemoNavProvider.cs
+++ b/examples/Demo/Shared/Shared/DemoNavProvider.cs
@@ -231,6 +231,11 @@ public DemoNavProvider()
icon: new Icons.Regular.Size20.RadioButton(),
title: "Radio Group"
),
+ new NavLink(
+ href: "/Rating",
+ icon: new Icons.Regular.Size20.Star(),
+ title: "Rating"
+ ),
new NavLink(
href: "/Search",
icon: new Icons.Regular.Size20.SearchSquare(),
diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs
index d1ab13659e..dfb7bf051f 100644
--- a/src/Core/Components/Icons/CoreIcons.cs
+++ b/src/Core/Components/Icons/CoreIcons.cs
@@ -41,7 +41,8 @@ public class Info : Icon { public Info() : base("Info", IconVariant.Filled, Icon
public class Warning : Icon { public Warning() : base("Warning", IconVariant.Filled, IconSize.Size20, "") { } }
public class CheckboxChecked : Icon { public CheckboxChecked() : base("CheckboxChecked", IconVariant.Filled, IconSize.Size20, "") { } }
public class CheckboxIndeterminate : Icon { public CheckboxIndeterminate() : base("CheckboxIndeterminate", IconVariant.Filled, IconSize.Size20, "") { } }
- public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Filled, IconSize.Size20, "") { } };
+ public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } };
+ public class Star : Icon { public Star() : base("Star", IconVariant.Filled, IconSize.Size20, "") { } };
}
}
@@ -99,6 +100,7 @@ public class Dismiss : Icon { public Dismiss() : base("Dismiss", IconVariant.Reg
public class DismissCircle : Icon { public DismissCircle() : base("DismissCircle", IconVariant.Regular, IconSize.Size20, "") { } }
public class CheckboxUnchecked : Icon { public CheckboxUnchecked() : base("CheckboxUnchecked", IconVariant.Regular, IconSize.Size20, "") { } }
public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } };
+ public class Star : Icon { public Star() : base("Star", IconVariant.Regular, IconSize.Size20, "") { } };
}
}
diff --git a/src/Core/Components/Rating/FluentRating.razor b/src/Core/Components/Rating/FluentRating.razor
new file mode 100644
index 0000000000..0106a40652
--- /dev/null
+++ b/src/Core/Components/Rating/FluentRating.razor
@@ -0,0 +1,43 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@inherits FluentInputBase
+
+@if (!ReadOnly && !Disabled)
+{
+
+}
+
+@LabelTemplate
+
+
+ @for (int i = 1; i <= MaxValue; i++)
+ {
+ var currentValue = i;
+ @if (ReadOnly || Disabled)
+ {
+
+ }
+ else
+ {
+ await OnPointerOverAsync(currentValue))"
+ @onpointerout="@(async () => await OnPointerOutAsync())" />
+ }
+ }
+
diff --git a/src/Core/Components/Rating/FluentRating.razor.cs b/src/Core/Components/Rating/FluentRating.razor.cs
new file mode 100644
index 0000000000..c566cb7b10
--- /dev/null
+++ b/src/Core/Components/Rating/FluentRating.razor.cs
@@ -0,0 +1,149 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+public partial class FluentRating : FluentInputBase
+{
+ private int? _mouseOverValue;
+ private bool _mouseOverDisabled;
+
+ public FluentRating() => Id = Identifier.NewId();
+
+ ///
+ /// Gets or sets the maximum value.
+ ///
+ [Parameter]
+ public int MaxValue { get; set; } = 5;
+
+ ///
+ /// Gets or sets the icon drawing and fill color.
+ /// Value comes from the enumeration. Defaults to Accent.
+ ///
+ [Parameter]
+ public Color? IconColor { get; set; }
+
+ ///
+ /// Gets or sets the icon drawing and fill color to a custom value.
+ /// Needs to be formatted as an HTML hex color string (#rrggbb or #rgb) or CSS variable.
+ /// ⚠️ Only available when Color is set to Color.Custom.
+ ///
+ [Parameter]
+ public string? IconCustomColor { get; set; }
+
+ ///
+ /// The icon width.
+ ///
+ [Parameter]
+ public string IconWidth { get; set; } = "28px";
+
+ ///
+ /// The icon to display when the rating value is greater than or equal to the item's value.
+ ///
+ [Parameter]
+ public Icon IconFilled { get; set; } = new CoreIcons.Filled.Size20.Star();
+
+ ///
+ /// The icon to display when the rating value is less than the item's value.
+ ///
+ [Parameter]
+ public Icon IconOutline { get; set; } = new CoreIcons.Regular.Size20.Star();
+
+ ///
+ /// Gets or sets a value that whether to allow clear when click again.
+ ///
+ [Parameter]
+ public bool AllowReset { get; set; }
+
+ ///
+ /// Fires when hovered value changes. Value will be null if no rating item is hovered.
+ ///
+ [Parameter]
+ public EventCallback OnPointerOver { get; set; }
+
+ ///
+ protected override string? ClassValue => new CssBuilder(base.ClassValue)
+ .AddClass("fluent-rating")
+ .Build();
+
+ ///
+ protected override string? StyleValue => new StyleBuilder(base.StyleValue).Build();
+
+ private Icon GetIcon(int index) => index <= (_mouseOverValue ?? Value) ? IconFilled : IconOutline;
+
+ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int result, [NotNullWhen(false)] out string?
+ validationErrorMessage)
+ {
+ if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result))
+ {
+ validationErrorMessage = null;
+ return true;
+ }
+ else
+ {
+ validationErrorMessage = string.Format(CultureInfo.InvariantCulture,
+ "The {0} field must be a number.",
+ FieldBound ? FieldIdentifier.FieldName : UnknownBoundField);
+ return false;
+ }
+ }
+
+ protected internal async Task HandleKeyDownAsync(FluentKeyCodeEventArgs e)
+ {
+ if (e.TargetId != Id)
+ {
+ return;
+ }
+
+ int value = e.Key switch
+ {
+ KeyCode.Right or KeyCode.Up when e.ShiftKey => value = MaxValue,
+ KeyCode.Right or KeyCode.Up => Math.Min(Value + 1, MaxValue),
+ KeyCode.Left or KeyCode.Down when e.ShiftKey => value = 0,
+ KeyCode.Left or KeyCode.Down => Math.Max(Value - 1, 1),
+ _ => Value
+ };
+
+ _mouseOverValue = null;
+ _mouseOverDisabled = true;
+
+ await SetCurrentValueAsync(value);
+ }
+
+ private async Task OnPointerOutAsync()
+ {
+ _mouseOverValue = null;
+ _mouseOverDisabled = false;
+ if (OnPointerOver.HasDelegate)
+ {
+ await OnPointerOver.InvokeAsync(_mouseOverValue);
+ }
+ }
+
+ private async Task OnPointerOverAsync(int value)
+ {
+ if (_mouseOverDisabled)
+ {
+ return;
+ }
+
+ _mouseOverValue = value;
+ if (OnPointerOver.HasDelegate)
+ {
+ await OnPointerOver.InvokeAsync(_mouseOverValue);
+ }
+ }
+
+ private async Task OnClickAsync(int value)
+ {
+ if (value == Value && AllowReset)
+ {
+ value = 0;
+ _mouseOverValue = null;
+ _mouseOverDisabled = true;
+ }
+ await SetCurrentValueAsync(value);
+ }
+}
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html
new file mode 100644
index 0000000000..bc82c8c16a
--- /dev/null
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html
new file mode 100644
index 0000000000..5e20fd113b
--- /dev/null
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Core/Rating/FluentRatingTests.razor b/tests/Core/Rating/FluentRatingTests.razor
new file mode 100644
index 0000000000..6a6aaa243c
--- /dev/null
+++ b/tests/Core/Rating/FluentRatingTests.razor
@@ -0,0 +1,77 @@
+@using Xunit;
+@inherits TestContext
+@code
+{
+ public FluentRatingTests()
+ {
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ Services.AddSingleton(new LibraryConfiguration());
+ }
+
+ [Fact]
+ public void FluentRating_Empty()
+ {
+ // Arrange && Act
+ var cut = Render(@);
+
+ // Assert
+ cut.Verify();
+ }
+
+ [Fact]
+ public void FluentRating_Value()
+ {
+ // Arrange && Act
+ var cut = Render(@);
+
+ // Assert
+ cut.Verify();
+ }
+
+ [Fact]
+ public void FluentRating_SelectValue()
+ {
+ int value = 0;
+
+ // Arrange
+ var cut = Render(@);
+
+ // Act: Click on the second star
+ cut.FindAll("svg").ElementAt(1).Click();
+
+ // Assert
+ Assert.Equal(2, value);
+ }
+
+ [Fact]
+ public void FluentRating_AllowResetFalse()
+ {
+ int value = 0;
+
+ // Arrange
+ var cut = Render(@);
+
+ // Act: Click twice on the second star
+ cut.FindAll("svg").ElementAt(1).Click();
+ cut.FindAll("svg").ElementAt(1).Click();
+
+ // Assert
+ Assert.Equal(2, value);
+ }
+
+ [Fact]
+ public void FluentRating_AllowResetTrue()
+ {
+ int value = 0;
+
+ // Arrange
+ var cut = Render(@);
+
+ // Act: Click twice on the second star
+ cut.FindAll("svg").ElementAt(1).Click();
+ cut.FindAll("svg").ElementAt(1).Click();
+
+ // Assert
+ Assert.Equal(0, value);
+ }
+}