diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs
index 4234d17e6a0b107029121e832342526f2a8d673d..204c1cd104efe60133d078c4bc64812a7d35d878 100644
--- a/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs
+++ b/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs
@@ -19,11 +19,6 @@ public sealed partial class DatePickerBody
{
get
{
- if (CurrentDate == DateTime.MinValue)
- {
- CurrentDate = DateTime.Today;
- }
-
var d = CurrentDate.AddDays(1 - CurrentDate.Day);
d = d.AddDays(0 - (int)d.DayOfWeek);
return d;
@@ -73,7 +68,7 @@ public sealed partial class DatePickerBody
.AddClass("disabled", IsDisabled(day))
.Build();
- private bool IsDisabled(DateTime day) => (MinValue != null && MaxValue != null) && (day < MinValue || day > MaxValue);
+ private bool IsDisabled(DateTime day) => (MinValue != null && day < MinValue) || (MaxValue != null && day > MaxValue);
///
/// 获得 年月日时分秒视图样式
@@ -517,7 +512,7 @@ public sealed partial class DatePickerBody
///
///
private string? GetMonthClassName(int month) => CssBuilder.Default()
- .AddClass("current", CurrentDate.Year == Value.Year && month == Value.Month)
+ .AddClass("current", month == Value.Month)
.AddClass("today", CurrentDate.Year == DateTime.Today.Year && month == DateTime.Today.Month)
.Build();
diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs
index a4b29062d164d31e825698d7a2f6a6b31858faf4..407ed5ce103882b2ea33dc442bd66838fd5f65f0 100644
--- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs
+++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs
@@ -169,12 +169,14 @@ public sealed partial class DateTimePicker
// 泛型设置为可为空
AllowNull = typeof(TValue) == typeof(DateTime?);
+ }
- // 不允许为空时设置 Value 默认值
- if (!AllowNull && Value == null)
- {
- CurrentValue = (TValue)(object)DateTime.Now;
- }
+ ///
+ /// OnParametersSet 方法
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
// Value 为 MinValue 时 设置 Value 默认值
if (Value?.ToString() == DateTime.MinValue.ToString())
@@ -222,7 +224,7 @@ public sealed partial class DateTimePicker
///
private async Task OnClear()
{
- CurrentValue = default!;
+ CurrentValue = default;
await JSRuntime.InvokeVoidAsync(Picker, "bb_datetimePicker", "hide");
if (OnDateTimeChanged != null)
{
diff --git a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerBody.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerBody.razor.cs
index fcb284fe36fbf2258e77f6395fbd1b24fb76e2ea..9b15155d515c626b42dcee5e93d3e9472d3ecb4d 100644
--- a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerBody.razor.cs
+++ b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerBody.razor.cs
@@ -126,7 +126,6 @@ public sealed partial class TimePickerBody
{
await ValueChanged.InvokeAsync(Value);
}
-
OnConfirm?.Invoke();
}
}
diff --git a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerCell.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerCell.razor.cs
index 160e6add76ae0b724322c28708b804166e3f54a9..c55853f70ddb138920fe4c7452d0c70117c37698 100644
--- a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerCell.razor.cs
+++ b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerCell.razor.cs
@@ -22,22 +22,19 @@ public partial class TimePickerCell : IDisposable
{
TimePickerCellViewModel.Hour => Value.Hours - 1 == index,
TimePickerCellViewModel.Minute => Value.Minutes - 1 == index,
- TimePickerCellViewModel.Second => Value.Seconds - 1 == index,
- _ => false
+ _ => Value.Seconds - 1 == index
})
.AddClass("active", ViewModel switch
{
TimePickerCellViewModel.Hour => Value.Hours == index,
TimePickerCellViewModel.Minute => Value.Minutes == index,
- TimePickerCellViewModel.Second => Value.Seconds == index,
- _ => false
+ _ => Value.Seconds == index
})
.AddClass("next", ViewModel switch
{
TimePickerCellViewModel.Hour => Value.Hours + 1 == index,
TimePickerCellViewModel.Minute => Value.Minutes + 1 == index,
- TimePickerCellViewModel.Second => Value.Seconds + 1 == index,
- _ => false
+ _ => Value.Seconds + 1 == index
})
.Build();
@@ -137,8 +134,7 @@ public partial class TimePickerCell : IDisposable
{
TimePickerCellViewModel.Hour => TimeSpan.FromHours(1),
TimePickerCellViewModel.Minute => TimeSpan.FromMinutes(1),
- TimePickerCellViewModel.Second => TimeSpan.FromSeconds(1),
- _ => TimeSpan.Zero
+ _ => TimeSpan.FromSeconds(1)
};
Value = Value.Add(ts);
if (Value.Days > 0)
@@ -159,8 +155,7 @@ public partial class TimePickerCell : IDisposable
{
TimePickerCellViewModel.Hour => (Value.Hours) * height,
TimePickerCellViewModel.Minute => (Value.Minutes) * height,
- TimePickerCellViewModel.Second => (Value.Seconds) * height,
- _ => 0
+ _ => (Value.Seconds) * height
};
}
@@ -170,11 +165,13 @@ public partial class TimePickerCell : IDisposable
///
protected virtual void Dispose(bool disposing)
{
-
- if (disposing && Interop != null)
+ if (disposing)
{
- Interop.Dispose();
- Interop = null;
+ if (Interop != null)
+ {
+ Interop.Dispose();
+ Interop = null;
+ }
}
}
@@ -183,7 +180,7 @@ public partial class TimePickerCell : IDisposable
///
public void Dispose()
{
- Dispose(disposing: true);
+ Dispose(true);
GC.SuppressFinalize(this);
}
}
diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..1861e5b47f1ff6cc46372c9a2e680e0b6696abb9
--- /dev/null
+++ b/test/UnitTest/Components/DateTimePickerTest.cs
@@ -0,0 +1,495 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+using BootstrapBlazor.Shared;
+
+namespace UnitTest.Components;
+
+public class DateTimePickerTest : BootstrapBlazorTestBase
+{
+ #region DateTimePicker
+ [Fact]
+ public void Value_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Value, DateTime.MinValue);
+ });
+ // 设置 Value 为 MinValue 内部更改为 DateTime.Now
+ Assert.NotEqual(DateTime.MinValue, cut.Instance.Value);
+ }
+
+ [Fact]
+ public void ShowSiderBar_Ok()
+ {
+ var cut = Context.RenderComponent>(builder => builder.Add(a => a.ShowSidebar, true));
+
+ var ele = cut.Find(".picker-panel-sidebar");
+ Assert.NotNull(ele);
+ }
+
+ [Fact]
+ public void Placement_Ok()
+ {
+ var cut = Context.RenderComponent>(builder => builder.Add(a => a.Placement, Placement.Top));
+
+ Assert.Contains("data-bs-placement=\"top\"", cut.Markup);
+ }
+
+ [Fact]
+ public void Format_OK()
+ {
+ var cut = Context.RenderComponent>(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.Format, "yyyy/MM/dd");
+ });
+
+ var value = cut.Find(".datetime-picker-bar").Children.First().GetAttribute("value");
+
+ Assert.Equal(value, DateTime.Now.ToString("yyyy/MM/dd"));
+ }
+
+ [Fact]
+ public void MaxValue_Ok()
+ {
+ var cut = Context.RenderComponent>(builder => builder.Add(a => a.MaxValue, DateTime.Today.AddDays(1)));
+ }
+
+ [Fact]
+ public void MinValue_Ok()
+ {
+ var cut = Context.RenderComponent>(builder => builder.Add(a => a.MinValue, DateTime.Today.AddDays(-1)));
+ }
+
+ [Fact]
+ public void OnDateTimeChanged_Ok()
+ {
+ var res = false;
+ var cut = Context.RenderComponent>(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.OnDateTimeChanged, new Func(d =>
+ {
+ res = true;
+ return Task.CompletedTask;
+ }));
+ });
+
+ cut.Find(".picker-panel-footer").Children.Last().Click();
+
+ Assert.True(res);
+ }
+
+ [Fact]
+ public void OnClear_Ok()
+ {
+ var changed = false;
+ var cut = Context.RenderComponent>(pb =>
+ {
+ pb.Add(a => a.OnDateTimeChanged, v =>
+ {
+ changed = true;
+ return Task.CompletedTask;
+ });
+ });
+ Assert.Null(cut.Instance.Value);
+
+ cut.InvokeAsync(() => cut.Find(".current .cell").Click());
+ // confirm
+ var buttons = cut.FindAll(".picker-panel-footer button");
+ cut.InvokeAsync(() => buttons[2].Click());
+ Assert.NotNull(cut.Instance.Value);
+ Assert.True(changed);
+
+ cut.InvokeAsync(() => buttons[0].Click());
+ Assert.Null(cut.Instance.Value);
+ }
+
+ [Fact]
+ public void ValidateForm_Ok()
+ {
+ var foo = new Foo();
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Model, foo);
+ pb.AddChildContent>(pb =>
+ {
+ pb.Add(a => a.Value, foo.DateTime);
+ pb.Add(a => a.ValueExpression, foo.GenerateValueExpression(nameof(Foo.DateTime), typeof(DateTime?)));
+ });
+ });
+ cut.Contains("class=\"form-label\"");
+ }
+
+ [Fact]
+ public void NotDateTime_Error()
+ {
+ Assert.ThrowsAny(() =>
+ {
+ Context.RenderComponent>();
+ });
+ }
+ #endregion
+
+ #region DatePicker
+ [Fact]
+ public void DateFormat_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.ShowFooter, false);
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.DateFormat, "yyyy/MM/dd");
+ });
+
+ cut.InvokeAsync(() => cut.Find(".current .cell").Click());
+ cut.Contains($"value=\"{DateTime.Today:yyyy/MM/dd}\"");
+ }
+
+ [Fact]
+ public void DatePickerViewModel_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.ViewModel, DatePickerViewModel.Year);
+ });
+
+ var labels = cut.FindAll(".date-picker-header-label");
+ Assert.Equal(GetYearPeriod(), labels[0].TextContent);
+
+ // 上一年
+ var buttons = cut.FindAll(".date-picker-header button");
+ cut.InvokeAsync(() => buttons[0].Click());
+
+ // 下一年
+ cut.InvokeAsync(() => buttons[3].Click());
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ViewModel, DatePickerViewModel.Month);
+ pb.Add(a => a.Value, GetToday());
+ });
+
+ DateTime GetToday()
+ {
+ var buffer = 1;
+ var month = DateTime.Today.Month + buffer;
+ if (month > 6)
+ {
+ buffer = -1;
+ }
+ return DateTime.Today.AddMonths(month);
+ }
+
+ string GetYearPeriod()
+ {
+ var start = DateTime.Today.AddYears(0 - DateTime.Today.Year % 20).Year;
+ return string.Format("{0} 年 - {1} 年", start, start + 19);
+ }
+ }
+
+ [Fact]
+ public void ShowSidebar_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.ShowSidebar, true);
+ });
+
+ var ele = cut.Find(".picker-panel-sidebar");
+ Assert.NotNull(ele);
+
+ var cells = cut.FindAll(".cell");
+ cut.InvokeAsync(() => cells[0].Click());
+ cut.InvokeAsync(() => cells[1].Click());
+ cut.InvokeAsync(() => cells[2].Click());
+ }
+
+ [Fact]
+ public void ShowButtons_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ });
+
+ var buttons = cut.FindAll(".date-picker-header button");
+
+ // 上一年
+ cut.InvokeAsync(() => buttons[0].Click());
+ var labels = cut.FindAll(".date-picker-header-label");
+ Assert.Equal((DateTime.Today.Year - 1).ToString() + " 年", labels[0].TextContent);
+
+ // 下一年
+ cut.InvokeAsync(() => buttons[3].Click());
+ labels = cut.FindAll(".date-picker-header-label");
+ Assert.Equal(DateTime.Today.Year.ToString() + " 年", labels[0].TextContent);
+
+ // 上一月
+ cut.InvokeAsync(() => buttons[1].Click());
+ labels = cut.FindAll(".date-picker-header-label");
+ Assert.Equal((DateTime.Today.Month - 1).ToString() + " 月", labels[1].TextContent);
+
+ // 下一月
+ cut.InvokeAsync(() => buttons[2].Click());
+ labels = cut.FindAll(".date-picker-header-label");
+ Assert.Equal(DateTime.Today.Month.ToString() + " 月", labels[1].TextContent);
+
+ // 年视图
+ labels = cut.FindAll(".date-picker-header-label");
+ cut.InvokeAsync(() => labels[0].Click());
+ cut.Contains("class=\"year-table\"");
+
+ cut.InvokeAsync(() => cut.Find(".year-table .current.today .cell").Click());
+ }
+
+ [Fact]
+ public void NotShowButtons_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.ShowLeftButtons, false);
+ });
+ Assert.DoesNotContain("fa fa-angle-double-left", cut.Find(".date-picker-header").ToMarkup());
+
+ cut.SetParametersAndRender(pb => pb.Add(a => a.ShowRightButtons, false));
+ Assert.DoesNotContain("fa fa-angle-double-right", cut.Find(".date-picker-header").ToMarkup());
+ }
+
+ [Fact]
+ public void MonthView_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ });
+ var labels = cut.FindAll(".date-picker-header-label");
+ cut.InvokeAsync(() => labels[1].Click());
+ cut.Contains("class=\"month-table\"");
+
+ cut.InvokeAsync(() => cut.Find(".month-table .current.today .cell").Click());
+ }
+
+ [Fact]
+ public void IsDiabledCell_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.MinValue, DateTime.Today.AddDays(-1));
+ builder.Add(a => a.MaxValue, DateTime.Today.AddDays(7));
+ });
+ }
+
+ [Fact]
+ public void IsShown_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.IsShown, true);
+ });
+
+ var value = cut.Find(".picker-panel").ClassList.Contains("d-none");
+
+ Assert.False(value);
+ }
+
+ [Fact]
+ public void ShowFooter_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, DateTime.Now);
+ builder.Add(a => a.ShowFooter, true);
+ builder.Add(a => a.AllowNull, true);
+ });
+
+ var ele = cut.Find(".picker-panel-footer");
+ Assert.NotNull(ele);
+
+ var buttons = cut.FindAll(".picker-panel-footer button");
+
+ // Click Now
+ cut.InvokeAsync(() => buttons[1].Click());
+ Assert.Equal(DateTime.Today, cut.Instance.Value.Date);
+
+ cut.InvokeAsync(() => buttons[0].Click());
+ Assert.Equal(DateTime.Today, cut.Instance.Value);
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ShowFooter, false);
+ });
+ cut.InvokeAsync(() => cut.Find(".current.today .cell").Click());
+ }
+
+ [Fact]
+ public void ClickNowButton_Ok()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.ViewModel, DatePickerViewModel.DateTime);
+ builder.Add(a => a.ShowFooter, true);
+ builder.Add(a => a.Value, DateTime.Today.AddDays(-10));
+ });
+ var button = cut.Find(".is-now");
+ cut.InvokeAsync(() => button.Click());
+
+ // 有最小值 无 Now 按钮
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.MinValue, DateTime.Today.AddDays(10));
+ });
+ cut.DoesNotContain(".picker-panel-footer .is-now");
+ }
+
+ [Fact]
+ public void ClickDay_Validate()
+ {
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.ViewModel, DatePickerViewModel.DateTime);
+ builder.Add(a => a.ShowFooter, false);
+ builder.Add(a => a.Value, DateTime.Today);
+ });
+
+ // 不显示 Footer 点击日期直接确认 OnConfirm
+ var cell = cut.Find(".current.today .cell");
+ cut.InvokeAsync(() => cell.Click());
+
+ }
+
+ [Fact]
+ public void PlaceholderString_Ok()
+ {
+ using var cut = Context.RenderComponent>(pb =>
+ {
+ pb.Add(a => a.ViewModel, DatePickerViewModel.DateTime);
+ });
+
+ // 打开 Time 弹窗
+ var inputs = cut.FindAll(".date-picker-time-header input");
+ cut.InvokeAsync(() => inputs[1].Click());
+ cut.Contains("date-picker-time-header is-open");
+
+ // 关闭 Time 弹窗
+ var buttons = cut.FindAll(".time-panel-footer button");
+ cut.InvokeAsync(() => buttons[0].Click());
+
+ cut.InvokeAsync(() => inputs[1].Click());
+ cut.InvokeAsync(() => buttons[1].Click());
+
+ using var cut1 = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Value, TimeSpan.FromSeconds(1));
+ });
+ buttons = cut1.FindAll(".time-panel-footer button");
+ cut1.InvokeAsync(() => buttons[0].Click());
+ cut1.InvokeAsync(() => buttons[1].Click());
+ }
+
+ [Fact]
+ public void Validate_Ok()
+ {
+ // (!MinValue.HasValue || Value >= MinValue.Value) && (!MaxValue.HasValue || Value <= MaxValue.Value)
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.MinValue, DateTime.Now.AddDays(-1));
+ pb.Add(a => a.Value, DateTime.Now);
+ });
+ var button = cut.Find(".is-confirm");
+ cut.InvokeAsync(() => button.Click());
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.Value, DateTime.Now.AddDays(-2));
+ });
+ button = cut.Find(".is-confirm");
+ cut.InvokeAsync(() => button.Click());
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.MinValue, null);
+ pb.Add(a => a.MaxValue, DateTime.Now.AddDays(1));
+ pb.Add(a => a.Value, DateTime.Now);
+ });
+ button = cut.Find(".is-confirm");
+ cut.InvokeAsync(() => button.Click());
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.MaxValue, DateTime.Now.AddDays(1));
+ pb.Add(a => a.Value, DateTime.Now.AddDays(2));
+ });
+ button = cut.Find(".is-confirm");
+ cut.InvokeAsync(() => button.Click());
+ }
+
+ [Fact]
+ public void IsDiabled_Ok()
+ {
+ // MinValue != null && MaxValue != null && (day < MinValue || day > MaxValue)
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.MinValue, new DateTime(2022, 02, 15));
+ pb.Add(a => a.MaxValue, new DateTime(2022, 02, 17));
+ pb.Add(a => a.Value, new DateTime(2022, 02, 16));
+ });
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.MinValue, null);
+ });
+ }
+ #endregion
+
+ #region TimePicker
+ [Fact]
+ public void OnClose_Ok()
+ {
+ var res = false;
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, TimeSpan.FromDays(1));
+ builder.Add(a => a.OnClose, new Action(() =>
+ {
+ res = true;
+ }));
+ });
+
+ cut.Find(".time-panel-footer .cancel").Click();
+
+ Assert.True(res);
+ }
+
+ [Fact]
+ public void OnConfirm_Ok()
+ {
+ var res = false;
+ var value = false;
+ var cut = Context.RenderComponent(builder =>
+ {
+ builder.Add(a => a.Value, TimeSpan.FromDays(1));
+ builder.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, t =>
+ {
+ value = true;
+ }));
+ builder.Add(a => a.OnConfirm, new Action(() =>
+ {
+ res = true;
+ }));
+ });
+
+ cut.Find(".time-panel-footer .confirm").Click();
+
+ Assert.True(res);
+ Assert.True(value);
+ }
+ #endregion
+}