diff --git a/BitBlazor.sln b/BitBlazor.sln index 240f987..b80f897 100644 --- a/BitBlazor.sln +++ b/BitBlazor.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.12.35527.113 d17.12 +# Visual Studio Version 18 +VisualStudioVersion = 18.2.11415.280 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject @@ -47,6 +47,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utilities", "utilities", "{ docs\utilities\icon.md = docs\utilities\icon.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "form", "form", "{F518AFB8-A63F-43AA-A476-7580B228283C}" + ProjectSection(SolutionItems) = preProject + docs\form\checkbox.md = docs\form\checkbox.md + docs\form\datepicker.md = docs\form\datepicker.md + docs\form\form-components.md = docs\form\form-components.md + docs\form\number-field.md = docs\form\number-field.md + docs\form\password-field.md = docs\form\password-field.md + docs\form\radio.md = docs\form\radio.md + docs\form\select-field.md = docs\form\select-field.md + docs\form\text-area-field.md = docs\form\text-area-field.md + docs\form\text-field.md = docs\form\text-field.md + docs\form\timepicker.md = docs\form\timepicker.md + docs\form\toggle.md = docs\form\toggle.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -103,6 +118,7 @@ Global {83213F81-05BC-4E9D-8484-95061A10EC05} = {82151F3B-8000-4279-8313-B91EE0FCCE63} {06530751-809C-4345-9E87-E218FC5B1A03} = {19614F26-5CCD-41B4-9B79-81A8A88217CC} {8E852CC8-E88D-402A-8F0B-B57794EA8F69} = {19614F26-5CCD-41B4-9B79-81A8A88217CC} + {F518AFB8-A63F-43AA-A476-7580B228283C} = {19614F26-5CCD-41B4-9B79-81A8A88217CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {62D0C14D-C5B1-48FB-AD7E-810A9B818C32} diff --git a/docs/README.md b/docs/README.md index 028988e..1d2699e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,6 +97,14 @@ Numeric input field component with increment/decrement controls. - Min/max value constraints - Custom step values and symbol content +#### [BitSelectField](form/select-field.md) +Dropdown select input field component. +- Generic type support for any value type +- Support for option grouping +- Disabled options and groups +- Form validation integration +- Accessible and responsive + #### [BitCheckbox](form/checkbox.md) Checkbox component for binary choices. - Boolean value selection diff --git a/docs/form/form-components.md b/docs/form/form-components.md index 900a82f..3888378 100644 --- a/docs/form/form-components.md +++ b/docs/form/form-components.md @@ -21,6 +21,7 @@ BitBlazor provides a comprehensive set of form components that integrate with: | [`BitPasswordField`](password-field.md) | Password input with toggle | Show/hide functionality, secure input | | [`BitTextAreaField`](text-area-field.md) | Multi-line text input | Configurable rows, auto-sizing | | [`BitNumberField`](number-field.md) | Numeric input with controls | Increment/decrement buttons, type safety, min/max | +| [`BitSelectField`](select-field.md) | Dropdown select input | Generic type support, option grouping, form validation | | [`BitDatepicker`](datepicker.md) | Date input with picker | DateTime/DateOnly support, native browser UI, validation | | [`BitTimepicker`](timepicker.md) | Time input with picker | TimeOnly support, native browser UI, validation | | [`BitCheckbox`](checkbox.md) | Boolean checkbox input | Inline/grouped layouts, form validation | @@ -152,6 +153,18 @@ All input components support three sizes: @bind-Value="registrationModel.Phone" For="@(() => registrationModel.Phone)" /> +
+ + Select your country... + Italy + United States + United Kingdom + France + Germany + +
` component is designed to handle selection from a list of options and provides built-in support for form integration and validation. It is a generic component that can work with any data type supported by Blazor's value converters (or with an appropriate custom converter) and supports option grouping, disabled options, and accessibility attributes. + +## Parameters + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `Value` | `T?` | ✗ | `null` | The current value of the select field | +| `ValueChanged` | `EventCallback` | ✗ | - | Callback fired when the value changes | +| `ValueExpression` | `Expression>?` | ✗ | - | Expression for model binding and validation | +| `Label` | `string` | ✓ | - | The label text for the select field | +| `Disabled` | `bool` | ✗ | `false` | Whether the select field is disabled | +| `ChildContent` | `RenderFragment` | ✓ | - | Content defining the selectable options (BitSelectItem or BitSelectItemGroup) | +| `AdditionalText` | `RenderFragment?` | ✗ | `null` | Additional descriptive text displayed below the select | +| `AdditionalTextId` | `string?` | ✗ | `null` | ID for the additional text (used for aria-describedby) | +| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes | + +## Child Components + +### BitSelectItem + +Represents an individual selectable option within a BitSelectField component. + +**Namespace**: `BitBlazor.Form` + +#### Parameters + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `Value` | `TValue?` | ✗ | `null` | The value of the option | +| `ChildContent` | `RenderFragment?` | ✗ | `null` | Content to be rendered as the option text | +| `Disabled` | `bool` | ✗ | `false` | Whether the option is disabled | +| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes | + +### BitSelectItemGroup + +Represents a group of selectable items within a BitSelectField component. + +**Namespace**: `BitBlazor.Form` + +#### Parameters + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `Label` | `string` | ✓ | - | The label text for the option group | +| `ChildContent` | `RenderFragment` | ✓ | - | Content defining the options in the group | +| `Disabled` | `bool` | ✗ | `false` | Whether the entire option group is disabled | +| `AdditionalAttributes` | `Dictionary` | ✗ | `{}` | Additional HTML attributes | + +## Usage Examples + +### Basic select field + +```razor + + Select a country... + Italy + United States + United Kingdom + France + + +@code { + private FormModel model = new(); + + private class FormModel + { + public string Country { get; set; } = string.Empty; + } +} +``` + +### Select field with validation + +```razor + + + + + Select a role... + Administrator + User + Guest + + + + + + Submit + + + +@code { + private UserModel model = new(); + + private class UserModel + { + [Required(ErrorMessage = "Please select a role")] + public string Role { get; set; } = string.Empty; + } + + private async Task HandleSubmit() + { + // Handle form submission + } +} +``` + +### Select field with grouped options + +```razor + + Choose a browser... + + + Google Chrome + Mozilla Firefox + Microsoft Edge + Apple Safari + + + + Internet Explorer 11 + Opera + + + +@code { + private BrowserModel model = new(); + + private class BrowserModel + { + public string Browser { get; set; } = string.Empty; + } +} +``` + +### Select field with disabled options + +```razor + + Select a plan... + Free - Basic features + Pro - Advanced features + + Enterprise - Contact sales + + + +@code { + private SubscriptionModel model = new(); + + private class SubscriptionModel + { + public string Plan { get; set; } = string.Empty; + } +} +``` + +### Select field with numeric values + +```razor + + Select priority... + Low + Medium + High + Critical + + +@code { + private TaskModel model = new(); + + private class TaskModel + { + public int Priority { get; set; } + } +} +``` + +### Select field with enum values + +```razor + + Pending + Processing + Shipped + Delivered + Cancelled + + +@code { + private OrderModel model = new() { Status = OrderStatus.Pending }; + + private class OrderModel + { + public OrderStatus Status { get; set; } + } + + private enum OrderStatus + { + Pending, + Processing, + Shipped, + Delivered, + Cancelled + } +} +``` + +### Select field with additional text + +```razor + + + Select your preferred language for the application interface. + + Select a language... + English + Italiano + Español + Français + + +@code { + private SettingsModel model = new(); + + private class SettingsModel + { + public string Language { get; set; } = "en"; + } +} +``` + +### Disabled select field + +```razor + + Select payment method... + Credit Card + PayPal + Bank Transfer + + +@code { + private PaymentModel model = new(); + + private class PaymentModel + { + public string PaymentMethod { get; set; } = string.Empty; + } +} +``` + +## Generated HTML Structure + +### Basic select field + +```html +
+ + +
+``` + +### Select field with grouped options + +```html +
+ + +
+``` + +### Select field with validation error + +```html +
+ + +
+``` + +### Select field with disabled option + +```html +
+ + +
+``` + +## Generated CSS Classes + +### Container classes + +- `select-wrapper` - Container wrapper for the entire field + +## Accessibility + +- Automatically associates labels with select elements using the `for` attribute +- Supports `aria-describedby` when AdditionalText is provided +- Maintains focus management and keyboard navigation +- Compatible with screen readers +- Supports validation message announcements +- Option groups enhance screen reader navigation and organization +- Disabled options are properly announced to assistive technologies + +## Form Integration + +- Full support for `EditForm` and model binding +- Compatible with `DataAnnotationsValidator` +- Integrates with `ValidationSummary` and `ValidationMessage` +- Supports `For` expression for validation binding +- Automatic validation state styling +- Generic type support allows binding to any value type (string, int, enum, etc.) + +## Notes + +- The component automatically generates unique IDs for form fields using the "select" prefix +- BitSelectField is a generic component - you can use it with any type: `BitSelectField`, `BitSelectField`, `BitSelectField`, etc. +- Use BitSelectItem components as direct children to define individual options +- Use BitSelectItemGroup to organize related options under a common label (rendered as optgroup) +- The first option typically serves as a placeholder (with empty or default value) +- Options and groups can be individually disabled +- The component extends `BitFormComponentBase` for consistent form behavior +- All Bootstrap Italia form styling is automatically applied +- The component supports all standard HTML select attributes through `AdditionalAttributes` +- When working with enums, ensure you pass enum values directly to BitSelectItem without converting to string diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 50ebe41..9dbe37c 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -154,6 +154,34 @@ This guide provides a quick overview of all BitBlazor components with basic exam Step="1" /> ``` +### BitSelectField +```razor + + + Select a country... + Italy + United States + United Kingdom + + + + + Choose a browser... + + Google Chrome + Mozilla Firefox + Microsoft Edge + + + + + + Pending + Processing + Shipped + +``` + ### BitCheckbox ```razor @@ -379,6 +407,18 @@ Size.Large // Large @bind-Value="model.Phone" For="@(() => model.Phone)" />
+
+ + Select a country... + Italy + United States + United Kingdom + France + Germany + +
/// - /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs - /// that are not explicitly defined in the component's parameters. The keys represent attribute names, and the - /// values represent their corresponding values. + /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters. + /// The keys represent attribute names, and the values represent their corresponding values. /// [Parameter(CaptureUnmatchedValues = true)] public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); diff --git a/src/BitBlazor/Form/BitFormComponentBase.cs b/src/BitBlazor/Form/BitFormComponentBase.cs index f42eb17..309043b 100644 --- a/src/BitBlazor/Form/BitFormComponentBase.cs +++ b/src/BitBlazor/Form/BitFormComponentBase.cs @@ -63,12 +63,6 @@ public abstract class BitFormComponentBase : BitComponentBase [Parameter] public bool Disabled { get; set; } - /// - /// Gets or sets the placeholder to show in the component - /// - [Parameter] - public string? Placeholder { get; set; } - /// /// Gets or sets an optional fragment of additional content to render. /// @@ -102,7 +96,7 @@ public abstract class BitFormComponentBase : BitComponentBase /// protected BitFormComponentBase() { - if (!SupportedTypes.Contains(ComponentType)) + if (SupportedTypes.Length > 0 && !SupportedTypes.Contains(ComponentType)) { throw new NotSupportedException("Type not supported"); } @@ -123,22 +117,9 @@ protected override void OnParametersSet() { base.OnParametersSet(); SetRequiredAttribute(); - SetPlaceholderAttribute(); SetAdditionalTextAttributes(); } - private void SetPlaceholderAttribute() - { - if (!string.IsNullOrWhiteSpace(Placeholder)) - { - AdditionalAttributes["placeholder"] = Placeholder; - } - else - { - AdditionalAttributes.Remove("placeholder"); - } - } - private void SetRequiredAttribute() { if (Required) diff --git a/src/BitBlazor/Form/BitInputFieldBase.cs b/src/BitBlazor/Form/BitInputFieldBase.cs index 62ba74d..05a1d37 100644 --- a/src/BitBlazor/Form/BitInputFieldBase.cs +++ b/src/BitBlazor/Form/BitInputFieldBase.cs @@ -27,6 +27,12 @@ public abstract class BitInputFieldBase : BitFormComponentBase [Parameter] public bool Plaintext { get; set; } + /// + /// Gets or sets the placeholder to show in the component + /// + [Parameter] + public string? Placeholder { get; set; } + /// /// Gets or sets the size of the text field component. /// @@ -41,6 +47,25 @@ public abstract class BitInputFieldBase : BitFormComponentBase private bool isLabelActive = false; + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + SetPlaceholderAttribute(); + } + + private void SetPlaceholderAttribute() + { + if (!string.IsNullOrWhiteSpace(Placeholder)) + { + AdditionalAttributes["placeholder"] = Placeholder; + } + else + { + AdditionalAttributes.Remove("placeholder"); + } + } + /// /// Sets the active state of the label. /// diff --git a/src/BitBlazor/Form/SelectField/BitSelectField.razor b/src/BitBlazor/Form/SelectField/BitSelectField.razor new file mode 100644 index 0000000..25715c6 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectField.razor @@ -0,0 +1,23 @@ +@namespace BitBlazor.Form + +@typeparam T +@inherits BitFormComponentBase + +
+ + + + @ChildContent + + + + @RenderValidationMessage() + + @RenderAdditionalText() +
\ No newline at end of file diff --git a/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs new file mode 100644 index 0000000..18dfbec --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectField.razor.cs @@ -0,0 +1,36 @@ +using BitBlazor.Core; +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Form; + +/// +/// Represents a select field component. +/// +/// +/// Use to define the selectable options for the field. +/// This component is intended for use within Bit form layouts and renders options using the native HTML <select> element, +/// so BitSelectItem option content should be plain text only (nested markup inside options is not supported). +/// The type of the value represented and selected by the field. +public partial class BitSelectField : BitFormComponentBase, IBitSelectField +{ + /// + protected override string FieldIdPrefix { get; } = "select"; + + /// + protected override Type[] SupportedTypes { get; } = []; + + /// + /// Gets or sets the content to be rendered inside the component. + /// + [Parameter] + [EditorRequired] + public RenderFragment ChildContent { get; set; } = default!; + + private string ComputeContainerCssClass() + { + var builder = new CssClassBuilder("select-wrapper"); + AddCustomCssClass(builder); + + return builder.Build(); + } +} diff --git a/src/BitBlazor/Form/SelectField/BitSelectItem.razor b/src/BitBlazor/Form/SelectField/BitSelectItem.razor new file mode 100644 index 0000000..c054565 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectItem.razor @@ -0,0 +1,7 @@ +@namespace BitBlazor.Form + +@typeparam TValue + + diff --git a/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs new file mode 100644 index 0000000..189f753 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectItem.razor.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Form; + +/// +/// Represents an individual selectable option within a BitSelectField component. +/// +/// +/// Use BitSelectItem as a child of BitSelectField to define selectable options. +/// The Value property specifies the underlying value for the option, and ChildContent defines the display content shown to the user. +/// +/// The type of the value associated with the select item. +public partial class BitSelectItem +{ + [CascadingParameter] + BitSelectField Parent { get; set; } = default!; + + /// + /// Gets or sets the value of the option. + /// + [Parameter] + public TValue? Value { get; set; } + + /// + /// Gets or sets the content to be rendered inside the component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Gets or sets whether the option is disabled + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Gets or sets additional attributes that do not match any of the explicitly defined parameters. + /// + /// + /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters. + /// The keys represent attribute names, and the values represent their corresponding values. + /// + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); +} diff --git a/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor new file mode 100644 index 0000000..2e2c663 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor @@ -0,0 +1,5 @@ +@namespace BitBlazor.Form + + + @ChildContent + diff --git a/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs new file mode 100644 index 0000000..e1c27a6 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/BitSelectItemGroup.razor.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Form; + +/// +/// Represents a group of selectable items within a BitSelectField component. +/// +/// +/// Use BitSelectItemGroup to organize related BitSelectItem components under a common label within a BitSelectField component. +/// This enhances accessibility and user experience by grouping options logically. +/// The group label is typically rendered as an optgroup label in the resulting markup. +/// +public partial class BitSelectItemGroup +{ + [CascadingParameter] + IBitSelectField Parent { get; set; } = default!; + + /// + /// Gets or sets the text label displayed for the component. + /// + [Parameter] + [EditorRequired] + public string Label { get; set; } = string.Empty; + + /// + /// Gets or sets the content to be rendered inside the component. + /// + [Parameter] + [EditorRequired] + public RenderFragment ChildContent { get; set; } = default!; + + /// + /// Gets or sets whether the option group is disabled + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Gets or sets additional attributes that do not match any of the explicitly defined parameters. + /// + /// + /// This property is typically used to capture arbitrary HTML attributes or other key-value pairs that are not explicitly defined in the component's parameters. + /// The keys represent attribute names, and the values represent their corresponding values. + /// + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); +} diff --git a/src/BitBlazor/Form/SelectField/IBitSelectField.cs b/src/BitBlazor/Form/SelectField/IBitSelectField.cs new file mode 100644 index 0000000..a5ca674 --- /dev/null +++ b/src/BitBlazor/Form/SelectField/IBitSelectField.cs @@ -0,0 +1,11 @@ +namespace BitBlazor.Form; + +/// +/// Represents a generic select field. +/// +/// +/// This is a marker interface and it's intended for internal use and is not intended to be implemented or used directly in application code. +/// +internal interface IBitSelectField +{ +} \ No newline at end of file diff --git a/stories/BitBlazor.Stories/BitBlazor.Stories.csproj b/stories/BitBlazor.Stories/BitBlazor.Stories.csproj index aac5ac2..1ef21b8 100644 --- a/stories/BitBlazor.Stories/BitBlazor.Stories.csproj +++ b/stories/BitBlazor.Stories/BitBlazor.Stories.csproj @@ -9,9 +9,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor b/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor new file mode 100644 index 0000000..f4a0a75 --- /dev/null +++ b/stories/BitBlazor.Stories/Components/Stories/Form/BitSelectField.stories.razor @@ -0,0 +1,77 @@ +@attribute [Stories("Form/BitSelectField")] + + + + + + + + + + + + + + + + + + + + + + + + +@code { + private readonly ViewModel model = new(); + + class ViewModel + { + public string DefaultStoryValue { get; set; } = string.Empty; + + public string DisabledStoryValue { get; set; } = string.Empty; + + public string GroupedStoryValue { get; set; } = string.Empty; + } +} diff --git a/tests/BitBlazor.Test/Components/Breadcrumb/BitBreadcrumbTest.Rendering.razor b/tests/BitBlazor.Test/Components/Breadcrumb/BitBreadcrumbTest.Rendering.razor index 6c1d565..9e160ec 100644 --- a/tests/BitBlazor.Test/Components/Breadcrumb/BitBreadcrumbTest.Rendering.razor +++ b/tests/BitBlazor.Test/Components/Breadcrumb/BitBreadcrumbTest.Rendering.razor @@ -1,4 +1,4 @@ -@inherits TestContext +@inherits BunitContext @code { [Theory] @@ -17,25 +17,25 @@ var component = Render(@); component.MarkupMatches( - @); + @); } [Theory] @@ -54,29 +54,29 @@ var component = Render(@); component.MarkupMatches( - @); + @); } [Theory] @@ -95,28 +95,28 @@ var component = Render(@); component.MarkupMatches( - @); + @); } } diff --git a/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Behaviors.cs b/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Behaviors.cs new file mode 100644 index 0000000..da79592 --- /dev/null +++ b/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Behaviors.cs @@ -0,0 +1,237 @@ +using BitBlazor.Form; +using Bunit; + +namespace BitBlazor.Test.Form.SelectField; + +public class BitSelectFieldTest +{ + private enum TestEnum + { + Option1, + Option2, + Option3 + } + + [Fact] + public void BitSelectField_Should_Change_String_Value_Correctly() + { + string? value = string.Empty; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a value") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, string.Empty) + .AddChildContent("Choose an option")) + .AddChildContent>(child => child + .Add(p => p.Value, "value1") + .AddChildContent("Value 1")) + .AddChildContent>(child => child + .Add(p => p.Value, "value2") + .AddChildContent("Value 2"))); + + var select = component.Find("select"); + select.Change("value1"); + + Assert.Equal("value1", value); + } + + [Fact] + public void BitSelectField_Should_Change_Int_Value_Correctly() + { + int value = 0; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a number") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, 0) + .AddChildContent("Choose a number")) + .AddChildContent>(child => child + .Add(p => p.Value, 1) + .AddChildContent("One")) + .AddChildContent>(child => child + .Add(p => p.Value, 2) + .AddChildContent("Two")) + .AddChildContent>(child => child + .Add(p => p.Value, 3) + .AddChildContent("Three"))); + + var select = component.Find("select"); + select.Change(2); + + Assert.Equal(2, value); + } + + [Fact] + public void BitSelectField_Should_Change_Enum_Value_Correctly() + { + TestEnum value = TestEnum.Option1; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select an option") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, TestEnum.Option1) + .AddChildContent("Option 1")) + .AddChildContent>(child => child + .Add(p => p.Value, TestEnum.Option2) + .AddChildContent("Option 2")) + .AddChildContent>(child => child + .Add(p => p.Value, TestEnum.Option3) + .AddChildContent("Option 3"))); + + var select = component.Find("select"); + select.Change(TestEnum.Option3); + + Assert.Equal(TestEnum.Option3, value); + } + + [Fact] + public void BitSelectField_Should_Update_Bound_Value_When_Selection_Changes() + { + string? value = "initial"; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a value") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, "initial") + .AddChildContent("Initial Value")) + .AddChildContent>(child => child + .Add(p => p.Value, "changed") + .AddChildContent("Changed Value"))); + + Assert.Equal("initial", value); + + var select = component.Find("select"); + select.Change("changed"); + + Assert.Equal("changed", value); + } + + [Fact] + public void BitSelectField_Should_Set_Selected_Attribute_On_Correct_Option() + { + string? value = "value2"; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a value") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, "value1") + .AddChildContent("Value 1")) + .AddChildContent>(child => child + .Add(p => p.Value, "value2") + .AddChildContent("Value 2")) + .AddChildContent>(child => child + .Add(p => p.Value, "value3") + .AddChildContent("Value 3"))); + + var options = component.FindAll("option"); + var selectedOption = options.FirstOrDefault(o => o.HasAttribute("selected")); + + Assert.NotNull(selectedOption); + Assert.Equal("value2", selectedOption.GetAttribute("value")); + } + + [Fact] + public void BitSelectField_Should_Update_Selected_Attribute_When_Value_Changes() + { + int value = 1; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a number") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, 1) + .AddChildContent("One")) + .AddChildContent>(child => child + .Add(p => p.Value, 2) + .AddChildContent("Two")) + .AddChildContent>(child => child + .Add(p => p.Value, 3) + .AddChildContent("Three"))); + + var select = component.Find("select"); + select.Change(3); + + var options = component.FindAll("option"); + var selectedOption = options.FirstOrDefault(o => o.HasAttribute("selected")); + + Assert.NotNull(selectedOption); + Assert.Equal("3", selectedOption.GetAttribute("value")); + } + + [Fact] + public void BitSelectField_Should_Handle_Nullable_Int_Value_Changes() + { + int? value = null; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select a number") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, (int?)null) + .AddChildContent("None")) + .AddChildContent>(child => child + .Add(p => p.Value, 1) + .AddChildContent("One")) + .AddChildContent>(child => child + .Add(p => p.Value, 2) + .AddChildContent("Two"))); + + var select = component.Find("select"); + select.Change(1); + + Assert.Equal(1, value); + } + + [Fact] + public void BitSelectField_Should_Handle_Nullable_Enum_Value_Changes() + { + TestEnum? value = null; + + using var ctx = new BunitContext(); + + var component = ctx.Render>(parameters => parameters + .Add(p => p.Label, "Select an option") + .Add(p => p.Id, "test-select") + .Bind(p => p.Value, value, v => value = v) + .AddChildContent>(child => child + .Add(p => p.Value, (TestEnum?)null) + .AddChildContent("None")) + .AddChildContent>(child => child + .Add(p => p.Value, TestEnum.Option1) + .AddChildContent("Option 1")) + .AddChildContent>(child => child + .Add(p => p.Value, TestEnum.Option2) + .AddChildContent("Option 2"))); + + var select = component.Find("select"); + select.Change(TestEnum.Option2); + + Assert.Equal(TestEnum.Option2, value); + } +} diff --git a/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Rendering.razor b/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Rendering.razor new file mode 100644 index 0000000..ce87085 --- /dev/null +++ b/tests/BitBlazor.Test/Form/SelectField/BitSelectFieldTest.Rendering.razor @@ -0,0 +1,197 @@ +@inherits BunitContext + +@code { + [Fact] + public void BitSelectField_Should_Render_Default_Markup_Correctly() + { + string value = string.Empty; + + var component = Render( + @ + Choose your option + value 1 + value 2 + ); + + component.MarkupMatches( + @
+ + +
); + } + + [Fact] + public void BitSelectField_Should_Render_Disabled_Select_Correctly() + { + string value = string.Empty; + + var component = Render( + @ + Choose your option + value 1 + value 2 + ); + + component.MarkupMatches( + @
+ + +
); + } + + [Fact] + public void BitSelectField_Should_Render_Option_Group_Correctly() + { + string value = string.Empty; + + var component = Render( + @ + Choose your option + + value 1 + value 2 + + + value 3 + value 4 + + ); + + component.MarkupMatches( + @
+ + +
); + } + + [Fact] + public void BitSelectField_Should_Render_Additional_Text_Correctly() + { + string value = string.Empty; + + var component = Render( + @ + + Choose your option + + value 1 + value 2 + + + value 3 + value 4 + + + + This is a help text + + ); + + component.MarkupMatches( + @
+ + + This is a help text +
); + } + + [Fact] + public void BitSelectField_Should_Render_Option_As_Disabled_If_Option_Disabled_Property_Is_Set_To_True() + { + string value = string.Empty; + + var component = Render( + @ + Choose your option + value 1 + value 2 + ); + + component.MarkupMatches( + @
+ + +
); + } + + [Fact] + public void BitSelectField_Should_Render_Option_Group_Disabled_If_Disabled_Property_Is_Set_To_True() + { + string value = string.Empty; + + var component = Render( + @ + Choose your option + + value 1 + value 2 + + + value 3 + value 4 + + ); + + component.MarkupMatches( + @
+ + +
); + } +}