Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ export class BrowserRenderer {
let value = attributeFrame ? frameReader.attributeValue(attributeFrame) : null;

if (value && element.tagName === 'INPUT') {
value = normalizeInputValue(value, element.getAttribute('type'));
value = normalizeInputValue(value, element);
}

switch (element.tagName) {
Expand Down Expand Up @@ -499,20 +499,23 @@ function parseMarkup(markup: string, isSvg: boolean) {
}
}

function normalizeInputValue(value: string, type: string | null): string {
function normalizeInputValue(value: string, element: Element): string {
// Time inputs (e.g. 'time' and 'datetime-local') misbehave on chromium-based
// browsers when a time is set that includes a seconds value of '00', most notably
// when entered from keyboard input. This behavior is not limited to specific
// 'step' attribute values, so we always remove the trailing seconds value if the
// time ends in '00'.
// Similarly, if a time-related element doesn't have any 'step' attribute, browsers
// treat this as "round to whole number of minutes" making it invalid to pass any
// 'seconds' value, so in that case we strip off the 'seconds' part of the value.

switch (type) {
switch (element.getAttribute('type')) {
case 'time':
return value.length === 8 && value.endsWith('00')
return value.length === 8 && (value.endsWith('00') || !element.hasAttribute('step'))
? value.substring(0, 5)
: value;
case 'datetime-local':
return value.length === 19 && value.endsWith('00')
return value.length === 19 && (value.endsWith('00') || !element.hasAttribute('step'))
? value.substring(0, 16)
: value;
default:
Expand Down
72 changes: 72 additions & 0 deletions src/Components/test/E2ETest/Tests/BindTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,78 @@ public void CanBindTimeStepTextboxNullableTimeOnly()
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}

[Fact]
public void CanBindDateTimeLocalDefaultStepTextboxDateTime()
{
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)

var target = Browser.Exists(By.Id("datetime-local-default-step-textbox-datetime"));
var boundValue = Browser.Exists(By.Id("datetime-local-default-step-textbox-datetime-value"));
var expected = DateTime.Now.Date.Add(new TimeSpan(8, 5, 0)); // Notice the "seconds" part is zero here, even though the original data has seconds=30
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));

// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(default, () => DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
Assert.Equal(default, DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));

// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
ApplyInputValue("#datetime-local-default-step-textbox-datetime", "2000-01-02T04:05");
expected = new DateTime(2000, 1, 2, 04, 05, 0);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
}

[Fact]
public void CanBindTimeDefaultStepTextboxDateTime()
{
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)

var target = Browser.Exists(By.Id("time-default-step-textbox-datetime"));
var boundValue = Browser.Exists(By.Id("time-default-step-textbox-datetime-value"));
var expected = DateTime.Now.Date.Add(new TimeSpan(8, 5, 0)); // Notice the "seconds" part is zero here, even though the original data has seconds=30
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));

// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
Assert.Equal(default, DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));

// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
ApplyInputValue("#time-default-step-textbox-datetime", "04:05");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
}

[Fact]
public void CanBindTimeDefaultStepTextboxTimeOnly()
{
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)

var target = Browser.Exists(By.Id("time-default-step-textbox-timeonly"));
var boundValue = Browser.Exists(By.Id("time-default-step-textbox-timeonly-value"));
var expected = new TimeOnly(8, 5, 0); // Notice the "seconds" part is zero here, even though the original data has seconds=30
Assert.Equal(expected, TimeOnly.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));

// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(default, () => TimeOnly.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
Assert.Equal(default, TimeOnly.Parse(boundValue.Text, CultureInfo.InvariantCulture));

// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
ApplyInputValue("#time-default-step-textbox-timeonly", "04:05");
expected = new TimeOnly(4, 5, 0);
Browser.Equal(expected, () => TimeOnly.Parse(boundValue.Text, CultureInfo.InvariantCulture));
}

// Applies an input through javascript to datetime-local/month/time controls.
private void ApplyInputValue(string cssSelector, string value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,25 @@
<input id="time-step-textbox-nullable-timeonly-mirror" @bind="timeStepTextboxNullableTimeOnlyValue" @bind:format="HH:mm:ss" readonly />
</p>

<h3>datetime-local with no step attribute bound to a value with seconds</h3>
<p>
DateTime:
<input id="datetime-local-default-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="datetime-local" />
<span id="datetime-local-default-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
</p>

<h3>time with no step attribute bound to a value with seconds</h3>
<p>
DateTime:
<input id="time-default-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="time" />
<span id="time-default-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
</p>
<p>
TimeOnly:
<input id="time-default-step-textbox-timeonly" @bind="timeStepTextboxTimeOnlyValue" type="time" />
<span id="time-default-step-textbox-timeonly-value">@timeStepTextboxTimeOnlyValue.ToLongTimeString()</span>
</p>

@code {
string textboxInitiallyBlankValue = null;
string textboxInitiallyPopulatedValue = "Hello";
Expand Down