From 722c9593f966754888d67f4d44cbf1eb5281da78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 2 May 2026 17:29:56 +0300 Subject: [PATCH 1/6] New Component: MudDateTimePicker --- .../wwwroot/CodeBeam.MudBlazor.Extensions.xml | 7 +- .../DateTimePicker/DateTimePickerPage.razor | 11 + .../Examples/DateTimePickerExample1.razor | 55 ++ .../Examples/SelectExtendedExample1.razor | 4 + .../Services/MudExtensionsDocsService.cs | 1 + .../DateTimePicker/MudBaseDatePickerX.cs | 261 ++++++ .../DateTimePicker/MudDateTimePicker.razor | 343 ++++++++ .../DateTimePicker/MudDateTimePicker.razor.cs | 764 ++++++++++++++++++ .../Enums/PickerMode.cs | 8 + 9 files changed, 1453 insertions(+), 1 deletion(-) create mode 100644 docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor create mode 100644 docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs create mode 100644 src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs 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 d00d32ec..bef67a18 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 @@ -5985,8 +5985,13 @@ - + + + + + + The timezone of the watch. If null, DateTime.Now will be used. diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor new file mode 100644 index 00000000..75e91d84 --- /dev/null +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor @@ -0,0 +1,11 @@ +@page "/muddatetimepicker" +@namespace MudExtensions.Docs.Pages + + + + + + + + + diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor new file mode 100644 index 00000000..982f1b09 --- /dev/null +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor @@ -0,0 +1,55 @@ +@namespace MudExtensions.Docs.Examples +@using MudBlazor.Extensions + + + + + + + + + + @foreach (DateView item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + @foreach (Variant item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + + + + + + @foreach (Color item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + Set Today + + + + +@code { + private DateTime? _date = DateTime.Now; + private DateView _dateView = DateView.Date; + private bool _editable = false; + private bool _showToolbar = true; + private bool _showHeader = true; + private bool _submitOnClose = true; + private Color _color = Color.Primary; + private string? _dateFormat; + private bool _isAdornmentEnd = true; + private bool _clearable = false; + private Variant _variant = Variant.Outlined; +} diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor index c2707c04..9715f6b7 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor @@ -12,6 +12,8 @@ + + @@ -28,4 +30,6 @@ private bool _modal = true; private string[] _collection = new string[] { "Foo", "Bar", "Fizz", "Buzz" }; private string? _nullItemText = "None"; + + private DateTimeOffset _selectedDate = DateTimeOffset.Now; } \ No newline at end of file diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs b/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs index 223769ee..55087eb4 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs @@ -15,6 +15,7 @@ public class MudExtensionsDocsService new MudExtensionComponentInfo() {Title = "MudComboBox", Component = typeof(MudComboBox<>), RelatedComponents = new List() {typeof(MudComboBoxItem)}, Usage = ComponentUsage.Input, IsUnique = true, Description = "Unites MudSelect and MudAutocomplete features."}, new MudExtensionComponentInfo() {Title = "MudCssManager", Component = typeof(MudCssManager), Usage = ComponentUsage.Utility, IsUnique = true, IsUtility = true, Description = "Directly and dynamically get or set component's css property."}, new MudExtensionComponentInfo() {Title = "MudCsvMapper", Component = typeof(MudCsvMapper), Usage = ComponentUsage.Display, IsUnique = true, Description = "A .csv file uploader that matches the .csv file headers to supplied / expected headers."}, + new MudExtensionComponentInfo() {Title = "MudDateTimePicker", Component = typeof(MudDateTimePicker<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "Unified generic date and time picker component."}, new MudExtensionComponentInfo() {Title = "MudDateWheelPicker", Component = typeof(MudDateWheelPicker), Usage = ComponentUsage.Input, IsUnique = true, Description = "A date time picker with MudWheels."}, new MudExtensionComponentInfo() {Title = "MudGallery", Component = typeof(MudGallery), Usage = ComponentUsage.Display, IsUnique = true, Description = "Mobile friendly image gallery component."}, new MudExtensionComponentInfo() {Title = "MudInputStyler", Component = typeof(MudInputStyler), Usage = ComponentUsage.Utility, IsUnique = true, Description = "Applies colors or other CSS styles easily for mud inputs like MudTextField and MudSelect."}, diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs new file mode 100644 index 00000000..b3ffb906 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs @@ -0,0 +1,261 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; +using MudBlazor.Extensions; +using MudBlazor.State; +using MudBlazor.Utilities; +using System.Globalization; +using System.Runtime.Serialization; + +namespace MudExtensions +{ + public abstract partial class MudBaseDatePickerX : MudPicker + { + internal readonly string _mudPickerCalendarContentElementId; + private readonly ParameterState _formatState; + + protected MudBaseDatePickerX() + { + _mudPickerCalendarContentElementId = Identifier.Create(); + Culture = CultureInfo.CurrentCulture; + + using var registerScope = CreateRegisterScope(); + _formatState = registerScope.RegisterParameter(nameof(Format)) + .WithParameter(() => Format) + .WithChangeHandler(FormatChangedAsync); + } + + // 🔥 GENERIC CONVERSION LAYER + protected DateTime? ToDateTime(T? value) + { + if (value == null) + return null; + + if (value is DateTime dt) + return dt; + + if (value is DateTimeOffset dto) + return dto.LocalDateTime; + + throw new NotSupportedException($"Type {typeof(T)} not supported"); + } + + protected T? FromDateTime(DateTime? date) + { + if (date == null) + return default; + + var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + + if (t == typeof(DateTime)) + return (T)(object)date.Value; + + if (t == typeof(DateTimeOffset)) + { + var offset = TimeZoneInfo.Local.GetUtcOffset(date.Value); + return (T)(object)new DateTimeOffset(date.Value, offset); + } + + throw new NotSupportedException($"Type {typeof(T)} not supported"); + } + + protected void ValidateType() + { + var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + + if (t != typeof(DateTime) && t != typeof(DateTimeOffset)) + throw new NotSupportedException($"Type {typeof(T)} not supported."); + } + + protected override void OnInitialized() + { + ValidateType(); + base.OnInitialized(); + } + + [Inject] protected IScrollManager ScrollManager { get; set; } = null!; + [Inject] private IJsApiService JsApiService { get; set; } = null!; + [Inject] protected TimeProvider TimeProvider { get; set; } = null!; + + [Parameter] public DateTime? MaxDate { get; set; } + [Parameter] public DateTime? MinDate { get; set; } + [Parameter] public OpenTo OpenTo { get; set; } = OpenTo.Date; + + [Parameter, ParameterState] + public string? Format { get; set; } + + protected virtual Task FormatChangedAsync(string? newFormat) => Task.CompletedTask; + + private Task FormatChangedAsync(ParameterChangedEventArgs args) + => FormatChangedAsync(args.Value); + + [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; } + + internal DateTime? _picker_month; + + [Parameter] + public DateTime? PickerMonth + { + get => _picker_month; + set + { + if (value == _picker_month) + return; + _picker_month = value; + InvokeAsync(StateHasChanged); + PickerMonthChanged.InvokeAsync(value); + } + } + + protected internal DateTime? HighlightedDate { get; set; } + + [Parameter] public EventCallback PickerMonthChanged { get; set; } + + [Parameter] public int ClosingDelay { get; set; } = 100; + [Parameter] public int DisplayMonths { get; set; } = 1; + [Parameter] public int? MaxMonthColumns { get; set; } + [Parameter] public DateTime? StartMonth { get; set; } + [Parameter] public bool ShowWeekNumbers { get; set; } + [Parameter] public string TitleDateFormat { get; set; } = "ddd, dd MMM"; + [Parameter] public bool AutoClose { get; set; } + + [Parameter] + public Func IsDateDisabledFunc { get; set; } = _ => false; + + [Parameter] public Func? AdditionalDateClassesFunc { get; set; } + [Parameter] public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; + [Parameter] public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; + + [Parameter] public int? FixYear { get; set; } + [Parameter] public int? FixMonth { get; set; } + [Parameter] public int? FixDay { get; set; } + + protected OpenTo CurrentView; + + protected override async Task OnPickerOpenedAsync() + { + await base.OnPickerOpenedAsync(); + + var dateTime = ToDateTime(_value); + + if (dateTime.HasValue) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + PickerMonth = new DateTime( + calendar.GetYear(dateTime.Value), + calendar.GetMonth(dateTime.Value), + 1, + calendar); + } + + CurrentView = OpenTo; + } + + protected DateTime GetMonthStart(int month) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + var baseDate = _picker_month ?? DateTime.Today; + + return calendar.AddMonths(new DateTime(baseDate.Year, baseDate.Month, 1), month); + } + + protected IEnumerable GetWeek(int month, int index) + { + if (index is < 0 or > 5) + throw new ArgumentException("Index must be between 0 and 5", nameof(index)); + + var culture = GetCulture(); + var monthFirst = GetMonthStart(month); + + var weekFirst = monthFirst + .AddDays(index * 7) + .StartOfWeek(GetFirstDayOfWeek(), culture); + + for (var i = 0; i < 7; i++) + yield return weekFirst.AddDays(i); + } + + protected virtual bool IsDayDisabled(DateTime date) + { + return date < MinDate || + date > MaxDate || + IsDateDisabledFunc(date); + } + + protected abstract string GetDayClasses(int month, DateTime day); + protected abstract Task OnDayClickedAsync(DateTime dateTime); + + protected string FormatTitleDate(DateTime? date) + { + return date?.ToString(TitleDateFormat, GetCulture()) ?? ""; + } + + protected string GetFormattedYearString() + { + var selectedYear = HighlightedDate ?? GetMonthStart(0); + return selectedYear.Year.ToString(); + } + + protected IEnumerable GetAbbreviatedDayNames() + { + var culture = GetCulture(); + var names = culture.DateTimeFormat.AbbreviatedDayNames; + + var firstDay = (int)GetFirstDayOfWeek(); + + return Enumerable.Range(0, 7).Select(i => names[(i + firstDay) % 7]); + } + + protected override IConverter GetDefaultConverter() + { + return new DefaultConverter + { + Culture = GetCulture, + Format = GetFormat + }; + } + + protected override string? ConvertSet(T? value) + { + var dt = ToDateTime(value); + + if (dt == null) + return null; + + return dt.Value.ToString(GetFormat(), GetCulture()); + } + + protected override string GetFormat() + { + if (!string.IsNullOrWhiteSpace(_formatState.Value)) + return _formatState.Value; + + return $"{CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern} HH:mm"; + } + + protected abstract DateTime GetCalendarStartOfMonth(); + protected abstract int GetCalendarYear(DateTime yearDate); + + protected DayOfWeek GetFirstDayOfWeek() + { + return FirstDayOfWeek ?? GetCulture().DateTimeFormat.FirstDayOfWeek; + } + + protected DateTime GetMonthEnd(int month) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + var monthStartDate = PickerMonth ?? DateTime.Today.StartOfMonth(culture); + + return calendar + .AddMonths(monthStartDate, month) + .EndOfMonth(culture); + } + + //private ValueTask HandleMouseoverOnPickerCalendarDayButton(int tempId) + //{ + // return JsApiService.UpdateStyleProperty(_mudPickerCalendarContentElementId, "--selected-day", tempId); + //} + } +} \ No newline at end of file diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor new file mode 100644 index 00000000..eecded7f --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor @@ -0,0 +1,343 @@ +@typeparam T +@namespace MudExtensions +@inherits MudBaseDatePickerX + +@Render + +@code { + + protected override RenderFragment PickerContent => + @ + + @GetFormattedYearString() + @GetTitleDateString() + + + + + + @if (_mode == PickerMode.Date) + { +
+ + @{ + int dayId = 0; + var culture = GetCulture(); + var calendar = culture.Calendar; + + if (_picker_month.HasValue && calendar.GetYear(_picker_month.Value) == 1 && calendar.GetMonth(_picker_month.Value) == 1) + { + dayId = -1; + } + } + + @for (int displayMonth = 0; displayMonth < DisplayMonths; ++displayMonth) + { + int tempMonth = displayMonth; + +
+ + @* 🔥 YEAR VIEW *@ + @if (tempMonth == 0 && CurrentView == OpenTo.Year) + { +
+ @for (int i = GetMinYear(); i <= GetMaxYear(); i++) + { + var year = i; + +
+ + @year +
+ } +
+ } + + @* 🔥 MONTH VIEW *@ + else if (tempMonth == 0 && CurrentView == OpenTo.Month) + { + var calendarYear = GetCalendarYear(PickerMonth ?? DateTime.Today); + +
+
+ + @if (!FixYear.HasValue) + { + + + + + + } + else + { + + @calendarYear + + } +
+
+ +
+ @foreach (var month in GetAllMonths()) + { + + } +
+ } + + @* 🔥 DATE VIEW *@ + else if (CurrentView == OpenTo.Date || tempMonth > 0) + { +
+
+ + @if (!FixMonth.HasValue) + { + + + + + + } + else + { + + @GetMonthName(tempMonth) + + } +
+ +
+ + @if (ShowWeekNumbers) + { +
+ +
+ } + + @foreach (var dayName in GetAbbreviatedDayNames()) + { + + @dayName + + } +
+
+ +
+
+ + @for (int week = 0; week < 6; week++) + { + int tempWeek = week; + + var firstMonthFirstYear = + _picker_month.HasValue && + calendar.GetYear(_picker_month.Value) == 1 && + calendar.GetMonth(_picker_month.Value) == 1; + + @if (ShowWeekNumbers) + { +
+ + @GetWeekNumber(tempMonth, tempWeek) + +
+ } + + var wasMaxValue = false; + + @foreach (var day in GetWeek(tempMonth, tempWeek)) + { + var tempId = ++dayId; + + @if ((tempId != 0 || !firstMonthFirstYear) && !wasMaxValue) + { + var selectedDay = !firstMonthFirstYear ? day : day.AddDays(-1); + + // onpointerover = "@(async () => await HandleMouseoverOnPickerCalendarDayButton(tempId))" + + } + else + { + + } + + wasMaxValue = day == calendar.MaxSupportedDateTime; + } + } +
+
+ } +
+ } +
+ } + else + { +
+
+ +
+
+ } + +
+ +
; +} diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs new file mode 100644 index 00000000..bde97c40 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs @@ -0,0 +1,764 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using MudBlazor; +using MudBlazor.Extensions; +using MudBlazor.Utilities; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace MudExtensions; + +public partial class MudDateTimePicker : MudBaseDatePickerX +{ + [Inject] private IJSRuntime JsRuntime { get; set; } + + [DynamicDependency(nameof(OnStickClick))] + [DynamicDependency(nameof(SelectTimeFromStick))] + public MudDateTimePicker() + { + _dotNetReferenceLazy = new Lazy>>(CreateDotNetObjectReference); + } + + private DateTime? _selectedDate; + private readonly string _componentId = Identifier.Create(); + private string? _clockElementReferenceId; + private readonly Lazy>> _dotNetReferenceLazy; + + private DotNetObjectReference> CreateDotNetObjectReference() => DotNetObjectReference.Create(this); + + private DateTime? _workingValue; + + private readonly SetTime _timeSet = new(); + + private record SetTime + { + public int Hour { get; set; } + public int Minute { get; set; } + } + + public bool PointerMoving { get; set; } + + protected ElementReference ClockElementReference { get; private set; } + private bool _amPm = false; + + private enum TimeView + { + Hours, + Minutes + } + + private TimeView _timeView = TimeView.Hours; + + protected override void OnInitialized() + { + base.OnInitialized(); + _workingValue = ToDateTime(Value); + } + + protected override void OnParametersSet() + { + base.OnParametersSet(); + _workingValue = ToDateTime(Value); + SyncTimeFromValue(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + // Initialize the pointer events for the clock every time it's created (ex: popover opening and closing). + if (ClockElementReference.Id != _clockElementReferenceId) + { + _clockElementReferenceId = ClockElementReference.Id; + + await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudTimePicker.initPointerEvents", ClockElementReference, _dotNetReferenceLazy.Value); + } + } + + private void SyncTimeFromValue() + { + if (_workingValue == null) + { + _timeSet.Hour = 0; + _timeSet.Minute = 0; + return; + } + + _timeSet.Hour = _workingValue.Value.Hour; + _timeSet.Minute = _workingValue.Value.Minute; + } + + protected PickerMode _mode = PickerMode.Date; + + [Parameter] + public T? Value + { + get => _value; + set => SetDateAsync(ToDateTime(value), true); + } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public bool AmPm + { + get => _amPm; + set + { + if (_amPm == value) + return; + + _amPm = value; + + Touched = true; + _ = SetTextAsync(ConvertSet(_value), false); + } + } + + [Parameter] + public int MinuteSelectionStep { get; set; } = 1; + + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public TimeEditMode TimeEditMode { get; set; } = TimeEditMode.Normal; + + private int RoundToStepInterval(int value) + { + if (MinuteSelectionStep > 1) + { + var interval = MinuteSelectionStep % 60; + value = (value + (interval / 2)) / interval * interval; + + if (value == 60) + value = 0; + } + + return value; + } + + protected override async Task WriteTextAsync(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + _workingValue = null; + _value = default; + + await ValueChanged.InvokeAsync(_value); + return; + } + + var culture = GetCulture(); + + if (DateTime.TryParseExact(text, GetFormat(), culture, DateTimeStyles.None, out var parsed)) + { + _workingValue = parsed; + _value = FromDateTime(parsed); + + SyncTimeFromValue(); + + PickerMonth = new DateTime(parsed.Year, parsed.Month, 1); + + await ValueChanged.InvokeAsync(_value); + await BeginValidateAsync(); + FieldChanged(_value); + } + else + { + await SetTextAsync(ConvertSet(_value), false); + } + } + + private bool IsAm => _timeSet.Hour >= 0 && _timeSet.Hour < 12; + private bool IsPm => _timeSet.Hour >= 12 && _timeSet.Hour < 24; + + private async Task OnAmClickedAsync() + { + _timeSet.Hour %= 12; + await UpdateTimeAsync(); + } + + private async Task OnPmClickedAsync() + { + if (_timeSet.Hour < 12) + _timeSet.Hour += 12; + + _timeSet.Hour %= 24; + + await UpdateTimeAsync(); + } + + private DateTimeOffset _lastSetTime = DateTimeOffset.MinValue; + private const int DebounceTimeoutMs = 100; + + protected async Task SetDateAsync(DateTime? date, bool updateValue) + { + var current = ToDateTime(_value); + + if (current != null && date != null && date.Value.Kind == DateTimeKind.Unspecified) + { + date = DateTime.SpecifyKind(date.Value, current.Value.Kind); + } + + var now = TimeProvider.GetUtcNow(); + + if (current == date && (now - _lastSetTime).TotalMilliseconds < DebounceTimeoutMs) + return; + + _lastSetTime = now; + + if (current != date || (date is null && Text != null)) + { + Touched = true; + + HighlightedDate = date; + + if (date is not null && IsDateDisabledFunc(date.Value.Date)) + { + await SetTextAsync(null, false); + return; + } + + if (date is not null) + { + var culture = GetCulture(); + PickerMonth = new DateTime( + culture.Calendar.GetYear(date.Value), + culture.Calendar.GetMonth(date.Value), + 1, + culture.Calendar); + } + + var converted = FromDateTime(date); + _value = converted; + + if (updateValue) + { + ResetConverterErrors(); + await SetTextAsync(ConvertSet(_value), false); + } + + await ValueChanged.InvokeAsync(_value); + await BeginValidateAsync(); + FieldChanged(_value); + } + } + + private async Task UpdateTimeAsync() + { + if (_workingValue == null) + _workingValue = TimeProvider.GetLocalNow().Date; + + _workingValue = new DateTime( + _workingValue.Value.Year, + _workingValue.Value.Month, + _workingValue.Value.Day, + _timeSet.Hour, + _timeSet.Minute, + 0 + ); + + //if ((PickerVariant == PickerVariant.Static && PickerActions == null) || + // (PickerActions != null && AutoClose)) + //{ + // await SubmitAsync(); + //} + + _value = FromDateTime(_workingValue); + + await SetTextAsync(ConvertSet(_value), false); + } + + private void SetDatePart(DateTime date) + { + var current = _workingValue ?? TimeProvider.GetLocalNow().Date; + + _workingValue = new DateTime( + date.Year, + date.Month, + date.Day, + current.Hour, + current.Minute, + current.Second + ); + } + + private void SetTimePart(int hour, int minute) + { + var current = _workingValue ?? TimeProvider.GetLocalNow().Date; + + _workingValue = new DateTime( + current.Year, + current.Month, + current.Day, + hour, + minute, + 0 + ); + } + + protected override string GetDayClasses(int month, DateTime day) + { + var b = new CssBuilder("mud-day"); + + b.AddClass(AdditionalDateClassesFunc?.Invoke(day) ?? string.Empty); + + if (day < GetMonthStart(month) || day > GetMonthEnd(month)) + return b.AddClass("mud-hidden").Build(); + + var current = ToDateTime(Value); + + if ((current?.Date == day.Date && _selectedDate == null) || _selectedDate?.Date == day.Date) + return b.AddClass("mud-selected") + .AddClass($"mud-theme-{Color.ToStringFast(true)}") + .Build(); + + if (day.Date == TimeProvider.GetLocalNow().Date) + return b.AddClass("mud-current mud-button-outlined") + .AddClass($"mud-button-outlined-{Color.ToStringFast(true)} mud-{Color.ToStringFast(true)}-text") + .Build(); + + return b.Build(); + } + + protected override async Task OnDayClickedAsync(DateTime dateTime) + { + await FocusAsync(); + + _selectedDate = dateTime; + + if (PickerActions == null || AutoClose || PickerVariant == PickerVariant.Static) + { + await Task.Run(() => InvokeAsync(SubmitAsync)); + + if (PickerVariant != PickerVariant.Static) + { + await Task.Delay(TimeSpan.FromMilliseconds(ClosingDelay), TimeProvider); + await CloseAsync(false); + } + } + } + + protected override async Task SubmitAsync() + { + if (GetReadOnlyState()) + return; + + if (_selectedDate != null) + { + SetDatePart(_selectedDate.Value); + _selectedDate = null; + } + + if (_workingValue == null) + return; + + var converted = FromDateTime(_workingValue); + + _value = converted; + + await ValueChanged.InvokeAsync(_value); + await SetTextAsync(ConvertSet(_value), false); + await BeginValidateAsync(); + FieldChanged(_value); + } + + public override async Task ClearAsync(bool close = true) + { + _selectedDate = null; + await SetDateAsync(null, true); + + if (AutoClose) + await CloseAsync(false); + } + + protected virtual string GetTitleDateString() + { + return FormatTitleDate(_selectedDate ?? ToDateTime(Value)); + } + + protected override DateTime GetCalendarStartOfMonth() + { + var date = ToDateTime(Value) ?? HighlightedDate ?? TimeProvider.GetLocalNow().Date; + return date.StartOfMonth(GetCulture()); + } + + protected override int GetCalendarYear(DateTime yearDate) + { + var date = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; + var diff = GetCulture().Calendar.GetYear(date) - GetCulture().Calendar.GetYear(yearDate); + + return GetCulture().Calendar.GetYear(date) - diff; + } + + protected string GetMonthName(int month) + { + var date = GetMonthStart(month); + return date.ToString("MMMM yyyy", GetCulture()); + } + + protected Task OnPreviousMonthClick() + { + PickerMonth = GetMonthStart(0).AddMonths(-1); + return Task.CompletedTask; + } + + protected Task OnNextMonthClick() + { + PickerMonth = GetMonthStart(0).AddMonths(1); + return Task.CompletedTask; + } + + private void GoToSelectedYear() + { + PickerMonth = HighlightedDate; + OnYearClick(); + } + + private void OnYearClick() + { + if (!FixYear.HasValue) + { + CurrentView = OpenTo.Year; + StateHasChanged(); + //_scrollToYearAfterRender = true; + } + } + + protected int GetMinYear() + { + return MinDate?.Year ?? 1900; + } + + protected int GetMaxYear() + { + return MaxDate?.Year ?? 2100; + } + + protected Task OnYearClickedAsync(int year) + { + var current = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; + PickerMonth = new DateTime(year, current.Month, 1); + CurrentView = OpenTo.Month; + return Task.CompletedTask; + } + + protected Typo GetYearTypo(int year) + { + var current = ToDateTime(Value); + return current?.Year == year ? Typo.h5 : Typo.body1; + } + + protected string GetYearClasses(int year) + { + var current = ToDateTime(Value); + + return new CssBuilder("mud-picker-year-text") + .AddClass("mud-selected", current?.Year == year) + .Build(); + } + + protected Task OnPreviousYearClick() + { + PickerMonth = (PickerMonth ?? DateTime.Today).AddYears(-1); + return Task.CompletedTask; + } + + protected Task OnNextYearClick() + { + PickerMonth = (PickerMonth ?? DateTime.Today).AddYears(1); + return Task.CompletedTask; + } + + protected IEnumerable GetAllMonths() + { + return Enumerable.Range(1, 12); + } + + protected Task OnMonthSelectedAsync(int month) + { + var current = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; + PickerMonth = new DateTime(current.Year, month, 1); + CurrentView = OpenTo.Date; + return Task.CompletedTask; + } + + protected bool IsMonthDisabled(int month) + { + if (!MinDate.HasValue && !MaxDate.HasValue) + return false; + + var year = (PickerMonth ?? DateTime.Today).Year; + + var start = new DateTime(year, month, 1); + var end = start.AddMonths(1).AddDays(-1); + + return (MinDate.HasValue && end < MinDate.Value) + || (MaxDate.HasValue && start > MaxDate.Value); + } + + protected Typo GetMonthTypo(int month) + { + var current = ToDateTime(Value); + return current?.Month == month ? Typo.h6 : Typo.body2; + } + + protected string GetMonthClasses(int month) + { + var current = ToDateTime(Value); + + return new CssBuilder() + .AddClass("mud-selected", current?.Month == month) + .Build(); + } + + protected string GetAbbreviatedMonthName(int month) + { + return GetCulture().DateTimeFormat.AbbreviatedMonthNames[month - 1]; + } + + protected int GetWeekNumber(int month, int week) + { + var firstDay = GetWeek(month, week).First(); + + return GetCulture().Calendar.GetWeekOfYear( + firstDay, + CalendarWeekRule.FirstFourDayWeek, + GetFirstDayOfWeek()); + } + + protected string GetCalendarDayOfMonth(DateTime date) + { + return date.Day.ToString(GetCulture()); + } + + protected void OnFormattedDateClick() + { + CurrentView = OpenTo.Month; + } + + protected void OnMonthClicked(int month) + { + CurrentView = OpenTo.Month; + } + + private string GetCalendarHeaderClasses(int month) + { + return new CssBuilder("mud-picker-calendar-header") + .AddClass($"mud-picker-calendar-header-{month + 1}") + .AddClass($"mud-picker-calendar-header-last", month == DisplayMonths - 1) + .Build(); + } + + private string HourDialClassname => + new CssBuilder("mud-time-picker-dial") + .AddClass("mud-time-picker-hour") + .AddClass("mud-time-picker-dial-out", _timeView != TimeView.Hours) + .AddClass("mud-time-picker-dial-hidden", _timeView != TimeView.Hours) + .Build(); + + private string MinuteDialClassname => + new CssBuilder("mud-time-picker-dial") + .AddClass("mud-time-picker-minute") + .AddClass("mud-time-picker-dial-out", _timeView != TimeView.Minutes) + .AddClass("mud-time-picker-dial-hidden", _timeView != TimeView.Minutes) + .Build(); + + private string GetPointerRotation() + { + return $"rotateZ({GetDeg()}deg);"; + } + + private double GetDeg() + { + double deg = 0; + + if (_timeView == TimeView.Hours) + { + deg = _timeSet.Hour * 30 % 360; + } + + if (_timeView == TimeView.Minutes) + { + deg = _timeSet.Minute * 6 % 360; + } + + return deg; + } + + private string GetPointerHeight() + { + var height = 40; + + if (_timeView == TimeView.Minutes) + { + height = 40; + } + + if (_timeView == TimeView.Hours) + { + if (!AmPm && _timeSet.Hour > 0 && _timeSet.Hour < 13) + { + height = 26; + } + else + { + height = 40; + } + } + + return $"{height}%;"; + } + + private string GetNumberColor(int value) + { + if (_timeView == TimeView.Hours) + { + var h = _timeSet.Hour; + + if (AmPm) + { + h = _timeSet.Hour % 12; + if (_timeSet.Hour % 12 == 0) + { + h = 12; + } + } + + if (h == value) + { + return $"mud-clock-number mud-theme-{Color.ToStringFast(true)}"; + } + } + else if (_timeView == TimeView.Minutes && _timeSet.Minute == value) + { + return $"mud-clock-number mud-theme-{Color.ToStringFast(true)}"; + } + + return "mud-clock-number"; + } + + private async Task OnHourSelected(int hour) + { + _timeSet.Hour = hour % 24; + SetTimePart(_timeSet.Hour, _timeSet.Minute); + _timeView = TimeView.Minutes; + await InvokeAsync(StateHasChanged); + } + + private async Task OnMinuteSelected(int minute) + { + _timeSet.Minute = minute; + + SetTimePart(_timeSet.Hour, _timeSet.Minute); + + await InvokeAsync(StateHasChanged); + } + + private void ToggleMode() + { + _mode = _mode == PickerMode.Date ? PickerMode.Time : PickerMode.Date; + + if (_mode == PickerMode.Time) + SyncTimeFromValue(); + } + + private string GetClockPointerColor() + { + return PointerMoving + ? $"mud-picker-time-clock-pointer mud-{Color.ToStringFast(true)}" + : $"mud-picker-time-clock-pointer mud-picker-time-clock-pointer-animation mud-{Color.ToStringFast(true)}"; + } + + private string GetClockPinColor() + { + return $"mud-picker-time-clock-pin mud-{Color.ToStringFast(true)}"; + } + + private string GetClockPointerThumbColor() + { + var deg = GetDeg(); + return deg % 30 == 0 + ? $"mud-picker-time-clock-pointer-thumb mud-onclock-text mud-onclock-primary mud-{Color.ToStringFast(true)}" + : $"mud-picker-time-clock-pointer-thumb mud-onclock-minute mud-{Color.ToStringFast(true)}-text"; + } + + private static string GetTransform(double angle, double radius, double offsetX, double offsetY) + { + angle = angle / 180 * Math.PI; + var x = ((Math.Sin(angle) * radius) + offsetX).ToString("F3", CultureInfo.InvariantCulture); + var y = (((Math.Cos(angle) + 1) * radius) + offsetY).ToString("F3", CultureInfo.InvariantCulture); + return $"transform: translate({x}px, {y}px);"; + } + + [JSInvokable] + public async Task SelectTimeFromStick(int value, bool pointerMoving) + { + PointerMoving = pointerMoving; + + if (_timeView == TimeView.Minutes) + _timeSet.Minute = RoundToStepInterval(value); + else + _timeSet.Hour = value; + + await UpdateTimeAsync(); + + StateHasChanged(); + } + + [JSInvokable] + public async Task OnStickClick(int value) + { + // The pointer is up and not moving so animations can be enabled again. + PointerMoving = false; + + // Clicking a stick will submit the time. + if (_timeView == TimeView.Minutes) + { + await SubmitAndCloseAsync(); + } + else if (_timeView == TimeView.Hours) + { + if (TimeEditMode == TimeEditMode.Normal) + { + _timeView = TimeView.Minutes; + } + else if (TimeEditMode == TimeEditMode.OnlyHours) + { + await SubmitAndCloseAsync(); + } + } + + // Manually update because the event won't do it from JavaScript. + StateHasChanged(); + } + + protected async Task SubmitAndCloseAsync() + { + if (PickerActions == null || AutoClose) + { + await SubmitAsync(); + + if (PickerVariant != PickerVariant.Static) + { + await Task.Delay(TimeSpan.FromMilliseconds(ClosingDelay), TimeProvider); + await CloseAsync(false); + } + } + } + + protected override async ValueTask DisposeAsyncCore() + { + await base.DisposeAsyncCore(); + + if (IsJSRuntimeAvailable) + { + await JsRuntime.InvokeVoidAsyncWithErrorHandling("mudTimePicker.destroyPointerEvents", ClockElementReference); + } + + if (_dotNetReferenceLazy.IsValueCreated) + { + _dotNetReferenceLazy.Value.Dispose(); + } + } +} diff --git a/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs b/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs new file mode 100644 index 00000000..875d2307 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs @@ -0,0 +1,8 @@ +namespace MudExtensions +{ + public enum PickerMode + { + Date, + Time + } +} From c63fc2261537868cbc2c7d36d6b7347b45935430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 3 May 2026 16:42:28 +0300 Subject: [PATCH 2/6] ToolBar Implementations --- .../wwwroot/CodeBeam.MudBlazor.Extensions.xml | 12 +++ .../Examples/DateTimePickerExample1.razor | 11 ++- .../Examples/DateTimePickerExample2.razor | 63 +++++++++++++++ .../CodeBeam.MudBlazor.Extensions.csproj | 4 +- .../DateTimePicker/MudDateTimePicker.razor | 58 +++++++++++--- .../DateTimePicker/MudDateTimePicker.razor.cs | 80 ++++++++++++++++++- .../Styles/Components/_datetimepicker.scss | 14 ++++ .../Styles/MudExtensions.scss | 1 + .../wwwroot/MudExtensions.min.css | 2 +- 9 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor create mode 100644 src/CodeBeam.MudBlazor.Extensions/Styles/Components/_datetimepicker.scss 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 bef67a18..aa69f989 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 @@ -1289,6 +1289,18 @@
+ + + Gets the hour portion of the selected time. + + A two-character string depending on whether is set, or -- if no value is set. + + + + Gets the minute portion of the selected time. + + A two-digit string for minutes, or -- if no value is set. + diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor index 982f1b09..755d2024 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor @@ -3,14 +3,14 @@ - - + @foreach (DateView item in Enum.GetValues()) { @@ -24,10 +24,10 @@ } + - - + @foreach (Color item in Enum.GetValues()) { @@ -46,10 +46,9 @@ private bool _editable = false; private bool _showToolbar = true; private bool _showHeader = true; - private bool _submitOnClose = true; private Color _color = Color.Primary; private string? _dateFormat; - private bool _isAdornmentEnd = true; private bool _clearable = false; private Variant _variant = Variant.Outlined; + private bool _amPm = false; } 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 new file mode 100644 index 00000000..63770cc6 --- /dev/null +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample2.razor @@ -0,0 +1,63 @@ +@namespace MudExtensions.Docs.Examples +@using MudBlazor.Extensions + + + + + + Clear + Cancel + OK + + + + + + + + @foreach (DateView item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + @foreach (Variant item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + + + + + + @foreach (Color item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + Set Today + + + + +@code { + private MudDateTimePicker _picker; + + private DateTime? _date = DateTime.Now; + private DateView _dateView = DateView.Date; + private bool _editable = false; + private bool _showToolbar = true; + private bool _showHeader = true; + private bool _submitOnClose = true; + private Color _color = Color.Primary; + private string? _dateFormat; + private bool _isAdornmentEnd = true; + private bool _clearable = false; + private Variant _variant = Variant.Outlined; +} diff --git a/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj b/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj index 8ac5c2e4..3ceb537e 100644 --- a/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj +++ b/src/CodeBeam.MudBlazor.Extensions/CodeBeam.MudBlazor.Extensions.csproj @@ -35,9 +35,9 @@ - + diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor index eecded7f..2c560212 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor @@ -9,10 +9,48 @@ protected override RenderFragment PickerContent => @ - @GetFormattedYearString() - @GetTitleDateString() - - + + @GetFormattedYearString() + + @if (_mode == PickerMode.Time) + { + @GetTitleDateString() + } + + + + + + + + + + + + + @if (_mode == PickerMode.Date) + { + @GetTitleDateString() + } + else + { +
+ @GetHourString() + : + @GetMinuteString() +
+ + @if (AmPm) + { + + + + + + + } + } +
@@ -203,14 +241,13 @@ { var selectedDay = !firstMonthFirstYear ? day : day.AddDays(-1); - // onpointerover = "@(async () => await HandleMouseoverOnPickerCalendarDayButton(tempId))" + // onpointerover = "@(async () => await HandleMouseoverOnPickerCalendarDayButton(tempId))" - + } else { @@ -161,9 +153,7 @@ @if (!FixMonth.HasValue) { - + - + } else { - + @GetMonthName(tempMonth) } @@ -194,15 +180,13 @@ @if (ShowWeekNumbers) {
- +
} @foreach (var dayName in GetAbbreviatedDayNames()) { - + @dayName } @@ -224,8 +208,7 @@ @if (ShowWeekNumbers) {
- + @GetWeekNumber(tempMonth, tempWeek)
diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs index 2ef80864..d3af3cfa 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs @@ -19,8 +19,7 @@ public MudDateTimePicker() _dotNetReferenceLazy = new Lazy>>(CreateDotNetObjectReference); } - private DateTime? _selectedDate; - private readonly string _componentId = Identifier.Create(); + //private DateTime? _selectedDate; private string? _clockElementReferenceId; private readonly Lazy>> _dotNetReferenceLazy; @@ -49,12 +48,14 @@ private enum TimeView private TimeView _timeView = TimeView.Hours; + /// protected override void OnInitialized() { base.OnInitialized(); _workingValue = ToDateTime(Value); } + /// protected override void OnParametersSet() { base.OnParametersSet(); @@ -62,6 +63,7 @@ protected override void OnParametersSet() SyncTimeFromValue(); } + /// protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); @@ -260,16 +262,6 @@ private async Task UpdateTimeAsync() _timeSet.Minute, 0 ); - - //if ((PickerVariant == PickerVariant.Static && PickerActions == null) || - // (PickerActions != null && AutoClose)) - //{ - // await SubmitAsync(); - //} - - _value = FromDateTime(_workingValue); - await SetTextAsync(ConvertSet(_value), false); - await ValueChanged.InvokeAsync(_value); } private void SetDatePart(DateTime date) @@ -309,9 +301,9 @@ protected override string GetDayClasses(int month, DateTime day) if (day < GetMonthStart(month) || day > GetMonthEnd(month)) return b.AddClass("mud-hidden").Build(); - var current = ToDateTime(Value); + var current = _workingValue ?? ToDateTime(Value); - if ((current?.Date == day.Date && _selectedDate == null) || _selectedDate?.Date == day.Date) + if (current?.Date == day.Date) return b.AddClass("mud-selected") .AddClass($"mud-theme-{Color.ToStringFast(true)}") .Build(); @@ -328,7 +320,7 @@ protected override async Task OnDayClickedAsync(DateTime dateTime) { await FocusAsync(); - _selectedDate = dateTime; + SetDatePart(dateTime); if (PickerActions == null || AutoClose || PickerVariant == PickerVariant.Static) { @@ -347,12 +339,6 @@ protected override async Task SubmitAsync() if (GetReadOnlyState()) return; - if (_selectedDate != null) - { - SetDatePart(_selectedDate.Value); - _selectedDate = null; - } - if (_workingValue == null) return; @@ -368,16 +354,19 @@ protected override async Task SubmitAsync() public override async Task ClearAsync(bool close = true) { - _selectedDate = null; await SetDateAsync(null, true); if (AutoClose) await CloseAsync(false); } - protected virtual string GetTitleDateString() + protected string GetTitleDateString() { - return FormatTitleDate(_selectedDate ?? ToDateTime(Value)); + var date = _workingValue + ?? ToDateTime(Value) + ?? TimeProvider.GetLocalNow().Date; + + return FormatTitleDate(date); } protected override DateTime GetCalendarStartOfMonth() @@ -424,7 +413,7 @@ private void OnYearClick() { CurrentView = OpenTo.Year; StateHasChanged(); - //_scrollToYearAfterRender = true; + _scrollToYearAfterRender = true; } } @@ -442,6 +431,16 @@ protected Task OnYearClickedAsync(int year) { var current = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; PickerMonth = new DateTime(year, current.Month, 1); + + _workingValue = new DateTime( + year, + current.Month, + current.Day, + current.Hour, + current.Minute, + current.Second + ); + CurrentView = OpenTo.Month; return Task.CompletedTask; } @@ -480,8 +479,18 @@ protected IEnumerable GetAllMonths() protected Task OnMonthSelectedAsync(int month) { - var current = ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; + var current = _workingValue ?? ToDateTime(Value) ?? TimeProvider.GetLocalNow().Date; PickerMonth = new DateTime(current.Year, month, 1); + + _workingValue = new DateTime( + current.Year, + month, + current.Day, + current.Hour, + current.Minute, + current.Second + ); + CurrentView = OpenTo.Date; return Task.CompletedTask; } @@ -567,16 +576,6 @@ private string GetCalendarHeaderClasses(int month) .AddClass("mud-time-picker-dial-hidden", _timeView != TimeView.Minutes) .Build(); - protected string HoursButtonClassname => - new CssBuilder("mud-timepicker-button") - .AddClass("mud-timepicker-toolbar-text", _timeView == TimeView.Minutes) - .Build(); - - protected string MinuteButtonClassname => - new CssBuilder("mud-timepicker-button") - .AddClass("mud-timepicker-toolbar-text", _timeView == TimeView.Hours) - .Build(); - private string GetPointerRotation() { return $"rotateZ({GetDeg()}deg);"; @@ -804,18 +803,44 @@ private async Task HourFormatChanged(string value) { if (value == "am") { - AmPm = true; await OnAmClickedAsync(); } else if (value == "pm") { - AmPm = true; await OnPmClickedAsync(); } - else if(value == "24") - { - AmPm = false; - } + StateHasChanged(); + } + + protected string GetFormattedYearString() + { + var date = _workingValue + ?? ToDateTime(Value) + ?? TimeProvider.GetLocalNow().Date; + + return date.Year.ToString(); + } + + /// + /// Scrolls to the current year. + /// + public override async Task ScrollToYearAsync(DateTime? date = null) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + + _scrollToYearAfterRender = false; + + var dateTime = + date + ?? _workingValue + ?? ToDateTime(Value) + ?? TimeProvider.GetLocalNow().Date; + + var id = $"{_componentId}{calendar.GetYear(dateTime)}"; + + await ScrollManager.ScrollToYearAsync(id); + StateHasChanged(); } From df3bdf17802a37a17612125b6a0482a0116e6a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 3 May 2026 21:38:06 +0300 Subject: [PATCH 4/6] Completed Component --- .../wwwroot/CodeBeam.MudBlazor.Extensions.xml | 390 +++++++++++ .../DateTimePicker/DateTimePickerPage.razor | 7 +- .../Examples/DateTimePickerExample1.razor | 19 +- .../Examples/DateTimePickerExample2.razor | 54 +- .../Examples/DateTimePickerExample3.razor | 52 ++ .../Examples/SelectExtendedExample1.razor | 4 - .../Services/MudExtensionsDocsService.cs | 85 ++- .../DateTimePicker/MudBaseDatePickerX.cs | 647 ++++++++++++------ .../DateTimePicker/MudDateTimePicker.razor | 50 +- .../DateTimePicker/MudDateTimePicker.razor.cs | 215 ++++-- .../Enums/PickerMode.cs | 12 +- .../Styles/Components/_datetimepicker.scss | 1 - 12 files changed, 1116 insertions(+), 420 deletions(-) create mode 100644 docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor 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 aa69f989..152a43e8 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 @@ -1289,6 +1289,381 @@
+ + + Represents a base class for designing date picker components. + + + + + Is set to true to scroll to the actual year after the next render + + + + + Represents the currently selected date + + + This date is highlighted in the UI + + + + + Represents the current view state of the object. + + + + + Initializes a new instance of the MudBaseDatePickerX class. + + + + + Gets or sets the scroll manager used to control scrolling behavior within the component. + + + + + Gets or sets the JavaScript interop service used to invoke JavaScript APIs from .NET code. + + This property is typically provided by dependency injection in Blazor applications to enable + communication between .NET and JavaScript. It should be set by the framework and not manually assigned in most + scenarios. + + + + Gets or sets the time provider used to obtain the current time within the component. + + This property allows for abstraction of time-related operations, enabling easier testing and + customization of time sources. When overriding the default time behavior, provide a suitable implementation of + the TimeProvider. + + + + Time zone information used to convert the selected date and time to a specific time zone. If not set, the component will use the local time zone of the user's device. This property is particularly useful when you want to display or store the selected date and time in a different time zone than the user's local time zone. + + + + + The maximum selectable date. + + + + + The minimum selectable date. + + + + + The initial view to display. + + + Defaults to . + + + + + The format for selected dates. + + + + + The current month shown in the date picker. + + + 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. +
+
+ + + Occurs when has changed. + + + + + The delay, in milliseconds, before closing the picker after a value is selected. + + + Defaults to 100.
+ This delay helps the user see that a date has been selected before the popover disappears. +
+
+ + + The number of months to display in the calendar. + + + Defaults to 1. + + + + + The maximum number of months allowed in one row. + + + Defaults to null.
+ When null, the is used. +
+
+ + + The start month when opening the picker. + + + + + Shows week numbers at the start of each week. + + + Defaults to false. + + + + + The format of the selected date in the title. + + + Defaults to ddd, dd MMM.
+ Supported date formats can be found here: . +
+
+ + + Closes this picker when a value is selected. + + + Defaults to false. + + + + + The function used to disable one or more dates. + + + Defaults to null.
+ When set, a date will be disabled if the function returns true. +
+
+ + + The function which returns CSS classes for a date. + + + Multiple classes must be separated by spaces. + + + + + The icon for the button that navigates to the previous month or year. + + + Defaults to . + + + + + The icon for the button which navigates to the next month or year. + + + Defaults to . + + + + + The year to use, which cannot be changed. + + + Defaults to null. + + + + + The month to use, which cannot be changed. + + + Defaults to null. + + + + + The day to use, which cannot be changed. + + + Defaults to null. + + + + + True if the generic type T is DateOnly or nullable, false otherwise. + + + + + Generic conversion method to convert the generic type T to DateTime. Supports DateTime and DateTimeOffset. + + The value to convert. + The converted DateTime value. + Thrown when the type T is not supported. + + + + Converts a nullable value to an instance of type , if supported. + + If is , the local time zone offset is + applied to the converted value. + The nullable value to convert. If , the method returns the default + value for . + An instance of type representing the specified date, or the default value for + if is . + Thrown if is not or . + + + + + + + + + + True if the current view is either hours or minutes, false otherwise. + + + + + True if the current view is either date, month or year, false otherwise. + + + + + Returns the date and time format string to use for formatting operations. + + The returned format string is suitable for use with date and time formatting methods. If no + custom format is specified, the method combines the current culture's short date pattern with a 24-hour time + component. + A format string representing the date and time pattern. If a custom format is set, that value is returned; + otherwise, a default pattern based on the current culture's short date pattern and a 24-hour time format is + used. + + + + Scrolls to the current year. + + + + + Provides date and time selection in a single component. The date and time can be submitted together or separately. The time selection is done through an interactive clock interface where the user can select hours and minutes by clicking or dragging a pointer. + + + + + + + + + + + + + + + The currently selected value. + + + When this value changes, occurs. + + + + + Occurs when has changed. + + + + + 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.
+
+
+ + + The step interval when selecting minutes. + + + Defaults to 1. For example: a value of 15 would allow minutes 0, 15, + 30, and 45 be selected. + + + + + Controls which values can be edited. + + + Defaults to . + + + + + Gets or sets the text displayed for the AM period in a time picker or similar control. + + + + + Gets or sets the text displayed for the post-meridiem (PM) indicator. + + + + + 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. + + The date that was clicked. + A task that represents the asynchronous operation. + + + + Submits the current value asynchronously, triggering value change notifications and validation as appropriate. + + The method does not perform any action if the control is in a read-only state or if the + working value is null. Upon successful submission, the method updates the value, invokes change notifications, + updates the displayed text, and initiates validation. + A task that represents the asynchronous submit operation. + + + + 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. + + Indicates whether the picker should close after clearing the value. + A task that represents the asynchronous operation. + + + + Gets the formatted date string for the title of the picker, based on the current working value, the component's value, or the current local date if neither is set. The date is formatted according to the culture settings and the specified format for the title. + + The formatted date string for the title of the picker. + + + + Calculates the first day of the month for the current calendar context. + + The returned date is determined using the culture-specific calendar, which may affect the + calculation of the month's start depending on the culture in use. + A representing the first day of the month, based on the current value, highlighted date, + or the current local date if neither is set. + + + + Calculates the calendar year corresponding to the specified date, adjusted according to the current value and + culture settings. + + The result is determined using the calendar of the current culture. If the current value is + not set, the calculation uses the current local date. + The date for which to determine the calendar year. The calculation is based on the calendar associated with the + current culture. + The calendar year as an integer, adjusted based on the current value and the specified date. + Gets the hour portion of the selected time. @@ -1301,6 +1676,21 @@ A two-digit string for minutes, or -- if no value is set. + + + Scrolls to the current year. + + + + + Sets the current view of the picker to the specified value. + + + + + Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. + + 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 75e91d84..a2daa74d 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/DateTimePickerPage.razor @@ -2,10 +2,13 @@ @namespace MudExtensions.Docs.Pages - + - + + + + diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor index 755d2024..a4ce5a8e 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample1.razor @@ -4,20 +4,12 @@ + ShowToolbar="_showToolbar" Variant="_variant" Color="_color" DateFormat="@_dateFormat" /> - - @foreach (DateView item in Enum.GetValues()) - { - @item.ToDescriptionString() - } - - + @foreach (Variant item in Enum.GetValues()) { @item.ToDescriptionString() @@ -26,15 +18,14 @@ - - + @foreach (Color item in Enum.GetValues()) { @item.ToDescriptionString() } - + Set Today @@ -42,10 +33,8 @@ @code { private DateTime? _date = DateTime.Now; - private DateView _dateView = DateView.Date; private bool _editable = false; private bool _showToolbar = true; - private bool _showHeader = true; private Color _color = Color.Primary; private string? _dateFormat; private bool _clearable = false; 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 63770cc6..deb8e99f 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,61 +3,23 @@ - - - Clear - Cancel - OK - - + + + - - - @foreach (DateView item in Enum.GetValues()) - { - @item.ToDescriptionString() - } - - - @foreach (Variant item in Enum.GetValues()) - { - @item.ToDescriptionString() - } - + - - - - - - @foreach (Color item in Enum.GetValues()) - { - @item.ToDescriptionString() - } - - - Set Today + @code { - private MudDateTimePicker _picker; - private DateTime? _date = DateTime.Now; - private DateView _dateView = DateView.Date; - private bool _editable = false; - private bool _showToolbar = true; - private bool _showHeader = true; - private bool _submitOnClose = true; - private Color _color = Color.Primary; - private string? _dateFormat; - private bool _isAdornmentEnd = true; + private DateTimeOffset _date2 = DateTimeOffset.UtcNow; + private DateOnly _date3 = DateOnly.FromDateTime(DateTime.Now); private bool _clearable = false; - private Variant _variant = Variant.Outlined; + 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 new file mode 100644 index 00000000..2e0ca6d7 --- /dev/null +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/DateTimePicker/Examples/DateTimePickerExample3.razor @@ -0,0 +1,52 @@ +@namespace MudExtensions.Docs.Examples +@using MudBlazor.Extensions + + + + + + Clear + Cancel + OK + + + + + + + + @foreach (Variant item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + + + + + @foreach (Color item in Enum.GetValues()) + { + @item.ToDescriptionString() + } + + + Set Today + + + + +@code { + private MudDateTimePicker _picker = null!; + + private DateTime? _date = DateTime.Now; + private bool _editable = false; + private bool _showToolbar = true; + private bool _submitOnClose = true; + private Color _color = Color.Primary; + private string? _dateFormat; + private bool _isAdornmentEnd = true; + private bool _clearable = false; + private Variant _variant = Variant.Outlined; +} diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor index 9715f6b7..c2707c04 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample1.razor @@ -12,8 +12,6 @@ - - @@ -30,6 +28,4 @@ private bool _modal = true; private string[] _collection = new string[] { "Foo", "Bar", "Fizz", "Buzz" }; private string? _nullItemText = "None"; - - private DateTimeOffset _selectedDate = DateTimeOffset.Now; } \ No newline at end of file diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs b/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs index 55087eb4..ca3254ab 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Services/MudExtensionsDocsService.cs @@ -1,50 +1,49 @@ using MudExtensions.Utilities; -namespace MudExtensions.Docs.Services +namespace MudExtensions.Docs.Services; + +public class MudExtensionsDocsService { - public class MudExtensionsDocsService + List _components = new() { - List _components = new() - { - new MudExtensionComponentInfo() {Title = "MudAnimate", Component = typeof(MudAnimate), Usage = ComponentUsage.Utility, IsUnique = true, Description = "A revolutionary next generation animate component."}, - new MudExtensionComponentInfo() {Title = "MudBarcode", Component = typeof(MudBarcode), Usage = ComponentUsage.Display, IsUnique = true, Description = "A QR and barcode viewer with defined value."}, - new MudExtensionComponentInfo() {Title = "MudChipField", Component = typeof(MudChipField<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A textfield which transform a text to a chip when pressed pre-defined key."}, - new MudExtensionComponentInfo() {Title = "MudCodeInput", Component = typeof(MudCodeInput<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A group of textfields that each one contains a char."}, - new MudExtensionComponentInfo() {Title = "MudCodeViewer", Component = typeof(MudCodeViewer), Usage = ComponentUsage.Display, IsUnique = true, Description = "A viewer for code blocks with basic editable options."}, - new MudExtensionComponentInfo() {Title = "MudColorProvider", Component = typeof(MudColorProvider), Usage = ComponentUsage.Utility, IsUnique = true, Description = "An extension for primary, secondary and tertiary colors to support Material 3.", IsMaterial3 = true}, - new MudExtensionComponentInfo() {Title = "MudComboBox", Component = typeof(MudComboBox<>), RelatedComponents = new List() {typeof(MudComboBoxItem)}, Usage = ComponentUsage.Input, IsUnique = true, Description = "Unites MudSelect and MudAutocomplete features."}, - new MudExtensionComponentInfo() {Title = "MudCssManager", Component = typeof(MudCssManager), Usage = ComponentUsage.Utility, IsUnique = true, IsUtility = true, Description = "Directly and dynamically get or set component's css property."}, - new MudExtensionComponentInfo() {Title = "MudCsvMapper", Component = typeof(MudCsvMapper), Usage = ComponentUsage.Display, IsUnique = true, Description = "A .csv file uploader that matches the .csv file headers to supplied / expected headers."}, - new MudExtensionComponentInfo() {Title = "MudDateTimePicker", Component = typeof(MudDateTimePicker<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "Unified generic date and time picker component."}, - new MudExtensionComponentInfo() {Title = "MudDateWheelPicker", Component = typeof(MudDateWheelPicker), Usage = ComponentUsage.Input, IsUnique = true, Description = "A date time picker with MudWheels."}, - new MudExtensionComponentInfo() {Title = "MudGallery", Component = typeof(MudGallery), Usage = ComponentUsage.Display, IsUnique = true, Description = "Mobile friendly image gallery component."}, - new MudExtensionComponentInfo() {Title = "MudInputStyler", Component = typeof(MudInputStyler), Usage = ComponentUsage.Utility, IsUnique = true, Description = "Applies colors or other CSS styles easily for mud inputs like MudTextField and MudSelect."}, - new MudExtensionComponentInfo() {Title = "MudJsonTreeView", Component = typeof(MudJsonTreeView), Usage = ComponentUsage.Display, RelatedComponents = new List() {typeof(MudJsonTreeViewNode)}, IsUnique = true, Description = "A treeview for display json data."}, - new MudExtensionComponentInfo() {Title = "MudListExtended", Component = typeof(MudListExtended<>), Usage = ComponentUsage.Input, RelatedComponents = new List() {typeof(MudListItemExtended)}, IsUnique = false, Description = "The extended MudList component with richer features."}, - new MudExtensionComponentInfo() {Title = "MudLoading", Component = typeof(MudLoading), Usage = ComponentUsage.Display, IsUnique = true, Description = "Loading container for a whole page or a specific section."}, - new MudExtensionComponentInfo() {Title = "MudLoadingButton", Component = typeof(MudLoadingButton), Usage = ComponentUsage.Button, IsUnique = true, Description = "A classic MudButton with loading improvements."}, - new MudExtensionComponentInfo() {Title = "MudPage", Component = typeof(MudPage), Usage = ComponentUsage.Layout, RelatedComponents = new List() {typeof(MudSection)}, IsUnique = true, Description = "A CSS grid layout component that builds columns and rows, supports ColSpan & RowSpan."}, - new MudExtensionComponentInfo() {Title = "MudPasswordField", Component = typeof(MudPasswordField<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A specialized textfield that designed for working easily with passwords."}, - new MudExtensionComponentInfo() {Title = "MudPopup", Component = typeof(MudPopup), Usage = ComponentUsage.Display, IsUnique = true, Description = "A mobile friendly multi-functional popup content."}, - new MudExtensionComponentInfo() {Title = "MudRangeSlider", Component = typeof(MudRangeSlider), Usage = ComponentUsage.Input, IsUnique = true, Description = "A slider with range capabilities, set upper and lower values."}, - new MudExtensionComponentInfo() {Title = "MudScrollbar", Component = typeof(MudScrollbar), Usage = ComponentUsage.Utility, IsUnique = true, Description = "Customize all or defined scrollbars."}, - new MudExtensionComponentInfo() {Title = "MudSelectExtended", Component = typeof(MudSelectExtended<>), Usage = ComponentUsage.Input, RelatedComponents = new List() {typeof(MudSelectItemExtended)}, IsUnique = false, Description = "The extended MudSelect component with richer features."}, - new MudExtensionComponentInfo() {Title = "MudSignaturePad", Component = typeof(MudSignaturePad), Usage = ComponentUsage.Display, IsUnique = true, Description = "Draw and export a signature on a canvas."}, - new MudExtensionComponentInfo() {Title = "MudSpeedDial", Component = typeof(MudSpeedDial), Usage = ComponentUsage.Button, IsUnique = true, Description = "One button that stack other buttons in a popover."}, - new MudExtensionComponentInfo() {Title = "MudSplitter", Component = typeof(MudSplitter), Usage = ComponentUsage.Layout, IsUnique = true, Description = "A resizeable content splitter."}, - new MudExtensionComponentInfo() {Title = "MudStepperExtended", Component = typeof(MudStepperExtended), Usage = ComponentUsage.Display, RelatedComponents = new List() {typeof(MudStepExtended)}, IsUnique = false, Description = "A wizard-like steps to control the flow with rich options."}, - new MudExtensionComponentInfo() {Title = "MudSwitchM3", Component = typeof(MudSwitchM3), Usage = ComponentUsage.Input, IsUnique = true, IsMaterial3 = true, Description = "Material 3 switch component that has all MudSwitch features."}, - new MudExtensionComponentInfo() {Title = "MudTeleport", Component = typeof(MudTeleport), Usage = ComponentUsage.Layout, IsUnique = true, Description = "Teleport the content to the specified parent and redesign the DOM hierarchy."}, - new MudExtensionComponentInfo() {Title = "MudTextFieldExtended", Component = typeof(MudTextFieldExtended<>), Usage = ComponentUsage.Input, IsUnique = false, Description = "The extended MudTextField component with richer features."}, - new MudExtensionComponentInfo() {Title = "MudTextM3", Component = typeof(MudTextM3), Usage = ComponentUsage.Display, IsUnique = true, IsMaterial3 = true, Description = "Material 3 typography."}, - new MudExtensionComponentInfo() {Title = "MudTransferList", Component = typeof(MudTransferList<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A component that has 2 lists that transfer items to another."}, - new MudExtensionComponentInfo() {Title = "MudWatch", Component = typeof(MudWatch), Usage = ComponentUsage.Display, IsUnique = true, Description = "A performance optimized watch to show current time or show stopwatch or countdown."}, - new MudExtensionComponentInfo() {Title = "MudWheel", Component = typeof(MudWheel<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "Smoothly changes values in a wheel within defined ItemCollection."}, - }; + new MudExtensionComponentInfo() {Title = "MudAnimate", Component = typeof(MudAnimate), Usage = ComponentUsage.Utility, IsUnique = true, Description = "A revolutionary next generation animate component."}, + new MudExtensionComponentInfo() {Title = "MudBarcode", Component = typeof(MudBarcode), Usage = ComponentUsage.Display, IsUnique = true, Description = "A QR and barcode viewer with defined value."}, + new MudExtensionComponentInfo() {Title = "MudChipField", Component = typeof(MudChipField<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A textfield which transform a text to a chip when pressed pre-defined key."}, + new MudExtensionComponentInfo() {Title = "MudCodeInput", Component = typeof(MudCodeInput<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A group of textfields that each one contains a char."}, + new MudExtensionComponentInfo() {Title = "MudCodeViewer", Component = typeof(MudCodeViewer), Usage = ComponentUsage.Display, IsUnique = true, Description = "A viewer for code blocks with basic editable options."}, + new MudExtensionComponentInfo() {Title = "MudColorProvider", Component = typeof(MudColorProvider), Usage = ComponentUsage.Utility, IsUnique = true, Description = "An extension for primary, secondary and tertiary colors to support Material 3.", IsMaterial3 = true}, + new MudExtensionComponentInfo() {Title = "MudComboBox", Component = typeof(MudComboBox<>), RelatedComponents = new List() {typeof(MudComboBoxItem)}, Usage = ComponentUsage.Input, IsUnique = true, Description = "Unites MudSelect and MudAutocomplete features."}, + new MudExtensionComponentInfo() {Title = "MudCssManager", Component = typeof(MudCssManager), Usage = ComponentUsage.Utility, IsUnique = true, IsUtility = true, Description = "Directly and dynamically get or set component's css property."}, + new MudExtensionComponentInfo() {Title = "MudCsvMapper", Component = typeof(MudCsvMapper), Usage = ComponentUsage.Display, IsUnique = true, Description = "A .csv file uploader that matches the .csv file headers to supplied / expected headers."}, + new MudExtensionComponentInfo() {Title = "MudDateTimePicker", Component = typeof(MudDateTimePicker<>), Usage = ComponentUsage.Input, IsUnique = false, Description = "Unified generic date and time picker component."}, + new MudExtensionComponentInfo() {Title = "MudDateWheelPicker", Component = typeof(MudDateWheelPicker), Usage = ComponentUsage.Input, IsUnique = true, Description = "A date time picker with MudWheels."}, + new MudExtensionComponentInfo() {Title = "MudGallery", Component = typeof(MudGallery), Usage = ComponentUsage.Display, IsUnique = true, Description = "Mobile friendly image gallery component."}, + new MudExtensionComponentInfo() {Title = "MudInputStyler", Component = typeof(MudInputStyler), Usage = ComponentUsage.Utility, IsUnique = true, Description = "Applies colors or other CSS styles easily for mud inputs like MudTextField and MudSelect."}, + new MudExtensionComponentInfo() {Title = "MudJsonTreeView", Component = typeof(MudJsonTreeView), Usage = ComponentUsage.Display, RelatedComponents = new List() {typeof(MudJsonTreeViewNode)}, IsUnique = true, Description = "A treeview for display json data."}, + new MudExtensionComponentInfo() {Title = "MudListExtended", Component = typeof(MudListExtended<>), Usage = ComponentUsage.Input, RelatedComponents = new List() {typeof(MudListItemExtended)}, IsUnique = false, Description = "The extended MudList component with richer features."}, + new MudExtensionComponentInfo() {Title = "MudLoading", Component = typeof(MudLoading), Usage = ComponentUsage.Display, IsUnique = true, Description = "Loading container for a whole page or a specific section."}, + new MudExtensionComponentInfo() {Title = "MudLoadingButton", Component = typeof(MudLoadingButton), Usage = ComponentUsage.Button, IsUnique = true, Description = "A classic MudButton with loading improvements."}, + new MudExtensionComponentInfo() {Title = "MudPage", Component = typeof(MudPage), Usage = ComponentUsage.Layout, RelatedComponents = new List() {typeof(MudSection)}, IsUnique = true, Description = "A CSS grid layout component that builds columns and rows, supports ColSpan & RowSpan."}, + new MudExtensionComponentInfo() {Title = "MudPasswordField", Component = typeof(MudPasswordField<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A specialized textfield that designed for working easily with passwords."}, + new MudExtensionComponentInfo() {Title = "MudPopup", Component = typeof(MudPopup), Usage = ComponentUsage.Display, IsUnique = true, Description = "A mobile friendly multi-functional popup content."}, + new MudExtensionComponentInfo() {Title = "MudRangeSlider", Component = typeof(MudRangeSlider), Usage = ComponentUsage.Input, IsUnique = true, Description = "A slider with range capabilities, set upper and lower values."}, + new MudExtensionComponentInfo() {Title = "MudScrollbar", Component = typeof(MudScrollbar), Usage = ComponentUsage.Utility, IsUnique = true, Description = "Customize all or defined scrollbars."}, + new MudExtensionComponentInfo() {Title = "MudSelectExtended", Component = typeof(MudSelectExtended<>), Usage = ComponentUsage.Input, RelatedComponents = new List() {typeof(MudSelectItemExtended)}, IsUnique = false, Description = "The extended MudSelect component with richer features."}, + new MudExtensionComponentInfo() {Title = "MudSignaturePad", Component = typeof(MudSignaturePad), Usage = ComponentUsage.Display, IsUnique = true, Description = "Draw and export a signature on a canvas."}, + new MudExtensionComponentInfo() {Title = "MudSpeedDial", Component = typeof(MudSpeedDial), Usage = ComponentUsage.Button, IsUnique = true, Description = "One button that stack other buttons in a popover."}, + new MudExtensionComponentInfo() {Title = "MudSplitter", Component = typeof(MudSplitter), Usage = ComponentUsage.Layout, IsUnique = true, Description = "A resizeable content splitter."}, + new MudExtensionComponentInfo() {Title = "MudStepperExtended", Component = typeof(MudStepperExtended), Usage = ComponentUsage.Display, RelatedComponents = new List() {typeof(MudStepExtended)}, IsUnique = false, Description = "A wizard-like steps to control the flow with rich options."}, + new MudExtensionComponentInfo() {Title = "MudSwitchM3", Component = typeof(MudSwitchM3), Usage = ComponentUsage.Input, IsUnique = true, IsMaterial3 = true, Description = "Material 3 switch component that has all MudSwitch features."}, + new MudExtensionComponentInfo() {Title = "MudTeleport", Component = typeof(MudTeleport), Usage = ComponentUsage.Layout, IsUnique = true, Description = "Teleport the content to the specified parent and redesign the DOM hierarchy."}, + new MudExtensionComponentInfo() {Title = "MudTextFieldExtended", Component = typeof(MudTextFieldExtended<>), Usage = ComponentUsage.Input, IsUnique = false, Description = "The extended MudTextField component with richer features."}, + new MudExtensionComponentInfo() {Title = "MudTextM3", Component = typeof(MudTextM3), Usage = ComponentUsage.Display, IsUnique = true, IsMaterial3 = true, Description = "Material 3 typography."}, + new MudExtensionComponentInfo() {Title = "MudTransferList", Component = typeof(MudTransferList<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "A component that has 2 lists that transfer items to another."}, + new MudExtensionComponentInfo() {Title = "MudWatch", Component = typeof(MudWatch), Usage = ComponentUsage.Display, IsUnique = true, Description = "A performance optimized watch to show current time or show stopwatch or countdown."}, + new MudExtensionComponentInfo() {Title = "MudWheel", Component = typeof(MudWheel<>), Usage = ComponentUsage.Input, IsUnique = true, Description = "Smoothly changes values in a wheel within defined ItemCollection."}, + }; - public List GetAllComponentInfo() - { - return _components; - } + public List GetAllComponentInfo() + { + return _components; } } diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs index 1d47c13f..29a032b5 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs @@ -2,287 +2,500 @@ using MudBlazor; using MudBlazor.Extensions; using MudBlazor.State; -using MudBlazor.Utilities; using System.Globalization; -using static MudBlazor.Colors; -namespace MudExtensions +namespace MudExtensions; + +/// +/// Represents a base class for designing date picker components. +/// +public abstract partial class MudBaseDatePickerX : MudPicker { - public abstract partial class MudBaseDatePickerX : MudPicker + internal readonly string _mudPickerCalendarContentElementId; + private readonly ParameterState _formatState; + protected readonly string _componentId = Identifier.Create(); + + internal DateTime? _picker_month; + + /// + /// Is set to true to scroll to the actual year after the next render + /// + protected bool _scrollToYearAfterRender = false; + + /// + /// Represents the currently selected date + /// + /// + /// This date is highlighted in the UI + /// + protected internal DateTime? HighlightedDate { get; set; } + + /// + /// Represents the current view state of the object. + /// + protected OpenTo CurrentView; + + /// + /// Initializes a new instance of the MudBaseDatePickerX class. + /// + protected MudBaseDatePickerX() { - internal readonly string _mudPickerCalendarContentElementId; - private readonly ParameterState _formatState; - protected readonly string _componentId = Identifier.Create(); - - protected MudBaseDatePickerX() - { - _mudPickerCalendarContentElementId = Identifier.Create(); - Culture = CultureInfo.CurrentCulture; + _mudPickerCalendarContentElementId = Identifier.Create(); + Culture = CultureInfo.CurrentCulture; - using var registerScope = CreateRegisterScope(); - _formatState = registerScope.RegisterParameter(nameof(Format)) - .WithParameter(() => Format) - .WithChangeHandler(FormatChangedAsync); - } + using var registerScope = CreateRegisterScope(); + _formatState = registerScope.RegisterParameter(nameof(DateFormat)) + .WithParameter(() => DateFormat) + .WithChangeHandler(DateFormatChangedAsync); + } - // 🔥 GENERIC CONVERSION LAYER - protected DateTime? ToDateTime(T? value) + /// + /// Gets or sets the scroll manager used to control scrolling behavior within the component. + /// + [Inject] protected IScrollManager ScrollManager { get; set; } = null!; + + /// + /// Gets or sets the JavaScript interop service used to invoke JavaScript APIs from .NET code. + /// + /// This property is typically provided by dependency injection in Blazor applications to enable + /// communication between .NET and JavaScript. It should be set by the framework and not manually assigned in most + /// scenarios. + [Inject] private IJsApiService JsApiService { get; set; } = null!; + + /// + /// Gets or sets the time provider used to obtain the current time within the component. + /// + /// This property allows for abstraction of time-related operations, enabling easier testing and + /// customization of time sources. When overriding the default time behavior, provide a suitable implementation of + /// the TimeProvider. + [Inject] protected TimeProvider TimeProvider { get; set; } = null!; + + + /// + /// Time zone information used to convert the selected date and time to a specific time zone. If not set, the component will use the local time zone of the user's device. This property is particularly useful when you want to display or store the selected date and time in a different time zone than the user's local time zone. + /// + [Parameter] + public TimeZoneInfo? TimeZone { get; set; } + + /// + /// The maximum selectable date. + /// + [Parameter] public DateTime? MaxDate { get; set; } + + /// + /// The minimum selectable date. + /// + [Parameter] public DateTime? MinDate { get; set; } + + /// + /// The initial view to display. + /// + /// + /// Defaults to . + /// + [Parameter] public OpenTo OpenTo { get; set; } = OpenTo.Date; + + /// + /// The format for selected dates. + /// + [Parameter, ParameterState] + public string? DateFormat { get; set; } + + [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; } + + /// + /// The current month shown in the date picker. + /// + /// + /// 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] + public DateTime? PickerMonth + { + get => _picker_month; + set { - if (value == null) - return null; - - if (value is DateTime dt) - return dt; - - if (value is DateTimeOffset dto) - return dto.LocalDateTime; - - throw new NotSupportedException($"Type {typeof(T)} not supported"); + if (value == _picker_month) + return; + _picker_month = value; + InvokeAsync(StateHasChanged); + PickerMonthChanged.InvokeAsync(value); } + } - protected T? FromDateTime(DateTime? date) - { - if (date == null) - return default; - - var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + /// + /// Occurs when has changed. + /// + [Parameter] public EventCallback PickerMonthChanged { get; set; } + + /// + /// The delay, in milliseconds, before closing the picker after a value is selected. + /// + /// + /// 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; + + /// + /// The number of months to display in the calendar. + /// + /// + /// Defaults to 1. + /// + [Parameter] public int DisplayMonths { get; set; } = 1; + + /// + /// The maximum number of months allowed in one row. + /// + /// + /// Defaults to null.
+ /// When null, the is used. + ///
+ [Parameter] public int? MaxMonthColumns { get; set; } + + /// + /// The start month when opening the picker. + /// + [Parameter] public DateTime? StartMonth { get; set; } + + /// + /// Shows week numbers at the start of each week. + /// + /// + /// Defaults to false. + /// + [Parameter] public bool ShowWeekNumbers { get; set; } + + /// + /// The format of the selected date in the title. + /// + /// + /// Defaults to ddd, dd MMM.
+ /// Supported date formats can be found here: . + ///
+ [Parameter] public string TitleDateFormat { get; set; } = "ddd, dd MMM"; + + /// + /// Closes this picker when a value is selected. + /// + /// + /// Defaults to false. + /// + [Parameter] public bool AutoClose { get; set; } + + /// + /// The function used to disable one or more dates. + /// + /// + /// Defaults to null.
+ /// When set, a date will be disabled if the function returns true. + ///
+ [Parameter] public Func IsDateDisabledFunc { get; set; } = _ => false; + + /// + /// The function which returns CSS classes for a date. + /// + /// + /// Multiple classes must be separated by spaces. + /// + [Parameter] public Func? AdditionalDateClassesFunc { get; set; } + + /// + /// The icon for the button that navigates to the previous month or year. + /// + /// + /// Defaults to . + /// + [Parameter] public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; + + /// + /// The icon for the button which navigates to the next month or year. + /// + /// + /// Defaults to . + /// + [Parameter] public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; + + /// + /// The year to use, which cannot be changed. + /// + /// + /// Defaults to null. + /// + [Parameter] public int? FixYear { get; set; } + + /// + /// The month to use, which cannot be changed. + /// + /// + /// Defaults to null. + /// + [Parameter] public int? FixMonth { get; set; } + + /// + /// The day to use, which cannot be changed. + /// + /// + /// Defaults to null. + /// + [Parameter] public int? FixDay { get; set; } + + /// + /// True if the generic type T is DateOnly or nullable, false otherwise. + /// + protected internal bool IsDateOnly => (Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T)) == typeof(DateOnly); + + /// + /// Generic conversion method to convert the generic type T to DateTime. Supports DateTime and DateTimeOffset. + /// + /// The value to convert. + /// The converted DateTime value. + /// Thrown when the type T is not supported. + protected DateTime? ToDateTime(T? value) + { + if (value == null) + return null; - if (t == typeof(DateTime)) - return (T)(object)date.Value; + var tz = TimeZone ?? TimeZoneInfo.Local; - if (t == typeof(DateTimeOffset)) - { - var offset = TimeZoneInfo.Local.GetUtcOffset(date.Value); - return (T)(object)new DateTimeOffset(date.Value, offset); - } + if (value is DateTime dt) + return dt; - throw new NotSupportedException($"Type {typeof(T)} not supported"); - } + if (value is DateTimeOffset dto) + return TimeZoneInfo.ConvertTime(dto, tz).DateTime; - protected void ValidateType() - { - var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (value is DateOnly d) + return d.ToDateTime(TimeOnly.MinValue); - if (t != typeof(DateTime) && t != typeof(DateTimeOffset)) - throw new NotSupportedException($"Type {typeof(T)} not supported."); - } + throw new NotSupportedException($"Type {typeof(T)} not supported"); + } - protected override void OnInitialized() - { - ValidateType(); - base.OnInitialized(); - } + /// + /// Converts a nullable value to an instance of type , if supported. + /// + /// If is , the local time zone offset is + /// applied to the converted value. + /// The nullable value to convert. If , the method returns the default + /// value for . + /// An instance of type representing the specified date, or the default value for + /// if is . + /// Thrown if is not or . + protected T? FromDateTime(DateTime? date) + { + if (date == null) + return default; - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); + var tz = TimeZone ?? TimeZoneInfo.Local; - if (firstRender) - { - _picker_month ??= GetCalendarStartOfMonth(); - } + var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - if (firstRender && CurrentView == OpenTo.Year) - { - ScrollToYearAsync().CatchAndLog(); - return; - } + if (t == typeof(DateTime)) + return (T)(object)date.Value; - if (_scrollToYearAfterRender) - ScrollToYearAsync().CatchAndLog(); + if (t == typeof(DateTimeOffset)) + { + var offset = tz.GetUtcOffset(date.Value); + return (T)(object)new DateTimeOffset(date.Value, offset); } - [Inject] protected IScrollManager ScrollManager { get; set; } = null!; - [Inject] private IJsApiService JsApiService { get; set; } = null!; - [Inject] protected TimeProvider TimeProvider { get; set; } = null!; - - [Parameter] public DateTime? MaxDate { get; set; } - [Parameter] public DateTime? MinDate { get; set; } - [Parameter] public OpenTo OpenTo { get; set; } = OpenTo.Date; + if (t == typeof(DateOnly)) + return (T)(object)DateOnly.FromDateTime(date.Value); - [Parameter, ParameterState] - public string? Format { get; set; } - - protected virtual Task FormatChangedAsync(string? newFormat) => Task.CompletedTask; + throw new NotSupportedException($"Type {typeof(T)} not supported"); + } - private Task FormatChangedAsync(ParameterChangedEventArgs args) - => FormatChangedAsync(args.Value); + protected void ValidateType() + { + var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; } + if (t != typeof(DateTime) && t != typeof(DateTimeOffset) && t != typeof(DateOnly)) + throw new NotSupportedException($"Type {typeof(T)} not supported."); + } - internal DateTime? _picker_month; + /// + protected override void OnInitialized() + { + ValidateType(); + base.OnInitialized(); + } - /// - /// Is set to true to scroll to the actual year after the next render - /// - protected bool _scrollToYearAfterRender = false; + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); - [Parameter] - public DateTime? PickerMonth + if (firstRender) { - get => _picker_month; - set - { - if (value == _picker_month) - return; - _picker_month = value; - InvokeAsync(StateHasChanged); - PickerMonthChanged.InvokeAsync(value); - } + _picker_month ??= GetCalendarStartOfMonth(); } - protected internal DateTime? HighlightedDate { get; set; } + if (firstRender && CurrentView == OpenTo.Year) + { + ScrollToYearAsync().CatchAndLog(); + return; + } - [Parameter] public EventCallback PickerMonthChanged { get; set; } + if (_scrollToYearAfterRender) + ScrollToYearAsync().CatchAndLog(); + } - [Parameter] public int ClosingDelay { get; set; } = 100; - [Parameter] public int DisplayMonths { get; set; } = 1; - [Parameter] public int? MaxMonthColumns { get; set; } - [Parameter] public DateTime? StartMonth { get; set; } - [Parameter] public bool ShowWeekNumbers { get; set; } - [Parameter] public string TitleDateFormat { get; set; } = "ddd, dd MMM"; - [Parameter] public bool AutoClose { get; set; } + protected virtual Task DateFormatChangedAsync(string? newFormat) => Task.CompletedTask; - [Parameter] - public Func IsDateDisabledFunc { get; set; } = _ => false; + private Task DateFormatChangedAsync(ParameterChangedEventArgs args) => DateFormatChangedAsync(args.Value); - [Parameter] public Func? AdditionalDateClassesFunc { get; set; } - [Parameter] public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; - [Parameter] public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; + /// + /// True if the current view is either hours or minutes, false otherwise. + /// + public bool IsTimeView => CurrentView == OpenTo.Hours || CurrentView == OpenTo.Minutes; - [Parameter] public int? FixYear { get; set; } - [Parameter] public int? FixMonth { get; set; } - [Parameter] public int? FixDay { get; set; } + /// + /// True if the current view is either date, month or year, false otherwise. + /// + public bool IsDateView => + CurrentView == OpenTo.Date + || CurrentView == OpenTo.Month + || CurrentView == OpenTo.Year; - protected OpenTo CurrentView; + protected override async Task OnPickerOpenedAsync() + { + await base.OnPickerOpenedAsync(); - protected override async Task OnPickerOpenedAsync() - { - await base.OnPickerOpenedAsync(); - - var dateTime = ToDateTime(_value); - - if (dateTime.HasValue) - { - var culture = GetCulture(); - var calendar = culture.Calendar; - PickerMonth = new DateTime( - calendar.GetYear(dateTime.Value), - calendar.GetMonth(dateTime.Value), - 1, - calendar); - } - - CurrentView = OpenTo; - } + var dateTime = ToDateTime(_value); - protected DateTime GetMonthStart(int month) + if (dateTime.HasValue) { var culture = GetCulture(); var calendar = culture.Calendar; - var baseDate = _picker_month ?? DateTime.Today; - - return calendar.AddMonths(new DateTime(baseDate.Year, baseDate.Month, 1), month); + PickerMonth = new DateTime( + calendar.GetYear(dateTime.Value), + calendar.GetMonth(dateTime.Value), + 1, + calendar); } - protected IEnumerable GetWeek(int month, int index) - { - if (index is < 0 or > 5) - throw new ArgumentException("Index must be between 0 and 5", nameof(index)); + CurrentView = OpenTo; + } - var culture = GetCulture(); - var monthFirst = GetMonthStart(month); + protected DateTime GetMonthStart(int month) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + var baseDate = _picker_month ?? DateTime.Today; + + return calendar.AddMonths(new DateTime(baseDate.Year, baseDate.Month, 1), month); + } - var weekFirst = monthFirst - .AddDays(index * 7) - .StartOfWeek(GetFirstDayOfWeek(), culture); + protected IEnumerable GetWeek(int month, int index) + { + if (index is < 0 or > 5) + throw new ArgumentException("Index must be between 0 and 5", nameof(index)); - for (var i = 0; i < 7; i++) - yield return weekFirst.AddDays(i); - } + var culture = GetCulture(); + var monthFirst = GetMonthStart(month); - protected virtual bool IsDayDisabled(DateTime date) - { - return date < MinDate || - date > MaxDate || - IsDateDisabledFunc(date); - } + var weekFirst = monthFirst + .AddDays(index * 7) + .StartOfWeek(GetFirstDayOfWeek(), culture); - protected abstract string GetDayClasses(int month, DateTime day); - protected abstract Task OnDayClickedAsync(DateTime dateTime); + for (var i = 0; i < 7; i++) + yield return weekFirst.AddDays(i); + } - protected string FormatTitleDate(DateTime? date) - { - return date?.ToString(TitleDateFormat, GetCulture()) ?? ""; - } + protected virtual bool IsDayDisabled(DateTime date) + { + return date < MinDate || + date > MaxDate || + IsDateDisabledFunc(date); + } - protected IEnumerable GetAbbreviatedDayNames() - { - var culture = GetCulture(); - var names = culture.DateTimeFormat.AbbreviatedDayNames; + protected abstract string GetDayClasses(int month, DateTime day); + protected abstract Task OnDayClickedAsync(DateTime dateTime); - var firstDay = (int)GetFirstDayOfWeek(); + protected string FormatTitleDate(DateTime? date) + { + return date?.ToString(TitleDateFormat, GetCulture()) ?? ""; + } - return Enumerable.Range(0, 7).Select(i => names[(i + firstDay) % 7]); - } + protected IEnumerable GetAbbreviatedDayNames() + { + var culture = GetCulture(); + var names = culture.DateTimeFormat.AbbreviatedDayNames; - protected override IConverter GetDefaultConverter() - { - return new DefaultConverter - { - Culture = GetCulture, - Format = GetFormat - }; - } + var firstDay = (int)GetFirstDayOfWeek(); + + return Enumerable.Range(0, 7).Select(i => names[(i + firstDay) % 7]); + } - protected override string? ConvertSet(T? value) + protected override IConverter GetDefaultConverter() + { + return new DefaultConverter { - var dt = ToDateTime(value); + Culture = GetCulture, + Format = GetFormat + }; + } - if (dt == null) - return null; + protected override string? ConvertSet(T? value) + { + var dt = ToDateTime(value); - return dt.Value.ToString(GetFormat(), GetCulture()); - } + if (dt == null) + return null; - protected override string GetFormat() - { - if (!string.IsNullOrWhiteSpace(_formatState.Value)) - return _formatState.Value; + return dt.Value.ToString(GetFormat(), GetCulture()); + } - return $"{CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern} HH:mm"; - } + /// + /// Returns the date and time format string to use for formatting operations. + /// + /// The returned format string is suitable for use with date and time formatting methods. If no + /// custom format is specified, the method combines the current culture's short date pattern with a 24-hour time + /// component. + /// A format string representing the date and time pattern. If a custom format is set, that value is returned; + /// otherwise, a default pattern based on the current culture's short date pattern and a 24-hour time format is + /// used. + protected override string GetFormat() + { + if (!string.IsNullOrWhiteSpace(_formatState.Value)) + return _formatState.Value; - protected abstract DateTime GetCalendarStartOfMonth(); - protected abstract int GetCalendarYear(DateTime yearDate); + if (IsDateOnly) + return CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; - protected DayOfWeek GetFirstDayOfWeek() - { - return FirstDayOfWeek ?? GetCulture().DateTimeFormat.FirstDayOfWeek; - } + return $"{CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern} HH:mm"; + } - protected DateTime GetMonthEnd(int month) - { - var culture = GetCulture(); - var calendar = culture.Calendar; - var monthStartDate = PickerMonth ?? DateTime.Today.StartOfMonth(culture); + protected abstract DateTime GetCalendarStartOfMonth(); + protected abstract int GetCalendarYear(DateTime yearDate); - return calendar - .AddMonths(monthStartDate, month) - .EndOfMonth(culture); - } + protected DayOfWeek GetFirstDayOfWeek() + { + return FirstDayOfWeek ?? GetCulture().DateTimeFormat.FirstDayOfWeek; + } - /// - /// Scrolls to the current year. - /// - public virtual async Task ScrollToYearAsync(DateTime? date = null) - { - - } + protected DateTime GetMonthEnd(int month) + { + var culture = GetCulture(); + var calendar = culture.Calendar; + var monthStartDate = PickerMonth ?? DateTime.Today.StartOfMonth(culture); - //private ValueTask HandleMouseoverOnPickerCalendarDayButton(int tempId) - //{ - // return JsApiService.UpdateStyleProperty(_mudPickerCalendarContentElementId, "--selected-day", tempId); - //} + return calendar + .AddMonths(monthStartDate, month) + .EndOfMonth(culture); } + + /// + /// Scrolls to the current year. + /// + public virtual Task ScrollToYearAsync(DateTime? date = null) + { + return Task.CompletedTask; + } + + //private ValueTask HandleMouseoverOnPickerCalendarDayButton(int tempId) + //{ + // return JsApiService.UpdateStyleProperty(_mudPickerCalendarContentElementId, "--selected-day", tempId); + //} } \ No newline at end of file diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor index fe98ee02..6e0aecc3 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor @@ -16,15 +16,18 @@ { @GetTitleDateString() } - - - - - - - - - + + @if (!IsDateOnly) + { + + + + + + + + + } @@ -43,10 +46,8 @@ @if (AmPm) { - - - - + + } } @@ -54,12 +55,9 @@ - @if (_mode == PickerMode.Date) + @if (_mode == PickerMode.Date || IsDateOnly) { -
- +
@{ int dayId = 0; var culture = GetCulture(); @@ -240,15 +238,10 @@ } else { - } - wasMaxValue = day == calendar.MaxSupportedDateTime; } } @@ -318,11 +311,8 @@ var outer = (_i + 12) % 24;
-
-
- -
-
+
+
} } @@ -353,8 +343,6 @@
} -
- ; } diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs index d3af3cfa..2103ad20 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs @@ -8,9 +8,13 @@ namespace MudExtensions; +/// +/// Provides date and time selection in a single component. The date and time can be submitted together or separately. The time selection is done through an interactive clock interface where the user can select hours and minutes by clicking or dragging a pointer. +/// +/// public partial class MudDateTimePicker : MudBaseDatePickerX { - [Inject] private IJSRuntime JsRuntime { get; set; } + [Inject] private IJSRuntime JsRuntime { get; set; } = null!; [DynamicDependency(nameof(OnStickClick))] [DynamicDependency(nameof(SelectTimeFromStick))] @@ -19,7 +23,6 @@ public MudDateTimePicker() _dotNetReferenceLazy = new Lazy>>(CreateDotNetObjectReference); } - //private DateTime? _selectedDate; private string? _clockElementReferenceId; private readonly Lazy>> _dotNetReferenceLazy; @@ -40,14 +43,6 @@ private record SetTime protected ElementReference ClockElementReference { get; private set; } private bool _amPm = false; - private enum TimeView - { - Hours, - Minutes - } - - private TimeView _timeView = TimeView.Hours; - /// protected override void OnInitialized() { @@ -92,16 +87,33 @@ private void SyncTimeFromValue() protected PickerMode _mode = PickerMode.Date; + /// + /// The currently selected value. + /// + /// + /// When this value changes, occurs. + /// [Parameter] public T? Value { get => _value; - set => SetDateAsync(ToDateTime(value), true); + set => SetDateAsync(ToDateTime(value), true).CatchAndLog(); } + /// + /// Occurs when has changed. + /// [Parameter] public EventCallback ValueChanged { get; set; } + /// + /// 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.
+ ///
[Parameter] public bool AmPm { @@ -118,9 +130,22 @@ public bool AmPm } } + /// + /// The step interval when selecting minutes. + /// + /// + /// Defaults to 1. For example: a value of 15 would allow minutes 0, 15, + /// 30, and 45 be selected. + /// [Parameter] public int MinuteSelectionStep { get; set; } = 1; + /// + /// Controls which values can be edited. + /// + /// + /// Defaults to . + /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public TimeEditMode TimeEditMode { get; set; } = TimeEditMode.Normal; @@ -139,6 +164,18 @@ private int RoundToStepInterval(int value) return value; } + /// + /// Gets or sets the text displayed for the AM period in a time picker or similar control. + /// + [Parameter] + public string AmText { get; set; } = "AM"; + + /// + /// Gets or sets the text displayed for the post-meridiem (PM) indicator. + /// + [Parameter] + public string PmText { get; set; } = "PM"; + protected override async Task WriteTextAsync(string? text) { if (string.IsNullOrWhiteSpace(text)) @@ -171,9 +208,6 @@ protected override async Task WriteTextAsync(string? text) } } - private bool IsAm => _timeSet.Hour >= 0 && _timeSet.Hour < 12; - private bool IsPm => _timeSet.Hour >= 12 && _timeSet.Hour < 24; - private async Task OnAmClickedAsync() { _timeSet.Hour %= 12; @@ -316,6 +350,11 @@ protected override string GetDayClasses(int month, DateTime day) return b.Build(); } + /// + /// 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. + /// + /// The date that was clicked. + /// A task that represents the asynchronous operation. protected override async Task OnDayClickedAsync(DateTime dateTime) { await FocusAsync(); @@ -334,6 +373,13 @@ protected override async Task OnDayClickedAsync(DateTime dateTime) } } + /// + /// Submits the current value asynchronously, triggering value change notifications and validation as appropriate. + /// + /// The method does not perform any action if the control is in a read-only state or if the + /// working value is null. Upon successful submission, the method updates the value, invokes change notifications, + /// updates the displayed text, and initiates validation. + /// A task that represents the asynchronous submit operation. protected override async Task SubmitAsync() { if (GetReadOnlyState()) @@ -352,6 +398,11 @@ protected override async Task SubmitAsync() FieldChanged(_value); } + /// + /// 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. + /// + /// Indicates whether the picker should close after clearing the value. + /// A task that represents the asynchronous operation. public override async Task ClearAsync(bool close = true) { await SetDateAsync(null, true); @@ -360,6 +411,10 @@ public override async Task ClearAsync(bool close = true) await CloseAsync(false); } + /// + /// Gets the formatted date string for the title of the picker, based on the current working value, the component's value, or the current local date if neither is set. The date is formatted according to the culture settings and the specified format for the title. + /// + /// The formatted date string for the title of the picker. protected string GetTitleDateString() { var date = _workingValue @@ -369,12 +424,28 @@ protected string GetTitleDateString() return FormatTitleDate(date); } + /// + /// Calculates the first day of the month for the current calendar context. + /// + /// The returned date is determined using the culture-specific calendar, which may affect the + /// calculation of the month's start depending on the culture in use. + /// A representing the first day of the month, based on the current value, highlighted date, + /// or the current local date if neither is set. protected override DateTime GetCalendarStartOfMonth() { var date = ToDateTime(Value) ?? HighlightedDate ?? TimeProvider.GetLocalNow().Date; return date.StartOfMonth(GetCulture()); } + /// + /// Calculates the calendar year corresponding to the specified date, adjusted according to the current value and + /// culture settings. + /// + /// The result is determined using the calendar of the current culture. If the current value is + /// not set, the calculation uses the current local date. + /// The date for which to determine the calendar year. The calculation is based on the calendar associated with the + /// current culture. + /// 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; @@ -411,6 +482,7 @@ private void OnYearClick() { if (!FixYear.HasValue) { + _mode = PickerMode.Date; CurrentView = OpenTo.Year; StateHasChanged(); _scrollToYearAfterRender = true; @@ -565,15 +637,15 @@ private string GetCalendarHeaderClasses(int month) private string HourDialClassname => new CssBuilder("mud-time-picker-dial") .AddClass("mud-time-picker-hour") - .AddClass("mud-time-picker-dial-out", _timeView != TimeView.Hours) - .AddClass("mud-time-picker-dial-hidden", _timeView != TimeView.Hours) + .AddClass("mud-time-picker-dial-out", CurrentView != OpenTo.Hours) + .AddClass("mud-time-picker-dial-hidden", CurrentView != OpenTo.Hours) .Build(); private string MinuteDialClassname => new CssBuilder("mud-time-picker-dial") .AddClass("mud-time-picker-minute") - .AddClass("mud-time-picker-dial-out", _timeView != TimeView.Minutes) - .AddClass("mud-time-picker-dial-hidden", _timeView != TimeView.Minutes) + .AddClass("mud-time-picker-dial-out", CurrentView != OpenTo.Minutes) + .AddClass("mud-time-picker-dial-hidden", CurrentView != OpenTo.Minutes) .Build(); private string GetPointerRotation() @@ -585,12 +657,12 @@ private double GetDeg() { double deg = 0; - if (_timeView == TimeView.Hours) + if (CurrentView == OpenTo.Hours) { deg = _timeSet.Hour * 30 % 360; } - if (_timeView == TimeView.Minutes) + if (CurrentView == OpenTo.Minutes) { deg = _timeSet.Minute * 6 % 360; } @@ -602,12 +674,12 @@ private string GetPointerHeight() { var height = 40; - if (_timeView == TimeView.Minutes) + if (CurrentView == OpenTo.Minutes) { height = 40; } - if (_timeView == TimeView.Hours) + if (CurrentView == OpenTo.Hours) { if (!AmPm && _timeSet.Hour > 0 && _timeSet.Hour < 13) { @@ -624,7 +696,7 @@ private string GetPointerHeight() private string GetNumberColor(int value) { - if (_timeView == TimeView.Hours) + if (CurrentView == OpenTo.Hours) { var h = _timeSet.Hour; @@ -642,7 +714,7 @@ private string GetNumberColor(int value) return $"mud-clock-number mud-theme-{Color.ToStringFast(true)}"; } } - else if (_timeView == TimeView.Minutes && _timeSet.Minute == value) + else if (CurrentView == OpenTo.Minutes && _timeSet.Minute == value) { return $"mud-clock-number mud-theme-{Color.ToStringFast(true)}"; } @@ -650,31 +722,6 @@ private string GetNumberColor(int value) return "mud-clock-number"; } - private async Task OnHourSelected(int hour) - { - _timeSet.Hour = hour % 24; - SetTimePart(_timeSet.Hour, _timeSet.Minute); - _timeView = TimeView.Minutes; - await InvokeAsync(StateHasChanged); - } - - private async Task OnMinuteSelected(int minute) - { - _timeSet.Minute = minute; - - SetTimePart(_timeSet.Hour, _timeSet.Minute); - - await InvokeAsync(StateHasChanged); - } - - private void ToggleMode() - { - _mode = _mode == PickerMode.Date ? PickerMode.Time : PickerMode.Date; - - if (_mode == PickerMode.Time) - SyncTimeFromValue(); - } - private string GetClockPointerColor() { return PointerMoving @@ -708,7 +755,7 @@ public async Task SelectTimeFromStick(int value, bool pointerMoving) { PointerMoving = pointerMoving; - if (_timeView == TimeView.Minutes) + if (CurrentView == OpenTo.Minutes) _timeSet.Minute = RoundToStepInterval(value); else _timeSet.Hour = value; @@ -725,15 +772,15 @@ public async Task OnStickClick(int value) PointerMoving = false; // Clicking a stick will submit the time. - if (_timeView == TimeView.Minutes) + if (CurrentView == OpenTo.Minutes) { await SubmitAndCloseAsync(); } - else if (_timeView == TimeView.Hours) + else if (CurrentView == OpenTo.Hours) { if (TimeEditMode == TimeEditMode.Normal) { - _timeView = TimeView.Minutes; + CurrentView = OpenTo.Minutes; } else if (TimeEditMode == TimeEditMode.OnlyHours) { @@ -789,13 +836,13 @@ private string GetMinuteString() private async Task OnHourClickAsync() { - _timeView = TimeView.Hours; + CurrentView = OpenTo.Hours; await FocusAsync(); } private async Task OnMinutesClick() { - _timeView = TimeView.Minutes; + CurrentView = OpenTo.Minutes; await FocusAsync(); } @@ -812,6 +859,18 @@ private async Task HourFormatChanged(string value) StateHasChanged(); } + private void HandleModeChange(PickerMode mode) + { + if (mode == PickerMode.Date) + { + CurrentView = OpenTo.Date; + } + else if (mode == PickerMode.Time) + { + CurrentView = OpenTo.Hours; + } + } + protected string GetFormattedYearString() { var date = _workingValue @@ -844,6 +903,52 @@ public override async Task ScrollToYearAsync(DateTime? date = null) StateHasChanged(); } + protected override async Task OnOpenedAsync() + { + _mode = PickerMode.Date; + CurrentView = OpenTo.Hours; + await base.OnOpenedAsync(); + } + + /// + /// Sets the current view of the picker to the specified value. + /// + public void SetView(OpenTo view) + { + switch (view) + { + case OpenTo.Date: + _mode = PickerMode.Date; + CurrentView = OpenTo.Date; + break; + + case OpenTo.Month: + _mode = PickerMode.Date; + CurrentView = OpenTo.Month; + break; + + case OpenTo.Year: + _mode = PickerMode.Date; + CurrentView = OpenTo.Year; + break; + + case OpenTo.Hours: + _mode = PickerMode.Time; + CurrentView = OpenTo.Hours; + break; + + case OpenTo.Minutes: + _mode = PickerMode.Time; + CurrentView = OpenTo.Minutes; + break; + } + + StateHasChanged(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. + /// protected override async ValueTask DisposeAsyncCore() { await base.DisposeAsyncCore(); diff --git a/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs b/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs index 875d2307..b95df001 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Enums/PickerMode.cs @@ -1,8 +1,8 @@ -namespace MudExtensions +namespace MudExtensions; + +#pragma warning disable CS1591 +public enum PickerMode { - public enum PickerMode - { - Date, - Time - } + Date, + Time } diff --git a/src/CodeBeam.MudBlazor.Extensions/Styles/Components/_datetimepicker.scss b/src/CodeBeam.MudBlazor.Extensions/Styles/Components/_datetimepicker.scss index c720f708..fd1557fa 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Styles/Components/_datetimepicker.scss +++ b/src/CodeBeam.MudBlazor.Extensions/Styles/Components/_datetimepicker.scss @@ -1,6 +1,5 @@ .mud-date-time-mode-selected { background: rgba(0, 0, 0, 0.25) !important; - color: var(--mud-palette-primary-contrast-text); } .mud-date-time-toggle { From c45c8c5f2c4e0c6a4fb8c18fc4ea45956db6b346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 3 May 2026 21:58:29 +0300 Subject: [PATCH 5/6] Added Basic Tests --- .../Examples/DateTimePickerExample2.razor | 2 + .../DateTimePicker/MudBaseDatePickerX.cs | 2 +- .../DateTimePicker/MudDateTimePicker.razor.cs | 2 +- .../Components/DateTimePickerTests.cs | 103 ++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs 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 deb8e99f..e599abe7 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 @@ -5,6 +5,8 @@ + diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs index 29a032b5..31f994db 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs @@ -253,7 +253,7 @@ public DateTime? PickerMonth /// The value to convert. /// The converted DateTime value. /// Thrown when the type T is not supported. - protected DateTime? ToDateTime(T? value) + protected internal DateTime? ToDateTime(T? value) { if (value == null) return null; diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs index 2103ad20..2cf7af50 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudDateTimePicker.razor.cs @@ -230,7 +230,7 @@ private async Task OnPmClickedAsync() private DateTimeOffset _lastSetTime = DateTimeOffset.MinValue; private const int DebounceTimeoutMs = 100; - protected async Task SetDateAsync(DateTime? date, bool updateValue) + protected internal async Task SetDateAsync(DateTime? date, bool updateValue) { var current = ToDateTime(_value); diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs new file mode 100644 index 00000000..a57c5a88 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs @@ -0,0 +1,103 @@ +using AwesomeAssertions; +using Bunit; +using Microsoft.AspNetCore.Components; + +namespace MudExtensions.UnitTests.Components; + +[TestFixture] +public class DateTimePickerTests : BunitTest +{ + private IRenderedComponent> RenderPicker( + T? value = default, + EventCallback? valueChanged = null, + string? format = null) + { + return Context.Render>(parameters => + { + parameters.Add(p => p.Value, value); + + if (valueChanged.HasValue) + parameters.Add(p => p.ValueChanged, valueChanged.Value); + + if (format is not null) + parameters.Add(p => p.DateFormat, format); + }); + } + + [Test] + public void DateTimePicker_DefaultRender_Should_Render_Input() + { + var comp = Context.Render>(); + comp.Find("input").Should().NotBeNull(); + } + + [Test] + public void DateTimePicker_Should_Render_Formatted_Text() + { + var comp = RenderPicker( + value: new DateTime(2026, 5, 3, 14, 30, 0), + format: "dd.MM.yyyy HH:mm"); + + var input = comp.Find("input"); + + input.GetAttribute("value") + .Should() + .Be("3.05.2026 14:30"); + } + + [Test] + public void DateTimePicker_DateOnly_Should_Not_Render_Time() + { + var comp = Context.Render>(p => p + .Add(x => x.Value, new DateOnly(2026, 5, 3)) + ); + + comp.Markup.Should().NotContain("mud-picker-time"); + } + + [Test] + public void DateTimePicker_DateTimeOffset_Should_Convert_Correctly() + { + DateTimeOffset? value = new DateTimeOffset(2026, 5, 3, 10, 0, 0, TimeSpan.Zero); + + var comp = Context.Render>(p => p + .Add(x => x.Value, value) + .Add(x => x.TimeZone, TimeZoneInfo.Utc) + ); + + var dt = comp.Instance.ToDateTime(value); + + dt.Should().Be(new DateTime(2026, 5, 3, 10, 0, 0)); + } + + [Test] + public async Task DateTimePicker_Input_Change_Should_Update_Value() + { + DateTime? value = null; + + var callback = EventCallback.Factory.Create( + this, + v => value = v); + + var comp = RenderPicker( + value: default, + valueChanged: callback, + format: "dd.MM.yyyy HH:mm"); + + var input = comp.Find("input"); + + await input.ChangeAsync(new ChangeEventArgs + { + Value = "03.05.2026 14:30" + }); + + value.Should().Be(new DateTime(2026, 5, 3, 14, 30, 0)); + } + + [Test] + public void DateTimePicker_Null_Value_Should_Render_Empty() + { + var comp = RenderPicker(); + comp.Find("input").GetAttribute("value").Should().BeNullOrEmpty(); + } +} From aa078fd241ae3ea24e2a275076789051d864ed3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 3 May 2026 22:11:52 +0300 Subject: [PATCH 6/6] Fix Test --- .../Components/DateTimePicker/MudBaseDatePickerX.cs | 5 +++++ .../Components/DateTimePickerTests.cs | 8 +++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs index 31f994db..f6b9c008 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/DateTimePicker/MudBaseDatePickerX.cs @@ -447,6 +447,11 @@ protected IEnumerable GetAbbreviatedDayNames() return dt.Value.ToString(GetFormat(), GetCulture()); } + protected internal string? ConvertSetInternal(T? value) + { + return ConvertSet(value); + } + /// /// Returns the date and time format string to use for formatting operations. /// diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs index a57c5a88..0fec0d9d 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/DateTimePickerTests.cs @@ -32,17 +32,15 @@ public void DateTimePicker_DefaultRender_Should_Render_Input() } [Test] - public void DateTimePicker_Should_Render_Formatted_Text() + public void DateTimePicker_Should_Format_Correctly() { var comp = RenderPicker( value: new DateTime(2026, 5, 3, 14, 30, 0), format: "dd.MM.yyyy HH:mm"); - var input = comp.Find("input"); + var text = comp.Instance.ConvertSetInternal(comp.Instance.Value); - input.GetAttribute("value") - .Should() - .Be("3.05.2026 14:30"); + text.Should().Be("03.05.2026 14:30"); } [Test]