_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
index a84073aa86..8f0969a788 100644
--- a/examples/Demo/Shared/Pages/Rating/RatingPage.razor
+++ b/examples/Demo/Shared/Pages/Rating/RatingPage.razor
@@ -14,20 +14,14 @@
Accessibility
- You can use the arrow keys to increase (→ / ↑ ) or decrease (← / ↓ ) the value. Pressing Shift + arrow sets the value to 0 or the maximum.
+ You can use the arrow keys to increase (→ / ↑ ) or decrease (← / ↓ ) the value.
Examples
-
-
-
-
- You can use the OnPointerOver
event to capture a changing rating value.
-
-
+
Documentation
diff --git a/src/Core/Components/Rating/FluentRating.razor b/src/Core/Components/Rating/FluentRating.razor
index ca5c8ea961..ac352ec85c 100644
--- a/src/Core/Components/Rating/FluentRating.razor
+++ b/src/Core/Components/Rating/FluentRating.razor
@@ -1,45 +1,45 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
@inherits FluentInputBase
-@if (!ReadOnly && !Disabled)
+@if (!string.IsNullOrEmpty(Label) || LabelTemplate is not null)
{
-
+ @LabelTemplate
}
-@LabelTemplate
+
-
- @for (int i = 1; i <= MaxValue; i++)
+ @foreach (var index in Enumerable.Range(1, Max))
{
- var currentValue = i;
@if (ReadOnly || Disabled)
{
-
+ selected="@(index == Value)"
+ aria-hidden="true" />
}
else
{
- OnClickAsync(index, fromFocus: true))" />
+
+ await OnPointerOverAsync(currentValue))"
- @onpointerout="@(async () => await OnPointerOutAsync())"
- aria-label="@currentValue" />
+ OnClick="@(() => OnClickAsync(index))"
+ @onmouseleave="@(() => _updatingCurrentValue = false)"
+ @onmouseenter="@(() => OnMouseEnterAsync(index))"
+ selected="@(index == Value)"
+ aria-hidden="true" />
}
}
-
+
diff --git a/src/Core/Components/Rating/FluentRating.razor.cs b/src/Core/Components/Rating/FluentRating.razor.cs
index c566cb7b10..376c7ffcaf 100644
--- a/src/Core/Components/Rating/FluentRating.razor.cs
+++ b/src/Core/Components/Rating/FluentRating.razor.cs
@@ -7,74 +7,76 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
public partial class FluentRating : FluentInputBase
{
- private int? _mouseOverValue;
- private bool _mouseOverDisabled;
+ private bool _updatingCurrentValue = false;
+ private int? _hoverValue = null;
- public FluentRating() => Id = Identifier.NewId();
+ ///
+ protected override string? ClassValue => new CssBuilder(base.ClassValue)
+ .AddClass("fluent-rating")
+ .Build();
+
+ ///
+ protected override string? StyleValue => new StyleBuilder(base.StyleValue).Build();
///
- /// Gets or sets the maximum value.
+ /// Gets or sets the number of elements.
///
[Parameter]
- public int MaxValue { get; set; } = 5;
+ public int Max { get; set; } = 5;
///
- /// Gets or sets the icon drawing and fill color.
- /// Value comes from the enumeration. Defaults to Accent.
+ /// The icon to display when the rating value is greater than or equal to the item's value.
///
[Parameter]
- public Color? IconColor { get; set; }
+ public Icon IconFilled { get; set; } = new CoreIcons.Filled.Size20.Star();
///
- /// 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 to display when the rating value is less than the item's value.
///
[Parameter]
- public string? IconCustomColor { get; set; }
+ public Icon IconOutline { get; set; } = new CoreIcons.Regular.Size20.Star();
///
- /// The icon width.
+ /// Gets or sets the icon drawing and fill color.
+ /// Value comes from the enumeration. Defaults to Accent.
///
[Parameter]
- public string IconWidth { get; set; } = "28px";
+ public Color? IconColor { get; set; }
///
- /// The icon to display when the rating value is greater than or equal to the item's value.
+ /// 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 Icon IconFilled { get; set; } = new CoreIcons.Filled.Size20.Star();
+ public string? IconCustomColor { get; set; }
///
- /// The icon to display when the rating value is less than the item's value.
+ /// The icon width.
///
[Parameter]
- public Icon IconOutline { get; set; } = new CoreIcons.Regular.Size20.Star();
+ public string IconWidth { get; set; } = "28px";
///
/// Gets or sets a value that whether to allow clear when click again.
///
[Parameter]
- public bool AllowReset { get; set; }
+ public bool AllowReset { get; set; } = false;
///
/// Fires when hovered value changes. Value will be null if no rating item is hovered.
///
[Parameter]
- public EventCallback OnPointerOver { get; set; }
+ public EventCallback OnHoverValueChanged { get; set; }
///
- protected override string? ClassValue => new CssBuilder(base.ClassValue)
- .AddClass("fluent-rating")
- .Build();
+ private string GroupName => Id ?? $"rating-{Id}";
///
- protected override string? StyleValue => new StyleBuilder(base.StyleValue).Build();
-
- private Icon GetIcon(int index) => index <= (_mouseOverValue ?? Value) ? IconFilled : IconOutline;
+ private Icon GetIcon(int index) => index <= (_hoverValue ?? Value) ? IconFilled : IconOutline;
- protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int result, [NotNullWhen(false)] out string?
- validationErrorMessage)
+ ///
+ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int result, [NotNullWhen(false)] out string? validationErrorMessage)
{
if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result))
{
@@ -90,60 +92,53 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa
}
}
- protected internal async Task HandleKeyDownAsync(FluentKeyCodeEventArgs e)
+ ///
+ private async Task OnClickAsync(int value, bool fromFocus = false)
{
- if (e.TargetId != Id)
+ _updatingCurrentValue = true;
+
+ // Reset ?
+ if (AllowReset && value == Value && !fromFocus)
{
- return;
+ await SetCurrentValueAsync(0);
+ await UpdateHoverValueAsync(null);
}
-
- int value = e.Key switch
+ else
{
- 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);
+ await SetCurrentValueAsync(value);
+ }
}
- private async Task OnPointerOutAsync()
+ ///
+ private async Task OnMouseEnterAsync(int value)
{
- _mouseOverValue = null;
- _mouseOverDisabled = false;
- if (OnPointerOver.HasDelegate)
+ if (_updatingCurrentValue)
{
- await OnPointerOver.InvokeAsync(_mouseOverValue);
+ return;
}
+
+ await UpdateHoverValueAsync(value);
+ }
+
+ ///
+ private async Task OnMouseLeaveAsync()
+ {
+ await UpdateHoverValueAsync(null);
}
- private async Task OnPointerOverAsync(int value)
+ ///
+ private async Task UpdateHoverValueAsync(int? value)
{
- if (_mouseOverDisabled)
+ if (_hoverValue == value)
{
return;
}
- _mouseOverValue = value;
- if (OnPointerOver.HasDelegate)
- {
- await OnPointerOver.InvokeAsync(_mouseOverValue);
- }
- }
+ _hoverValue = value;
- private async Task OnClickAsync(int value)
- {
- if (value == Value && AllowReset)
+ if (OnHoverValueChanged.HasDelegate)
{
- value = 0;
- _mouseOverValue = null;
- _mouseOverDisabled = true;
+ await OnHoverValueChanged.InvokeAsync(value);
}
- await SetCurrentValueAsync(value);
}
}
diff --git a/src/Core/Components/Rating/FluentRating.razor.css b/src/Core/Components/Rating/FluentRating.razor.css
new file mode 100644
index 0000000000..04a7d8ccd7
--- /dev/null
+++ b/src/Core/Components/Rating/FluentRating.razor.css
@@ -0,0 +1,18 @@
+.fluent-rating {
+ justify-content: start;
+ align-items: start;
+ column-gap: 0px;
+}
+
+ .fluent-rating ::deep input[type='radio'] {
+ opacity: 0;
+ position: absolute;
+ width: 0px;
+ height: 0px;
+ }
+
+ .fluent-rating:focus-within ::deep svg[selected] {
+ outline-style: auto;
+ outline-color: var(--focus-stroke-outer);
+ outline-width: var(--focus-stroke-width);
+ }
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html
index e691ceb16e..802c672852 100644
--- a/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_Empty.verified.razor.html
@@ -1,20 +1,23 @@
-
-
-
-
+
\ No newline at end of file
+
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_Label.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_Label.verified.razor.html
new file mode 100644
index 0000000000..afa83bb68a
--- /dev/null
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_Label.verified.razor.html
@@ -0,0 +1,25 @@
+
+My Label
+
+
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_LabelTemplate.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_LabelTemplate.verified.razor.html
new file mode 100644
index 0000000000..57a7aad7a8
--- /dev/null
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_LabelTemplate.verified.razor.html
@@ -0,0 +1,28 @@
+
+
+ My
+ Label
+ Template
+
+
diff --git a/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html b/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html
index ca192a3250..dad2dea65e 100644
--- a/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html
+++ b/tests/Core/Rating/FluentRatingTests.FluentRating_Value.verified.razor.html
@@ -1,35 +1,43 @@
-
-
-
-
+
\ No newline at end of file
+
diff --git a/tests/Core/Rating/FluentRatingTests.razor b/tests/Core/Rating/FluentRatingTests.razor
index c115e0b4f0..f79e9b4e60 100644
--- a/tests/Core/Rating/FluentRatingTests.razor
+++ b/tests/Core/Rating/FluentRatingTests.razor
@@ -18,11 +18,31 @@
cut.Verify();
}
+ [Fact]
+ public void FluentRating_Label()
+ {
+ // Arrange && Act
+ var cut = Render(@ );
+
+ // Assert
+ cut.Verify();
+ }
+
+ [Fact]
+ public void FluentRating_LabelTemplate()
+ {
+ // Arrange && Act
+ var cut = Render(@My Label Template );
+
+ // Assert
+ cut.Verify();
+ }
+
[Fact]
public void FluentRating_Value()
{
// Arrange && Act
- var cut = Render(@ );
+ var cut = Render(@ );
// Assert
cut.Verify();
@@ -44,11 +64,25 @@
}
[Fact]
- public void FluentRating_AllowResetFalse()
+ public void FluentRating_FocusOnRadio()
{
int value = 0;
// Arrange
+ var cut = Render(@ );
+
+ // Act: Focus on the second star
+ cut.FindAll("input").ElementAt(1).Focus();
+
+ // Assert
+ Assert.Equal(2, value);
+ }
+
+ [Fact]
+ public void FluentRating_AllowResetFalse()
+ {
+ // Arrange
+ int value = 0;
var cut = Render(@ );
// Act: Click twice on the second star
@@ -62,9 +96,8 @@
[Fact]
public void FluentRating_AllowResetTrue()
{
- int value = 0;
-
// Arrange
+ int value = 0;
var cut = Render(@ );
// Act: Click twice on the second star
@@ -92,9 +125,9 @@
[Fact]
public void FluentRating_ReadOnly()
{
- int value = 3;
// Arrange
- var cut = Render(@ );
+ int value = 3;
+ var cut = Render(@ );
// Act: Click on the second star
cut.FindAll("svg").ElementAt(1).Click();
@@ -104,162 +137,66 @@
}
[Fact]
- public async Task FluentRating_KeyDown()
+ public async Task FluentRating_Hover()
{
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
- // Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(40, "ArrowDown", false, false, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(2, value);
- }
-
- [Fact]
- public async Task FluentRating_ShiftKeyDown()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
// Arrange
- var cut = Render(@ );
+ int value = 0;
+ int? hover = null;
+ var cut = Render(@ );
// Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(40, "ArrowDown", false, true, false, false, 0, "myrating"));
+ var item = cut.FindAll("svg").ElementAt(3);
+ await item.TriggerEventAsync("onmouseenter", new MouseEventArgs());
// Assert
Assert.Equal(0, value);
+ Assert.Equal(4, hover);
}
[Fact]
- public async Task FluentRating_KeyLeft()
+ public async Task FluentRating_HoverMultiple()
{
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
// Arrange
- var cut = Render(@ );
+ int value = 0;
+ int? hover = null;
+ var cut = Render(@ );
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(37, "ArrowLeft", false, false, false, false, 0, "myrating"));
+ // Enter on the 4th star
+ var item4 = cut.FindAll("svg").ElementAt(3);
+ await item4.TriggerEventAsync("onmouseenter", new MouseEventArgs());
- // Assert
- Assert.Equal(2, value);
- }
+ Assert.Equal(4, hover);
- [Fact]
- public async Task FluentRating_ShiftKeyLeft()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
+ // Enter on the 3rd star
+ var item3 = cut.FindAll("svg").ElementAt(2);
+ await item3.TriggerEventAsync("onmouseenter", new MouseEventArgs());
- int value = 3;
- // Arrange
- var cut = Render(@ );
+ Assert.Equal(3, hover);
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(37, "ArrowLeft", false, true, false, false, 0, "myrating"));
+ // Leave the 3rd star
+ await item3.TriggerEventAsync("onmouseleave", new MouseEventArgs());
- // Assert
Assert.Equal(0, value);
}
[Fact]
- public async Task FluentRating_KeyUp()
+ public async Task FluentRating_HoverAndLeave()
{
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
- // Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(38, "ArrowUp", false, false, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(4, value);
- }
-
- [Fact]
- public async Task FluentRating_ShiftKeyUp()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
- // Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(38, "ArrowUp", false, true, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(5, value);
- }
-
- [Fact]
- public async Task FluentRating_KeyRight()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
- // Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(39, "ArrowRight", false, false, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(4, value);
- }
-
- [Fact]
- public async Task FluentRating_ShiftKeyRight()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
// Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(39, "ArrowRight", false, true, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(5, value);
- }
-
- [Fact]
- public async Task FluentRating_KeyRandom()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
-
- int value = 3;
- // Arrange
- var cut = Render(@ );
-
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(65, "A", false, false, false, false, 0, "myrating"));
-
- // Assert
- Assert.Equal(3, value);
- }
+ int value = 0;
+ int? hover = null;
+ var cut = Render(@ );
- [Fact]
- public async Task FluentRating_KeyRandomWithDifferentId()
- {
- JSInterop.Mode = JSRuntimeMode.Loose;
+ // Act: Hover
+ var item = cut.FindAll("svg").ElementAt(3);
+ await item.TriggerEventAsync("onmouseenter", new MouseEventArgs());
- int value = 3;
- // Arrange
- var cut = Render(@ );
+ Assert.Equal(4, hover);
- // Act
- await cut.InvokeAsync(() => cut.FindComponent().Instance.OnKeyDownRaisedAsync(65, "A", false, false, false, false, 0, "myrating2"));
+ // Act: Leave
+ var rating = cut.Find("div");
+ await rating.TriggerEventAsync("onmouseleave", new MouseEventArgs());
- // Assert
- Assert.Equal(3, value);
+ Assert.Null(hover);
}
}