Repository: radzenhq/radzen-blazor (csharp)

Issue:
Support floating label for RadzenDatePicker
**Is your feature request related to a problem? Please describe.**

It seems like RadzenDatePicker is currently not intented to be used inside a RadzenFormField I could not find any examples for this in the docs. Thus the floating label is not working if you use it like this anyway.

It would be great if RadzenDatePicker would have full support for use inside RadzenFormField because date fields are an essential part for a lot of forms and should behave consistently.

From my inspection it is basically 3 things that prevent this from working currently.
1. `rz-state-empty` css class is not set for empty date picker values
2. Focused RadzenLabel styles are only considered for intented form fields (RadzenTextBox, RadzenTextArea etc.)
3. Inline variant of the date picker will obviously not work in this concept

**Describe the solution you'd like**

Thoughts about addressing those three issues:

1. This could be a simple one. Add the `rz-state-empty` css class based on the `HasValue` property.
2. As RadzenFormField has a special `rz-state-focused` css class set when focused, the focused label styles could be defined based on this.
   https://github.com/radzenhq/radzen-blazor/blob/716d1a6cba131b2bab82b0612cac9a1e39a652a6/Radzen.Blazor/themes/components/blazor/_form-field.scss#L346-L388
   Maybe add `.rz-form-field.rz-state-focused .rz-form-field-label` respectively `.rz-form-field:not(.rz-variant-outlined).rz-state-focused .rz-form-field-label` to the selectors?
   
   This could possibly also enable us to create custom form field components that behave as expected.

3. This could be the toughest one. Only solution that comes to my mind is to separate the components. Do a RadzenCalenderPicker or something like that as the inline variant. Make use of it in the date picker field popup....

**Describe alternatives you've considered**

- Fixed variant by a wrapping component that adds the `rz-state-empty` css class and adds the focused label styles. (Tough one as you have to pass a lot of parameters through the wrapper, at least the ones that are used).
- Live with the always floating label for now ;)

**Additional context**

See it in action.

![Image](https://github.com/user-attachments/assets/8a159bbf-82b6-4e60-9d1f-5c2c3ab7abdd)

Pull request:
This PR enables the RadzenDatePicker to be used inside a RadzenFormField including floating label support.

- Therefore the CSS class `rz-state-empty` is added to the component if there is no value.
- Stylesheets are adjusted so that the focused state of RadzenFormField is considered by the floating label.

resolves #1929

Patch:
diff --git a/Radzen.Blazor/RadzenDatePicker.razor b/Radzen.Blazor/RadzenDatePicker.razor
index ee9c6f5a84a..a3c21c1e2fd 100644
--- a/Radzen.Blazor/RadzenDatePicker.razor
+++ b/Radzen.Blazor/RadzenDatePicker.razor
@@ -1,21 +1,17 @@
 ﻿@using Radzen
 @using Radzen.Blazor.Rendering
-@using Microsoft.AspNetCore.Components.Forms
-@using System.Linq.Expressions
-@using System.Globalization
-@using Microsoft.JSInterop
 
 @typeparam TValue
 @inherits RadzenComponent
 @implements IRadzenFormComponent
 @if (Visible)
 {
-    <div @ref="@Element" @attributes="Attributes" class="@($"rz-datepicker{(Disabled ? " rz-state-disabled" : "")}") @GetCssClass()" style="@getStyle()" id="@GetId()">
+    <div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" style="@getStyle()" id="@GetId()">
         @if (!Inline)
         {
             <input @ref="@input" @attributes="InputAttributes" disabled="@Disabled" readonly="@IsReadonly" value="@FormattedValue" tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
                     @onchange="@ParseDate" autocomplete="off" type="text" name="@Name" @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation
-                   class="rz-inputtext @InputClass @(ReadOnly ? "rz-readonly" : "") @(!ShowButton ? "rz-input-trigger" : "")" id="@Name" placeholder="@Placeholder" />
+                   class="rz-inputtext @InputClass @(ReadOnly ? "rz-readonly" : "") @(!ShowButton ? "rz-input-trigger" : "")" id="@Name" placeholder="@CurrentPlaceholder" />
             @if (ShowButton)
             {
                 <button aria-label="@ToggleAriaLabel" @onmousedown=@OnToggle class="@($"rz-datepicker-trigger rz-button rz-button-icon-only{(Disabled ? " rz-state-disabled" : "")} {ButtonClass}")" tabindex="-1" type="button">
@@ -33,10 +29,10 @@
                     @if (!TimeOnly)
                     {
                         <div class="rz-calendar-header">
-                            <a id="@(GetId() + "pm")" tabindex="-1" aria-label="@PrevMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-prev" @onclick="@(async () => { if (!Disabled) { try { if(CurrentDate.AddMonths(-1).Year >= YearFrom) {CurrentDate = CurrentDate.AddMonths(-1);}} catch (ArgumentOutOfRangeException) {}} })">
+                            <a id="@(GetId() + "pm")" tabindex="-1" aria-label="@PrevMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-prev" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(-1).Year >= YearFrom) {CurrentDate = CurrentDate.AddMonths(-1);}} catch (ArgumentOutOfRangeException) {}} })">
                                 <span class="notranslate rzi rz-calendar-prev-icon"></span>
                             </a>
-                            <a id="@(GetId() + "nm")" tabindex="-1" aria-label="@NextMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-next" @onclick="@(async () => { if (!Disabled) { try { if(CurrentDate.AddMonths(1).Year <= YearTo) {CurrentDate = CurrentDate.AddMonths(1);}} catch (ArgumentOutOfRangeException) {} } })">
+                            <a id="@(GetId() + "nm")" tabindex="-1" aria-label="@NextMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-next" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(1).Year <= YearTo) {CurrentDate = CurrentDate.AddMonths(1);}} catch (ArgumentOutOfRangeException) {} } })">
                                 <span class="notranslate rzi rz-calendar-next-icon"></span>
                             </a>
                             <div class="rz-calendar-title">
@@ -116,19 +112,19 @@
                 {
                 <div class="rz-timepicker" @onmousedown:stopPropagation>
                         <RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "hour" }})" TValue="int" Disabled="@Disabled" Value="@(HourFormat == "12" ? ((CurrentDate.Hour + 11) % 12) + 1 : CurrentDate.Hour)"
-                           Min="@(HourFormat == "12" ? 1 : -1)" Max="@(HourFormat == "12" ? 12 : 24)" TValue="double" Step="@HoursStep"
+                           Min="@(HourFormat == "12" ? 1 : -1)" Max="@(HourFormat == "12" ? 12 : 24)" Step="@HoursStep"
                            Change="@UpdateHour" class="rz-hour-picker" @oninput=@OnUpdateHourInput Format="@(PadHours ? "00" : "")" Name="@($"{UniqueID}-h")" />
                         <div class="rz-separator">
                             <span>:</span>
                         </div>
-                        <RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "minutes" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Minute" TValue="double" Step="@MinutesStep" Min="0" Max="59"
+                        <RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "minutes" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Minute" Step="@MinutesStep" Min="0" Max="59"
                            Change="@UpdateMinutes" class="rz-minute-picker" @oninput=@OnUpdateHourMinutes Format="@(PadMinutes ? "00" : "")" Name="@($"{UniqueID}-m")"/>
                         @if (ShowSeconds)
                         {
                             <div class="rz-separator">
                                 <span>:</span>
                             </div>
-                            <RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "seconds" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Second" TValue="double" Step="@SecondsStep" Min="0" Max="59"
+                            <RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "seconds" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Second" Step="@SecondsStep" Min="0" Max="59"
                                Change="@UpdateSeconds" class="rz-second-picker" @oninput=@OnUpdateHourSeconds Format="@(PadSeconds ? "00" : "")" Name="@($"{UniqueID}-s")"/>
                         }
                         @if (HourFormat == "12")
diff --git a/Radzen.Blazor/RadzenDatePicker.razor.cs b/Radzen.Blazor/RadzenDatePicker.razor.cs
index 39bc2339924..1b4bf825db4 100644
--- a/Radzen.Blazor/RadzenDatePicker.razor.cs
+++ b/Radzen.Blazor/RadzenDatePicker.razor.cs
@@ -753,6 +753,7 @@ private string ButtonClasses
         /// <summary>
         /// Gets or sets the FormFieldContext of the component
         /// </summary>
+        [CascadingParameter]
         public IFormFieldContext FormFieldContext { get; set; } = null;
 
         /// <summary>
@@ -922,6 +923,9 @@ private string getStyle()
             return $"{(Inline ? "overflow:auto;" : "")}{(Style != null ? Style : "")}";
         }
 
+        /// <summary> Gets the current placeholder. Returns empty string if this component is inside a RadzenFormField.</summary>
+        protected string CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
+
         /// <summary>
         /// Closes this instance popup.
         /// </summary>
@@ -982,10 +986,13 @@ async Task OnChange()
         /// <inheritdoc />
         protected override string GetComponentCssClass()
         {
-            return ClassList.Create()
+            return ClassList.Create("rz-datepicker")
                             .Add("rz-datepicker-inline", Inline)
+                            .AddDisabled(Disabled)
+                            .Add("rz-state-empty", !HasValue)
                             .Add(FieldIdentifier, EditContext)
                             .ToString();
+
         }
 
         private async Task SetDay(DateTime newValue)
@@ -1062,8 +1069,15 @@ public override async Task SetParametersAsync(ParameterView parameters)
                 shouldClose = !visible;
             }
 
+            var disabledChanged = parameters.DidParameterChange(nameof(Disabled), Disabled);
+
             await base.SetParametersAsync(parameters);
 
+            if (disabledChanged)
+            {
+                FormFieldContext?.DisabledChanged(Disabled);
+            }
+
             if (shouldClose && !firstRender && IsJSRuntimeAvailable)
             {
                 await JSRuntime.InvokeVoidAsync("Radzen.destroyPopup", PopupID);
diff --git a/Radzen.Blazor/themes/components/blazor/_form-field.scss b/Radzen.Blazor/themes/components/blazor/_form-field.scss
index 191752845c4..e7d988a417f 100644
--- a/Radzen.Blazor/themes/components/blazor/_form-field.scss
+++ b/Radzen.Blazor/themes/components/blazor/_form-field.scss
@@ -360,7 +360,8 @@ $form-field-helper-padding: 0 0.5rem !default;
     .rz-checkbox-list-horizontal  ~ &,
     .rz-chkbox ~ &,
     .rz-fileupload ~ &,
-    .rz-state-empty:has(.rz-placeholder) ~ & {
+    .rz-state-empty:has(.rz-placeholder) ~ &,
+    .rz-form-field.rz-state-focused & {
         inset-inline-end: auto;
         inset-block-start: var(--rz-form-field-label-floating-top);
         padding-block-start: 0;
@@ -383,7 +384,8 @@ $form-field-helper-padding: 0 0.5rem !default;
     .rz-form-field:not(.rz-variant-outlined) .rz-checkbox-list-horizontal  ~ &,
     .rz-form-field:not(.rz-variant-outlined) .rz-chkbox ~ &,
     .rz-form-field:not(.rz-variant-outlined) .rz-fileupload ~ &,
-    .rz-form-field:not(.rz-variant-outlined) .rz-state-empty:has(.rz-placeholder) ~ & {
+    .rz-form-field:not(.rz-variant-outlined) .rz-state-empty:has(.rz-placeholder) ~ &,
+    .rz-form-field:not(.rz-variant-outlined).rz-state-focused & {
         background-color: inherit !important;
     }
 
@@ -393,7 +395,8 @@ $form-field-helper-padding: 0 0.5rem !default;
 
     .rz-state-focused &,
     .rz-variant-filled.rz-state-focused &,
-    .rz-variant-flat.rz-state-focused & {
+    .rz-variant-flat.rz-state-focused &,
+    .rz-form-field.rz-state-focused & {
         color: var(--rz-form-field-label-focus-color);
     }
 
diff --git a/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor b/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor
index e8a17e249fb..eeea15152d3 100644
--- a/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor
+++ b/RadzenBlazorDemos/Pages/FormFieldInputTypes.razor
@@ -31,6 +31,9 @@
                 <RadzenFormField Text="RadzenAutoComplete" Variant="@variant">
                     <RadzenAutoComplete Data=@companyNames @bind-Value="@autoCompleteValue" />
                 </RadzenFormField>
+                <RadzenFormField Text="RadzenDatePicker" Variant="@variant">
+                    <RadzenDatePicker @bind-Value="@date" ShowTime="@true" />
+                </RadzenFormField>
                 <RadzenFormField Text="RadzenColorPicker" Variant="@variant">
                     <RadzenColorPicker @bind-Value="@color" />
                 </RadzenFormField>
@@ -61,6 +64,7 @@
     string dropDownDataGridValue = "";
     string autoCompleteValue = "";
     string color = "rgb(68, 58, 110)";
+    DateTime? date = DateTime.Today;
 
     IEnumerable<string> companyNames;
 


Test patch:
diff --git a/Radzen.Blazor.Tests/DatePickerTests.cs b/Radzen.Blazor.Tests/DatePickerTests.cs
index bf28fe1223b..2699659a694 100644
--- a/Radzen.Blazor.Tests/DatePickerTests.cs
+++ b/Radzen.Blazor.Tests/DatePickerTests.cs
@@ -174,6 +174,34 @@ public void DatePicker_Renders_TabIndexParameter()
             Assert.Contains(@$"tabindex=""{value}""", component.Markup);
         }
 
+        [Fact]
+        public void DatePicker_Renders_EmptyCssClass_WhenValueIsEmpty()
+        {
+            using var ctx = new TestContext();
+            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
+            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
+
+            var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
+
+            component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, null));
+
+            Assert.Contains(@$"rz-state-empty", component.Markup);
+        }
+
+        [Fact]
+        public void DatePicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty()
+        {
+            using var ctx = new TestContext();
+            ctx.JSInterop.Mode = JSRuntimeMode.Loose;
+            ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
+
+            var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
+
+            component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, DateTime.Now));
+
+            Assert.DoesNotContain(@$"rz-state-empty", component.Markup);
+        }
+
         [Fact]
         public void DatePicker_Renders_DisabledParameter()
         {