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..762f518c 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 @@ -1387,7 +1387,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 +1401,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 +1418,7 @@ The maximum number of months allowed in one row. - Defaults to null.
+ Defaults to null. When null, the is used.
@@ -1440,7 +1440,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 +1457,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.
@@ -1599,9 +1599,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.
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..b1800cd7 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs @@ -13,6 +13,9 @@ 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; @@ -107,7 +110,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] @@ -133,7 +136,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 +153,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 +175,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 +192,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 +250,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 +320,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 +358,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 +380,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 +411,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 +425,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 +447,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 +459,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 +498,7 @@ protected IEnumerable GetAbbreviatedDayNames() return Enumerable.Range(0, 7).Select(i => names[(i + firstDay) % 7]); } + /// protected override IConverter GetDefaultConverter() { return new DefaultConverter @@ -437,6 +508,7 @@ protected IEnumerable GetAbbreviatedDayNames() }; } + /// protected override string? ConvertSet(T? value) { var dt = ToDateTime(value); @@ -447,6 +519,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 +549,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 +599,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..c7c3d9bc 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; @@ -85,6 +102,9 @@ private void SyncTimeFromValue() _timeSet.Minute = _workingValue.Value.Minute; } + /// + /// The active picker mode. + /// protected PickerMode _mode = PickerMode.Date; /// @@ -110,9 +130,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 +196,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 +251,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 +353,7 @@ private void SetTimePart(int hour, int minute) ); } + /// protected override string GetDayClasses(int month, DateTime day) { var b = new CssBuilder("mud-day"); @@ -399,7 +427,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 +445,28 @@ 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(_workingValue ?? ToDateTime(Value)); 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; + + return TimeProvider.GetLocalNow().Date; + } + /// /// Calculates the first day of the month for the current calendar context. /// @@ -454,18 +497,31 @@ protected override int GetCalendarYear(DateTime 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,16 +545,29 @@ 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; @@ -517,12 +586,22 @@ 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); 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); @@ -532,23 +611,40 @@ protected string GetYearClasses(int 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; @@ -567,6 +663,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,12 +682,22 @@ 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); 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); @@ -596,11 +707,22 @@ protected string GetMonthClasses(int 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 +733,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 +884,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 +905,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 +937,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 +1020,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 ?? ToDateTime(Value)); return date.Year.ToString(); } @@ -903,6 +1054,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..e740b0e1 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs @@ -2,6 +2,8 @@ using Bunit; using Microsoft.AspNetCore.Components; +using System.Reflection; + namespace MudExtensions.UnitTests.Components; [TestFixture] @@ -98,4 +100,49 @@ 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)!; + } }