diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs.Wasm/wwwroot/CodeBeam.MudBlazor.Extensions.xml b/docs/CodeBeam.MudBlazor.Extensions.Docs.Wasm/wwwroot/CodeBeam.MudBlazor.Extensions.xml
index c79cee8a..0a6c751f 100644
--- a/docs/CodeBeam.MudBlazor.Extensions.Docs.Wasm/wwwroot/CodeBeam.MudBlazor.Extensions.xml
+++ b/docs/CodeBeam.MudBlazor.Extensions.Docs.Wasm/wwwroot/CodeBeam.MudBlazor.Extensions.xml
@@ -1310,6 +1310,11 @@
Represents a base class for designing date picker components.
+
+
+ The unique identifier used to associate picker elements.
+
+
Is set to true to scroll to the actual year after the next render
@@ -1387,7 +1392,7 @@
The current month shown in the date picker.
- Defaults to the current month.
+ Defaults to the current month.
When bound via @bind-PickerMonth, controls the initial month displayed. This value is always the first day of a month.
@@ -1401,7 +1406,7 @@
The delay, in milliseconds, before closing the picker after a value is selected.
- Defaults to 100.
+ Defaults to 100.
This delay helps the user see that a date has been selected before the popover disappears.
@@ -1418,7 +1423,7 @@
The maximum number of months allowed in one row.
- Defaults to null.
+ Defaults to null.
When null, the is used.
@@ -1440,7 +1445,7 @@
The format of the selected date in the title.
- Defaults to ddd, dd MMM.
+ Defaults to ddd, dd MMM.
Supported date formats can be found here: .
@@ -1457,7 +1462,7 @@
The function used to disable one or more dates.
- Defaults to null.
+ Defaults to null.
When set, a date will be disabled if the function returns true.
@@ -1514,6 +1519,13 @@
True if the generic type T is DateOnly or nullable, false otherwise.
+
+
+ Determines whether a non-nullable picker value is its default value.
+
+ The value to evaluate.
+ when is non-nullable and equals its default value; otherwise, .
+
Generic conversion method to convert the generic type T to DateTime. Supports DateTime and DateTimeOffset.
@@ -1534,12 +1546,24 @@
if is .
Thrown if is not or .
+
+
+ Validates that is a supported date type.
+
+
+
+
+ Handles a change to the configured date format.
+
+ The newly configured date format.
+ A task that represents the asynchronous operation.
+
True if the current view is either hours or minutes, false otherwise.
@@ -1550,6 +1574,72 @@
True if the current view is either date, month or year, false otherwise.
+
+
+
+
+
+ Gets the first day of the month at the specified offset from the displayed month.
+
+ The zero-based offset from the displayed month.
+ The first day of the requested month.
+
+
+
+ Gets the seven days displayed for a calendar week.
+
+ The zero-based offset from the displayed month.
+ The zero-based week index.
+ The dates in the requested week.
+
+
+
+ Determines whether a date is unavailable for selection.
+
+ The date to evaluate.
+ when the date is disabled; otherwise, .
+
+
+
+ Gets the CSS classes for a calendar day.
+
+ The zero-based offset from the displayed month.
+ The day to format.
+ The CSS classes for the day.
+
+
+
+ Handles selection of a calendar day.
+
+ The selected date and time.
+ A task that represents the asynchronous operation.
+
+
+
+ Formats a date for the picker title.
+
+ The date to format.
+ The formatted title, or an empty string when is .
+
+
+
+ Gets abbreviated day names ordered according to the configured first day of the week.
+
+ The ordered abbreviated day names.
+
+
+
+
+
+
+
+
+
+ Converts a picker value to its display text.
+
+ The value to convert.
+ The formatted text, or when the value has no date representation.
+
Returns the date and time format string to use for formatting operations.
@@ -1561,6 +1651,32 @@
otherwise, a default pattern based on the current culture's short date pattern and a 24-hour time format is
used.
+
+
+ Gets the initial month shown by the calendar.
+
+ The first day of the initial month.
+
+
+
+ Gets the culture-specific calendar year for a date.
+
+ The date whose calendar year is required.
+ The calendar year.
+
+
+
+ Gets the first day of the calendar week.
+
+ The configured first day of the week, or the culture default.
+
+
+
+ Gets the last day of the month at the specified offset from the displayed month.
+
+ The zero-based offset from the displayed month.
+ The last day of the requested month.
+
Scrolls to the current year.
@@ -1572,6 +1688,31 @@
+
+
+ Initializes a new instance of the class.
+
+
+
+
+ Gets or sets the selected hour.
+
+
+
+
+ Gets or sets the selected minute.
+
+
+
+
+ Gets or sets whether the clock pointer is currently being dragged.
+
+
+
+
+ Gets the element reference for the clock surface.
+
+
@@ -1581,6 +1722,11 @@
+
+
+ The active picker mode.
+
+
The currently selected value.
@@ -1599,9 +1745,9 @@
Shows a 12-hour selection clock.
- Defaults to false.
- When true, hours 1-12 are displayed with an AM or PM marker.
- When false, hours 0-23 are displayed.
+ Defaults to false.
+ When true, hours 1-12 are displayed with an AM or PM marker.
+ When false, hours 0-23 are displayed.
@@ -1631,6 +1777,20 @@
Gets or sets the text displayed for the post-meridiem (PM) indicator.
+
+
+
+
+
+ Sets the selected date and optionally updates the input text and bound value.
+
+ The date to set, or to clear the selection.
+ Whether to update the displayed text and reset conversion errors.
+ A task that represents the asynchronous operation.
+
+
+
+
Handles the event when a day is clicked in the calendar view. This method updates the working value with the selected date, and if appropriate based on the component's configuration, submits the new value and closes the picker.
@@ -1649,7 +1809,7 @@
- Clears the selected date and time, resetting the component to its initial state. If is true, the picker will also close after clearing the value.
+ Clears the selected date and time, resetting the component to its initial state. If AutoClose is true, the picker will also close after clearing the value.
Indicates whether the picker should close after clearing the value.
A task that represents the asynchronous operation.
@@ -1660,6 +1820,15 @@
The formatted date string for the title of the picker.
+
+
+ Gets the date used in the picker header.
+
+
+ A non-nullable date picker uses its default value as its cleared value. The header uses the current local date
+ as the picker reference date for that sentinel instead of presenting it as a selected value.
+
+
Calculates the first day of the month for the current calendar context.
@@ -1680,6 +1849,158 @@
current culture.
The calendar year as an integer, adjusted based on the current value and the specified date.
+
+
+ Gets the localized name of a displayed month.
+
+ The zero-based offset from the displayed month.
+ The localized month name and year.
+
+
+
+ Moves the calendar to the previous month.
+
+ A completed task.
+
+
+
+ Moves the calendar to the next month.
+
+ A completed task.
+
+
+
+ Gets the earliest selectable year.
+
+ The year from MinDate, or 1900 when no minimum is set.
+
+
+
+ Gets the latest selectable year.
+
+ The year from MaxDate, or 2100 when no maximum is set.
+
+
+
+ Handles selection of a year in the calendar.
+
+ The selected year.
+ A task that represents the asynchronous operation.
+
+
+
+ Gets the typography style for a year.
+
+ The year to evaluate.
+ The typography style for the year.
+
+
+
+ Gets the CSS classes for a year.
+
+ The year to evaluate.
+ The CSS classes for the year.
+
+
+
+ Moves the calendar to the previous year.
+
+ A completed task.
+
+
+
+ Moves the calendar to the next year.
+
+ A completed task.
+
+
+
+ Gets all month numbers used by the month-selection view.
+
+ The month numbers from 1 through 12.
+
+
+
+ Handles selection of a month in the calendar.
+
+ The selected month number.
+ A task that represents the asynchronous operation.
+
+
+
+ Determines whether every day in a month is outside the configured date range.
+
+ The month number to evaluate.
+ when the month is disabled; otherwise, .
+
+
+
+ Gets the typography style for a month.
+
+ The month number to evaluate.
+ The typography style for the month.
+
+
+
+ Gets the CSS classes for a month.
+
+ The month number to evaluate.
+ The CSS classes for the month.
+
+
+
+ Gets the localized abbreviated name of a month.
+
+ The month number.
+ The abbreviated month name.
+
+
+
+ Gets the culture-specific week number for a displayed calendar week.
+
+ The zero-based offset from the displayed month.
+ The zero-based week index.
+ The week number.
+
+
+
+ Gets the day-of-month text for a calendar date.
+
+ The date to format.
+ The localized day-of-month text.
+
+
+
+ Switches the picker to month selection.
+
+
+
+
+ Switches the picker to month selection.
+
+ The clicked month number.
+
+
+
+ Updates the working time from a clock-pointer movement.
+
+ The hour or minute indicated by the clock pointer.
+ Whether the pointer is currently being dragged.
+ A task that represents the asynchronous operation.
+
+
+
+ Handles a click on the clock pointer and advances or submits the time selection.
+
+ The selected hour or minute.
+ A task that represents the asynchronous operation.
+
+
+
+ Submits the working value and closes the picker when its configuration permits it.
+
+ A task that represents the asynchronous operation.
+
Gets the hour portion of the selected time.
@@ -1692,11 +2013,20 @@
A two-digit string for minutes, or -- if no value is set.
+
+
+ Gets the year displayed in the picker toolbar.
+
+ The formatted year.
+
Scrolls to the current year.
+
+
+
Sets the current view of the picker to the specified value.
diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor
index a2daa74d..abe7efff 100644
--- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor
+++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor
@@ -5,7 +5,7 @@
-
+
diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor
index e599abe7..2b8a39bf 100644
--- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor
+++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor
@@ -3,11 +3,25 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -19,9 +33,14 @@
@code {
- private DateTime? _date = DateTime.Now;
- private DateTimeOffset _date2 = DateTimeOffset.UtcNow;
- private DateOnly _date3 = DateOnly.FromDateTime(DateTime.Now);
+ private static readonly TimeZoneInfo IstanbulTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul");
+ private DateTime _dateTime = DateTime.Now;
+ private DateTime? _nullableDateTime;
+ private DateTimeOffset _dateTimeOffset = DateTimeOffset.UtcNow;
+ private DateTimeOffset _istanbulDateTimeOffset = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, IstanbulTimeZone);
+ private DateTimeOffset? _nullableDateTimeOffset;
+ private DateOnly _dateOnly = DateOnly.FromDateTime(DateTime.Now);
+ private DateOnly? _nullableDateOnly;
private bool _clearable = false;
private bool _editable = false;
}
diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor
index 2e0ca6d7..8e78683d 100644
--- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor
+++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor
@@ -3,14 +3,61 @@
-
-
- Clear
- Cancel
- OK
-
-
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
+
+
+ Clear
+ Cancel
+ OK
+
+
+
@@ -32,15 +79,25 @@
}
- Set Today
+ Set Current Values
@code {
- private MudDateTimePicker _picker = null!;
+ private MudDateTimePicker _dateTimePicker = null!;
+ private MudDateTimePicker _nullableDateTimePicker = null!;
+ private MudDateTimePicker _dateTimeOffsetPicker = null!;
+ private MudDateTimePicker _nullableDateTimeOffsetPicker = null!;
+ private MudDateTimePicker _dateOnlyPicker = null!;
+ private MudDateTimePicker _nullableDateOnlyPicker = null!;
- private DateTime? _date = DateTime.Now;
+ private DateTime _dateTime = DateTime.Now;
+ private DateTime? _nullableDateTime;
+ private DateTimeOffset _dateTimeOffset = DateTimeOffset.UtcNow;
+ private DateTimeOffset? _nullableDateTimeOffset;
+ private DateOnly _dateOnly = DateOnly.FromDateTime(DateTime.Now);
+ private DateOnly? _nullableDateOnly;
private bool _editable = false;
private bool _showToolbar = true;
private bool _submitOnClose = true;
@@ -49,4 +106,16 @@
private bool _isAdornmentEnd = true;
private bool _clearable = false;
private Variant _variant = Variant.Outlined;
+
+ private void SetCurrentValues()
+ {
+ _dateTime = DateTime.Now;
+ _nullableDateTime = DateTime.Now;
+ _dateTimeOffset = DateTimeOffset.UtcNow;
+ _nullableDateTimeOffset = DateTimeOffset.UtcNow;
+ _dateOnly = DateOnly.FromDateTime(DateTime.Now);
+ _nullableDateOnly = DateOnly.FromDateTime(DateTime.Now);
+ }
}
+
+
\ No newline at end of file
diff --git a/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj b/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj
index 360039a8..4ea95298 100644
--- a/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj
+++ b/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj
@@ -58,7 +58,7 @@
<_Parameter1>CodeBeam.MudBlazor.Extensions.UnitTests
-
+
True
diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs
index f6b9c008..30be27fd 100644
--- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs
+++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs
@@ -13,9 +13,13 @@ public abstract partial class MudBaseDatePickerX : MudPicker
{
internal readonly string _mudPickerCalendarContentElementId;
private readonly ParameterState _formatState;
+ ///
+ /// The unique identifier used to associate picker elements.
+ ///
protected readonly string _componentId = Identifier.Create();
internal DateTime? _picker_month;
+ internal bool _picker_month_set;
///
/// Is set to true to scroll to the actual year after the next render
@@ -107,7 +111,7 @@ protected MudBaseDatePickerX()
/// The current month shown in the date picker.
///
///
- /// Defaults to the current month.
+ /// Defaults to the current month.
/// When bound via @bind-PickerMonth, controls the initial month displayed. This value is always the first day of a month.
///
[Parameter]
@@ -119,6 +123,7 @@ public DateTime? PickerMonth
if (value == _picker_month)
return;
_picker_month = value;
+ _picker_month_set = true;
InvokeAsync(StateHasChanged);
PickerMonthChanged.InvokeAsync(value);
}
@@ -133,7 +138,7 @@ public DateTime? PickerMonth
/// The delay, in milliseconds, before closing the picker after a value is selected.
///
///
- /// Defaults to 100.
+ /// Defaults to 100.
/// This delay helps the user see that a date has been selected before the popover disappears.
///
[Parameter] public int ClosingDelay { get; set; } = 100;
@@ -150,7 +155,7 @@ public DateTime? PickerMonth
/// The maximum number of months allowed in one row.
///
///
- /// Defaults to null.
+ /// Defaults to null.
/// When null, the is used.
///
[Parameter] public int? MaxMonthColumns { get; set; }
@@ -172,7 +177,7 @@ public DateTime? PickerMonth
/// The format of the selected date in the title.
///
///
- /// Defaults to ddd, dd MMM.
+ /// Defaults to ddd, dd MMM.
/// Supported date formats can be found here: .
///
[Parameter] public string TitleDateFormat { get; set; } = "ddd, dd MMM";
@@ -189,7 +194,7 @@ public DateTime? PickerMonth
/// The function used to disable one or more dates.
///
///
- /// Defaults to null.
+ /// Defaults to null.
/// When set, a date will be disabled if the function returns true.
///
[Parameter] public Func IsDateDisabledFunc { get; set; } = _ => false;
@@ -247,6 +252,17 @@ public DateTime? PickerMonth
///
protected internal bool IsDateOnly => (Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T)) == typeof(DateOnly);
+ ///
+ /// Determines whether a non-nullable picker value is its default value.
+ ///
+ /// The value to evaluate.
+ /// when is non-nullable and equals its default value; otherwise, .
+ protected internal bool IsDefaultValue(T? value)
+ {
+ return Nullable.GetUnderlyingType(typeof(T)) is null &&
+ EqualityComparer.Default.Equals(value, default);
+ }
+
///
/// Generic conversion method to convert the generic type T to DateTime. Supports DateTime and DateTimeOffset.
///
@@ -306,6 +322,9 @@ public DateTime? PickerMonth
throw new NotSupportedException($"Type {typeof(T)} not supported");
}
+ ///
+ /// Validates that is a supported date type.
+ ///
protected void ValidateType()
{
var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
@@ -341,6 +360,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
ScrollToYearAsync().CatchAndLog();
}
+ ///
+ /// Handles a change to the configured date format.
+ ///
+ /// The newly configured date format.
+ /// A task that represents the asynchronous operation.
protected virtual Task DateFormatChangedAsync(string? newFormat) => Task.CompletedTask;
private Task DateFormatChangedAsync(ParameterChangedEventArgs args) => DateFormatChangedAsync(args.Value);
@@ -358,13 +382,24 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
|| CurrentView == OpenTo.Month
|| CurrentView == OpenTo.Year;
+ ///
protected override async Task OnPickerOpenedAsync()
{
await base.OnPickerOpenedAsync();
var dateTime = ToDateTime(_value);
- if (dateTime.HasValue)
+ if (IsDefaultValue(_value))
+ {
+ var culture = GetCulture();
+ var calendar = culture.Calendar;
+ PickerMonth = new DateTime(
+ calendar.GetYear(DateTime.Today),
+ calendar.GetMonth(DateTime.Today),
+ 1,
+ calendar);
+ }
+ else if (dateTime.HasValue)
{
var culture = GetCulture();
var calendar = culture.Calendar;
@@ -378,6 +413,11 @@ protected override async Task OnPickerOpenedAsync()
CurrentView = OpenTo;
}
+ ///
+ /// Gets the first day of the month at the specified offset from the displayed month.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The first day of the requested month.
protected DateTime GetMonthStart(int month)
{
var culture = GetCulture();
@@ -387,6 +427,12 @@ protected DateTime GetMonthStart(int month)
return calendar.AddMonths(new DateTime(baseDate.Year, baseDate.Month, 1), month);
}
+ ///
+ /// Gets the seven days displayed for a calendar week.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The zero-based week index.
+ /// The dates in the requested week.
protected IEnumerable GetWeek(int month, int index)
{
if (index is < 0 or > 5)
@@ -403,6 +449,11 @@ protected IEnumerable GetWeek(int month, int index)
yield return weekFirst.AddDays(i);
}
+ ///
+ /// Determines whether a date is unavailable for selection.
+ ///
+ /// The date to evaluate.
+ /// when the date is disabled; otherwise, .
protected virtual bool IsDayDisabled(DateTime date)
{
return date < MinDate ||
@@ -410,14 +461,35 @@ protected virtual bool IsDayDisabled(DateTime date)
IsDateDisabledFunc(date);
}
+ ///
+ /// Gets the CSS classes for a calendar day.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The day to format.
+ /// The CSS classes for the day.
protected abstract string GetDayClasses(int month, DateTime day);
+
+ ///
+ /// Handles selection of a calendar day.
+ ///
+ /// The selected date and time.
+ /// A task that represents the asynchronous operation.
protected abstract Task OnDayClickedAsync(DateTime dateTime);
+ ///
+ /// Formats a date for the picker title.
+ ///
+ /// The date to format.
+ /// The formatted title, or an empty string when is .
protected string FormatTitleDate(DateTime? date)
{
return date?.ToString(TitleDateFormat, GetCulture()) ?? "";
}
+ ///
+ /// Gets abbreviated day names ordered according to the configured first day of the week.
+ ///
+ /// The ordered abbreviated day names.
protected IEnumerable GetAbbreviatedDayNames()
{
var culture = GetCulture();
@@ -428,6 +500,7 @@ protected IEnumerable GetAbbreviatedDayNames()
return Enumerable.Range(0, 7).Select(i => names[(i + firstDay) % 7]);
}
+ ///
protected override IConverter GetDefaultConverter()
{
return new DefaultConverter
@@ -437,6 +510,7 @@ protected IEnumerable GetAbbreviatedDayNames()
};
}
+ ///
protected override string? ConvertSet(T? value)
{
var dt = ToDateTime(value);
@@ -447,6 +521,11 @@ protected IEnumerable GetAbbreviatedDayNames()
return dt.Value.ToString(GetFormat(), GetCulture());
}
+ ///
+ /// Converts a picker value to its display text.
+ ///
+ /// The value to convert.
+ /// The formatted text, or when the value has no date representation.
protected internal string? ConvertSetInternal(T? value)
{
return ConvertSet(value);
@@ -472,14 +551,33 @@ protected override string GetFormat()
return $"{CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern} HH:mm";
}
+ ///
+ /// Gets the initial month shown by the calendar.
+ ///
+ /// The first day of the initial month.
protected abstract DateTime GetCalendarStartOfMonth();
+
+ ///
+ /// Gets the culture-specific calendar year for a date.
+ ///
+ /// The date whose calendar year is required.
+ /// The calendar year.
protected abstract int GetCalendarYear(DateTime yearDate);
+ ///
+ /// Gets the first day of the calendar week.
+ ///
+ /// The configured first day of the week, or the culture default.
protected DayOfWeek GetFirstDayOfWeek()
{
return FirstDayOfWeek ?? GetCulture().DateTimeFormat.FirstDayOfWeek;
}
+ ///
+ /// Gets the last day of the month at the specified offset from the displayed month.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The last day of the requested month.
protected DateTime GetMonthEnd(int month)
{
var culture = GetCulture();
@@ -503,4 +601,4 @@ public virtual Task ScrollToYearAsync(DateTime? date = null)
//{
// return JsApiService.UpdateStyleProperty(_mudPickerCalendarContentElementId, "--selected-day", tempId);
//}
-}
\ No newline at end of file
+}
diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs
index d8049512..4950cedf 100644
--- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs
+++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs
@@ -16,6 +16,10 @@ public partial class MudDateTimePicker : MudBaseDatePickerX
{
[Inject] private IJSRuntime JsRuntime { get; set; } = null!;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
[DynamicDependency(nameof(OnStickClick))]
[DynamicDependency(nameof(SelectTimeFromStick))]
public MudDateTimePicker()
@@ -34,12 +38,25 @@ public MudDateTimePicker()
private record SetTime
{
+ ///
+ /// Gets or sets the selected hour.
+ ///
public int Hour { get; set; }
+
+ ///
+ /// Gets or sets the selected minute.
+ ///
public int Minute { get; set; }
}
+ ///
+ /// Gets or sets whether the clock pointer is currently being dragged.
+ ///
public bool PointerMoving { get; set; }
+ ///
+ /// Gets the element reference for the clock surface.
+ ///
protected ElementReference ClockElementReference { get; private set; }
private bool _amPm = false;
@@ -47,14 +64,14 @@ private record SetTime
protected override void OnInitialized()
{
base.OnInitialized();
- _workingValue = ToDateTime(Value);
+ _workingValue = BoundDateOrNull();
}
///
protected override void OnParametersSet()
{
base.OnParametersSet();
- _workingValue = ToDateTime(Value);
+ _workingValue = BoundDateOrNull();
SyncTimeFromValue();
}
@@ -85,8 +102,13 @@ private void SyncTimeFromValue()
_timeSet.Minute = _workingValue.Value.Minute;
}
+ ///
+ /// The active picker mode.
+ ///
protected PickerMode _mode = PickerMode.Date;
+ private DateTime? BoundDateOrNull() => IsDefaultValue(_value) ? null : ToDateTime(Value);
+
///
/// The currently selected value.
///
@@ -110,9 +132,9 @@ public T? Value
/// Shows a 12-hour selection clock.
///
///
- /// Defaults to false.
- /// When true, hours 1-12 are displayed with an AM or PM marker.
- /// When false, hours 0-23 are displayed.
+ /// Defaults to false.
+ /// When true, hours 1-12 are displayed with an AM or PM marker.
+ /// When false, hours 0-23 are displayed.
///
[Parameter]
public bool AmPm
@@ -176,6 +198,7 @@ private int RoundToStepInterval(int value)
[Parameter]
public string PmText { get; set; } = "PM";
+ ///
protected override async Task WriteTextAsync(string? text)
{
if (string.IsNullOrWhiteSpace(text))
@@ -230,6 +253,12 @@ private async Task OnPmClickedAsync()
private DateTimeOffset _lastSetTime = DateTimeOffset.MinValue;
private const int DebounceTimeoutMs = 100;
+ ///
+ /// Sets the selected date and optionally updates the input text and bound value.
+ ///
+ /// The date to set, or to clear the selection.
+ /// Whether to update the displayed text and reset conversion errors.
+ /// A task that represents the asynchronous operation.
protected internal async Task SetDateAsync(DateTime? date, bool updateValue)
{
var current = ToDateTime(_value);
@@ -326,6 +355,7 @@ private void SetTimePart(int hour, int minute)
);
}
+ ///
protected override string GetDayClasses(int month, DateTime day)
{
var b = new CssBuilder("mud-day");
@@ -335,7 +365,7 @@ protected override string GetDayClasses(int month, DateTime day)
if (day < GetMonthStart(month) || day > GetMonthEnd(month))
return b.AddClass("mud-hidden").Build();
- var current = _workingValue ?? ToDateTime(Value);
+ var current = _workingValue ?? BoundDateOrNull();
if (current?.Date == day.Date)
return b.AddClass("mud-selected")
@@ -399,7 +429,7 @@ protected override async Task SubmitAsync()
}
///
- /// Clears the selected date and time, resetting the component to its initial state. If is true, the picker will also close after clearing the value.
+ /// Clears the selected date and time, resetting the component to its initial state. If AutoClose is true, the picker will also close after clearing the value.
///
/// Indicates whether the picker should close after clearing the value.
/// A task that represents the asynchronous operation.
@@ -417,13 +447,35 @@ public override async Task ClearAsync(bool close = true)
/// The formatted date string for the title of the picker.
protected string GetTitleDateString()
{
- var date = _workingValue
- ?? ToDateTime(Value)
- ?? TimeProvider.GetLocalNow().Date;
+ var date = GetPickerHeaderDate(BoundDateOrNull() ?? _workingValue);
return FormatTitleDate(date);
}
+ ///
+ /// Gets the date used in the picker header.
+ ///
+ ///
+ /// A non-nullable date picker uses its default value as its cleared value. The header uses the current local date
+ /// as the picker reference date for that sentinel instead of presenting it as a selected value.
+ ///
+ private DateTime GetPickerHeaderDate(DateTime? date)
+ {
+ var boundDate = ToDateTime(_value);
+
+ if (date is not null && (!IsDefaultValue(_value) || date != boundDate))
+ return date.Value;
+
+ // Prefer MinDate, then PickerMonth (only if explicitly set), then local current date.
+ if (MinDate.HasValue)
+ return MinDate.Value;
+
+ if (PickerMonth.HasValue && _picker_month_set)
+ return PickerMonth.Value;
+
+ return TimeProvider.GetLocalNow().Date;
+ }
+
///
/// Calculates the first day of the month for the current calendar context.
///
@@ -433,7 +485,7 @@ protected string GetTitleDateString()
/// or the current local date if neither is set.
protected override DateTime GetCalendarStartOfMonth()
{
- var date = ToDateTime(Value) ?? HighlightedDate ?? TimeProvider.GetLocalNow().Date;
+ var date = BoundDateOrNull() ?? HighlightedDate ?? TimeProvider.GetLocalNow().Date;
return date.StartOfMonth(GetCulture());
}
@@ -448,24 +500,37 @@ protected override DateTime GetCalendarStartOfMonth()
/// The calendar year as an integer, adjusted based on the current value and the specified date.
protected override int GetCalendarYear(DateTime yearDate)
{
- var date = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date;
+ var date = BoundDateOrNull() ?? TimeProvider.GetLocalNow().Date;
var diff = GetCulture().Calendar.GetYear(date) - GetCulture().Calendar.GetYear(yearDate);
return GetCulture().Calendar.GetYear(date) - diff;
}
+ ///
+ /// Gets the localized name of a displayed month.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The localized month name and year.
protected string GetMonthName(int month)
{
var date = GetMonthStart(month);
return date.ToString("MMMM yyyy", GetCulture());
}
+ ///
+ /// Moves the calendar to the previous month.
+ ///
+ /// A completed task.
protected Task OnPreviousMonthClick()
{
PickerMonth = GetMonthStart(0).AddMonths(-1);
return Task.CompletedTask;
}
+ ///
+ /// Moves the calendar to the next month.
+ ///
+ /// A completed task.
protected Task OnNextMonthClick()
{
PickerMonth = GetMonthStart(0).AddMonths(1);
@@ -489,19 +554,32 @@ private void OnYearClick()
}
}
+ ///
+ /// Gets the earliest selectable year.
+ ///
+ /// The year from MinDate, or 1900 when no minimum is set.
protected int GetMinYear()
{
return MinDate?.Year ?? 1900;
}
+ ///
+ /// Gets the latest selectable year.
+ ///
+ /// The year from MaxDate, or 2100 when no maximum is set.
protected int GetMaxYear()
{
return MaxDate?.Year ?? 2100;
}
+ ///
+ /// Handles selection of a year in the calendar.
+ ///
+ /// The selected year.
+ /// A task that represents the asynchronous operation.
protected Task OnYearClickedAsync(int year)
{
- var current = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date;
+ var current = BoundDateOrNull() ?? TimeProvider.GetLocalNow().Date;
PickerMonth = new DateTime(year, current.Month, 1);
_workingValue = new DateTime(
@@ -517,41 +595,68 @@ protected Task OnYearClickedAsync(int year)
return Task.CompletedTask;
}
+ ///
+ /// Gets the typography style for a year.
+ ///
+ /// The year to evaluate.
+ /// The typography style for the year.
protected Typo GetYearTypo(int year)
{
- var current = ToDateTime(Value);
+ var current = BoundDateOrNull();
return current?.Year == year ? Typo.h5 : Typo.body1;
}
+ ///
+ /// Gets the CSS classes for a year.
+ ///
+ /// The year to evaluate.
+ /// The CSS classes for the year.
protected string GetYearClasses(int year)
{
- var current = ToDateTime(Value);
+ var current = BoundDateOrNull();
return new CssBuilder("mud-picker-year-text")
.AddClass("mud-selected", current?.Year == year)
.Build();
}
+ ///
+ /// Moves the calendar to the previous year.
+ ///
+ /// A completed task.
protected Task OnPreviousYearClick()
{
PickerMonth = (PickerMonth ?? DateTime.Today).AddYears(-1);
return Task.CompletedTask;
}
+ ///
+ /// Moves the calendar to the next year.
+ ///
+ /// A completed task.
protected Task OnNextYearClick()
{
PickerMonth = (PickerMonth ?? DateTime.Today).AddYears(1);
return Task.CompletedTask;
}
+ ///
+ /// Gets all month numbers used by the month-selection view.
+ ///
+ /// The month numbers from 1 through 12.
protected IEnumerable GetAllMonths()
{
return Enumerable.Range(1, 12);
}
+ ///
+ /// Handles selection of a month in the calendar.
+ ///
+ /// The selected month number.
+ /// A task that represents the asynchronous operation.
protected Task OnMonthSelectedAsync(int month)
{
- var current = _workingValue ?? ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date;
+ var current = _workingValue ?? BoundDateOrNull() ?? TimeProvider.GetLocalNow().Date;
PickerMonth = new DateTime(current.Year, month, 1);
_workingValue = new DateTime(
@@ -567,6 +672,11 @@ protected Task OnMonthSelectedAsync(int month)
return Task.CompletedTask;
}
+ ///
+ /// Determines whether every day in a month is outside the configured date range.
+ ///
+ /// The month number to evaluate.
+ /// when the month is disabled; otherwise, .
protected bool IsMonthDisabled(int month)
{
if (!MinDate.HasValue && !MaxDate.HasValue)
@@ -581,26 +691,47 @@ protected bool IsMonthDisabled(int month)
|| (MaxDate.HasValue && start > MaxDate.Value);
}
+ ///
+ /// Gets the typography style for a month.
+ ///
+ /// The month number to evaluate.
+ /// The typography style for the month.
protected Typo GetMonthTypo(int month)
{
- var current = ToDateTime(Value);
+ var current = BoundDateOrNull();
return current?.Month == month ? Typo.h6 : Typo.body2;
}
+ ///
+ /// Gets the CSS classes for a month.
+ ///
+ /// The month number to evaluate.
+ /// The CSS classes for the month.
protected string GetMonthClasses(int month)
{
- var current = ToDateTime(Value);
+ var current = BoundDateOrNull();
return new CssBuilder()
.AddClass("mud-selected", current?.Month == month)
.Build();
}
+ ///
+ /// Gets the localized abbreviated name of a month.
+ ///
+ /// The month number.
+ /// The abbreviated month name.
protected string GetAbbreviatedMonthName(int month)
{
return GetCulture().DateTimeFormat.AbbreviatedMonthNames[month - 1];
}
+ ///
+ /// Gets the culture-specific week number for a displayed calendar week.
+ ///
+ /// The zero-based offset from the displayed month.
+ /// The zero-based week index.
+ /// The week number.
protected int GetWeekNumber(int month, int week)
{
var firstDay = GetWeek(month, week).First();
@@ -611,16 +742,28 @@ protected int GetWeekNumber(int month, int week)
GetFirstDayOfWeek());
}
+ ///
+ /// Gets the day-of-month text for a calendar date.
+ ///
+ /// The date to format.
+ /// The localized day-of-month text.
protected string GetCalendarDayOfMonth(DateTime date)
{
return date.Day.ToString(GetCulture());
}
+ ///
+ /// Switches the picker to month selection.
+ ///
protected void OnFormattedDateClick()
{
CurrentView = OpenTo.Month;
}
+ ///
+ /// Switches the picker to month selection.
+ ///
+ /// The clicked month number.
protected void OnMonthClicked(int month)
{
CurrentView = OpenTo.Month;
@@ -750,6 +893,12 @@ private static string GetTransform(double angle, double radius, double offsetX,
return $"transform: translate({x}px, {y}px);";
}
+ ///
+ /// Updates the working time from a clock-pointer movement.
+ ///
+ /// The hour or minute indicated by the clock pointer.
+ /// Whether the pointer is currently being dragged.
+ /// A task that represents the asynchronous operation.
[JSInvokable]
public async Task SelectTimeFromStick(int value, bool pointerMoving)
{
@@ -765,6 +914,11 @@ public async Task SelectTimeFromStick(int value, bool pointerMoving)
StateHasChanged();
}
+ ///
+ /// Handles a click on the clock pointer and advances or submits the time selection.
+ ///
+ /// The selected hour or minute.
+ /// A task that represents the asynchronous operation.
[JSInvokable]
public async Task OnStickClick(int value)
{
@@ -792,6 +946,10 @@ public async Task OnStickClick(int value)
StateHasChanged();
}
+ ///
+ /// Submits the working value and closes the picker when its configuration permits it.
+ ///
+ /// A task that represents the asynchronous operation.
protected async Task SubmitAndCloseAsync()
{
if (PickerActions == null || AutoClose)
@@ -871,11 +1029,13 @@ private void HandleModeChange(PickerMode mode)
}
}
+ ///
+ /// Gets the year displayed in the picker toolbar.
+ ///
+ /// The formatted year.
protected string GetFormattedYearString()
{
- var date = _workingValue
- ?? ToDateTime(Value)
- ?? TimeProvider.GetLocalNow().Date;
+ var date = GetPickerHeaderDate(_workingValue ?? BoundDateOrNull());
return date.Year.ToString();
}
@@ -893,7 +1053,7 @@ public override async Task ScrollToYearAsync(DateTime? date = null)
var dateTime =
date
?? _workingValue
- ?? ToDateTime(Value)
+ ?? BoundDateOrNull()
?? TimeProvider.GetLocalNow().Date;
var id = $"{_componentId}{calendar.GetYear(dateTime)}";
@@ -903,6 +1063,7 @@ public override async Task ScrollToYearAsync(DateTime? date = null)
StateHasChanged();
}
+ ///
protected override async Task OnOpenedAsync()
{
_mode = PickerMode.Date;
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs
index 0fec0d9d..b001a521 100644
--- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs
@@ -2,6 +2,9 @@
using Bunit;
using Microsoft.AspNetCore.Components;
+using System.Reflection;
+using System.Linq;
+
namespace MudExtensions.UnitTests.Components;
[TestFixture]
@@ -98,4 +101,234 @@ public void DateTimePicker_Null_Value_Should_Render_Empty()
var comp = RenderPicker();
comp.Find("input").GetAttribute("value").Should().BeNullOrEmpty();
}
+
+ [Test]
+ public async Task DateTimePicker_DefaultDateTime_Should_Open_Current_Month()
+ {
+ await AssertDefaultValueOpensCurrentMonthAsync();
+ }
+
+ [Test]
+ public async Task DateTimePicker_DefaultDateOnly_Should_Open_Current_Month()
+ {
+ await AssertDefaultValueOpensCurrentMonthAsync();
+ }
+
+ [Test]
+ public async Task DateTimePicker_DefaultDateTimeOffset_WithIstanbulTimeZone_Should_Open_Current_Month()
+ {
+ var istanbulTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul");
+ await AssertDefaultValueOpensCurrentMonthAsync(istanbulTimeZone);
+ }
+
+ private async Task AssertDefaultValueOpensCurrentMonthAsync(TimeZoneInfo? timeZone = null)
+ {
+ var today = DateTime.Today;
+ var expectedMonth = new DateTime(today.Year, today.Month, 1);
+ var comp = Context.Render>(parameters =>
+ {
+ parameters.Add(p => p.Value, default(T));
+
+ if (timeZone is not null)
+ parameters.Add(p => p.TimeZone, timeZone);
+ });
+
+ await InvokePickerOpenedAsync(comp.Instance);
+
+ comp.Instance.PickerMonth.Should().Be(expectedMonth);
+ }
+
+ private static Task InvokePickerOpenedAsync(MudDateTimePicker picker)
+ {
+ var method = typeof(MudBaseDatePickerX).GetMethod(
+ "OnPickerOpenedAsync",
+ BindingFlags.Instance | BindingFlags.NonPublic);
+
+ return (Task)method!.Invoke(picker, null)!;
+ }
+
+ [Test]
+ public void BoundDateOrNull_DefaultNonNullable_ReturnsNull()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, default(DateTime)));
+ var method = comp.Instance.GetType().GetMethod("BoundDateOrNull", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var result = (DateTime?)method.Invoke(comp.Instance, null);
+ result.Should().BeNull();
+ }
+
+ [Test]
+ public void BoundDateOrNull_ReturnsValue()
+ {
+ var date = new DateTime(2026, 5, 3);
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, date));
+ var method = comp.Instance.GetType().GetMethod("BoundDateOrNull", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var result = (DateTime?)method.Invoke(comp.Instance, null);
+ result.Should().Be(date);
+ }
+
+ [Test]
+ public void GetPickerHeaderDate_PrefersMinDate()
+ {
+ var min = new DateTime(2020, 1, 1);
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null).Add(p => p.MinDate, min));
+ var method = comp.Instance.GetType().GetMethod("GetPickerHeaderDate", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var result = (DateTime)method.Invoke(comp.Instance, new object?[] { null })!;
+ result.Should().Be(min.Date);
+ }
+
+ [Test]
+ public void GetPickerHeaderDate_PrefersPickerMonth()
+ {
+ var pm = new DateTime(2021, 6, 1);
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ comp.Instance.PickerMonth = pm;
+ var method = comp.Instance.GetType().GetMethod("GetPickerHeaderDate", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var result = (DateTime)method.Invoke(comp.Instance, new object?[] { null })!;
+ result.Should().Be(pm);
+ }
+
+ [Test]
+ public void GetPickerHeaderDate_FallbackToToday()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var method = comp.Instance.GetType().GetMethod("GetPickerHeaderDate", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var result = (DateTime)method.Invoke(comp.Instance, new object?[] { null })!;
+ result.Should().Be(DateTime.Today);
+ }
+
+ [Test]
+ public void GetTitleDateString_StableDuringTimeEdits()
+ {
+ var value = new DateTime(2026, 5, 3, 10, 0, 0);
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, value));
+ var methodTitle = comp.Instance.GetType().GetMethod("GetTitleDateString", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var before = (string)methodTitle.Invoke(comp.Instance, null)!;
+
+ var field = comp.Instance.GetType().GetField("_workingValue", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ field.SetValue(comp.Instance, new DateTime(2026, 5, 3, 22, 30, 0));
+
+ var after = (string)methodTitle.Invoke(comp.Instance, null)!;
+ after.Should().Be(before);
+ }
+
+ [Test]
+ public async Task ClearDoesNotChangePickerMonth()
+ {
+ var initial = new DateTime(2022, 5, 1);
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, (DateTime?)initial));
+ comp.Instance.PickerMonth = new DateTime(2022, 5, 1);
+
+ var method = comp.Instance.GetType().GetMethod("SetDateAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(async () =>
+ {
+ var t = (Task)method.Invoke(comp.Instance, new object?[] { null, true })!;
+ await t;
+ });
+
+ comp.Instance.PickerMonth.Should().Be(initial);
+ }
+
+ [Test]
+ public async Task SyncTimeFromValue_Sets_Zero_When_WorkingValueNull()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var wfield = comp.Instance.GetType().GetField("_workingValue", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ wfield.SetValue(comp.Instance, null);
+ var method = comp.Instance.GetType().GetMethod("SyncTimeFromValue", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(() => method.Invoke(comp.Instance, null));
+ var ts = comp.Instance.GetType().GetField("_timeSet", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(comp.Instance)!;
+ var hour = (int)ts.GetType().GetProperty("Hour")!.GetValue(ts)!;
+ var minute = (int)ts.GetType().GetProperty("Minute")!.GetValue(ts)!;
+ hour.Should().Be(0);
+ minute.Should().Be(0);
+ }
+
+ [Test]
+ public async Task UpdateTimeAsync_Applies_TimeSet_To_WorkingValue()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var tsField = comp.Instance.GetType().GetField("_timeSet", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var ts = tsField.GetValue(comp.Instance)!;
+ ts.GetType().GetProperty("Hour")!.SetValue(ts, 13);
+ ts.GetType().GetProperty("Minute")!.SetValue(ts, 45);
+ var method = comp.Instance.GetType().GetMethod("UpdateTimeAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(async () => await (Task)method.Invoke(comp.Instance, null)!);
+ var w = (DateTime?)comp.Instance.GetType().GetField("_workingValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(comp.Instance);
+ w.Should().NotBeNull();
+ w.Value.Hour.Should().Be(13);
+ w.Value.Minute.Should().Be(45);
+ }
+
+ [Test]
+ public async Task SetTimePart_Sets_WorkingValue_Hour_Minute()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var method = comp.Instance.GetType().GetMethod("SetTimePart", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(() => { method.Invoke(comp.Instance, new object?[] { 9, 15 }); return Task.CompletedTask; });
+ var w = (DateTime?)comp.Instance.GetType().GetField("_workingValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(comp.Instance);
+ w.Should().NotBeNull();
+ w.Value.Hour.Should().Be(9);
+ w.Value.Minute.Should().Be(15);
+ }
+
+ [Test]
+ public async Task OnAmClickedAsync_And_OnPmClickedAsync_Adjust_Hour()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var tsField = comp.Instance.GetType().GetField("_timeSet", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ var ts = tsField.GetValue(comp.Instance)!;
+ ts.GetType().GetProperty("Hour")!.SetValue(ts, 13);
+ var am = comp.Instance.GetType().GetMethod("OnAmClickedAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(async () => await (Task)am.Invoke(comp.Instance, null)!);
+ var hourAfterAm = (int)ts.GetType().GetProperty("Hour")!.GetValue(ts)!;
+ hourAfterAm.Should().Be(1);
+
+ ts.GetType().GetProperty("Hour")!.SetValue(ts, 1);
+ var pm = comp.Instance.GetType().GetMethod("OnPmClickedAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(async () => await (Task)pm.Invoke(comp.Instance, null)!);
+ var hourAfterPm = (int)ts.GetType().GetProperty("Hour")!.GetValue(ts)!;
+ hourAfterPm.Should().Be(13);
+ }
+
+ [Test]
+ public async Task SelectTimeFromStick_And_OnStickClick_Work()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ // test minutes selection
+ var currentViewField = comp.Instance.GetType().GetField("CurrentView", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;
+ var enumType = currentViewField.FieldType;
+ var hours = Enum.Parse(enumType, "Hours");
+ var minutes = Enum.Parse(enumType, "Minutes");
+ currentViewField.SetValue(comp.Instance, minutes);
+ await comp.InvokeAsync(async () => await comp.Instance.SelectTimeFromStick(30, true));
+ var ts = comp.Instance.GetType().GetField("_timeSet", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(comp.Instance)!;
+ var minute = (int)ts.GetType().GetProperty("Minute")!.GetValue(ts)!;
+ minute.Should().Be(30);
+ comp.Instance.PointerMoving.Should().BeTrue();
+
+ // test hours selection and stick click behaviour
+ comp.Instance.GetType().GetField("CurrentView", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!.SetValue(comp.Instance, hours);
+ await comp.InvokeAsync(async () => await comp.Instance.SelectTimeFromStick(5, false));
+ var hour = (int)ts.GetType().GetProperty("Hour")!.GetValue(ts)!;
+ hour.Should().Be(5);
+
+ await comp.InvokeAsync(async () => await comp.Instance.OnStickClick(5));
+ var currentView = comp.Instance.GetType().GetField("CurrentView", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(comp.Instance);
+ currentView.Should().Be(minutes);
+ }
+
+ [Test]
+ public async Task SubmitAsync_Updates_Value()
+ {
+ var comp = Context.Render>(parameters => parameters.Add(p => p.Value, null));
+ var wfield = comp.Instance.GetType().GetField("_workingValue", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ wfield.SetValue(comp.Instance, new DateTime(2026, 6, 2, 11, 11, 0));
+ var method = comp.Instance.GetType().GetMethod("SubmitAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+ await comp.InvokeAsync(async () => await (Task)method.Invoke(comp.Instance, null)!);
+ var value = comp.Instance.Value;
+ value.Should().NotBeNull();
+ var dt = (DateTime?)value;
+ dt.Value.Year.Should().Be(2026);
+ dt.Value.Hour.Should().Be(11);
+ }
}