From 0c99e76ffd20f6d5305396e0201a28ad6062beec Mon Sep 17 00:00:00 2001
From: Manuel Monteagudo <manuel@homelifeacademy.com>
Date: Mon, 25 Nov 2024 20:28:41 -0400
Subject: [PATCH] [HxMultiSelect] Adds Floating Label type to MultiSelect
 component (#899)

* Adds Floating label type to MultiSelect

* Adds floating label type to MultiSelect

* reverted basic demo + separate testing page

* CSS class moved to outermost element, CssClassHelper usage

* RenderOrder override consolidation

* HxFormValueComponentRenderer_Label usage (instead of direct <label> rendering)

* doc - add HxMultiSelect to list of components supporting floating labels

---------

Co-authored-by: Robert Haken <haken@havit.cz>
---
 .../Forms/HxMultiSelect.cs                    | 24 +++++++++-
 .../Internal/HxMultiSelectInternal.razor      | 33 ++++++++------
 .../Internal/HxMultiSelectInternal.razor.cs   |  4 ++
 .../Forms/MultiSelectSettings.cs              |  5 +++
 .../FormInputs/FormInputs_Documentation.razor |  4 +-
 .../Havit.Blazor.Components.Web.Bootstrap.xml |  8 ++++
 .../HxMultiSelect_FloatingLabel_Test.razor    | 45 +++++++++++++++++++
 7 files changed, 108 insertions(+), 15 deletions(-)
 create mode 100644 Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxMultiSelectTests/HxMultiSelect_FloatingLabel_Test.razor

diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs
index 1271cf2f6..d5db76184 100644
--- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs
+++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs
@@ -8,7 +8,7 @@ namespace Havit.Blazor.Components.Web.Bootstrap;
 /// </summary>
 /// <typeparam name="TValue">Type of values.</typeparam>
 /// <typeparam name="TItem">Type of items.</typeparam>
-public class HxMultiSelect<TValue, TItem> : HxInputBase<List<TValue>>, IInputWithSize
+public class HxMultiSelect<TValue, TItem> : HxInputBase<List<TValue>>, IInputWithSize, IInputWithLabelType
 {
 	/// <summary>
 	/// Return <see cref="HxMultiSelect{TValue, TItem}"/> defaults.
@@ -161,6 +161,26 @@ public class HxMultiSelect<TValue, TItem> : HxInputBase<List<TValue>>, IInputWit
 	[Parameter] public IconBase FilterClearIcon { get; set; }
 	protected IconBase FilterClearIconEffective => FilterClearIcon ?? GetSettings()?.FilterClearIcon ?? GetDefaults().FilterClearIcon;
 
+	/// <inheritdoc cref="Bootstrap.LabelType" />
+	[Parameter] public LabelType? LabelType { get; set; }
+	protected LabelType LabelTypeEffective => LabelType ?? GetSettings()?.LabelType ?? GetDefaults()?.LabelType ?? HxSetup.Defaults.LabelType;
+	LabelType IInputWithLabelType.LabelTypeEffective => LabelTypeEffective;
+	protected override LabelValueRenderOrder RenderOrder
+	{
+		get
+		{
+			if (LabelTypeEffective == Bootstrap.LabelType.Floating)
+			{
+				// Floating label type renders the label in HxMultiSelectInternal component.
+				return LabelValueRenderOrder.ValueOnly;
+			}
+			else
+			{
+				return LabelValueRenderOrder.LabelValue;
+			}
+		}
+	}
+
 	private List<TItem> _itemsToRender;
 	private HxMultiSelectInternal<TValue, TItem> _hxMultiSelectInternalComponent;
 
@@ -212,6 +232,8 @@ protected override void BuildRenderInput(RenderTreeBuilder builder)
 		builder.AddAttribute(102, nameof(HxMultiSelectInternal<TValue, TItem>.InputCssClass), GetInputCssClassToRender());
 		builder.AddAttribute(103, nameof(HxMultiSelectInternal<TValue, TItem>.InputText), GetInputText());
 		builder.AddAttribute(104, nameof(HxMultiSelectInternal<TValue, TItem>.EnabledEffective), EnabledEffective);
+		builder.AddAttribute(125, nameof(HxMultiSelectInternal<TValue, TItem>.LabelTypeEffective), LabelTypeEffective);
+		builder.AddAttribute(126, nameof(HxMultiSelectInternal<TValue, TItem>.FormValueComponent), this);
 		builder.AddAttribute(105, nameof(HxMultiSelectInternal<TValue, TItem>.ItemsToRender), _itemsToRender);
 		builder.AddAttribute(106, nameof(HxMultiSelectInternal<TValue, TItem>.TextSelector), TextSelector);
 		builder.AddAttribute(107, nameof(HxMultiSelectInternal<TValue, TItem>.ValueSelector), ValueSelector);
diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor
index ce92eccce..056c2f1ed 100644
--- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor
+++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor
@@ -6,7 +6,7 @@
 	bool enabled = EnabledEffective && (ItemsToRender != null);
 }
 <div class="@CssClassHelper.Combine("hx-multi-select",
-                                    HasInputGroups ? "input-group" : null,
+									HasInputGroups ? "input-group" : null,
                                     HasInputGroupEnd ? "input-group-end" : null,
 					                HasInputGroupStart ? "input-group-start" : null,
                                     InputGroupCssClass)">
@@ -16,19 +16,26 @@
 	}
 
 	@InputGroupStartTemplate
-	<div @ref="_elementReference" class="dropdown" role="listbox">
+	<div @ref="_elementReference"
+		 class="@CssClassHelper.Combine("dropdown", (LabelTypeEffective == LabelType.Floating) ? "form-floating": null)"
+		 role="listbox">
 
 		<input @ref="_inputElementReference"
-				type="text"
-				id="@InputId"
-				class="@InputCssClass"
-				value="@(((ItemsToRender == null) && !String.IsNullOrEmpty(NullDataText)) ? NullDataText : InputText)"
-				disabled="@(!enabled)"
-				readonly="true"
-				aria-expanded="@_isShown"
-				data-bs-toggle="@(enabled ? "dropdown" : null)" 
-				data-bs-auto-close="outside"
-				@attributes="this.AdditionalAttributes" />
+			   type="text"
+			   id="@InputId"
+			   class="@InputCssClass"
+			   value="@(((ItemsToRender == null) && !String.IsNullOrEmpty(NullDataText)) ? NullDataText : InputText)"
+			   disabled="@(!enabled)"
+			   readonly="true"
+			   aria-expanded="@_isShown"
+			   data-bs-toggle="@(enabled ? "dropdown" : null)"
+			   data-bs-auto-close="outside"
+			   @attributes="this.AdditionalAttributes" />
+
+		@if (LabelTypeEffective == LabelType.Floating)
+		{
+			<HxFormValueComponentRenderer_Label FormValueComponent="@FormValueComponent" />
+		}
 
 		@* Must be always rendered otherwise does not work after disable->enabled scenario *@
 		<ul class="@CssClassHelper.Combine("dropdown-menu", _isShown ? "show" : null)">
@@ -45,7 +52,7 @@
 								   autocomplete="off"
 								   value="@_filterText"
 								   @oninput="HandleFilterInputChanged"
-							@onclick:stopPropagation
+								   @onclick:stopPropagation
 								   onfocusin="this.select()" />
 
 							<div class="hx-multi-select-filter-input-icon">
diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs
index b8fce3638..0ecf31d96 100644
--- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs
+++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor.cs
@@ -13,8 +13,12 @@ public partial class HxMultiSelectInternal<TValue, TItem> : IAsyncDisposable
 
 	[Parameter] public string InputText { get; set; }
 
+	[Parameter] public IFormValueComponent FormValueComponent { get; set; }
+
 	[Parameter] public bool EnabledEffective { get; set; }
 
+	[Parameter] public LabelType LabelTypeEffective { get; set; }
+
 	[Parameter] public List<TItem> ItemsToRender { get; set; }
 
 	[Parameter] public List<TValue> SelectedValues { get; set; }
diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/MultiSelectSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/MultiSelectSettings.cs
index 27a621314..4bdca09f7 100644
--- a/Havit.Blazor.Components.Web.Bootstrap/Forms/MultiSelectSettings.cs
+++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/MultiSelectSettings.cs
@@ -12,6 +12,11 @@ public record MultiSelectSettings : InputSettings
 	/// </summary>
 	public InputSize? InputSize { get; set; }
 
+	/// <summary>
+	/// The label type.
+	/// </summary>
+	public LabelType? LabelType { get; set; }
+
 	/// <summary>
 	/// Enables filtering capabilities.
 	/// </summary>
diff --git a/Havit.Blazor.Documentation/Pages/Components/FormInputs/FormInputs_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/FormInputs/FormInputs_Documentation.razor
index c4e80608a..da14d6141 100644
--- a/Havit.Blazor.Documentation/Pages/Components/FormInputs/FormInputs_Documentation.razor
+++ b/Havit.Blazor.Documentation/Pages/Components/FormInputs/FormInputs_Documentation.razor
@@ -37,7 +37,9 @@
 <p>
 	Floating labels provide a sleek and simple design, floating elegantly over your input fields.
 	See <a href="https://getbootstrap.com/docs/5.3/forms/floating-labels/">Bootstrap 5 documentation on Floating labels</a>.<br />
-	They are supported by <code>HxInputText</code>, <code>HxInputTextArea</code>, <code>HxInputNumber</code>, <code>HxInputDate</code>, <code>HxAutosuggest</code>, <code>HxSelect</code>, and <code>HxInputTags</code>.
+	They are supported by <code>HxInputText</code>, <code>HxInputTextArea</code>,
+	<code>HxInputNumber</code>, <code>HxInputDate</code>, <code>HxAutosuggest</code>,
+	<code>HxSelect</code>, <code>HxMultiSelect</code> and <code>HxInputTags</code>.
 </p>
 <DocAlert Type="DocAlertType.Warning">
 	Inputs with floating labels can't have the <code>Placeholder</code> parameter set.
diff --git a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml
index 337ca541a..f720632d4 100644
--- a/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml
+++ b/Havit.Blazor.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml
@@ -4699,6 +4699,9 @@
             Icon displayed in filter input for clearing the filter.
             </summary>
         </member>
+        <member name="P:Havit.Blazor.Components.Web.Bootstrap.HxMultiSelect`2.LabelType">
+            <inheritdoc cref="T:Havit.Blazor.Components.Web.Bootstrap.LabelType" />
+        </member>
         <member name="M:Havit.Blazor.Components.Web.Bootstrap.HxMultiSelect`2.FocusAsync">
             <inheritdoc cref="M:Havit.Blazor.Components.Web.Bootstrap.HxInputBase`1.FocusAsync"/>
         </member>
@@ -5429,6 +5432,11 @@
             Input size.
             </summary>
         </member>
+        <member name="P:Havit.Blazor.Components.Web.Bootstrap.MultiSelectSettings.LabelType">
+            <summary>
+            The label type.
+            </summary>
+        </member>
         <member name="P:Havit.Blazor.Components.Web.Bootstrap.MultiSelectSettings.AllowFiltering">
             <summary>
             Enables filtering capabilities.
diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxMultiSelectTests/HxMultiSelect_FloatingLabel_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxMultiSelectTests/HxMultiSelect_FloatingLabel_Test.razor
new file mode 100644
index 000000000..49ff2a4da
--- /dev/null
+++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxMultiSelectTests/HxMultiSelect_FloatingLabel_Test.razor
@@ -0,0 +1,45 @@
+@page "/HxMultiSelect_FloatingLabel"
+@rendermode InteractiveServer
+@inject IDemoDataService DemoDataService
+
+<div class="m-3">
+
+	<HxMultiSelect Label="HxMultiSelect"
+				   TItem="EmployeeDto"
+				   TValue="int"
+				   Data="@employees"
+				   LabelType="LabelType.Floating"
+				   @bind-Value="selectedEmployeeIds"
+				   TextSelector="@(p => p.Name)"
+				   ValueSelector="@(p => p.Id)"
+				   NullDataText="Loading employees..."
+				   EmptyText="-select employees-" />
+
+	For visual reference:
+	<HxSelect TItem="EmployeeDto"
+			  TValue="int?"
+			  Label="HxSelect"
+			  LabelType="LabelType.Floating"
+			  Data="employees"
+			  @bind-Value="selectedEmployeeId"
+			  TextSelector="@(employee => employee.Name)"
+			  ValueSelector="@(employee => employee.Id)"
+			  Nullable="true"
+			  NullText="-select employee-"
+			  NullDataText="Loading employees..." />
+
+</div>
+
+
+<p class="mt-3">Selected employees (IDs): @String.Join(", ", selectedEmployeeIds.Select(e => e.ToString()))</p>
+
+@code {
+	private IEnumerable<EmployeeDto> employees;
+	private List<int> selectedEmployeeIds = new();
+	private int? selectedEmployeeId;
+
+	protected override async Task OnInitializedAsync()
+	{
+		employees = await DemoDataService.GetAllEmployeesAsync();
+	}
+}
\ No newline at end of file