From ba708e95367c5cb5d7d4c3885ed27d152a1225b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:46:00 +0000 Subject: [PATCH 1/4] Initial plan From 4de00ab2fb2739d96f84651e7a90a9979f5ed5c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:56:27 +0000 Subject: [PATCH 2/4] Implement auto-select input type in InputDate based on TValue Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/Web/src/Forms/InputDate.cs | 26 ++- .../Web/src/PublicAPI.Unshipped.txt | 2 + .../Web/test/Forms/InputDateTest.cs | 184 +++++++++++++++++- 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 4498dd539b48..131aebb06b8d 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -31,8 +31,14 @@ public class InputDate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberType /// /// Gets or sets the type of HTML input to be rendered. + /// If not specified, the type is automatically inferred based on : + /// + /// and default to + /// defaults to + /// defaults to + /// /// - [Parameter] public InputDateType Type { get; set; } = InputDateType.Date; + [Parameter] public InputDateType? Type { get; set; } /// /// Gets or sets the error message used when displaying an a parsing error. @@ -66,13 +72,15 @@ public InputDate() /// protected override void OnParametersSet() { - (_typeAttributeValue, _format, var formatDescription) = Type switch + var effectiveType = Type ?? GetDefaultInputDateType(); + + (_typeAttributeValue, _format, var formatDescription) = effectiveType switch { InputDateType.Date => ("date", DateFormat, "date"), InputDateType.DateTimeLocal => ("datetime-local", DateTimeLocalFormat, "date and time"), InputDateType.Month => ("month", MonthFormat, "year and month"), InputDateType.Time => ("time", TimeFormat, "time"), - _ => throw new InvalidOperationException($"Unsupported {nameof(InputDateType)} '{Type}'.") + _ => throw new InvalidOperationException($"Unsupported {nameof(InputDateType)} '{effectiveType}'.") }; _parsingErrorMessage = string.IsNullOrEmpty(ParsingErrorMessage) @@ -80,6 +88,18 @@ protected override void OnParametersSet() : ParsingErrorMessage; } + private static InputDateType GetDefaultInputDateType() + { + var type = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); + + return type switch + { + Type t when t == typeof(DateOnly) => InputDateType.Date, + Type t when t == typeof(TimeOnly) => InputDateType.Time, + _ => InputDateType.DateTimeLocal, // DateTime and DateTimeOffset + }; + } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 369f33715778..69eb3a7a5d7c 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -40,3 +40,5 @@ Microsoft.AspNetCore.Components.Web.Media.MediaSource.MediaSource(byte[]! data, Microsoft.AspNetCore.Components.Web.Media.MediaSource.MediaSource(System.IO.Stream! stream, string! mimeType, string! cacheKey) -> void Microsoft.AspNetCore.Components.Web.Media.MediaSource.MimeType.get -> string! Microsoft.AspNetCore.Components.Web.Media.MediaSource.Stream.get -> System.IO.Stream! +*REMOVED*Microsoft.AspNetCore.Components.Forms.InputDate.Type.get -> Microsoft.AspNetCore.Components.Forms.InputDateType +Microsoft.AspNetCore.Components.Forms.InputDate.Type.get -> Microsoft.AspNetCore.Components.Forms.InputDateType? diff --git a/src/Components/Web/test/Forms/InputDateTest.cs b/src/Components/Web/test/Forms/InputDateTest.cs index a698e550be1a..95ccf51a3cdc 100644 --- a/src/Components/Web/test/Forms/InputDateTest.cs +++ b/src/Components/Web/test/Forms/InputDateTest.cs @@ -26,9 +26,10 @@ public async Task ValidationErrorUsesDisplayAttributeName() await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); // Assert + // DateTime defaults to DateTimeLocal, so the error message is "date and time" var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); Assert.NotEmpty(validationMessages); - Assert.Contains("The Date property field must be a date.", validationMessages); + Assert.Contains("The Date property field must be a date and time.", validationMessages); } [Fact] @@ -49,11 +50,168 @@ public async Task InputElementIsAssignedSuccessfully() Assert.NotNull(inputSelectComponent.Element); } + [Fact] + public async Task DateTimeDefaultsToDateTimeLocal() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + }; + + // Act + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert - DateTime should default to DateTimeLocal, not explicitly set Type + Assert.Null(inputComponent.Type); + } + + [Fact] + public async Task DateTimeOffsetDefaultsToDateTimeLocal() + { + // Arrange + var model = new TestModelDateTimeOffset(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + }; + + // Act + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert - DateTimeOffset should default to DateTimeLocal + Assert.Null(inputComponent.Type); + } + + [Fact] + public async Task DateOnlyDefaultsToDate() + { + // Arrange + var model = new TestModelDateOnly(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + }; + + // Act + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert - DateOnly should default to Date + Assert.Null(inputComponent.Type); + } + + [Fact] + public async Task TimeOnlyDefaultsToTime() + { + // Arrange + var model = new TestModelTimeOnly(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.TimeProperty, + }; + + // Act + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert - TimeOnly should default to Time + Assert.Null(inputComponent.Type); + } + + [Fact] + public async Task ExplicitTypeOverridesAutoDetection() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + AdditionalAttributes = new Dictionary + { + { "Type", InputDateType.Date } + } + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Act + await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); + + // Assert - Explicitly set Type=Date should produce "date" error message + var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); + Assert.NotEmpty(validationMessages); + Assert.Contains("The DateProperty field must be a date.", validationMessages); + } + + [Fact] + public async Task TimeOnlyValidationErrorMessage() + { + // Arrange + var model = new TestModelTimeOnly(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.TimeProperty, + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.TimeProperty); + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Act + await inputComponent.SetCurrentValueAsStringAsync("invalidTime"); + + // Assert - TimeOnly should default to Time, so error message is "time" + var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); + Assert.NotEmpty(validationMessages); + Assert.Contains("The TimeProperty field must be a time.", validationMessages); + } + + [Fact] + public async Task DateOnlyValidationErrorMessage() + { + // Arrange + var model = new TestModelDateOnly(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Act + await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); + + // Assert - DateOnly should default to Date, so error message is "date" + var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); + Assert.NotEmpty(validationMessages); + Assert.Contains("The DateProperty field must be a date.", validationMessages); + } + private class TestModel { public DateTime DateProperty { get; set; } } + private class TestModelDateTimeOffset + { + public DateTimeOffset DateProperty { get; set; } + } + + private class TestModelDateOnly + { + public DateOnly DateProperty { get; set; } + } + + private class TestModelTimeOnly + { + public TimeOnly TimeProperty { get; set; } + } + private class TestInputDateComponent : InputDate { public async Task SetCurrentValueAsStringAsync(string value) @@ -65,4 +223,28 @@ public async Task SetCurrentValueAsStringAsync(string value) await InvokeAsync(() => { base.CurrentValueAsString = value; }); } } + + private class TestInputDateTimeOffsetComponent : InputDate + { + public async Task SetCurrentValueAsStringAsync(string value) + { + await InvokeAsync(() => { base.CurrentValueAsString = value; }); + } + } + + private class TestInputDateOnlyComponent : InputDate + { + public async Task SetCurrentValueAsStringAsync(string value) + { + await InvokeAsync(() => { base.CurrentValueAsString = value; }); + } + } + + private class TestInputTimeOnlyComponent : InputDate + { + public async Task SetCurrentValueAsStringAsync(string value) + { + await InvokeAsync(() => { base.CurrentValueAsString = value; }); + } + } } From ade5aeda89ae488a0f864bc87d009e2199aaba37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:58:02 +0000 Subject: [PATCH 3/4] Simplify GetDefaultInputDateType method based on code review feedback Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/Web/src/Forms/InputDate.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 131aebb06b8d..87a3fd347131 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -92,12 +92,17 @@ private static InputDateType GetDefaultInputDateType() { var type = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); - return type switch + if (type == typeof(DateOnly)) { - Type t when t == typeof(DateOnly) => InputDateType.Date, - Type t when t == typeof(TimeOnly) => InputDateType.Time, - _ => InputDateType.DateTimeLocal, // DateTime and DateTimeOffset - }; + return InputDateType.Date; + } + + if (type == typeof(TimeOnly)) + { + return InputDateType.Time; + } + + return InputDateType.DateTimeLocal; // DateTime and DateTimeOffset } /// From c691d7feebc3fc8bba10efca025e83dd5f5b6999 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:01:57 +0000 Subject: [PATCH 4/4] Update to conservative implementation - auto-detect TimeOnly only Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/Web/src/Forms/InputDate.cs | 11 +++------- .../Web/test/Forms/InputDateTest.cs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 87a3fd347131..9fb60b07ec63 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -33,9 +33,8 @@ public class InputDate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberType /// Gets or sets the type of HTML input to be rendered. /// If not specified, the type is automatically inferred based on : /// - /// and default to - /// defaults to /// defaults to + /// All other types (, , ) default to /// /// [Parameter] public InputDateType? Type { get; set; } @@ -92,17 +91,13 @@ private static InputDateType GetDefaultInputDateType() { var type = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); - if (type == typeof(DateOnly)) - { - return InputDateType.Date; - } - if (type == typeof(TimeOnly)) { return InputDateType.Time; } - return InputDateType.DateTimeLocal; // DateTime and DateTimeOffset + // DateTime, DateTimeOffset, and DateOnly all default to Date for backward compatibility + return InputDateType.Date; } /// diff --git a/src/Components/Web/test/Forms/InputDateTest.cs b/src/Components/Web/test/Forms/InputDateTest.cs index 95ccf51a3cdc..b37a8411c082 100644 --- a/src/Components/Web/test/Forms/InputDateTest.cs +++ b/src/Components/Web/test/Forms/InputDateTest.cs @@ -26,10 +26,10 @@ public async Task ValidationErrorUsesDisplayAttributeName() await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); // Assert - // DateTime defaults to DateTimeLocal, so the error message is "date and time" + // DateTime defaults to Date for backward compatibility var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); Assert.NotEmpty(validationMessages); - Assert.Contains("The Date property field must be a date and time.", validationMessages); + Assert.Contains("The Date property field must be a date.", validationMessages); } [Fact] @@ -51,7 +51,7 @@ public async Task InputElementIsAssignedSuccessfully() } [Fact] - public async Task DateTimeDefaultsToDateTimeLocal() + public async Task DateTimeDefaultsToDate() { // Arrange var model = new TestModel(); @@ -64,12 +64,12 @@ public async Task DateTimeDefaultsToDateTimeLocal() // Act var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); - // Assert - DateTime should default to DateTimeLocal, not explicitly set Type + // Assert - DateTime should default to Date (Type is null, auto-detected) Assert.Null(inputComponent.Type); } [Fact] - public async Task DateTimeOffsetDefaultsToDateTimeLocal() + public async Task DateTimeOffsetDefaultsToDate() { // Arrange var model = new TestModelDateTimeOffset(); @@ -82,7 +82,7 @@ public async Task DateTimeOffsetDefaultsToDateTimeLocal() // Act var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); - // Assert - DateTimeOffset should default to DateTimeLocal + // Assert - DateTimeOffset should default to Date (Type is null, auto-detected) Assert.Null(inputComponent.Type); } @@ -100,7 +100,7 @@ public async Task DateOnlyDefaultsToDate() // Act var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); - // Assert - DateOnly should default to Date + // Assert - DateOnly should default to Date (Type is null, auto-detected) Assert.Null(inputComponent.Type); } @@ -118,7 +118,7 @@ public async Task TimeOnlyDefaultsToTime() // Act var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); - // Assert - TimeOnly should default to Time + // Assert - TimeOnly should default to Time (Type is null, auto-detected) Assert.Null(inputComponent.Type); } @@ -133,7 +133,7 @@ public async Task ExplicitTypeOverridesAutoDetection() ValueExpression = () => model.DateProperty, AdditionalAttributes = new Dictionary { - { "Type", InputDateType.Date } + { "Type", InputDateType.DateTimeLocal } } }; var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); @@ -142,10 +142,10 @@ public async Task ExplicitTypeOverridesAutoDetection() // Act await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); - // Assert - Explicitly set Type=Date should produce "date" error message + // Assert - Explicitly set Type=DateTimeLocal should produce "date and time" error message var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier); Assert.NotEmpty(validationMessages); - Assert.Contains("The DateProperty field must be a date.", validationMessages); + Assert.Contains("The DateProperty field must be a date and time.", validationMessages); } [Fact]