Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/Components/Web/src/Forms/InputDate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ public class InputDate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberType

/// <summary>
/// Gets or sets the type of HTML input to be rendered.
/// If not specified, the type is automatically inferred based on <typeparamref name="TValue"/>:
/// <list type="bullet">
/// <item><description><see cref="TimeOnly"/> defaults to <see cref="InputDateType.Time"/></description></item>
/// <item><description>All other types (<see cref="DateTime"/>, <see cref="DateTimeOffset"/>, <see cref="DateOnly"/>) default to <see cref="InputDateType.Date"/></description></item>
/// </list>
/// </summary>
[Parameter] public InputDateType Type { get; set; } = InputDateType.Date;
[Parameter] public InputDateType? Type { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change the public API. Use = null! or default or change the getter to use a InputDateTime? field.


/// <summary>
/// Gets or sets the error message used when displaying an a parsing error.
Expand Down Expand Up @@ -66,20 +71,35 @@ public InputDate()
/// <inheritdoc />
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)
? $"The {{0}} field must be a {formatDescription}."
: 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;
}

/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>.Type.get -> Microsoft.AspNetCore.Components.Forms.InputDateType
Microsoft.AspNetCore.Components.Forms.InputDate<TValue>.Type.get -> Microsoft.AspNetCore.Components.Forms.InputDateType?
182 changes: 182 additions & 0 deletions src/Components/Web/test/Forms/InputDateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<DateTime, TestInputDateComponent>
{
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<DateTimeOffset, TestInputDateTimeOffsetComponent>
{
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<DateOnly, TestInputDateOnlyComponent>
{
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<TimeOnly, TestInputTimeOnlyComponent>
{
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<DateTime, TestInputDateComponent>
{
EditContext = new EditContext(model),
ValueExpression = () => model.DateProperty,
AdditionalAttributes = new Dictionary<string, object>
{
{ "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<TimeOnly, TestInputTimeOnlyComponent>
{
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<DateOnly, TestInputDateOnlyComponent>
{
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<DateTime>
{
public async Task SetCurrentValueAsStringAsync(string value)
Expand All @@ -65,4 +223,28 @@ public async Task SetCurrentValueAsStringAsync(string value)
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}

private class TestInputDateTimeOffsetComponent : InputDate<DateTimeOffset>
{
public async Task SetCurrentValueAsStringAsync(string value)
{
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}

private class TestInputDateOnlyComponent : InputDate<DateOnly>
{
public async Task SetCurrentValueAsStringAsync(string value)
{
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}

private class TestInputTimeOnlyComponent : InputDate<TimeOnly>
{
public async Task SetCurrentValueAsStringAsync(string value)
{
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}
}
Loading