From 7b198530f06060ce92e2688523a62e23395234e0 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Wed, 4 Nov 2026 00:45:26 +0530 Subject: [PATCH] Feature #1331: Add RatingInput component Add a RatingInput form component (int, int?, decimal, decimal?) for selecting or displaying a score with Bootstrap Icons. Built with native radio inputs and CSS only; no JavaScript interop. - Value/ValueChanged/ValueExpression with EditForm validation styling - MaxValue, ReadOnly, Disabled, AllowClear, opt-in AllowHalf (decimal only) - Read-only fractional display rounded to the nearest half - FilledIconName/EmptyIconName/HalfIconName, RatingInputColor (default Warning), RatingInputSize (Default/Small/Large) - CSS-only hover preview, disabled for touch, read-only and disabled - radiogroup/radio semantics, aria-readonly/aria-disabled, exact-value description, localizable ItemLabelFormat ("{0} of {1}") - Arrow keys with wrap and RTL mirroring; single tab stop - Throws for unsupported TValue, AllowHalf on integers, MaxValue < 1 - Demo page (10 scenarios), docs-host page, screenshot, navigation and Index entries, docs/docs/04-forms/rating-input.mdx --- .../Layout/DemosMainLayout.razor.cs | 1 + .../Components/Layout/DocsMainLayout.razor.cs | 1 + .../Components/Layout/MainLayout.razor.cs | 1 + .../RatingInputDocumentation.razor | 188 +++++++++ .../RatingInput_Demo_01_Basic_Usage.razor | 8 + ...ngInput_Demo_02_Read_Only_Fractional.razor | 14 + .../RatingInput_Demo_03_Custom_Icons.razor | 21 + .../RatingInput_Demo_04_Sizes.razor | 9 + .../RatingInput_Demo_05_Colors.razor | 22 ++ .../RatingInput_Demo_06_Disabled.razor | 9 + .../RatingInput_Demo_07_Allow_Clear.razor | 8 + .../RatingInput_Demo_08_Allow_Half.razor | 8 + ...ingInput_Demo_09_EditForm_Validation.razor | 31 ++ ...ingInput_Demo_10_Events_ValueChanged.razor | 20 + .../RatingInput_Doc_01_Documentation.razor | 33 ++ .../Components/Pages/Home/Index.razor | 10 + .../Constants/DemoRouteConstants.cs | 2 + .../Constants/DemoScreenshotSrcConstants.cs | 1 + .../images/screenshots/rating-input.png | Bin 0 -> 19048 bytes .../Form/RatingInput/RatingInput.razor | 64 +++ .../Form/RatingInput/RatingInput.razor.cs | 373 ++++++++++++++++++ .../Enums/Color/RatingInputColor.cs | 91 +++++ blazorbootstrap/Enums/RatingInputSize.cs | 28 ++ blazorbootstrap/wwwroot/blazor.bootstrap.css | 95 ++++- docs/docs/04-forms/rating-input.mdx | 199 ++++++++++ 25 files changed, 1236 insertions(+), 1 deletion(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInputDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_01_Basic_Usage.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_02_Read_Only_Fractional.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_03_Custom_Icons.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_04_Sizes.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_05_Colors.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_06_Disabled.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_07_Allow_Clear.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_08_Allow_Half.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_09_EditForm_Validation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_10_Events_ValueChanged.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/RatingInput/RatingInput_Doc_01_Documentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/rating-input.png create mode 100644 blazorbootstrap/Components/Form/RatingInput/RatingInput.razor create mode 100644 blazorbootstrap/Components/Form/RatingInput/RatingInput.razor.cs create mode 100644 blazorbootstrap/Enums/Color/RatingInputColor.cs create mode 100644 blazorbootstrap/Enums/RatingInputSize.cs create mode 100644 docs/docs/04-forms/rating-input.mdx diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index 2121bed2b..50791dba8 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -28,6 +28,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Demos_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, + new (){ Id = "491", Text = "Rating Input", Href = DemoRouteConstants.Demos_URL_RatingInput, IconName = IconName.StarHalf, ParentId = "4" }, new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, //new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Demos_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "416", Text = "RichTextEditor", Href = DemoRouteConstants.Demos_URL_RichTextEditor, IconName = IconName.Type, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index 5efd1fb9f..8d0c60a3f 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -27,6 +27,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Docs_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Docs_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, + new (){ Id = "491", Text = "Rating Input", Href = DemoRouteConstants.Docs_URL_RatingInput, IconName = IconName.StarHalf, ParentId = "4" }, new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Docs_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, //new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Docs_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "416", Text = "RichTextEditor", Href = DemoRouteConstants.Docs_URL_RichTextEditor, IconName = IconName.Type, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index 603f63583..dc49cdc30 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -27,6 +27,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, + new (){ Id = "491", Text = "Rating Input", Href = DemoRouteConstants.Demos_URL_RatingInput, IconName = IconName.StarHalf, ParentId = "4" }, new (){ Id = "408", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, //new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Demos_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "409", Text = "Switch", Href = DemoRouteConstants.Demos_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInputDocumentation.razor new file mode 100644 index 000000000..70a183a9d --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInputDocumentation.razor @@ -0,0 +1,188 @@ +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + + + RatingInput renders Bootstrap Icons. Reference the Bootstrap Icons stylesheet in your app, because Blazor Bootstrap does not bundle it. + + +
+
+ RatingInput lets users pick a score by clicking an icon or using the arrow keys. +
+ How to use: +
+
    +
  1. Add RatingInput and set TValue to int, int?, decimal, or decimal?.
  2. +
  3. Bind the score with @@bind-Value.
  4. +
  5. Optionally set MaxValue to change the number of icons (default 5).
  6. +
  7. Give the group an accessible name with aria-label or aria-labelledby.
  8. +
+
+ This demo binds an int score out of 5. +
+ +
+ +
+
+ Set ReadOnly to display a score that users cannot change. Fractional values are shown to the nearest half, and assistive technology announces the exact value. +
+ How to use: +
+
    +
  1. Set ReadOnly="true".
  2. +
  3. Bind a decimal value such as 4.3m.
  4. +
  5. Expect the display to round to the nearest half: 4.3 shows 4½ and 4.2 shows 4.
  6. +
+
+ This demo shows three read-only averages. +
+ +
+ +
+
+ Replace the default stars with any Bootstrap icon. +
+ How to use: +
+
    +
  1. Set FilledIconName for selected items.
  2. +
  3. Set EmptyIconName for unselected items.
  4. +
  5. Set HalfIconName for half-filled items shown for fractional values.
  6. +
+
+ This demo uses hearts and circles. +
+ +
+ +
+
+ Change the icon size with the Size parameter. +
+ How to use: +
+
    +
  1. Set Size to RatingInputSize.Small, RatingInputSize.Default, or RatingInputSize.Large.
  2. +
+
+ This demo shows each size. +
+ +
+ +
+
+ Change the icon color with the Color parameter. The default is RatingInputColor.Warning. +
+ How to use: +
+
    +
  1. Set Color to a RatingInputColor value such as RatingInputColor.Primary.
  2. +
  3. Use RatingInputColor.None to inherit the surrounding text color.
  4. +
+
+ This demo shows the theme colors. +
+ +
+ +
+
+ A disabled rating input cannot be focused or changed. +
+ How to use: +
+
    +
  1. Set Disabled="true".
  2. +
+
+ This demo toggles the disabled state. +
+ +
+ +
+
+ With AllowClear, clicking the selected icon again clears the value. +
+ How to use: +
+
    +
  1. Set AllowClear="true".
  2. +
  3. Click the selected icon to clear it. The value becomes null for nullable types and 0 otherwise.
  4. +
+
+ This demo binds an int? value. +
+ +
+ +
+
+ With AllowHalf, users can select half values. This works only with decimal and decimal?; integer types throw an exception. +
+ How to use: +
+
    +
  1. Set TValue to decimal or decimal?.
  2. +
  3. Set AllowHalf="true".
  4. +
  5. Click the left half of an icon, or use the arrow keys, to select .5 values.
  6. +
+
+ This demo binds a decimal value in half steps. +
+ +
+ +
+
+ RatingInput takes part in EditForm validation like other form inputs. +
+ How to use: +
+
    +
  1. Place RatingInput inside an EditForm with a DataAnnotationsValidator.
  2. +
  3. Bind a model property with @@bind-Value.
  4. +
  5. Add a ValidationMessage for the property.
  6. +
+
+ This demo requires a score before the form can be submitted. +
+ +
+ +
+
+ ValueChanged fires once for each change made by the user, and never when the component is read-only or disabled. +
+ How to use: +
+
    +
  1. Set Value and handle ValueChanged.
  2. +
  3. Update your field in the handler.
  4. +
+
+ This demo logs each change. +
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_RatingInput; + private const string pageTitle = "Blazor RatingInput"; + private const string pageDescription = "The Blazor Bootstrap RatingInput component lets users select or display a score using icons."; + private const string metaTitle = "Blazor RatingInput Component"; + private const string metaDescription = "The Blazor Bootstrap RatingInput component lets users select or display a score using icons."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_RatingInput; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_01_Basic_Usage.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_01_Basic_Usage.razor new file mode 100644 index 000000000..e685af99d --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_01_Basic_Usage.razor @@ -0,0 +1,8 @@ +
+ + @score of 5 +
+ +@code { + private int score = 3; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_02_Read_Only_Fractional.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_02_Read_Only_Fractional.razor new file mode 100644 index 000000000..d8b896a77 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_02_Read_Only_Fractional.razor @@ -0,0 +1,14 @@ +
+
+ + 4.3 +
+
+ + 3.75 +
+
+ + 4.2 +
+
diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_03_Custom_Icons.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_03_Custom_Icons.razor new file mode 100644 index 000000000..685b76152 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_03_Custom_Icons.razor @@ -0,0 +1,21 @@ +
+ + +
+ +@code { + private int hearts = 4; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_04_Sizes.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_04_Sizes.razor new file mode 100644 index 000000000..b2a55ea7c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_04_Sizes.razor @@ -0,0 +1,9 @@ +
+ + + +
+ +@code { + private int score = 3; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_05_Colors.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_05_Colors.razor new file mode 100644 index 000000000..ef582d5a9 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_05_Colors.razor @@ -0,0 +1,22 @@ +
+ @foreach (var color in colors) + { +
+ + RatingInputColor.@color +
+ } +
+ +@code { + private readonly RatingInputColor[] colors = + { + RatingInputColor.Primary, + RatingInputColor.Secondary, + RatingInputColor.Success, + RatingInputColor.Danger, + RatingInputColor.Warning, + RatingInputColor.Info, + RatingInputColor.Dark + }; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_06_Disabled.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_06_Disabled.razor new file mode 100644 index 000000000..894c3dc0e --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_06_Disabled.razor @@ -0,0 +1,9 @@ +
+ +
+ + +@code { + private int score = 2; + private bool disabled = true; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_07_Allow_Clear.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_07_Allow_Clear.razor new file mode 100644 index 000000000..8b9137067 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_07_Allow_Clear.razor @@ -0,0 +1,8 @@ +
+ + Value: @(score?.ToString() ?? "null") +
+ +@code { + private int? score = 3; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_08_Allow_Half.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_08_Allow_Half.razor new file mode 100644 index 000000000..01dbc90fe --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_08_Allow_Half.razor @@ -0,0 +1,8 @@ +
+ + Value: @score +
+ +@code { + private decimal score = 3.5m; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_09_EditForm_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_09_EditForm_Validation.razor new file mode 100644 index 000000000..6d2d7b012 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_09_EditForm_Validation.razor @@ -0,0 +1,31 @@ +@using System.ComponentModel.DataAnnotations + + + +
+ +
+ +
+ +
+ +
+ +@if (submitted) +{ +
Thanks! You rated @review.Score of 5.
+} + +@code { + private Review review = new(); + private bool submitted; + + private void HandleValidSubmit() => submitted = true; + + public class Review + { + [Required(ErrorMessage = "Please select a score.")] + public int? Score { get; set; } + } +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_10_Events_ValueChanged.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_10_Events_ValueChanged.razor new file mode 100644 index 000000000..f1a43d761 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/RatingInput/RatingInput_Demo_10_Events_ValueChanged.razor @@ -0,0 +1,20 @@ +
+ +
+
    + @foreach (var entry in log) + { +
  • @entry
  • + } +
+ +@code { + private int score = 2; + private readonly List log = new(); + + private void OnScoreChanged(int value) + { + score = value; // this is mandatory + log.Add($"ValueChanged: {value}"); + } +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/RatingInput/RatingInput_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/RatingInput/RatingInput_Doc_01_Documentation.razor new file mode 100644 index 000000000..440a72332 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/RatingInput/RatingInput_Doc_01_Documentation.razor @@ -0,0 +1,33 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ @metaTitle +
+ +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(RatingInput); + private const string pageUrl = DemoRouteConstants.Docs_URL_RatingInput; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_RatingInput; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index 67cf8f50f..56a9e4af1 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -210,6 +210,11 @@

Radio Input

+ +

Range Input

diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index 65ab47f61..084e4bc6f 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -37,6 +37,7 @@ public static class DemoRouteConstants public const string Demos_URL_OTPInput = Demos_URL_Forms_Prefix + "/otp-input"; public const string Demos_URL_PasswordInput = Demos_URL_Forms_Prefix + "/password-input"; public const string Demos_URL_RadioInput = Demos_URL_Forms_Prefix + "/radio-input"; + public const string Demos_URL_RatingInput = Demos_URL_Forms_Prefix + "/rating-input"; public const string Demos_URL_RangeInput = Demos_URL_Forms_Prefix + "/range-input"; public const string Demos_URL_RichTextEditor = Demos_URL_Forms_Prefix + "/rich-text-editor"; public const string Demos_URL_SelectInput = Demos_URL_Forms_Prefix + "/select-input"; @@ -154,6 +155,7 @@ public static class DemoRouteConstants public const string Docs_URL_OTPInput = Docs_URL_Forms_Prefix + "/otp-input"; public const string Docs_URL_PasswordInput = Docs_URL_Forms_Prefix + "/password-input"; public const string Docs_URL_RadioInput = Docs_URL_Forms_Prefix + "/radio-input"; + public const string Docs_URL_RatingInput = Docs_URL_Forms_Prefix + "/rating-input"; public const string Docs_URL_RangeInput = Docs_URL_Forms_Prefix + "/range-input"; public const string Docs_URL_RichTextEditor = Docs_URL_Forms_Prefix + "/rich-text-editor"; public const string Docs_URL_SelectInput = Docs_URL_Forms_Prefix + "/select-input"; diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs index 1a06c1222..6053b45b9 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs @@ -29,6 +29,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_OTPInput = DemoScreenshotSrcPrefix + "otp-input.png"; public const string Demos_URL_PasswordInput = DemoScreenshotSrcPrefix + "password-input.png"; public const string Demos_URL_RadioInput = DemoScreenshotSrcPrefix + "radio-input.png"; + public const string Demos_URL_RatingInput = DemoScreenshotSrcPrefix + "rating-input.png"; public const string Demos_URL_RangeInput = DemoScreenshotSrcPrefix + "range-input.png"; public const string Demos_URL_SelectInput = DemoScreenshotSrcPrefix + "select-input.png"; public const string Demos_URL_RichTextEditor = DemoScreenshotSrcPrefix + "rich-text-editor.png"; diff --git a/BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/rating-input.png b/BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/rating-input.png new file mode 100644 index 0000000000000000000000000000000000000000..9b3b6028db59bc379c38afd949bf07e9c1325d5b GIT binary patch literal 19048 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 literal 0 HcmV?d00001 diff --git a/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor b/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor new file mode 100644 index 000000000..259c87510 --- /dev/null +++ b/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor @@ -0,0 +1,64 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase +@typeparam TValue + +
+ @if (ReadOnly) + { + @for (var i = 1; i <= MaxValue; i++) + { + var itemValue = (decimal)i; + var isChecked = itemValue == readOnlyCheckedValue; + + @RenderIcons(isHalfItem: false) + + } + } + else + { + @foreach (var itemValue in GetSelectableValues()) + { + var selectedValue = itemValue; + var radioId = $"{Id}-{selectedValue.ToString(CultureInfo.InvariantCulture)}"; + var isStartHalf = isHalfStepEnabled && selectedValue % 1 != 0; + + + } + } + @GetItemLabel(descriptionValue) +
+ +@code { + private RenderFragment RenderIcons(bool isHalfItem) => __builder => + { + + @if (!isHalfItem) + { + + } + + }; +} diff --git a/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor.cs b/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor.cs new file mode 100644 index 000000000..83a1cc394 --- /dev/null +++ b/blazorbootstrap/Components/Form/RatingInput/RatingInput.razor.cs @@ -0,0 +1,373 @@ +namespace BlazorBootstrap; + +/// +/// Represents a Blazor component that lets users select or display a score using icons. +/// +/// The type of the rating value. +/// +/// Supported types for TValue: int, int?, decimal, decimal? +/// +[AddedVersion("4.1.0")] +public partial class RatingInput : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private decimal? currentValue; + + private string descriptionId = default!; + + private decimal descriptionValue; + + private decimal displayValue; + + private FieldIdentifier fieldIdentifier; + + private decimal readOnlyCheckedValue; + + private decimal readOnlyTabStopValue; + + #endregion + + #region Methods + + #region Protected Override Methods + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (ValueExpression is not null) + fieldIdentifier = FieldIdentifier.Create(ValueExpression); + } + + protected override void OnParametersSet() + { + if (!(typeof(TValue) == typeof(int) + || typeof(TValue) == typeof(int?) + || typeof(TValue) == typeof(decimal) + || typeof(TValue) == typeof(decimal?) + )) + throw new InvalidOperationException($"{typeof(TValue)} is not supported."); + + if (AllowHalf && IsIntegerType()) + throw new InvalidOperationException($"The {nameof(AllowHalf)} parameter is supported only for decimal and decimal? values. {typeof(TValue)} cannot hold half values."); + + if (MaxValue < 1) + throw new InvalidOperationException($"The {nameof(MaxValue)} parameter value must be greater than or equal to 1."); + + descriptionId = $"{Id}-description"; + currentValue = ToDecimal(Value); + UpdateDisplayState(); + + base.OnParametersSet(); + } + + #endregion + + #region Private Methods + + private TValue ConvertToValue(decimal? value) + { + if (typeof(TValue) == typeof(int)) + return (TValue)(object)(int)(value ?? 0); + + if (typeof(TValue) == typeof(int?)) + return value.HasValue ? (TValue)(object)(int)value.Value : default!; + + if (typeof(TValue) == typeof(decimal)) + return (TValue)(object)(value ?? 0m); + + return value.HasValue ? (TValue)(object)value.Value : default!; + } + + private string GetColorClass() => + Color switch + { + RatingInputColor.Primary => TextColor.Primary.ToTextColorClass(), + RatingInputColor.Secondary => TextColor.Secondary.ToTextColorClass(), + RatingInputColor.Success => TextColor.Success.ToTextColorClass(), + RatingInputColor.Danger => TextColor.Danger.ToTextColorClass(), + RatingInputColor.Warning => TextColor.Warning.ToTextColorClass(), + RatingInputColor.Info => TextColor.Info.ToTextColorClass(), + RatingInputColor.Light => TextColor.Light.ToTextColorClass(), + RatingInputColor.Dark => TextColor.Dark.ToTextColorClass(), + RatingInputColor.Body => TextColor.Body.ToTextColorClass(), + RatingInputColor.Muted => TextColor.Muted.ToTextColorClass(), + RatingInputColor.White => TextColor.White.ToTextColorClass(), + _ => "" + }; + + private string GetItemClassNames(decimal itemValue, bool isHalfItem, bool isStartHalf) => + BuildClassNames( + ("bb-rating-input-item", true), + ("bb-rating-input-item-start", isHalfItem && isStartHalf), + ("bb-rating-input-item-end", isHalfItem && !isStartHalf), + (GetItemStateClass(itemValue, isHalfItem), true)); + + private string GetItemLabel(decimal value) => + string.Format(CultureInfo.CurrentCulture, ItemLabelFormat, NormalizeDecimal(value), MaxValue); + + private string GetItemStateClass(decimal itemValue, bool isHalfItem) + { + if (displayValue >= itemValue) + return "bb-rating-input-item-filled"; + + if (!isHalfItem && displayValue == itemValue - 0.5m) + return "bb-rating-input-item-half"; + + return "bb-rating-input-item-empty"; + } + + private IEnumerable GetSelectableValues() + { + var step = isHalfStepEnabled ? 0.5m : 1m; + + for (var value = step; value <= MaxValue; value += step) + yield return value; + } + + private bool IsIntegerType() => typeof(TValue) == typeof(int) || typeof(TValue) == typeof(int?); + + /// + /// Removes trailing zeros so labels read "3" and "3.5" instead of "3.0" and "3.50". + /// + private static decimal NormalizeDecimal(decimal value) => value / 1.0000000000000000000000000000m; + + private async Task OnItemChangedAsync(decimal itemValue) + { + if (ReadOnly || Disabled) + return; + + await SetValueAsync(itemValue); + } + + private async Task OnItemClickedAsync(decimal itemValue) + { + if (!AllowClear || ReadOnly || Disabled || itemValue != currentValue) + return; + + await SetValueAsync(null); + } + + private async Task SetValueAsync(decimal? newValue) + { + var value = ConvertToValue(newValue); + + if (EqualityComparer.Default.Equals(value, Value)) + return; + + Value = value; + currentValue = ToDecimal(Value); + UpdateDisplayState(); + + await ValueChanged.InvokeAsync(Value); + + if (fieldIdentifier.FieldName is not null) + EditContext?.NotifyFieldChanged(fieldIdentifier); + } + + private static decimal? ToDecimal(TValue value) => + value switch + { + int intValue => intValue, + decimal decimalValue => decimalValue, + _ => null + }; + + private void UpdateDisplayState() + { + descriptionValue = Math.Clamp(currentValue ?? 0m, 0m, MaxValue); + displayValue = Math.Round(descriptionValue * 2, MidpointRounding.AwayFromZero) / 2; + readOnlyCheckedValue = Math.Round(descriptionValue, MidpointRounding.AwayFromZero); + readOnlyTabStopValue = readOnlyCheckedValue > 0 ? readOnlyCheckedValue : 1; + } + + #endregion + + #endregion + + #region Properties, Indexers + + /// + /// Gets or sets whether clicking the currently selected item clears the value. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(false)] + [Description("Gets or sets whether clicking the currently selected item clears the value to empty (null for nullable types, 0 otherwise).")] + [Parameter] + public bool AllowClear { get; set; } + + /// + /// Gets or sets whether half values can be selected. Supported only for decimal and decimal? values. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(false)] + [Description("Gets or sets whether half values can be selected. Supported only for decimal and decimal? values.")] + [Parameter] + public bool AllowHalf { get; set; } + + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-rating-input", true), + ("bb-rating-input-interactive", !ReadOnly && !Disabled), + ("bb-rating-input-readonly", ReadOnly), + ("bb-rating-input-disabled", Disabled), + ("bb-rating-input-sm", Size == RatingInputSize.Small), + ("bb-rating-input-lg", Size == RatingInputSize.Large), + (GetColorClass(), Color != RatingInputColor.None), + (fieldCssClasses, true)); + + /// + /// Gets or sets the color of the rating icons. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(RatingInputColor.Warning)] + [Description("Gets or sets the color of the rating icons.")] + [Parameter] + public RatingInputColor Color { get; set; } = RatingInputColor.Warning; + + /// + /// Gets or sets the disabled state. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(false)] + [Description("Gets or sets whether the rating input is disabled. When true, users cannot focus or change it.")] + [Parameter] + public bool Disabled { get; set; } + + [CascadingParameter] private EditContext EditContext { get; set; } = default!; + + /// + /// Gets or sets the icon used for empty items. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(IconName.Star)] + [Description("Gets or sets the icon used for empty items.")] + [Parameter] + public IconName EmptyIconName { get; set; } = IconName.Star; + + private string fieldCssClasses => + EditContext is not null && fieldIdentifier.FieldName is not null + ? EditContext.FieldCssClass(fieldIdentifier) + : ""; + + /// + /// Gets or sets the icon used for filled items. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(IconName.StarFill)] + [Description("Gets or sets the icon used for filled items.")] + [Parameter] + public IconName FilledIconName { get; set; } = IconName.StarFill; + + /// + /// Gets or sets the icon used for half-filled items when a fractional value is displayed. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(IconName.StarHalf)] + [Description("Gets or sets the icon used for half-filled items when a fractional value is displayed.")] + [Parameter] + public IconName HalfIconName { get; set; } = IconName.StarHalf; + + private bool isHalfStepEnabled => AllowHalf && !IsIntegerType(); + + /// + /// Gets or sets the format of each item's accessible label and of the value description. + /// Placeholder {0} is the item value and {1} is . + /// + /// Default value is "{0} of {1}". + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue("{0} of {1}")] + [Description("Gets or sets the format of each item's accessible label and of the value description. Placeholder {0} is the item value and {1} is the maximum value.")] + [Parameter] + public string ItemLabelFormat { get; set; } = "{0} of {1}"; + + /// + /// Gets or sets the number of rating items. + /// + /// Default value is 5. + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(5)] + [Description("Gets or sets the number of rating items. Must be greater than or equal to 1.")] + [Parameter] + public int MaxValue { get; set; } = 5; + + /// + /// Gets or sets the read-only state. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(false)] + [Description("Gets or sets whether the rating input is read-only. When true, the value is displayed, including half values, and cannot be changed.")] + [Parameter] + public bool ReadOnly { get; set; } + + /// + /// Gets or sets the size of the rating icons. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(RatingInputSize.Default)] + [Description("Gets or sets the size of the rating icons.")] + [Parameter] + public RatingInputSize Size { get; set; } = RatingInputSize.Default; + + /// + /// Gets or sets the rating value. + /// + [AddedVersion("4.1.0")] + [Description("Gets or sets the current rating value.")] + [Parameter] + public TValue Value { get; set; } = default!; + + /// + /// This event fires when the user changes the rating value. + /// + [AddedVersion("4.1.0")] + [Description("Fires once whenever user interaction changes the rating value. It does not fire when the component is read-only or disabled.")] + [Parameter] + public EventCallback ValueChanged { get; set; } + + /// + /// Gets or sets an expression that identifies the bound value. + /// + /// Default value is . + /// + /// + [AddedVersion("4.1.0")] + [DefaultValue(null)] + [Description("Gets or sets the expression that identifies the bound value for validation and EditContext notifications.")] + [Parameter] + public Expression> ValueExpression { get; set; } = default!; + + #endregion +} diff --git a/blazorbootstrap/Enums/Color/RatingInputColor.cs b/blazorbootstrap/Enums/Color/RatingInputColor.cs new file mode 100644 index 000000000..17cd14efd --- /dev/null +++ b/blazorbootstrap/Enums/Color/RatingInputColor.cs @@ -0,0 +1,91 @@ +namespace BlazorBootstrap; + +/// +/// Defines the supported icon colors for a . +/// +public enum RatingInputColor +{ + /// + /// Indicates no color is applied. Default color is inherited from parent element. + /// + [AddedVersion("4.1.0")] + [Description("Indicates no color is applied. Default color is inherited from parent element.")] + None, + + /// + /// Indicates the rating icons should use the primary color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the primary color.")] + Primary, + + /// + /// Indicates the rating icons should use the secondary color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the secondary color.")] + Secondary, + + /// + /// Indicates the rating icons should use the success color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the success color.")] + Success, + + /// + /// Indicates the rating icons should use the danger color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the danger color.")] + Danger, + + /// + /// Indicates the rating icons should use the warning color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the warning color.")] + Warning, + + /// + /// Indicates the rating icons should use the info color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the info color.")] + Info, + + /// + /// Indicates the rating icons should use the light color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the light color.")] + Light, + + /// + /// Indicates the rating icons should use the dark color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the dark color.")] + Dark, + + /// + /// Indicates the rating icons should use the body text color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the body text color.")] + Body, + + /// + /// Indicates the rating icons should use the muted text color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the muted text color.")] + Muted, + + /// + /// Indicates the rating icons should use the white color. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the rating icons should use the white color.")] + White +} diff --git a/blazorbootstrap/Enums/RatingInputSize.cs b/blazorbootstrap/Enums/RatingInputSize.cs new file mode 100644 index 000000000..307755bf4 --- /dev/null +++ b/blazorbootstrap/Enums/RatingInputSize.cs @@ -0,0 +1,28 @@ +namespace BlazorBootstrap; + +/// +/// Defines the supported sizes for a . +/// +public enum RatingInputSize +{ + /// + /// Indicates the default rating input size. + /// + [AddedVersion("4.1.0")] + [Description("Indicates the default rating input size.")] + Default, + + /// + /// Indicates a small rating input. + /// + [AddedVersion("4.1.0")] + [Description("Indicates a small rating input.")] + Small, + + /// + /// Indicates a large rating input. + /// + [AddedVersion("4.1.0")] + [Description("Indicates a large rating input.")] + Large +} diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 225561f1f..5eb306ce3 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -840,4 +840,97 @@ main { .bb-sortable-list-handle .bi-grip-vertical::before { cursor: grab !important; -} \ No newline at end of file +} + +/* rating-input */ +.bb-rating-input { + position: relative; + display: inline-flex; + align-items: center; + font-size: 1.5rem; + line-height: 1; +} + +.bb-rating-input-sm { + font-size: 1rem; +} + +.bb-rating-input-lg { + font-size: 2rem; +} + +.bb-rating-input-item { + position: relative; + display: inline-block; + width: 1.25em; + height: 1em; + overflow: hidden; +} + +.bb-rating-input-item-start { + width: .5em; +} + +.bb-rating-input-item-end { + width: .75em; +} + +.bb-rating-input-icon { + position: absolute; + top: 0; + inset-inline-start: 0; + display: none; + width: 1em; + height: 1em; +} + +.bb-rating-input-item-end .bb-rating-input-icon { + inset-inline-start: -.5em; +} + +.bb-rating-input-icon .bi::before { + vertical-align: top; +} + +.bb-rating-input-item-empty .bb-rating-input-icon-empty, +.bb-rating-input-item-half .bb-rating-input-icon-half, +.bb-rating-input-item-filled .bb-rating-input-icon-filled { + display: block; +} + +.bb-rating-input-interactive .bb-rating-input-item { + cursor: pointer; +} + +.bb-rating-input-disabled { + opacity: .65; +} + +.bb-rating-input-disabled .bb-rating-input-item { + cursor: not-allowed; +} + +.bb-rating-input-radio:focus-visible + .bb-rating-input-item, +.bb-rating-input-readonly .bb-rating-input-item:focus-visible { + outline: 2px solid var(--bs-focus-ring-color, rgba(13, 110, 253, .5)); + outline-offset: 1px; + border-radius: .125em; +} + +@media (hover: hover) { + .bb-rating-input-interactive:hover .bb-rating-input-item .bb-rating-input-icon { + display: none; + } + + .bb-rating-input-interactive:hover .bb-rating-input-item .bb-rating-input-icon-filled { + display: block; + } + + .bb-rating-input-interactive .bb-rating-input-item:hover ~ .bb-rating-input-item .bb-rating-input-icon-filled { + display: none; + } + + .bb-rating-input-interactive .bb-rating-input-item:hover ~ .bb-rating-input-item .bb-rating-input-icon-empty { + display: block; + } +} diff --git a/docs/docs/04-forms/rating-input.mdx b/docs/docs/04-forms/rating-input.mdx new file mode 100644 index 000000000..95582f7c2 --- /dev/null +++ b/docs/docs/04-forms/rating-input.mdx @@ -0,0 +1,199 @@ +--- +title: Blazor Rating Input Component +description: The Blazor Bootstrap RatingInput component lets users select or display a score using icons, with EditForm validation, half values, and keyboard support. + +sidebar_label: Rating Input +sidebar_position: 11.5 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor Rating Input + +The Blazor Bootstrap `RatingInput` component lets users select or display a score using icons, stars by default. It is built with native radio inputs and works without JavaScript. + + + +:::info +`RatingInput` renders Bootstrap Icons. Reference the Bootstrap Icons stylesheet in your app, because Blazor Bootstrap does not bundle it. +::: + +Supported types for `TValue`: `int`, `int?`, `decimal`, `decimal?`. Any other type throws an `InvalidOperationException`. + +## Parameters + +| Name | Type | Default | Required | Description | Added Version | +|:--|:--|:--|:--|:--|:--| +| AllowClear | bool | false | | Gets or sets whether clicking the currently selected item clears the value to empty (null for nullable types, 0 otherwise). | 4.1.0 | +| AllowHalf | bool | false | | Gets or sets whether half values can be selected. Supported only for decimal and decimal? values. | 4.1.0 | +| Color | `RatingInputColor` | `RatingInputColor.Warning` | | Gets or sets the color of the rating icons. | 4.1.0 | +| Disabled | bool | false | | Gets or sets whether the rating input is disabled. When true, users cannot focus or change it. | 4.1.0 | +| EmptyIconName | `IconName` | `IconName.Star` | | Gets or sets the icon used for empty items. | 4.1.0 | +| FilledIconName | `IconName` | `IconName.StarFill` | | Gets or sets the icon used for filled items. | 4.1.0 | +| HalfIconName | `IconName` | `IconName.StarHalf` | | Gets or sets the icon used for half-filled items when a fractional value is displayed. | 4.1.0 | +| ItemLabelFormat | string | `"{0} of {1}"` | | Gets or sets the format of each item's accessible label and of the value description. Placeholder `{0}` is the item value and `{1}` is the maximum value. | 4.1.0 | +| MaxValue | int | 5 | | Gets or sets the number of rating items. Must be greater than or equal to 1. | 4.1.0 | +| ReadOnly | bool | false | | Gets or sets whether the rating input is read-only. When true, the value is displayed, including half values, and cannot be changed. | 4.1.0 | +| Size | `RatingInputSize` | `RatingInputSize.Default` | | Gets or sets the size of the rating icons. | 4.1.0 | +| Value | TValue | | | Gets or sets the current rating value. | 4.1.0 | +| ValueExpression | `Expression>` | null | | Gets or sets the expression that identifies the bound value for validation and EditContext notifications. | 4.1.0 | + +## Events + +| Name | Description | Added Version | +|:--|:--|:--| +| ValueChanged | Fires once whenever user interaction changes the rating value. It does not fire when the component is read-only or disabled. | 4.1.0 | + +## Accessibility + +- The component renders a `radiogroup`. Give it an accessible name with `aria-label` or `aria-labelledby`. +- Each item is a radio labelled with `ItemLabelFormat`, for example "3 of 5". The group's description announces the exact value, for example "4.3 of 5". +- **Tab** moves focus into and out of the group in a single stop. **Arrow keys** change the value and wrap at the ends; Left and Right are mirrored in right-to-left layouts. +- A read-only rating can be focused but not changed. A disabled rating cannot be focused. + +## Examples + +### Basic usage + +```cshtml {2} showLineNumbers +
+ + @score of 5 +
+``` + +```cs {} showLineNumbers +@code { + private int score = 3; +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#basic-usage) + +### Read-only fractional values + +Set `ReadOnly` to display a score that users cannot change. Fractional values are shown to the nearest half: 4.3 shows 4½ and 4.2 shows 4. + +```cshtml {} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#read-only-fractional-values) + +### Custom icons + +Use `FilledIconName`, `EmptyIconName`, and `HalfIconName` to replace the default stars. + +```cshtml {3-5} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#custom-icons) + +### Sizes + +Set `Size` to `RatingInputSize.Small`, `RatingInputSize.Default`, or `RatingInputSize.Large`. + +```cshtml {} showLineNumbers + + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#sizes) + +### Colors + +Set `Color` to a `RatingInputColor` value. Use `RatingInputColor.None` to inherit the surrounding text color. + +```cshtml {} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#colors) + +### Disabled + +```cshtml {} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#disabled) + +### Allow clear + +With `AllowClear`, clicking the selected icon again clears the value. + +```cshtml {} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#allow-clear) + +### Allow half + +With `AllowHalf`, users can select half values by clicking the left half of an icon or using the arrow keys. + +:::caution +`AllowHalf` is supported only for `decimal` and `decimal?`. Setting it with `int` or `int?` throws an `InvalidOperationException`. +::: + +```cshtml {} showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#allow-half) + +### EditForm validation + +```cshtml {5} showLineNumbers + + + +
+ +
+ + +
+``` + +```cs {} showLineNumbers +@code { + private Review review = new(); + + private void HandleValidSubmit() { /* save the review */ } + + public class Review + { + [Required(ErrorMessage = "Please select a score.")] + public int? Score { get; set; } + } +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#edit-form-validation) + +### Events: ValueChanged + +```cshtml {} showLineNumbers + +``` + +```cs {} showLineNumbers +@code { + private int score = 2; + + private void OnScoreChanged(int value) + { + score = value; // this is mandatory + } +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/rating-input#events-value-changed)