Skip to content

Commit 8c5a59a

Browse files
Fix time/datetime-local binding with seconds when there's no step attribute (#41868)
1 parent c08123a commit 8c5a59a

File tree

5 files changed

+101
-7
lines changed

5 files changed

+101
-7
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Rendering/BrowserRenderer.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ export class BrowserRenderer {
402402
let value = attributeFrame ? frameReader.attributeValue(attributeFrame) : null;
403403

404404
if (value && element.tagName === 'INPUT') {
405-
value = normalizeInputValue(value, element.getAttribute('type'));
405+
value = normalizeInputValue(value, element);
406406
}
407407

408408
switch (element.tagName) {
@@ -499,20 +499,23 @@ function parseMarkup(markup: string, isSvg: boolean) {
499499
}
500500
}
501501

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

509-
switch (type) {
512+
switch (element.getAttribute('type')) {
510513
case 'time':
511-
return value.length === 8 && value.endsWith('00')
514+
return value.length === 8 && (value.endsWith('00') || !element.hasAttribute('step'))
512515
? value.substring(0, 5)
513516
: value;
514517
case 'datetime-local':
515-
return value.length === 19 && value.endsWith('00')
518+
return value.length === 19 && (value.endsWith('00') || !element.hasAttribute('step'))
516519
? value.substring(0, 16)
517520
: value;
518521
default:

src/Components/test/E2ETest/Tests/BindTest.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,6 +2021,78 @@ public void CanBindTimeStepTextboxNullableTimeOnly()
20212021
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
20222022
}
20232023

2024+
[Fact]
2025+
public void CanBindDateTimeLocalDefaultStepTextboxDateTime()
2026+
{
2027+
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
2028+
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
2029+
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)
2030+
2031+
var target = Browser.Exists(By.Id("datetime-local-default-step-textbox-datetime"));
2032+
var boundValue = Browser.Exists(By.Id("datetime-local-default-step-textbox-datetime-value"));
2033+
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
2034+
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2035+
2036+
// Clear textbox; value updates to 00:00 because that's the default
2037+
target.Clear();
2038+
expected = default;
2039+
Browser.Equal(default, () => DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2040+
Assert.Equal(default, DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2041+
2042+
// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
2043+
ApplyInputValue("#datetime-local-default-step-textbox-datetime", "2000-01-02T04:05");
2044+
expected = new DateTime(2000, 1, 2, 04, 05, 0);
2045+
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2046+
}
2047+
2048+
[Fact]
2049+
public void CanBindTimeDefaultStepTextboxDateTime()
2050+
{
2051+
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
2052+
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
2053+
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)
2054+
2055+
var target = Browser.Exists(By.Id("time-default-step-textbox-datetime"));
2056+
var boundValue = Browser.Exists(By.Id("time-default-step-textbox-datetime-value"));
2057+
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
2058+
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2059+
2060+
// Clear textbox; value updates to 00:00 because that's the default
2061+
target.Clear();
2062+
expected = default;
2063+
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2064+
Assert.Equal(default, DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2065+
2066+
// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
2067+
ApplyInputValue("#time-default-step-textbox-datetime", "04:05");
2068+
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 0));
2069+
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2070+
}
2071+
2072+
[Fact]
2073+
public void CanBindTimeDefaultStepTextboxTimeOnly()
2074+
{
2075+
// This test differs from the other "step"-related test in that the DOM element has no "step" attribute
2076+
// and hence defaults to step=60, and for this the framework has explicit logic to strip off the "seconds"
2077+
// part of the bound value (otherwise the browser reports it as invalid - issue #41731)
2078+
2079+
var target = Browser.Exists(By.Id("time-default-step-textbox-timeonly"));
2080+
var boundValue = Browser.Exists(By.Id("time-default-step-textbox-timeonly-value"));
2081+
var expected = new TimeOnly(8, 5, 0); // Notice the "seconds" part is zero here, even though the original data has seconds=30
2082+
Assert.Equal(expected, TimeOnly.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2083+
2084+
// Clear textbox; value updates to 00:00 because that's the default
2085+
target.Clear();
2086+
expected = default;
2087+
Browser.Equal(default, () => TimeOnly.Parse(target.GetAttribute("value"), CultureInfo.InvariantCulture));
2088+
Assert.Equal(default, TimeOnly.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2089+
2090+
// We have to do it this way because the browser gets in the way when sending keys to the input element directly.
2091+
ApplyInputValue("#time-default-step-textbox-timeonly", "04:05");
2092+
expected = new TimeOnly(4, 5, 0);
2093+
Browser.Equal(expected, () => TimeOnly.Parse(boundValue.Text, CultureInfo.InvariantCulture));
2094+
}
2095+
20242096
// Applies an input through javascript to datetime-local/month/time controls.
20252097
private void ApplyInputValue(string cssSelector, string value)
20262098
{

src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,25 @@
453453
<input id="time-step-textbox-nullable-timeonly-mirror" @bind="timeStepTextboxNullableTimeOnlyValue" @bind:format="HH:mm:ss" readonly />
454454
</p>
455455

456+
<h3>datetime-local with no step attribute bound to a value with seconds</h3>
457+
<p>
458+
DateTime:
459+
<input id="datetime-local-default-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="datetime-local" />
460+
<span id="datetime-local-default-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
461+
</p>
462+
463+
<h3>time with no step attribute bound to a value with seconds</h3>
464+
<p>
465+
DateTime:
466+
<input id="time-default-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="time" />
467+
<span id="time-default-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
468+
</p>
469+
<p>
470+
TimeOnly:
471+
<input id="time-default-step-textbox-timeonly" @bind="timeStepTextboxTimeOnlyValue" type="time" />
472+
<span id="time-default-step-textbox-timeonly-value">@timeStepTextboxTimeOnlyValue.ToLongTimeString()</span>
473+
</p>
474+
456475
@code {
457476
string textboxInitiallyBlankValue = null;
458477
string textboxInitiallyPopulatedValue = "Hello";

0 commit comments

Comments
 (0)