diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 4498dd539b48..9fb60b07ec63 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -31,8 +31,13 @@ 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 : + /// + /// defaults to + /// All other types (, , ) default 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 +71,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 +87,19 @@ protected override void OnParametersSet() : ParsingErrorMessage; } + private static InputDateType GetDefaultInputDateType() + { + var type = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); + + if (type == typeof(TimeOnly)) + { + return InputDateType.Time; + } + + // DateTime, DateTimeOffset, and DateOnly all default to Date for backward compatibility + return InputDateType.Date; + } + /// 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..b37a8411c082 100644 --- a/src/Components/Web/test/Forms/InputDateTest.cs +++ b/src/Components/Web/test/Forms/InputDateTest.cs @@ -26,6 +26,7 @@ public async Task ValidationErrorUsesDisplayAttributeName() await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); // Assert + // 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.", validationMessages); @@ -49,11 +50,168 @@ public async Task InputElementIsAssignedSuccessfully() Assert.NotNull(inputSelectComponent.Element); } + [Fact] + public async Task DateTimeDefaultsToDate() + { + // 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 Date (Type is null, auto-detected) + Assert.Null(inputComponent.Type); + } + + [Fact] + public async Task DateTimeOffsetDefaultsToDate() + { + // 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 Date (Type is null, auto-detected) + 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 (Type is null, auto-detected) + 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 (Type is null, auto-detected) + 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.DateTimeLocal } + } + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Act + await inputComponent.SetCurrentValueAsStringAsync("invalidDate"); + + // 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 and time.", 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; }); + } + } }