diff --git a/.editorconfig b/.editorconfig index e25e6a1835..d303be84ce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -307,7 +307,7 @@ dotnet_diagnostic.IDE0029.severity = warning dotnet_diagnostic.IDE0030.severity = warning # IDE0031: Use null propagation -dotnet_diagnostic.IDE0031.severity = warning +dotnet_diagnostic.IDE0031.severity = none # IDE0035: Remove unreachable code dotnet_diagnostic.IDE0035.severity = warning diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarCulture.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarCulture.razor index 308f34a211..ad633c97ee 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarCulture.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarCulture.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @@ -27,3 +27,4 @@ .ToArray(); private CultureInfo SelectedCulture = new CultureInfo("fa"); } + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDayCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDayCustomized.razor index da8dc923cc..d7a274626a 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDayCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDayCustomized.razor @@ -1,4 +1,4 @@ - + @if (!context.IsInactive && (context.Date.Day == 5 || context.Date.Day == 15)) @@ -14,16 +14,17 @@ -

Selected date @(SelectedValue?.ToString("yyyy-MM-dd"))

+

Selected date @(SelectedValue.ToString("yyyy-MM-dd"))

@code { - private DateTime? SelectedValue = null; + private DateOnly SelectedValue = DateOnly.FromDateTime(DateTime.Today); - private bool DisabledDate(DateTime date) + private bool DisabledDate(DateOnly date) { return (date.Day == 3 || date.Day == 8 || date.Day == 20); } } + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDefault.razor index b12443af18..6129981f69 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarDefault.razor @@ -1,11 +1,11 @@ - +
-

Selected @(SelectedDay?.ToString("yyyy-MM-dd"))

+

Selected @(SelectedDay.ToString("yyyy-MM-dd"))

Panel @(PickerDay.ToString("yyyy-MM-dd"))

@@ -15,7 +15,7 @@ @bind-Value="@SelectedMonth" @bind-PickerMonth="@PickerMonth" Style="height: 250px; align-content: start;" /> -

Selected @(SelectedMonth?.ToString("yyyy-MM-dd"))

+

Selected @(SelectedMonth.ToString("yyyy-MM-dd"))

Panel @(PickerMonth.ToString("yyyy-MM-dd"))

@@ -25,19 +25,22 @@ @bind-Value="@SelectedYear" @bind-PickerMonth="@PickerYear" Style="height: 250px; align-content: start;" /> -

Selected @(SelectedYear?.ToString("yyyy-MM-dd"))

+

Selected @(SelectedYear.ToString("yyyy-MM-dd"))

Panel @(PickerYear.ToString("yyyy-MM-dd"))

@code { - private DateTime? SelectedDay = null; - private DateTime PickerDay = DateTime.Today; - private DateTime? SelectedMonth = null; - private DateTime PickerMonth = DateTime.Today; - private DateTime? SelectedYear = null; - private DateTime PickerYear = DateTime.Today; + private static readonly DateTime Today = DateTime.Today; + private static readonly DateTime StartOfMonth = new DateTime(Today.Year, Today.Month, 1); + + private DateTime SelectedDay = Today; + private DateTime PickerDay = StartOfMonth; + private DateTime SelectedMonth = Today; + private DateTime PickerMonth = StartOfMonth; + private DateTime SelectedYear = Today; + private DateTime PickerYear = StartOfMonth; private bool DisabledDay(DateTime date) => date.Day == 3 || date.Day == 8 || date.Day == 20; private bool DisableMonth(DateTime date) => date.Month == 3 || date.Month == 8; diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarSelection.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarSelection.razor index 49e1c131f7..61c50a6def 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarSelection.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarSelection.razor @@ -4,7 +4,7 @@ @* SelectMode: Single *@
@@ -69,6 +69,7 @@ // Disable all days with the day number 3, 8, and 20. private bool DisabledDay(DateTime date) => date.Day == 3 || date.Day == 8 || date.Day == 20; + private bool DisabledNullableDay(DateTime? date) => date.HasValue && DisabledDay(date.Value); // Always select the full week, containing the "clicked" day. private IEnumerable SelectOneWeek(DateTime date) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarTypes.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarTypes.razor new file mode 100644 index 0000000000..b9faec7bb0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/Examples/CalendarTypes.razor @@ -0,0 +1,14 @@ + + + + + + + +@code +{ + private DateTime? NullableDateTime; + private DateTime DateTime = DateTime.Today; + private DateOnly? NullableDateOnly; + private DateOnly DateOnly = DateOnly.FromDateTime(DateTime.Today); +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/FluentCalendar.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/FluentCalendar.md index e1e2e25886..8f7c6b692f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/FluentCalendar.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/Calendar/FluentCalendar.md @@ -47,7 +47,14 @@ Example: `Culture="@(new CultureInfo("fr"))"` will display the calendar in Frenc {{ CalendarCulture }} +## Value type + +The **FluentCalendar** and **FluentDatePicker** components are a generic components, so you can use it with date types such as `DateTime?`, `DateTime`, `DateOnly?` or `DateOnly`. +Blazor will automatically infer the type based on the value you provide to the `Value` or `SelectedDates` parameters. +You can also explicitly set the type using the generic type parameter: `TValue` (i.e. `TValue="DateOnly?"`). + +{{ CalendarTypes }} ## API FluentCalendar -{{ API Type=FluentCalendar }} +{{ API Type=FluentCalendar }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerCulture.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerCulture.razor index 611b09bb77..aaba7f58e4 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerCulture.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerCulture.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @@ -26,3 +26,4 @@ .ToArray(); private CultureInfo SelectedCulture = new CultureInfo("fa"); } + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDefault.razor index 0d44084477..1d32f0cae0 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDefault.razor @@ -1,4 +1,4 @@ -
+
Selected Date: @(SelectedValue?.ToString("yyyy-MM-dd"))
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDoubleClickToDate.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDoubleClickToDate.razor index 6f7132ce12..b9eccda677 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDoubleClickToDate.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerDoubleClickToDate.razor @@ -1,6 +1,8 @@ +  @code { DateTime? SelectedValue = DateTime.Today.AddDays(-2); } + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerRendering.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerRendering.razor index 7fbad2c6c9..0acd5a162b 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerRendering.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/Examples/DatePickerRendering.razor @@ -1,4 +1,4 @@ -
+
Selected Date: @(SelectedValue?.ToString("yyyy-MM-dd"))
@@ -27,3 +27,4 @@ { DateTime? SelectedValue = DateTime.Today.AddDays(-2); } + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/FluentDatePicker.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/FluentDatePicker.md index 8924000a64..1f89d0d512 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/FluentDatePicker.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/DatePicker/FluentDatePicker.md @@ -58,6 +58,14 @@ In this case, the mobile picker will be used. This could be useful to use the na {{ DatePickerRendering }} +## Value type + +The **FluentCalendar** and **FluentDatePicker** components are a generic components, so you can use it with date types such as `DateTime?`, `DateTime`, `DateOnly?` or `DateOnly`. +Blazor will automatically infer the type based on the value you provide to the `Value` or `SelectedDates` parameters. +You can also explicitly set the type using the generic type parameter: `TValue` (i.e. `TValue="DateOnly?"`). + +{{ CalendarTypes }} + ## API FluentDatePicker -{{ API Type=FluentDatePicker }} +{{ API Type=FluentDatePicker }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/FluentDateTime.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/FluentDateTime.md index a544c737d6..e12c19e656 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/FluentDateTime.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/DateTime/FluentDateTime.md @@ -12,12 +12,15 @@ The `FluentDatePicker` is an input field that shows a calendar dropdown to selec The `FluentTimePicker` allows for picking a specific time of day by selecting hour, minutes and AM/PM -> [!Note] -> The FluentCalendar and FluentDatePicker components are not yet fully compatible with the EditForm and FluentEditForm elements. -> Some functionalities, such as error messages, the requirement message or the validation messages are missing. +> [!Note] The FluentCalendar and FluentDatePicker components are not yet fully compatible with the EditForm and FluentEditForm elements. +> Some functionalities, such as error messages, the required message or the validation messages are missing. ## Calendar Navigate to this [page](/DateTime/Calendar) for more details. {{ CalendarDefault }} + +## Minimum and Maximum dates + +The minimum selectable date is the 1st of February 0001 and the maximum selectable date is the 31st of December 9999. diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Models/Section.cs b/examples/Tools/FluentUI.Demo.DocViewer/Models/Section.cs index abd49f0083..48fb6badda 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Models/Section.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Models/Section.cs @@ -177,9 +177,9 @@ private static (string Name, Dictionary Arguments) ParseComponen /// private static string ReplaceMarkupKeyWord(string content) { - const string NOTE_ICON = ""; + const string NOTE_ICON = ""; const string WARN_ICON = NOTE_ICON; - const string TIP_ICON = ""; + const string TIP_ICON = ""; content = Regex.Replace(content, @"\[!NOTE\]", $"{NOTE_ICON}Note", RegexOptions.IgnoreCase); content = Regex.Replace(content, @"\[!NOTE_ICON\]", $"{NOTE_ICON}", RegexOptions.IgnoreCase); diff --git a/src/Core/Components/DateTime/CalendarExtended.cs b/src/Core/Components/DateTime/CalendarExtended.cs index c7f4b1095f..8b3fc79fdc 100644 --- a/src/Core/Components/DateTime/CalendarExtended.cs +++ b/src/Core/Components/DateTime/CalendarExtended.cs @@ -5,7 +5,7 @@ using System.Globalization; using Microsoft.FluentUI.AspNetCore.Components.Extensions; -namespace Microsoft.FluentUI.AspNetCore.Components; +namespace Microsoft.FluentUI.AspNetCore.Components.Calendar; /// /// Gets few calendar details in the right culture. diff --git a/src/Core/Components/DateTime/CalendarTValue.cs b/src/Core/Components/DateTime/CalendarTValue.cs new file mode 100644 index 0000000000..29a3b890cd --- /dev/null +++ b/src/Core/Components/DateTime/CalendarTValue.cs @@ -0,0 +1,166 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; + +namespace Microsoft.FluentUI.AspNetCore.Components.Calendar; + +internal static class CalendarTValue +{ + /// + /// Determines whether the specified type is not a supported date type. + /// + /// The type to evaluate for date type support. Supported types are DateTime, DateTime?, DateOnly, and DateOnly?. + /// true if the specified type is not DateTime, DateTime?, DateOnly, or DateOnly?; otherwise, false. + internal static bool IsNotDateType(this Type type) + { + var supportedTypes = new Type[] + { + typeof(DateTime), + typeof(DateTime?), + typeof(DateOnly), + typeof(DateOnly?), + typeof(DateTimeOffset), + typeof(DateTimeOffset?), + }; + + return !supportedTypes.Contains(type); + } + + /// + /// Convert TValue to DateTime? for internal use + /// + internal static DateTime? ConvertToDateTime(this TValue value, bool isNullOrDefault = true) + { + if (isNullOrDefault && value.IsNullOrDefault()) + { + return null; + } + + if (value == null) + { + return null; + } + + return value switch + { + DateTime dt => dt, + DateOnly d => d.ToDateTime(), + DateTimeOffset dto => dto.DateTime, + _ => null + }; + } + + /// + /// Convert TValue to DateTime for internal use + /// + internal static DateTime ConvertToRequiredDateTime(this TValue value, bool isNullOrDefault = true) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return ConvertToDateTime(value, isNullOrDefault) ?? DateTimeProvider.Today; + } + + /// + /// Convert DateTime? to TValue for external use + /// + internal static TValue ConvertToTValue(this DateTime value) + { + return typeof(TValue) switch + { + Type t when t == typeof(DateTime) => (TValue)(object)value, + Type t when t == typeof(DateTime?) => (TValue)(object)(DateTime?)value, + Type t when t == typeof(DateOnly) => (TValue)(object)DateOnly.FromDateTime(value), + Type t when t == typeof(DateOnly?) => (TValue)(object)(DateOnly?)DateOnly.FromDateTime(value), + _ => throw new ArgumentException($"Unsupported type: {typeof(TValue)}", nameof(value)) + }; + } + + /// + /// Determines whether the specified value is null or the default value for its type. + /// + /// + /// + /// + public static bool IsNullOrDefault(this TValue? value) + { + return value is null || EqualityComparer.Default.Equals(value, default); + } + + /// + /// Determines whether the specified value is not null or not the default value for its type. + /// + public static bool IsNotNull(this TValue? value) => !IsNullOrDefault(value); + + /// + /// Returns the DateTime resulting from adding the given number of + /// months to the specified DateTime. + /// + /// + /// + /// + /// + public static TValue AddMonths(this TValue? value, int months, CultureInfo culture) + { + if (value == null) + { + return default!; + } + + return DateTimeExtensions.AddMonths(value.ConvertToRequiredDateTime(), months, culture).ConvertToTValue(); + } + + /// + /// Returns the DateTime resulting from adding the given number of + /// years to the specified DateTime. + /// + /// + /// + /// + /// + public static TValue AddYears(this TValue? value, int years, CultureInfo culture) + { + if (value == null) + { + return default!; + } + + return DateTimeExtensions.AddYears(value.ConvertToRequiredDateTime(), years, culture).ConvertToTValue(); + } + + /// + /// Determines whether the specified date represents today's date. + /// + public static int GetYear(this TValue date, CultureInfo culture) + { + var dateValue = ConvertToDateTime(date); + return DateTimeExtensions.GetYear(dateValue ?? DateTime.MinValue, culture); + } + + /// + /// Returns the minimum of Date in + /// + /// + /// + /// + public static DateTime MinDateTime(this IEnumerable values) + { + return values.Select(i => i.ConvertToRequiredDateTime()).Min(); + } + + /// + /// Returns the maximum of Date in + /// + /// + /// + /// + public static DateTime MaxDateTime(this IEnumerable values) + { + return values.Select(i => i.ConvertToRequiredDateTime()).Max(); + } +} diff --git a/src/Core/Components/DateTime/CalendarTitles.cs b/src/Core/Components/DateTime/CalendarTitles.cs index 17ea8fc414..918a486092 100644 --- a/src/Core/Components/DateTime/CalendarTitles.cs +++ b/src/Core/Components/DateTime/CalendarTitles.cs @@ -2,25 +2,24 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using Microsoft.FluentUI.AspNetCore.Components.Extensions; - -namespace Microsoft.FluentUI.AspNetCore.Components; +namespace Microsoft.FluentUI.AspNetCore.Components.Calendar; /// /// Provides titles and navigation-related properties for a calendar, based on the current view and culture settings. /// -internal class CalendarTitles +/// The type of value handled by the calendar. +internal class CalendarTitles { - private readonly FluentCalendar _calendar; + private readonly FluentCalendar _calendar; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - public CalendarTitles(FluentCalendar calendar) + public CalendarTitles(FluentCalendar calendar) { _calendar = calendar; - CalendarExtended = new CalendarExtended(calendar.Culture, calendar.PickerMonth); + CalendarExtended = new CalendarExtended(calendar.Culture, calendar.PickerMonth.ConvertToRequiredDateTime()); View = calendar.View; } @@ -102,7 +101,7 @@ public bool PreviousDisabled { get { - var minDate = _calendar.Culture.Calendar.MinSupportedDateTime; + var minDate = _calendar.Culture.Calendar.MinSupportedDateTime.AddMonths(1); return View switch { diff --git a/src/Core/Components/DateTime/FluentCalendar.razor b/src/Core/Components/DateTime/FluentCalendar.razor index b9e82c7dc2..957b51bb2c 100644 --- a/src/Core/Components/DateTime/FluentCalendar.razor +++ b/src/Core/Components/DateTime/FluentCalendar.razor @@ -1,6 +1,8 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @using Microsoft.FluentUI.AspNetCore.Components.Extensions -@inherits FluentCalendarBase +@using Microsoft.FluentUI.AspNetCore.Components.Calendar +@inherits FluentCalendarBase +@typeparam TValue
+ DisabledDateFunc="@(e => DisabledDateFunc != null ? DisabledDateFunc(e.ConvertToTValue()) : false)" /> } @if (PickerView == CalendarViews.Years) { - + DisabledDateFunc="@(e => DisabledDateFunc != null ? DisabledDateFunc(e.ConvertToTValue()) : false)" /> } diff --git a/src/Core/Components/DateTime/FluentCalendar.razor.cs b/src/Core/Components/DateTime/FluentCalendar.razor.cs index a0f0ee7bf6..dace56b835 100644 --- a/src/Core/Components/DateTime/FluentCalendar.razor.cs +++ b/src/Core/Components/DateTime/FluentCalendar.razor.cs @@ -6,6 +6,7 @@ using System.Globalization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; using Microsoft.FluentUI.AspNetCore.Components.Extensions; using Microsoft.FluentUI.AspNetCore.Components.Utilities; using Microsoft.JSInterop; @@ -16,25 +17,26 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// Represents a customizable and interactive calendar component that supports various views, date selection modes, /// and animations for period changes. ///
-public partial class FluentCalendar : FluentCalendarBase +/// The type of value handled by the calendar. Must be one of: DateTime?, DateTime, DateOnly, or DateOnly?. +public partial class FluentCalendar : FluentCalendarBase { private ElementReference _calendarReference = default!; private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "DateTime/FluentCalendar.razor.js"; - + internal static string ArrowUp = ""; internal static string ArrowDown = ""; private CalendarViews _pickerView = CalendarViews.Days; private bool _refreshAccessibilityPending; private AnimationRunning _animationRunning = AnimationRunning.None; - private DateTime? _pickerMonth; + private TValue? _pickerMonth; private readonly RangeOfDates _rangeSelector = new(); private readonly RangeOfDates _rangeSelectorMouseOver = new(); private readonly List _selectedDatesMouseOver = []; /// - /// Initializes a new instance of the class with the specified library configuration. + /// Initializes a new instance of the class with the specified library configuration. /// /// The configuration settings used to initialize the calendar. Cannot be null. public FluentCalendar(LibraryConfiguration configuration) : base(configuration) @@ -49,7 +51,7 @@ public FluentCalendar(LibraryConfiguration configuration) : base(configuration) (Required ?? false) && !(Disabled ?? false) && !ReadOnly - && CurrentValue is null; + && CurrentValue.IsNullOrDefault(); }; } @@ -83,30 +85,32 @@ private CalendarViews PickerView /// /// Gets or sets the current month of the date picker (two-way bindable). /// This changes when the user browses through the calendar. - /// The month is represented as a DateTime which is always the first day of that month. + /// The month is represented as a TValue which is always the first day of that month. /// You can also set this to determine which month is displayed first. /// If not set, the current month is displayed. /// [Parameter] [SuppressMessage("Usage", "BL0007:Component parameters should be auto properties", Justification = "Need to refactor in future release")] - public virtual DateTime PickerMonth + public virtual TValue? PickerMonth { get { - return (_pickerMonth ?? Value ?? DateTimeProvider.Today).StartOfMonth(Culture); + var pickerMonthDateTime = _pickerMonth?.ConvertToDateTime() ?? ValueAsDateTime ?? DateTimeProvider.Today; + return pickerMonthDateTime.StartOfMonth(Culture).ConvertToTValue(); } set { - var month = value.StartOfMonth(Culture); + var monthDateTime = value.ConvertToDateTime()?.StartOfMonth(Culture); + var currentPickerMonthDateTime = _pickerMonth?.ConvertToDateTime(); - if (month == _pickerMonth) + if (monthDateTime == currentPickerMonthDateTime) { return; } - _pickerMonth = month; - _ = PickerMonthChanged.InvokeAsync(month); + _pickerMonth = monthDateTime.HasValue ? monthDateTime.Value.ConvertToTValue() : default; + _ = PickerMonthChanged.InvokeAsync(_pickerMonth); } } @@ -114,13 +118,13 @@ public virtual DateTime PickerMonth /// Fired when the display month changes. ///
[Parameter] - public virtual EventCallback PickerMonthChanged { get; set; } + public virtual EventCallback PickerMonthChanged { get; set; } /// /// Defines the appearance of a Day cell. /// [Parameter] - public RenderFragment? DaysTemplate { get; set; } + public RenderFragment>? DaysTemplate { get; set; } /// /// Gets or sets a value indicating whether today's date should be highlighted in the calendar. @@ -145,19 +149,19 @@ public virtual DateTime PickerMonth /// Gets or sets the list of all selected dates, only when is set to or . /// [Parameter] - public IEnumerable SelectedDates { get; set; } = []; + public IEnumerable SelectedDates { get; set; } = []; /// /// Fired when the selected dates change. /// [Parameter] - public EventCallback> SelectedDatesChanged { get; set; } + public EventCallback> SelectedDatesChanged { get; set; } /// /// Fired when the selected mouse over change, to display the future range of dates. /// [Parameter] - public Func>? SelectDatesHover { get; set; } + public Func>? SelectDatesHover { get; set; } /// internal bool IsReadOnlyOrDisabled => ReadOnly || Disabled == true; @@ -173,7 +177,7 @@ public virtual DateTime PickerMonth /// /// All days of this current month. /// - internal CalendarExtended CalendarExtended => new(Culture, PickerMonth); + internal CalendarExtended CalendarExtended => new(Culture, PickerMonth.ConvertToRequiredDateTime()); /// protected override async Task OnAfterRenderAsync(bool firstRender) @@ -207,6 +211,19 @@ private async Task RefreshAccessibilityKeyboardAsync(bool firstRender) await JSModule.ObjectReference.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Calendar.SetAccessibilityKeyboard", _calendarReference, firstRender ? null : defaultSelector); } + /// + /// Get the internal DateTime? value, synchronizing with CurrentValue if needed + /// + internal DateTime? ValueAsDateTime => CurrentValue.ConvertToDateTime(); + + /// + /// Implementation of the abstract method from FluentCalendarBase + /// + /// + /// + protected Task OnSelectedDateHandlerAsync(DateTime value) + => OnSelectedDateHandlerAsync(value.ConvertToTValue()); + /// internal async Task SetFirstFocusableAsync() { @@ -217,13 +234,13 @@ internal async Task SetFirstFocusableAsync() /// Gets titles to use in the calendar. /// /// - internal CalendarTitles GetTitles() + internal CalendarTitles GetTitles() { - return new CalendarTitles(this); + return new CalendarTitles(this); } /// - internal async Task OnPreviousButtonHandlerAsync(MouseEventArgs e) + internal async Task OnPreviousButtonHandlerAsync(MouseEventArgs _) { await StartNewAnimationAsync(AnimationRunning.Down); _refreshAccessibilityPending = true; @@ -245,7 +262,7 @@ internal async Task OnPreviousButtonHandlerAsync(MouseEventArgs e) } /// - internal async Task OnNextButtonHandlerAsync(MouseEventArgs e) + internal async Task OnNextButtonHandlerAsync(MouseEventArgs _) { await StartNewAnimationAsync(AnimationRunning.Up); _refreshAccessibilityPending = true; @@ -291,7 +308,7 @@ private async Task OnSelectYearHandlerAsync(int year, bool isReadOnly) /// /// /// - private FluentCalendarDay GetDayProperties(DateTime day) => new(this, day); + private FluentCalendarDay GetDayProperties(DateTime day) => new(this, day); /// /// Returns the class name to display a month (month, inactive, disable). @@ -299,14 +316,22 @@ private async Task OnSelectYearHandlerAsync(int year, bool isReadOnly) /// /// /// - private FluentCalendarMonth GetMonthProperties(int? year, int? month) => new(this, Culture.Calendar.ToDateTime(year ?? PickerMonth.GetYear(Culture), month ?? PickerMonth.GetMonth(Culture), 1, 0, 0, 0, 0)); + private FluentCalendarMonth GetMonthProperties(int? year, int? month) + { + var pickerDateTime = PickerMonth.ConvertToRequiredDateTime(); + return new(this, Culture.Calendar.ToDateTime(year ?? pickerDateTime.GetYear(Culture), month ?? pickerDateTime.GetMonth(Culture), 1, 0, 0, 0, 0)); + } /// /// Returns the class name to display a year (year, inactive, disable). /// /// /// - private FluentCalendarYear GetYearProperties(int? year) => new(this, Culture.Calendar.ToDateTime(year ?? PickerMonth.GetYear(Culture), 1, 1, 0, 0, 0, 0)); + private FluentCalendarYear GetYearProperties(int? year) + { + var pickerDateTime = PickerMonth.ConvertToRequiredDateTime(); + return new(this, Culture.Calendar.ToDateTime(year ?? pickerDateTime.GetYear(Culture), 1, 1, 0, 0, 0, 0)); + } /// private bool CanBeAnimated => AnimatePeriodChanges ?? (View != CalendarViews.Days && View != CalendarViews.Years); @@ -331,7 +356,7 @@ internal async Task StartNewAnimationAsync(AnimationRunning position) /// /// /// - private async Task TitleClickHandlerAsync(CalendarTitles title) + private async Task TitleClickHandlerAsync(CalendarTitles title) { if (title.ReadOnly) { @@ -359,9 +384,9 @@ private async Task TitleClickHandlerAsync(CalendarTitles title) /// /// /// - internal async Task PickerMonthSelectAsync(DateTime? month) + internal async Task PickerMonthSelectAsync(DateTime month) { - PickerMonth = month ?? DateTimeProvider.Today; + PickerMonth = month.ConvertToTValue(); PickerView = CalendarViews.Days; await Task.CompletedTask; } @@ -371,21 +396,13 @@ internal async Task PickerMonthSelectAsync(DateTime? month) /// /// /// - private async Task PickerYearSelectAsync(DateTime? year) + private async Task PickerYearSelectAsync(DateTime year) { - PickerMonth = year ?? DateTimeProvider.Today; + PickerMonth = year.ConvertToTValue(); PickerView = CalendarViews.Days; await Task.CompletedTask; } - /// - protected override bool TryParseValueFromString(string? value, out DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage) - { - BindConverter.TryConvertTo(value, Culture, out result); - validationErrorMessage = null; - return true; - } - /// private (bool IsMultiple, DateTime Min, DateTime Max, bool InProgress) GetMultipleSelection() { @@ -408,8 +425,8 @@ protected override bool TryParseValueFromString(string? value, out DateTime? res return ( (SelectMode == CalendarSelectMode.Multiple || SelectMode == CalendarSelectMode.Range) && SelectedDates.Skip(1).Any(), - SelectedDates.Min(), - SelectedDates.Max(), + SelectedDates.MinDateTime(), + SelectedDates.MaxDateTime(), inProgress ); } @@ -442,15 +459,17 @@ protected virtual async Task OnSelectDayHandlerAsync(DateTime value, bool dayDis /// private async Task OnSelectMultipleDatesAsync(DateTime value) { + var tValue = value.ConvertToTValue(); + if (SelectDatesHover is null) { - if (SelectedDates.Contains(value)) + if (SelectedDates.Any(d => d.ConvertToDateTime() == value)) { - SelectedDates = SelectedDates.Where(i => i != value); + SelectedDates = SelectedDates.Where(i => i.ConvertToDateTime() != value); } else { - SelectedDates = SelectedDates.Append(value); + SelectedDates = SelectedDates.Concat([tValue]); } if (SelectedDatesChanged.HasDelegate) @@ -460,9 +479,13 @@ private async Task OnSelectMultipleDatesAsync(DateTime value) } else { - var range = SelectDatesHover.Invoke(value); + var range = SelectDatesHover.Invoke(tValue); - SelectedDates = range.Where(day => DisabledDateFunc == null || !DisabledDateFunc(day)); + SelectedDates = range.Where(day => + { + var dateTime = day.ConvertToDateTime(); + return dateTime.HasValue && (DisabledDateFunc == null || !DisabledDateFunc(day)); + }); if (SelectedDatesChanged.HasDelegate) { @@ -492,10 +515,9 @@ private async Task OnSelectRangeDatesAsync(DateTime value) // Start and close a pre-selection else if (SelectDatesHover is not null) { - var range = SelectDatesHover.Invoke(value); - - _rangeSelector.Start = range.Min(); - _rangeSelector.End = range.Max(); + var range = SelectDatesHover.Invoke(value.ConvertToTValue()); + _rangeSelector.Start = range.MinDateTime(); + _rangeSelector.End = range.MaxDateTime(); } // Start the selection @@ -507,7 +529,9 @@ private async Task OnSelectRangeDatesAsync(DateTime value) await OnSelectDayMouseOverAsync(value, dayDisabled: false); } - SelectedDates = _rangeSelector.GetAllDates().Where(day => DisabledDateFunc == null || !DisabledDateFunc(day)); + SelectedDates = _rangeSelector.GetAllDates() + .Where(day => DisabledDateFunc == null || !DisabledDateFunc(day.ConvertToTValue())) + .Select(day => day.ConvertToTValue()); if (SelectedDatesChanged.HasDelegate) { @@ -525,6 +549,8 @@ internal Task OnSelectDayMouseOverAsync(DateTime value, bool dayDisabled) return Task.CompletedTask; } + var tValue = value.ConvertToTValue(); + if (SelectDatesHover is null) { _rangeSelectorMouseOver.Start = _rangeSelector.Start ?? value; @@ -532,14 +558,14 @@ internal Task OnSelectDayMouseOverAsync(DateTime value, bool dayDisabled) } else { - var range = SelectDatesHover.Invoke(value); - _rangeSelectorMouseOver.Start = range.Min(); - _rangeSelectorMouseOver.End = range.Max(); + var range = SelectDatesHover.Invoke(tValue); + _rangeSelectorMouseOver.Start = range.MinDateTime(); + _rangeSelectorMouseOver.End = range.MaxDateTime(); } var days = DisabledDateFunc is null ? _rangeSelectorMouseOver.GetAllDates() - : _rangeSelectorMouseOver.GetAllDates().Where(day => !DisabledDateFunc(day)); + : _rangeSelectorMouseOver.GetAllDates().Where(day => !DisabledDateFunc(day.ConvertToTValue())); _selectedDatesMouseOver.Clear(); _selectedDatesMouseOver.AddRange(days); @@ -572,7 +598,7 @@ internal bool AllDaysAreDisabled(DateTime start, DateTime end) for (var day = start; day <= end; day = day.AddDays(1)) { - if (!DisabledDateFunc.Invoke(day)) + if (!DisabledDateFunc.Invoke(day.ConvertToTValue())) { return false; } @@ -584,18 +610,20 @@ internal bool AllDaysAreDisabled(DateTime start, DateTime end) /// private string GetFormValue() { - switch (SelectMode) + return SelectMode switch { - case CalendarSelectMode.Single: - return Value?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? string.Empty; + CalendarSelectMode.Single + => ValueAsDateTime?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? string.Empty, - case CalendarSelectMode.Range: - case CalendarSelectMode.Multiple: - return string.Join(",", SelectedDates.Select(d => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))); + CalendarSelectMode.Range or CalendarSelectMode.Multiple + => string.Join(',', SelectedDates.Select(d => + { + var dateTime = d.ConvertToDateTime(); + return dateTime?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? string.Empty; + }).Where(s => !string.IsNullOrEmpty(s))), - default: - return string.Empty; - } + _ => string.Empty, + }; } internal enum AnimationRunning diff --git a/src/Core/Components/DateTime/FluentCalendarBase.cs b/src/Core/Components/DateTime/FluentCalendarBase.cs index fe54c16274..76ffee5a79 100644 --- a/src/Core/Components/DateTime/FluentCalendarBase.cs +++ b/src/Core/Components/DateTime/FluentCalendarBase.cs @@ -2,18 +2,33 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Provides a base class for building calendar components. /// -public abstract class FluentCalendarBase : FluentInputBase +/// The type of value handled by the calendar. Must be one of: DateTime?, DateTime, DateOnly, or DateOnly?. +public abstract class FluentCalendarBase : FluentInputBase { + /* ************************************************************************************ + * Dev Note: The TValue cannot be constrained to `where TValue : struct, IComparable` + * because it can be either a nullable or non-nullable value type. + * So, the CalendarTValue.IsNullOrDefault() extension method returns true if the value is null or equal to the default value (Date.Min). + * ************************************************************************************/ + /// - protected FluentCalendarBase(LibraryConfiguration configuration) : base(configuration) { } + protected FluentCalendarBase(LibraryConfiguration configuration) : base(configuration) + { + if (typeof(TValue).IsNotDateType()) + { + throw new InvalidOperationException($"The type parameter {typeof(TValue)} is not supported. Supported types are DateTime, DateTime?, DateOnly, and DateOnly?."); + } + } /// /// Gets or sets the verification to do when the selected value has changed. @@ -33,7 +48,7 @@ protected FluentCalendarBase(LibraryConfiguration configuration) : base(configur /// Function to know if a specific day must be disabled. /// [Parameter] - public virtual Func? DisabledDateFunc { get; set; } + public virtual Func? DisabledDateFunc { get; set; } /// /// By default, the check only the first day of the month and the first day of the year for the Month and Year views. @@ -56,20 +71,36 @@ protected FluentCalendarBase(LibraryConfiguration configuration) : base(configur public CalendarDayFormat? DayFormat { get; set; } = CalendarDayFormat.Numeric; /// - /// Defines the appearance of the component. + /// Defines the appearance of the component. /// [Parameter] public virtual CalendarViews View { get; set; } = CalendarViews.Days; /// - protected virtual Task OnSelectedDateHandlerAsync(DateTime? value) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + { + if (DateTime.TryParse(value, Culture, out var dateTime)) + { + result = dateTime.ConvertToTValue(); + validationErrorMessage = null; + return true; + } + + result = default!; + validationErrorMessage = string.Format(CultureInfo.InvariantCulture, Localizer[Localization.LanguageResource.Calendar_FieldMustBeADate], DisplayName ?? FieldIdentifier.FieldName); + return false; + } + + /// + protected virtual Task OnSelectedDateHandlerAsync(TValue? value) { if (ReadOnly || Disabled == true) { return Task.CompletedTask; } - if ((CheckIfSelectedValueHasChanged ?? true) && CurrentValue == value) + var dateTime = value.ConvertToDateTime(); + if ((CheckIfSelectedValueHasChanged ?? true) && CurrentValue.ConvertToDateTime() == dateTime) { return Task.CompletedTask; } diff --git a/src/Core/Components/DateTime/FluentCalendarDay.cs b/src/Core/Components/DateTime/FluentCalendarDay.cs index c8fd8b9a24..dba9b93ac6 100644 --- a/src/Core/Components/DateTime/FluentCalendarDay.cs +++ b/src/Core/Components/DateTime/FluentCalendarDay.cs @@ -3,36 +3,44 @@ // ------------------------------------------------------------------------ using System.Globalization; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Computes the properties of a day in the calendar, depending on the current culture. /// -public class FluentCalendarDay +/// The type of value handled by the calendar. +public class FluentCalendarDay { - private readonly FluentCalendar _calendar; + private readonly FluentCalendar _calendar; private readonly bool _isInDisabledList; private readonly bool _isOutsideCurrentMonth; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// - internal FluentCalendarDay(FluentCalendar calendar, DateTime day) + internal FluentCalendarDay(FluentCalendar calendar, DateTime day) { _calendar = calendar; - Date = day; + Date = day.Date.ConvertToTValue(); + DateTime = day.Date; - _isInDisabledList = calendar.DisabledDateFunc?.Invoke(day) ?? false; + _isInDisabledList = calendar.DisabledDateFunc?.Invoke(day.ConvertToTValue()) ?? false; _isOutsideCurrentMonth = !calendar.CalendarExtended.IsInCurrentMonth(day); } /// /// Current day /// - public DateTime Date { get; } + internal DateTime DateTime { get; } + + /// + /// Current day converted to TValue + /// + public TValue Date { get; } /// /// Gets a value indicating whether the day is disabled by the user. @@ -47,22 +55,22 @@ internal FluentCalendarDay(FluentCalendar calendar, DateTime day) /// /// Gets a value indicating whether the day is set to Today. /// - public bool IsToday => Date == DateTimeProvider.Today && !_isOutsideCurrentMonth; + public bool IsToday => DateTime == DateTimeProvider.Today && !_isOutsideCurrentMonth; /// /// Gets a value indicating whether the day is selected by the user. /// - public bool IsSelected => Date.Date == _calendar.Value?.Date; + public bool IsSelected => DateTime.Date == _calendar.ValueAsDateTime?.Date; /// - /// Gets a value indicating whether the day is selected by the user, using . + /// Gets a value indicating whether the day is selected by the user, using . /// public bool IsMultiDaySelected => _calendar.SelectMode != CalendarSelectMode.Single && _calendar.SelectedDates.Contains(Date) && !IsDisabled; /// /// Gets the name of the day and month in current culture. /// - public string Title => _calendar.CalendarExtended.GetCalendarDayWithMonthName(Date); + public string Title => _calendar.CalendarExtended.GetCalendarDayWithMonthName(DateTime); /// /// Gets the number of day of the month in the current Culture. @@ -71,7 +79,7 @@ public string DayNumber { get { - var day = _calendar.CalendarExtended.GetCalendarDayOfMonth(Date); + var day = _calendar.CalendarExtended.GetCalendarDayOfMonth(DateTime); return _calendar.DayFormat switch { @@ -84,5 +92,5 @@ public string DayNumber /// /// Gets the identifier of the day in the format yyyy-MM-dd. /// - public string DayIdentifier => Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + public string DayIdentifier => DateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } diff --git a/src/Core/Components/DateTime/FluentCalendarMonth.cs b/src/Core/Components/DateTime/FluentCalendarMonth.cs index 680b2bdbe1..be064a64f6 100644 --- a/src/Core/Components/DateTime/FluentCalendarMonth.cs +++ b/src/Core/Components/DateTime/FluentCalendarMonth.cs @@ -2,6 +2,7 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using Microsoft.FluentUI.AspNetCore.Components.Calendar; using Microsoft.FluentUI.AspNetCore.Components.Extensions; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -9,17 +10,18 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Computes the properties of a month in the calendar. /// -internal class FluentCalendarMonth +/// The type of value handled by the calendar. +internal class FluentCalendarMonth { - private readonly FluentCalendar _calendar; + private readonly FluentCalendar _calendar; private readonly bool _isInDisabledList; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// - internal FluentCalendarMonth(FluentCalendar calendar, DateTime month) + internal FluentCalendarMonth(FluentCalendar calendar, DateTime month) { _calendar = calendar; Month = month.GetDay(_calendar.Culture) == 1 ? month : month.StartOfMonth(_calendar.Culture); @@ -30,7 +32,7 @@ internal FluentCalendarMonth(FluentCalendar calendar, DateTime month) } else { - _isInDisabledList = calendar.DisabledDateFunc?.Invoke(Month) ?? false; + _isInDisabledList = calendar.DisabledDateFunc?.Invoke(Month.ConvertToTValue()) ?? false; } } @@ -52,7 +54,7 @@ internal FluentCalendarMonth(FluentCalendar calendar, DateTime month) /// /// Whether the month is selected by the user /// - public bool IsSelected => Month.GetYear(_calendar.Culture) == _calendar.Value?.GetYear(_calendar.Culture) && Month.GetMonth(_calendar.Culture) == _calendar.Value?.GetMonth(_calendar.Culture); + public bool IsSelected => Month.GetYear(_calendar.Culture) == _calendar.ValueAsDateTime?.GetYear(_calendar.Culture) && Month.GetMonth(_calendar.Culture) == _calendar.ValueAsDateTime?.GetMonth(_calendar.Culture); /// /// Gets the title of the month in the format [month] [year]. diff --git a/src/Core/Components/DateTime/FluentCalendarYear.cs b/src/Core/Components/DateTime/FluentCalendarYear.cs index da9fd68830..cd90433957 100644 --- a/src/Core/Components/DateTime/FluentCalendarYear.cs +++ b/src/Core/Components/DateTime/FluentCalendarYear.cs @@ -2,6 +2,7 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using Microsoft.FluentUI.AspNetCore.Components.Calendar; using Microsoft.FluentUI.AspNetCore.Components.Extensions; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -9,17 +10,18 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Computes the properties of a year in the calendar. /// -internal class FluentCalendarYear +/// The type of value handled by the calendar. +internal class FluentCalendarYear { - private readonly FluentCalendar _calendar; + private readonly FluentCalendar _calendar; private readonly bool _isInDisabledList; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// - internal FluentCalendarYear(FluentCalendar calendar, DateTime year) + internal FluentCalendarYear(FluentCalendar calendar, DateTime year) { _calendar = calendar; Year = year.GetDay(_calendar.Culture) == 1 && year.GetMonth(_calendar.Culture) == 1 ? year : year.StartOfYear(_calendar.Culture); @@ -30,7 +32,7 @@ internal FluentCalendarYear(FluentCalendar calendar, DateTime year) } else { - _isInDisabledList = calendar.DisabledDateFunc?.Invoke(Year) ?? false; + _isInDisabledList = calendar.DisabledDateFunc?.Invoke(Year.ConvertToTValue()) ?? false; } } @@ -53,7 +55,7 @@ internal FluentCalendarYear(FluentCalendar calendar, DateTime year) /// /// Whether the year is selected by the user /// - public bool IsSelected => Year.GetYear(_calendar.Culture) == _calendar.Value?.GetYear(_calendar.Culture); + public bool IsSelected => Year.GetYear(_calendar.Culture) == _calendar.ValueAsDateTime?.GetYear(_calendar.Culture); /// /// Gets the identifier of the year in the format yyyy. diff --git a/src/Core/Components/DateTime/FluentDatePicker.razor b/src/Core/Components/DateTime/FluentDatePicker.razor index 5d38349f51..32e32cb0c4 100644 --- a/src/Core/Components/DateTime/FluentDatePicker.razor +++ b/src/Core/Components/DateTime/FluentDatePicker.razor @@ -1,5 +1,6 @@ @namespace Microsoft.FluentUI.AspNetCore.Components -@inherits FluentCalendarBase +@inherits FluentCalendarBase +@typeparam TValue + PickerMonthChanged="@PickerMonthChangedHandlerAsync" /> } diff --git a/src/Core/Components/DateTime/FluentDatePicker.razor.cs b/src/Core/Components/DateTime/FluentDatePicker.razor.cs index 5a374eff22..dda1e20c0b 100644 --- a/src/Core/Components/DateTime/FluentDatePicker.razor.cs +++ b/src/Core/Components/DateTime/FluentDatePicker.razor.cs @@ -6,16 +6,18 @@ using System.Globalization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Represents a date picker control that enables users to select a date using a fluent user interface. /// -public partial class FluentDatePicker : FluentCalendarBase +/// The type of value handled by the date picker. Must be one of: DateTime?, DateTime, DateOnly, or DateOnly?. +public partial class FluentDatePicker : FluentCalendarBase { private bool _popupOpenedByKeyboard; - private FluentCalendar _calendar = default!; + private FluentCalendar _calendar = default!; private FluentIcon _icon = default!; /// @@ -34,7 +36,7 @@ public FluentDatePicker(LibraryConfiguration configuration) : base(configuration (Required ?? false) && !(Disabled ?? false) && !ReadOnly - && string.IsNullOrEmpty(CurrentValueAsString); + && CurrentValue.IsNullOrDefault(); }; } @@ -64,9 +66,9 @@ public FluentDatePicker(LibraryConfiguration configuration) : base(configuration /// [Parameter] public DatePickerRenderStyle RenderStyle { get; set; } = DatePickerRenderStyle.FluentUI; - + /// - /// Gets or sets the width of the component. + /// Gets or sets the width of the component. /// [Parameter] public string? Width { get; set; } @@ -87,22 +89,22 @@ public FluentDatePicker(LibraryConfiguration configuration) : base(configuration /// Gets or sets a value which will be set when double-clicking on the text field of date picker. /// [Parameter] - public DateTime? DoubleClickToDate { get; set; } + public TValue? DoubleClickToDate { get; set; } /// /// Gets or sets the template used to render each day in the calendar. /// /// Use this parameter to customize the appearance and content of individual days. The template - /// receives a parameter representing the day to render. + /// receives a parameter representing the day to render. /// [Parameter] - public RenderFragment? DaysTemplate { get; set; } + public RenderFragment>? DaysTemplate { get; set; } /// /// Gets or sets the callback that is invoked when the selected month in the picker changes. /// [Parameter] - public EventCallback PickerMonthChanged { get; set; } + public EventCallback PickerMonthChanged { get; set; } /// /// Gets or sets the callback that is invoked when the calendar is opened or closed. @@ -132,9 +134,9 @@ protected virtual async Task OnTextInputClickAsync(MouseEventArgs e) // Double click if (e.Detail >= 2 && !ReadOnly) { - if (DoubleClickToDate.HasValue) + if (DoubleClickToDate.IsNotNull()) { - await OnSelectedDateAsync(DoubleClickToDate.Value); + await OnSelectedDateAsync(DoubleClickToDate ?? default!); } if (OnDoubleClick.HasDelegate) @@ -157,15 +159,17 @@ protected virtual async Task OnIconKeydownAsync(KeyboardEventArgs e) } /// - protected async Task OnSelectedDateAsync(DateTime? value) + protected async Task OnSelectedDateAsync(TValue value) { - var updatedValue = value; + var dateTimeValue = value.ConvertToDateTime(); + var updatedValue = dateTimeValue; - if (Value is not null && value is not null) + if (CurrentValue.IsNotNull() && dateTimeValue is not null) { - updatedValue = Value?.TimeOfDay != TimeSpan.Zero - ? value?.Date + Value?.TimeOfDay - : value; + var currentDateTime = CurrentValue.ConvertToDateTime(); + updatedValue = currentDateTime?.TimeOfDay != TimeSpan.Zero + ? dateTimeValue?.Date + currentDateTime?.TimeOfDay + : dateTimeValue; } Opened = false; @@ -176,44 +180,45 @@ protected async Task OnSelectedDateAsync(DateTime? value) _popupOpenedByKeyboard = false; } - await OnSelectedDateHandlerAsync(updatedValue); + await OnSelectedDateHandlerAsync(updatedValue is null ? default : updatedValue.Value.ConvertToTValue()); } /// - protected override string? FormatValueAsString(DateTime? value) + protected override string? FormatValueAsString(TValue? value) { + var dateValue = value.ConvertToDateTime(); + // FluentUI style if (IsFluentUIStyle) { return View switch { - CalendarViews.Years => value?.ToString("yyyy", Culture), - CalendarViews.Months => value?.ToString(Culture.DateTimeFormat.YearMonthPattern, Culture), - _ => value?.ToString(Culture.DateTimeFormat.ShortDatePattern, Culture), + CalendarViews.Years => dateValue?.ToString("yyyy", Culture), + CalendarViews.Months => dateValue?.ToString(Culture.DateTimeFormat.YearMonthPattern, Culture), + _ => dateValue?.ToString(Culture.DateTimeFormat.ShortDatePattern, Culture), }; } // Native style return View switch { - CalendarViews.Years => value?.ToString("yyyy", CultureInfo.InvariantCulture), - CalendarViews.Months => value?.ToString("yyyy-MM", CultureInfo.InvariantCulture), - _ => value?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + CalendarViews.Years => dateValue?.ToString("yyyy", CultureInfo.InvariantCulture), + CalendarViews.Months => dateValue?.ToString("yyyy-MM", CultureInfo.InvariantCulture), + _ => dateValue?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), }; } /// - protected override bool TryParseValueFromString(string? value, out DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) { if (View == CalendarViews.Years && int.TryParse(value, Culture, out var year)) { - value = new DateTime(year, 1, 1).ToString(Culture.DateTimeFormat.ShortDatePattern, Culture); + result = new DateTime(year, 1, 1).ConvertToTValue(); + validationErrorMessage = null; + return true; } - BindConverter.TryConvertTo(value, Culture, out result); - - validationErrorMessage = null; - return true; + return base.TryParseValueFromString(value, out result, out validationErrorMessage); } /// @@ -252,4 +257,15 @@ private string GetPlaceholderAccordingToView() CalendarViews.Years => TextInputMode.Numeric, _ => null }; + + /// + private Task PickerMonthChangedHandlerAsync(TValue? month) + { + if (PickerMonthChanged.HasDelegate) + { + return PickerMonthChanged.InvokeAsync(month ?? default!); + } + + return Task.CompletedTask; + } } diff --git a/src/Core/Components/DateTime/RangeOfDates.cs b/src/Core/Components/DateTime/RangeOfDates.cs index e75d331437..d2881d36b2 100644 --- a/src/Core/Components/DateTime/RangeOfDates.cs +++ b/src/Core/Components/DateTime/RangeOfDates.cs @@ -10,7 +10,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Represents a range of dates with a defined start and end, providing functionality to retrieve all dates within the range. /// -public class RangeOfDates : RangeOf +internal class RangeOfDates : RangeOf { /// public RangeOfDates() : base() { } diff --git a/src/Core/Enums/CalendarDayFormat.cs b/src/Core/Enums/CalendarDayFormat.cs index 160aaf660e..a7da589a8a 100644 --- a/src/Core/Enums/CalendarDayFormat.cs +++ b/src/Core/Enums/CalendarDayFormat.cs @@ -7,7 +7,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Defines the format of days shown in a component. +/// Defines the format of days shown in a component. /// public enum CalendarDayFormat { diff --git a/src/Core/Enums/CalendarViews.cs b/src/Core/Enums/CalendarViews.cs index 0657052c0e..85d1012587 100644 --- a/src/Core/Enums/CalendarViews.cs +++ b/src/Core/Enums/CalendarViews.cs @@ -5,7 +5,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Defines the view display in a component. +/// Defines the view display in a component. /// public enum CalendarViews { diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index 634a2d8a4d..d5573dce9f 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -303,4 +303,7 @@ No data to show. + + The {0} field must be a date. + \ No newline at end of file diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs index d824249aa0..7b09fa3913 100644 --- a/tests/Core/Components/Base/ComponentBaseTests.cs +++ b/tests/Core/Components/Base/ComponentBaseTests.cs @@ -46,6 +46,8 @@ public class ComponentBaseTests : Bunit.TestContext { typeof(FluentDataGridCell<>), Loader.MakeGenericType(typeof(string)) .WithCascadingValue(new InternalGridContext(new FluentDataGrid(new LibraryConfiguration()))) .WithCascadingValue("OwningRow", new FluentDataGridRow(new LibraryConfiguration()) { InternalGridContext = new InternalGridContext(new FluentDataGrid(new LibraryConfiguration())) }) }, + { typeof(FluentCalendar<>), Loader.MakeGenericType(typeof(DateTime))}, + { typeof(FluentDatePicker<>), Loader.MakeGenericType(typeof(DateTime))}, }; /// diff --git a/tests/Core/Components/Base/InputBaseTests.cs b/tests/Core/Components/Base/InputBaseTests.cs index 8dced43fcd..8d774829e7 100644 --- a/tests/Core/Components/Base/InputBaseTests.cs +++ b/tests/Core/Components/Base/InputBaseTests.cs @@ -36,7 +36,8 @@ public class InputBaseTests : Bunit.TestContext { typeof(FluentCombobox<>), type => type.MakeGenericType(typeof(string)) }, { typeof(FluentSlider<>), type => type.MakeGenericType(typeof(int)) }, { typeof(FluentRadioGroup<>), type => type.MakeGenericType(typeof(string)) }, - //{ typeof(FluentRadio<>), type => type.MakeGenericType(typeof(string)) }, + { typeof(FluentCalendar<>), type => type.MakeGenericType(typeof(DateTime)) }, + { typeof(FluentDatePicker<>), type => type.MakeGenericType(typeof(DateTime)) }, }; /// diff --git a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-days.verified.razor.html b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-days.verified.razor.html index 3938cbbe7c..3b42dbb935 100644 --- a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-days.verified.razor.html +++ b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-days.verified.razor.html @@ -1,84 +1,84 @@ -
- -
-
-
January 1
-
- - +
+ +
+
+
February 1
+
+ + +
+
+
+
+
S
+
M
+
T
+
W
+
T
+
F
+
S
+
+
+
28
+
29
+
30
+
31
+
1
+
2
+
3
+
+
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
+
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
+
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
+
+
25
+
26
+
27
+
28
+
1
+
2
+
3
+
+
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
+
-
-
-
-
S
-
M
-
T
-
W
-
T
-
F
-
S
-
-
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
-
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
-
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
-
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
-
-
28
-
29
-
30
-
31
-
1
-
2
-
3
-
-
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
-
-
- \ No newline at end of file + diff --git a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-months.verified.razor.html b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-months.verified.razor.html index b4efba2b1e..7524bffbba 100644 --- a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-months.verified.razor.html +++ b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-months.verified.razor.html @@ -1,33 +1,33 @@ -
- -
-
-
1
-
- - +
+ +
+
+
1
+
+ + +
+
+
+
Jan
+
Feb
+
Mar
+
Apr
+
May
+
Jun
+
Jul
+
Aug
+
Sep
+
Oct
+
Nov
+
Dec
+
-
-
-
Jan
-
Feb
-
Mar
-
Apr
-
May
-
Jun
-
Jul
-
Aug
-
Sep
-
Oct
-
Nov
-
Dec
-
-
- \ No newline at end of file + diff --git a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-years.verified.razor.html b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-years.verified.razor.html index 16a6617297..5331c53da8 100644 --- a/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-years.verified.razor.html +++ b/tests/Core/Components/DateTimes/FluentCalendarTests.FluentCalendar_Minimum_View-years.verified.razor.html @@ -1,33 +1,33 @@ -
- -
-
-
1 - 12
-
- - +
+ +
+
+
1 - 12
+
+ + +
+
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
-
-
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
-
- \ No newline at end of file + diff --git a/tests/Core/Components/DateTimes/FluentCalendarTests.razor b/tests/Core/Components/DateTimes/FluentCalendarTests.razor index 17115316d7..850a4055c5 100644 --- a/tests/Core/Components/DateTimes/FluentCalendarTests.razor +++ b/tests/Core/Components/DateTimes/FluentCalendarTests.razor @@ -21,8 +21,8 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); // Assert calendar.Verify(); @@ -34,7 +34,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var cut = Render(@); + var cut = Render(@); var title = cut.Find(".title"); // Assert @@ -56,7 +56,7 @@ }; // Act - var calendar = Render(@); + var calendar = Render(@); var title = calendar.Find(".title"); // Assert @@ -69,7 +69,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); calendar.Verify(); } @@ -80,7 +80,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); calendar.Verify(); } @@ -91,7 +91,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); var day = calendar.Find("[value='2022-02-20']"); // Assert @@ -109,7 +109,7 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); var allDays = calendar.FindAll(".date"); // Assert @@ -130,11 +130,11 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); var monthName = calendar.Find(".label").InnerHtml; var juneFirst = System.DateTime.Parse("2022-06-01"); - var component = calendar.FindComponent(); + var component = calendar.FindComponent>(); // Assert Assert.Equal("June 2022", monthName); @@ -150,11 +150,11 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); var monthName = calendar.Find(".label").InnerHtml; var pickerMonthFirst = System.DateTime.Parse(expectedPickerMonthFirst); - var component = calendar.FindComponent(); + var component = calendar.FindComponent>(); // Assert Assert.Equal(expectedMonthName, monthName); @@ -169,7 +169,7 @@ value=""2022-02-20"">20
"); const string DAY = "2022-06-15"; // Act - var calendar = Render(@); + var calendar = Render(@); // Click to select 2022-06-24 var day = calendar.Find($"div[value='{DAY}']"); @@ -187,7 +187,7 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); // Click on Previous Month button var buttonMove = calendar.Find(".previous"); @@ -206,7 +206,7 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@ + var calendar = Render(@ ); // Click on Previous Month button @@ -224,7 +224,7 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); + var calendar = Render(@); // Click on Previous Month button var buttonMove = calendar.Find(".next"); @@ -243,7 +243,7 @@ value=""2022-02-20"">20
"); using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@ + var calendar = Render(@ ); // Click on Previous Month button @@ -263,7 +263,7 @@ value=""2022-02-20"">20
"); const string DAY = "2022-06-01"; // Act - var calendar = Render(@); + var calendar = Render(@); // Click to select 2022-06-24 var day = calendar.Find($"div[value='{DAY}']"); @@ -283,7 +283,7 @@ value=""2022-02-20"">20"); const string DAY = "2022-06-01"; // Act - var calendar = Render(@); + var calendar = Render(@); // Click to select 2022-06-01 var day = calendar.Find($"div[value='{DAY}']"); @@ -305,8 +305,8 @@ value=""2022-02-20"">20"); const string DAY = "2022-06-01"; // Act - var calendar = Render(@); + var calendar = Render(@); // Click to select 2022-06-01 var day = calendar.Find($"div[value='{DAY}']"); @@ -328,7 +328,7 @@ value=""2022-02-20"">20"); Action ChangedCallback = (e) => { value = e!.Value; }; // Arrange - var calendar = Render(@ + var calendar = Render(@ Pick a date ); @@ -367,8 +367,8 @@ value=""2022-02-20"">20"); }; // Arrange - var calendar = Render(@Pick a date); + var calendar = Render(@Pick a date); // Act var month = calendar.Find($"div[value='{clickOnValue}']"); @@ -388,10 +388,10 @@ value=""2022-02-20"">20"); { using var context = new DateTimeProviderContext(DateTime.Now); - DateTime? value = DateTime.MinValue; + DateTime? value = DateTime.MinValue.AddMonths(1); // Arrange & Act - var calendar = Render(@); + var calendar = Render(@); var previous = calendar.Find($".previous"); // Assert @@ -410,7 +410,7 @@ value=""2022-02-20"">20"); DateTime? value = DateTime.MaxValue; // Arrange & Act - var calendar = Render(@); + var calendar = Render(@); var previous = calendar.Find($".next"); // Assert @@ -429,7 +429,7 @@ value=""2022-02-20"">20"); DateTime? value = DateTime.Parse("2022-04-15"); // Arrange & Act - var calendar = Render(@); + var calendar = Render(@); var component = calendar.Find($"div"); var label = calendar.Find($".label"); @@ -451,7 +451,7 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo("en-us"); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act var nextButton = calendar.Find(".next"); @@ -475,7 +475,7 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo("en-us"); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act var nextButton = calendar.Find(".previous"); @@ -499,7 +499,7 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo("en-us"); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act var title = calendar.Find(".label"); @@ -518,7 +518,7 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo("en-us"); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act var title = calendar.Find(".label"); @@ -540,7 +540,7 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo("en-us"); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act var title = calendar.Find(".label"); @@ -561,7 +561,7 @@ value=""2022-02-20"">20"); string[] DAYS = ["2022-06-15", "2022-06-18", "2022-06-25"]; // Arrange - var calendar = Render(@); // Act: select 3 days @@ -586,8 +586,8 @@ value=""2022-02-20"">20"); string DAY = "2022-06-15"; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act: select 3 days using SelectDatesHover var day = calendar.Find($"div[value='{DAY}']"); @@ -611,7 +611,7 @@ value=""2022-02-20"">20"); string LAST_DAY = "2022-06-25"; // Arrange - var calendar = Render(@); // Act: select the first and last days @@ -634,8 +634,8 @@ value=""2022-02-20"">20"); var initialMonth = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act: Click next month button var nextButton = calendar.Find(".next"); @@ -652,8 +652,8 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act var task = component.Instance.FocusOutHandlerAsync(new Microsoft.AspNetCore.Components.Web.FocusEventArgs()); @@ -672,8 +672,8 @@ value=""2022-02-20"">20"); var endDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act var result = component.Instance.AllDaysAreDisabled(startDate, endDate); @@ -691,8 +691,8 @@ value=""2022-02-20"">20"); var endDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act var result = component.Instance.AllDaysAreDisabled(startDate, endDate); @@ -710,8 +710,8 @@ value=""2022-02-20"">20"); var endDate = new DateTime(2022, 6, 15); // Arrange: Only disable specific dates - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act var result = component.Instance.AllDaysAreDisabled(startDate, endDate); @@ -726,11 +726,11 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange & Act - var calendarWithToday = Render(@); - var calendarWithoutToday = Render(@); + var calendarWithToday = Render(@); + var calendarWithoutToday = Render(@); - var componentWithToday = calendarWithToday.FindComponent(); - var componentWithoutToday = calendarWithoutToday.FindComponent(); + var componentWithToday = calendarWithToday.FindComponent>(); + var componentWithoutToday = calendarWithoutToday.FindComponent>(); // Assert Assert.True(componentWithToday.Instance.DisplayToday); @@ -743,13 +743,13 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange & Act - var calendarWithAnimation = Render(@); - var calendarWithoutAnimation = Render(@); - var calendarDefaultAnimation = Render(@); + var calendarWithAnimation = Render(@); + var calendarWithoutAnimation = Render(@); + var calendarDefaultAnimation = Render(@); - var componentWithAnimation = calendarWithAnimation.FindComponent(); - var componentWithoutAnimation = calendarWithoutAnimation.FindComponent(); - var componentDefaultAnimation = calendarDefaultAnimation.FindComponent(); + var componentWithAnimation = calendarWithAnimation.FindComponent>(); + var componentWithoutAnimation = calendarWithoutAnimation.FindComponent>(); + var componentDefaultAnimation = calendarDefaultAnimation.FindComponent>(); // Assert Assert.True(componentWithAnimation.Instance.AnimatePeriodChanges); @@ -767,13 +767,13 @@ value=""2022-02-20"">20"); string UNSELECT_DAY = "2022-06-17"; // Inside the range // Arrange - var calendar = Render(@); // Act: select the same day twice to reset var startDay = calendar.Find($"div[value='{START_DAY}']"); startDay?.Click(); // First click to start the range - + var endDay = calendar.Find($"div[value='{END_DAY}']"); endDay?.Click(); // Second click to end the range @@ -792,7 +792,7 @@ value=""2022-02-20"">20"); string DAY = "2022-06-15"; // Arrange - var calendar = Render(@); // Act: select a day, then unselect it @@ -812,8 +812,8 @@ value=""2022-02-20"">20"); string DAY = "2022-06-15"; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act: select a day using SelectDatesHover var day = calendar.Find($"div[value='{DAY}']"); @@ -835,8 +835,8 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Assert Assert.True(component.Instance.IsReadOnlyOrDisabled); @@ -848,8 +848,8 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Assert Assert.True(component.Instance.IsReadOnlyOrDisabled); @@ -861,8 +861,8 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Assert Assert.False(component.Instance.IsReadOnlyOrDisabled); @@ -876,8 +876,8 @@ value=""2022-02-20"">20"); var testDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act var calendarExtended = component.Instance.CalendarExtended; @@ -903,9 +903,9 @@ value=""2022-02-20"">20"); }; // Arrange - var calendar = Render(@); + var calendar = Render(@); var formInput = calendar.Find("input[type='datetime']"); @@ -919,11 +919,11 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); var testDate = new DateTime(2022, 6, 15); - RenderFragment CustomDayTemplate = (day) => @
@day.DayNumber
; + RenderFragment> CustomDayTemplate = (day) => @
@day.DayNumber
; // Arrange - var calendar = Render(@); + var calendar = Render(@); var customElement = calendar.Find(".custom-day"); @@ -935,10 +935,10 @@ value=""2022-02-20"">20"); public void FluentCalendar_ArrowUpAndDown_Constants() { // Assert - Assert.Contains("svg", FluentCalendar.ArrowUp); - Assert.Contains("svg", FluentCalendar.ArrowDown); - Assert.Contains("path", FluentCalendar.ArrowUp); - Assert.Contains("path", FluentCalendar.ArrowDown); + Assert.Contains("svg", FluentCalendar.ArrowUp); + Assert.Contains("svg", FluentCalendar.ArrowDown); + Assert.Contains("path", FluentCalendar.ArrowUp); + Assert.Contains("path", FluentCalendar.ArrowDown); } #region OnSelectedDateHandlerAsync Behavior Tests @@ -952,10 +952,10 @@ value=""2022-02-20"">20"); DateTime? actualValue = initialValue; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act - Try to click on a different date var targetDay = calendar.Find($"div[value='2022-06-20']"); @@ -974,10 +974,10 @@ value=""2022-02-20"">20"); DateTime? actualValue = initialValue; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act - Try to click on a different date var targetDay = calendar.Find($"div[value='2022-06-20']"); @@ -999,10 +999,10 @@ value=""2022-02-20"">20"); // Arrange var calendar = Render( @ - - + + ); // Act - Click on the same already selected date @@ -1026,10 +1026,10 @@ value=""2022-02-20"">20"); // Arrange var calendar = Render( @ - - + + ); // Act - Click on the same already selected date @@ -1051,9 +1051,9 @@ value=""2022-02-20"">20"); var callbackTriggered = false; // Arrange - CheckIfSelectedValueHasChanged defaults to true when null - var calendar = Render(@); + var calendar = Render(@); // Act - Click on the same already selected date var sameDay = calendar.Find($"div[value='{selectedValue:yyyy-MM-dd}']"); @@ -1075,9 +1075,9 @@ value=""2022-02-20"">20"); var callbackTriggered = false; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act - Click on a different date var targetDay = calendar.Find($"div[value='{targetValue:yyyy-MM-dd}']"); @@ -1098,11 +1098,11 @@ value=""2022-02-20"">20"); var callbackTriggered = false; // Arrange - Both ReadOnly and Disabled are true - var calendar = Render(@); + var calendar = Render(@); // Act - Try to click on a different date var targetDay = calendar.Find($"div[value='2022-06-20']"); @@ -1126,10 +1126,10 @@ value=""2022-02-20"">20"); // Arrange var calendar = Render( @ - - + + ); // Act - Click on a different date @@ -1158,9 +1158,9 @@ value=""2022-02-20"">20"); } // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act - Click on a different date var targetDay = calendar.Find($"div[value='{targetValue:yyyy-MM-dd}']"); @@ -1181,9 +1181,9 @@ value=""2022-02-20"">20"); DateTime? actualValue = initialValue; // Arrange - var calendar = Render(@); + var calendar = Render(@); // Act - Click on a different date var targetDay = calendar.Find($"div[value='{targetValue:yyyy-MM-dd}']"); @@ -1215,16 +1215,16 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Convert string to enum value var animationRunning = animationRunningString switch { - "None" => FluentCalendar.AnimationRunning.None, - "Up" => FluentCalendar.AnimationRunning.Up, - "Down" => FluentCalendar.AnimationRunning.Down, - _ => FluentCalendar.AnimationRunning.None + "None" => FluentCalendar.AnimationRunning.None, + "Up" => FluentCalendar.AnimationRunning.Up, + "Down" => FluentCalendar.AnimationRunning.Down, + _ => FluentCalendar.AnimationRunning.None }; // Set the private _animationRunning field using reflection @@ -1253,12 +1253,12 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Set animation running to Up var animationRunningField = component.Instance.GetType().GetField("_animationRunning", BindingFlags.NonPublic | BindingFlags.Instance); - animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.Up); + animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.Up); // Act var result = component.Instance.GetAnimationClass("test-class"); @@ -1284,23 +1284,23 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - Use Months view which can be animated by default - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); var animationRunningField = component.Instance.GetType().GetField("_animationRunning", BindingFlags.NonPublic | BindingFlags.Instance); - + // Set initial animation state to Down - animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.Down); + animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.Down); // Act - Use InvokeAsync to handle the threading issue await component.InvokeAsync(async () => { - await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); + await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); }); // Assert var finalAnimationState = animationRunningField?.GetValue(component.Instance); - Assert.Equal(FluentCalendar.AnimationRunning.Up, finalAnimationState); // Should be AnimationRunning.Up + Assert.Equal(FluentCalendar.AnimationRunning.Up, finalAnimationState); // Should be AnimationRunning.Up } [Fact] @@ -1309,20 +1309,20 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - Use Days view which cannot be animated by default - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); var animationRunningField = component.Instance.GetType().GetField("_animationRunning", BindingFlags.NonPublic | BindingFlags.Instance); - + // Set initial animation state to None - animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.None); + animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.None); // Act - await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); + await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); // Assert var finalAnimationState = animationRunningField?.GetValue(component.Instance); - Assert.Equal(FluentCalendar.AnimationRunning.None, finalAnimationState); // Should remain AnimationRunning.None + Assert.Equal(FluentCalendar.AnimationRunning.None, finalAnimationState); // Should remain AnimationRunning.None } [Theory] @@ -1333,20 +1333,20 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - Use explicit animation setting to true - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); var animationRunningField = component.Instance.GetType().GetField("_animationRunning", BindingFlags.NonPublic | BindingFlags.Instance); // Act - Use InvokeAsync to handle the threading issue await component.InvokeAsync(async () => { - await component.Instance.StartNewAnimationAsync((FluentCalendar.AnimationRunning)animationValue); + await component.Instance.StartNewAnimationAsync((FluentCalendar.AnimationRunning)animationValue); }); // Assert var finalAnimationState = animationRunningField?.GetValue(component.Instance); - Assert.Equal((FluentCalendar.AnimationRunning)animationValue, finalAnimationState); + Assert.Equal((FluentCalendar.AnimationRunning)animationValue, finalAnimationState); } [Fact] @@ -1355,23 +1355,23 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); var animationRunningField = component.Instance.GetType().GetField("_animationRunning", BindingFlags.NonPublic | BindingFlags.Instance); - + // Set initial state - animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.None); + animationRunningField?.SetValue(component.Instance, FluentCalendar.AnimationRunning.None); // Act - Use InvokeAsync to handle the threading issue await component.InvokeAsync(async () => { - await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); + await component.Instance.StartNewAnimationAsync(FluentCalendar.AnimationRunning.Up); }); // Assert var finalAnimationState = animationRunningField?.GetValue(component.Instance); - Assert.Equal(FluentCalendar.AnimationRunning.None, finalAnimationState); // Should remain None since animation is disabled + Assert.Equal(FluentCalendar.AnimationRunning.None, finalAnimationState); // Should remain None since animation is disabled } #endregion @@ -1386,15 +1386,15 @@ value=""2022-02-20"">20"); var testMonth = new DateTime(2022, 8, 15); // Arrange - Start with Months view - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Get the private _pickerView field var pickerViewField = component.Instance.GetType().GetField("_pickerView", BindingFlags.NonPublic | BindingFlags.Instance); - + // Set the initial view to Months pickerViewField?.SetValue(component.Instance, CalendarViews.Months); - + // Verify initial state is Months Assert.Equal(CalendarViews.Months, pickerViewField?.GetValue(component.Instance)); @@ -1406,25 +1406,6 @@ value=""2022-02-20"">20"); Assert.Equal(CalendarViews.Days, pickerViewField?.GetValue(component.Instance)); // Should change to Days view } - [Fact] - public async Task PickerMonthSelectAsync_WithNullMonth_UsesTodayAndUpdatesView() - { - using var context = new DateTimeProviderContext(new DateTime(2023, 3, 10)); - - // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); - - var pickerViewField = component.Instance.GetType().GetField("_pickerView", BindingFlags.NonPublic | BindingFlags.Instance); - - // Act - await component.Instance.PickerMonthSelectAsync(null); - - // Assert - Assert.Equal(new DateTime(2023, 3, 1), component.Instance.PickerMonth); // Should use DateTimeProvider.Today - Assert.Equal(CalendarViews.Days, pickerViewField?.GetValue(component.Instance)); - } - [Fact] public async Task PickerMonthSelectAsync_WithDifferentCultures_RespectsCalendar() { @@ -1434,8 +1415,8 @@ value=""2022-02-20"">20"); var persianCulture = CultureInfo.GetCultureInfo("fa-IR"); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.PickerMonthSelectAsync(testMonth); @@ -1454,9 +1435,9 @@ value=""2022-02-20"">20"); DateTime? changedMonth = null; // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.PickerMonthSelectAsync(testMonth); @@ -1477,8 +1458,8 @@ value=""2022-02-20"">20"); var testDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(testDate, dayDisabled: true); @@ -1495,8 +1476,8 @@ value=""2022-02-20"">20"); var testDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(testDate, dayDisabled: false); @@ -1514,13 +1495,13 @@ value=""2022-02-20"">20"); var hoverDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Set up the range selector start date using reflection var rangeSelectorField = component.Instance.GetType().GetField("_rangeSelector", BindingFlags.NonPublic | BindingFlags.Instance); var rangeSelector = rangeSelectorField?.GetValue(component.Instance); - + if (rangeSelector != null) { var startProperty = rangeSelector.GetType().GetProperty("Start"); @@ -1533,12 +1514,12 @@ value=""2022-02-20"">20"); // Assert var rangeSelectorMouseOverField = component.Instance.GetType().GetField("_rangeSelectorMouseOver", BindingFlags.NonPublic | BindingFlags.Instance); var rangeSelectorMouseOver = rangeSelectorMouseOverField?.GetValue(component.Instance); - + if (rangeSelectorMouseOver != null) { var startProperty = rangeSelectorMouseOver.GetType().GetProperty("Start"); var endProperty = rangeSelectorMouseOver.GetType().GetProperty("End"); - + Assert.Equal(startDate, startProperty?.GetValue(rangeSelectorMouseOver)); Assert.Equal(hoverDate, endProperty?.GetValue(rangeSelectorMouseOver)); } @@ -1559,9 +1540,9 @@ value=""2022-02-20"">20"); }; // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(hoverDate, dayDisabled: false); @@ -1569,12 +1550,12 @@ value=""2022-02-20"">20"); // Assert var rangeSelectorMouseOverField = component.Instance.GetType().GetField("_rangeSelectorMouseOver", BindingFlags.NonPublic | BindingFlags.Instance); var rangeSelectorMouseOver = rangeSelectorMouseOverField?.GetValue(component.Instance); - + if (rangeSelectorMouseOver != null) { var startProperty = rangeSelectorMouseOver.GetType().GetProperty("Start"); var endProperty = rangeSelectorMouseOver.GetType().GetProperty("End"); - + Assert.Equal(hoverDate, startProperty?.GetValue(rangeSelectorMouseOver)); // Min of the range Assert.Equal(hoverDate.AddDays(2), endProperty?.GetValue(rangeSelectorMouseOver)); // Max of the range } @@ -1588,8 +1569,8 @@ value=""2022-02-20"">20"); var testDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(testDate, dayDisabled: false); @@ -1612,9 +1593,9 @@ value=""2022-02-20"">20"); }; // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(hoverDate, dayDisabled: false); @@ -1622,7 +1603,7 @@ value=""2022-02-20"">20"); // Assert var selectedDatesMouseOverField = component.Instance.GetType().GetField("_selectedDatesMouseOver", BindingFlags.NonPublic | BindingFlags.Instance); var selectedDatesMouseOver = selectedDatesMouseOverField?.GetValue(component.Instance) as List; - + Assert.NotNull(selectedDatesMouseOver); Assert.Equal(7, selectedDatesMouseOver.Count); Assert.Equal(hoverDate, selectedDatesMouseOver.First()); @@ -1646,10 +1627,10 @@ value=""2022-02-20"">20"); Func disabledDateFunc = (date) => date.Day % 2 == 0; // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Act await component.Instance.OnSelectDayMouseOverAsync(hoverDate, dayDisabled: false); @@ -1657,16 +1638,16 @@ value=""2022-02-20"">20"); // Assert var selectedDatesMouseOverField = component.Instance.GetType().GetField("_selectedDatesMouseOver", BindingFlags.NonPublic | BindingFlags.Instance); var selectedDatesMouseOver = selectedDatesMouseOverField?.GetValue(component.Instance) as List; - + Assert.NotNull(selectedDatesMouseOver); - + // Should only include odd days (15, 17, 19) and exclude even days (16, 18) - var expectedDates = new[] { + var expectedDates = new[] { new DateTime(2022, 6, 15), // 15 (odd) - new DateTime(2022, 6, 17), // 17 (odd) + new DateTime(2022, 6, 17), // 17 (odd) new DateTime(2022, 6, 19) // 19 (odd) }; - + Assert.Equal(expectedDates.Length, selectedDatesMouseOver.Count); foreach (var expectedDate in expectedDates) { @@ -1682,13 +1663,13 @@ value=""2022-02-20"">20"); var testDate = new DateTime(2022, 6, 15); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Set up the range selector to have same start and end (IsSingle condition) var rangeSelectorField = component.Instance.GetType().GetField("_rangeSelector", BindingFlags.NonPublic | BindingFlags.Instance); var rangeSelector = rangeSelectorField?.GetValue(component.Instance); - + if (rangeSelector != null) { var startProperty = rangeSelector.GetType().GetProperty("Start"); @@ -1712,9 +1693,9 @@ value=""2022-02-20"">20"); [InlineData("2022-06-15", true, "2022-06-15")] [InlineData("2022-12-31", true, "2022-12-31")] [InlineData("2000-01-01", true, "2000-01-01")] - [InlineData("invalid-date", true, null)] - [InlineData("", true, null)] - [InlineData(null, true, null)] + [InlineData("invalid-date", false, null)] + [InlineData("", false, null)] + [InlineData(null, false, null)] [InlineData("2022/06/15", true, "2022-06-15")] // Different format but should parse [InlineData("June 15, 2022", true, "2022-06-15")] // Long format should parse public void TryParseValueFromString_ReturnsExpectedResults(string? input, bool expectedSuccess, string? expectedDateString) @@ -1722,11 +1703,11 @@ value=""2022-02-20"">20"); using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Use reflection to access the protected method - var method = component.Instance.GetType().GetMethod("TryParseValueFromString", + var method = component.Instance.GetType().GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); var parameters = new object?[] { input, null, null }; @@ -1738,19 +1719,19 @@ value=""2022-02-20"">20"); // Assert Assert.Equal(expectedSuccess, result); - - if (expectedDateString != null) + + if (expectedSuccess && expectedDateString != null) { Assert.NotNull(parsedValue); Assert.Equal(DateTime.Parse(expectedDateString), parsedValue.Value); + Assert.Null(validationErrorMessage); } - else + else if (!expectedSuccess) { - // For invalid inputs, the method still returns true but result might be null or unexpected - // The actual behavior depends on BindConverter.TryConvertTo implementation + Assert.Equal(default(DateTime), parsedValue); + Assert.NotNull(validationErrorMessage); + Assert.Contains("must be a date", validationErrorMessage); } - - Assert.Null(validationErrorMessage); // Method always sets this to null } [Theory] @@ -1765,11 +1746,11 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo(cultureName); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Use reflection to access the protected method - var method = component.Instance.GetType().GetMethod("TryParseValueFromString", + var method = component.Instance.GetType().GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); var parameters = new object?[] { input, null, null }; @@ -1781,69 +1762,90 @@ value=""2022-02-20"">20"); // Assert Assert.True(result); - + if (parsedValue.HasValue) { var expectedDate = DateTime.Parse(expectedDateString); Assert.Equal(expectedDate, parsedValue.Value); } - + Assert.Null(validationErrorMessage); } [Fact] - public void TryParseValueFromString_AlwaysReturnsTrue() + public void TryParseValueFromString_HandlesInvalidInputsCorrectly() { using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Use reflection to access the protected method - var method = component.Instance.GetType().GetMethod("TryParseValueFromString", + var method = component.Instance.GetType().GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); - var testInputs = new string?[] { "invalid", "completely-wrong", "2022-13-45", "", null, "999999999999" }; + var invalidInputs = new string?[] { "invalid", "completely-wrong", "2022-13-45", "", null, "999999999999" }; + var validInputs = new string[] { "2022-06-15", "2022/06/15", "June 15, 2022" }; - foreach (var input in testInputs) + // Test invalid inputs + foreach (var input in invalidInputs) { var parameters = new object?[] { input, null, null }; // Act var result = (bool)(method?.Invoke(component.Instance, parameters) ?? false); + var validationErrorMessage = (string?)parameters[2]; // Assert - Assert.True(result); // Method always returns true according to implementation + Assert.False(result); + Assert.NotNull(validationErrorMessage); + } + + // Test valid inputs + foreach (var input in validInputs) + { + var parameters = new object?[] { input, null, null }; + + // Act + var result = (bool)(method?.Invoke(component.Instance, parameters) ?? false); + var validationErrorMessage = (string?)parameters[2]; + + // Assert + Assert.True(result); + Assert.Null(validationErrorMessage); } } [Fact] - public void TryParseValueFromString_AlwaysSetsValidationErrorMessageToNull() + public void TryParseValueFromString_SetsValidationErrorMessageCorrectly() { using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Use reflection to access the protected method - var method = component.Instance.GetType().GetMethod("TryParseValueFromString", + var method = component.Instance.GetType().GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); - var testInputs = new string?[] { "2022-06-15", "invalid", "", null }; + // Test valid input - should not set validation error message + var validParameters = new object?[] { "2022-06-15", null, "initial-error" }; + var validResult = (bool)(method?.Invoke(component.Instance, validParameters) ?? false); + var validValidationMessage = (string?)validParameters[2]; - foreach (var input in testInputs) - { - var parameters = new object?[] { input, null, "initial-error" }; + Assert.True(validResult); + Assert.Null(validValidationMessage); - // Act - var result = (bool)(method?.Invoke(component.Instance, parameters) ?? false); - var validationErrorMessage = (string?)parameters[2]; + // Test invalid input - should set validation error message + var invalidParameters = new object?[] { "invalid-date", null, "initial-error" }; + var invalidResult = (bool)(method?.Invoke(component.Instance, invalidParameters) ?? false); + var invalidValidationMessage = (string?)invalidParameters[2]; - // Assert - Assert.Null(validationErrorMessage); // Always set to null in the method - } + Assert.False(invalidResult); + Assert.NotNull(invalidValidationMessage); + Assert.Contains("must be a date", invalidValidationMessage); } [Theory] @@ -1857,11 +1859,11 @@ value=""2022-02-20"">20"); var culture = CultureInfo.GetCultureInfo(cultureName); // Arrange - var calendar = Render(@); - var component = calendar.FindComponent(); + var calendar = Render(@); + var component = calendar.FindComponent>(); // Use reflection to access the protected method - var method = component.Instance.GetType().GetMethod("TryParseValueFromString", + var method = component.Instance.GetType().GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); var parameters = new object?[] { "2022-06-15", null, null }; // Standard ISO format @@ -1871,10 +1873,47 @@ value=""2022-02-20"">20"); var parsedValue = (DateTime?)parameters[1]; var validationErrorMessage = (string?)parameters[2]; - // Assert - Assert.True(result); - Assert.Null(validationErrorMessage); - // The actual parsing result depends on BindConverter.TryConvertTo behavior with different cultures + // Assert - ISO format should parse successfully in most cultures + if (result) + { + Assert.Null(validationErrorMessage); + Assert.NotNull(parsedValue); + } + else + { + Assert.NotNull(validationErrorMessage); + Assert.Contains("must be a date", validationErrorMessage); + } + } + + #endregion + + #region FluentCalendarBase Constructor Validation Tests + + /// + /// Test classes that inherit from FluentCalendarBase to test constructor validation + /// + private class TestFluentCalendarBaseString : FluentCalendarBase + { + public TestFluentCalendarBaseString(LibraryConfiguration configuration) : base(configuration) + { + } + } + + [Fact] + public void FluentCalendarBase_Constructor_WithInvalidStringType_ThrowsInvalidOperationException() + { + // Arrange + var libraryConfiguration = Services.GetRequiredService(); + + // Act & Assert + var exception = Assert.Throws(() => + { + new TestFluentCalendarBaseString(libraryConfiguration); + }); + + Assert.Contains("The type parameter System.String is not supported", exception.Message); + Assert.Contains("Supported types are DateTime, DateTime?, DateOnly, and DateOnly?", exception.Message); } #endregion diff --git a/tests/Core/Components/DateTimes/FluentDatePickerTests.razor b/tests/Core/Components/DateTimes/FluentDatePickerTests.razor index 0d3f916672..972cb09bd5 100644 --- a/tests/Core/Components/DateTimes/FluentDatePickerTests.razor +++ b/tests/Core/Components/DateTimes/FluentDatePickerTests.razor @@ -24,7 +24,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var calendar = Render(@); // Assert @@ -38,21 +38,22 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Click text.Click(); // Assert - var calendar = picker.FindComponent(); + var calendar = picker.FindComponent>(); - Assert.True(picker.FindComponent().Instance.Opened); - Assert.Null(calendar.Instance.Value); + Assert.True(picker.FindComponent>().Instance.Opened); + // The calendar's Value can be null even if TValue is DateTime - that's the key difference + // FluentDatePicker can be nullable but the internal calendar might not be - Assert.Equal(System.DateTime.Today.Year, calendar.Instance.PickerMonth.Year); - Assert.Equal(System.DateTime.Today.Month, calendar.Instance.PickerMonth.Month); - Assert.Equal(1, calendar.Instance.PickerMonth.Day); + Assert.Equal(System.DateTime.Today.Year, calendar.Instance.PickerMonth?.Year); + Assert.Equal(System.DateTime.Today.Month, calendar.Instance.PickerMonth?.Month); + Assert.Equal(1, calendar.Instance.PickerMonth?.Day); } [Fact] @@ -63,14 +64,14 @@ using var context = new DateTimeProviderContext(today); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Click text.Click(); // Assert - var calendar = picker.FindComponent(); + var calendar = picker.FindComponent>(); Assert.Equal(today, calendar.Instance.Value); } @@ -86,7 +87,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); // Open the calendar if (openedByKeyboard) @@ -105,7 +106,7 @@ day.Click(); // Assert - var calendar = picker.FindComponent(); + var calendar = picker.FindComponent>(); Assert.Equal(DateTime.Parse(expectedDate, InvariantCulture), calendar.Instance.Value); } @@ -116,14 +117,14 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Set a new date value text.Change("3/12/2022"); // Assert - Assert.Equal(System.DateTime.Parse("2022-03-12"), picker.FindComponent().Instance.Value); + Assert.Equal(System.DateTime.Parse("2022-03-12"), picker.FindComponent>().Instance.Value); } [Fact] @@ -133,14 +134,14 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Set a new date value text.Change("3/32/2022"); // Assert - Assert.Null(picker.FindComponent().Instance.Value); + Assert.Null(picker.FindComponent>().Instance.Value); } [Fact] @@ -150,7 +151,7 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Assert @@ -164,7 +165,7 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Assert @@ -178,7 +179,7 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); var text = picker.Find("fluent-text-input"); @@ -196,14 +197,14 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Set a new date value text.Change("2022-03-12"); // Assert - Assert.Equal(System.DateTime.Parse("2022-03-12"), picker.FindComponent().Instance.Value); + Assert.Equal(System.DateTime.Parse("2022-03-12"), picker.FindComponent>().Instance.Value); } [Theory] @@ -216,12 +217,12 @@ var date = string.IsNullOrEmpty(plainDateTime) ? (DateTime?)null : System.DateTime.Parse(plainDateTime); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Double-Click text.Click(detail: 2); - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.False(pickerInstance.Opened); @@ -245,7 +246,7 @@ // Act var actual = string.Empty; - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Double-Click @@ -262,8 +263,8 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.NotNull(pickerInstance.Icon); @@ -278,8 +279,8 @@ var customIcon = new CoreIcons.Regular.Size20.Add(); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.Equal(customIcon, pickerInstance.Icon); @@ -296,8 +297,8 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.Equal(appearance, pickerInstance.Appearance); @@ -312,8 +313,8 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.Equal(renderingStyle, pickerInstance.RenderStyle); @@ -327,8 +328,8 @@ var expectedWidth = "300px"; // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert Assert.Equal(expectedWidth, pickerInstance.Width); @@ -342,7 +343,7 @@ var expectedPlaceholder = "Custom placeholder"; // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Assert @@ -354,11 +355,11 @@ { // Arrange using var context = new DateTimeProviderContext(System.DateTime.Now); - RenderFragment customTemplate = day => + RenderFragment> customTemplate = day => @
@day.DayNumber
; // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); // Click to open calendar @@ -378,9 +379,9 @@ DateTime changedMonth = default; // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; // Simulate the event by checking if the delegate exists if (pickerInstance.PickerMonthChanged.HasDelegate) @@ -404,7 +405,7 @@ var isOpenedState = false; // Act - var picker = Render(@); var text = picker.Find("fluent-text-input"); @@ -426,8 +427,8 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; Assert.False(pickerInstance.Opened); // Initially closed @@ -447,15 +448,15 @@ var newDate = new DateTime(2022, 3, 20); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Assert that the component has the expected initial value Assert.Equal(originalDate, pickerInstance.Value); // Test that the component preserves time when date changes (this tests the logic) // The actual time preservation is handled internally by OnSelectedDateAsync method - Assert.Equal(new TimeSpan(14, 30, 0), pickerInstance.Value?.TimeOfDay); + Assert.Equal(new TimeSpan(14, 30, 0), pickerInstance.Value.TimeOfDay); } [Theory] @@ -469,9 +470,9 @@ var testDate = new DateTime(2022, 3, 15); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; var textInput = picker.Find("fluent-text-input"); // Assert - Check that the input field has the correct value format @@ -504,9 +505,9 @@ var testDate = new DateTime(2022, 3, 15); // Act - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; var textInput = picker.Find("fluent-text-input"); // Assert - Check that the input field has the correct value format for native style @@ -534,7 +535,7 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); // Assert var icons = picker.FindAll("fluent-icon"); @@ -551,14 +552,14 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); // Assert - Check for FluentUI specific elements var textInput = picker.Find("fluent-text-input"); Assert.NotNull(textInput); // Check that the component is in FluentUI style - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; Assert.Equal(DatePickerRenderStyle.FluentUI, pickerInstance.RenderStyle); // In FluentUI style, the component should have the fluent-text-input element @@ -573,9 +574,9 @@ using var context = new DateTimeProviderContext(System.DateTime.Now); // Act - var picker = Render(@); + var picker = Render(@); var text = picker.Find("fluent-text-input"); - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; // Initially closed Assert.False(pickerInstance.Opened); @@ -594,16 +595,16 @@ [InlineData(DatePickerRenderStyle.Native, (CalendarViews)999, null, null)] [InlineData(DatePickerRenderStyle.FluentUI, CalendarViews.Days, null, null)] [InlineData(DatePickerRenderStyle.FluentUI, CalendarViews.Months, null, null)] - [InlineData(DatePickerRenderStyle.FluentUI, CalendarViews.Years, null, null)] - [InlineData(DatePickerRenderStyle.FluentUI, (CalendarViews)999, null, null)] + [InlineData(DatePickerRenderStyle.FluentUI, CalendarViews.Years, null, null)] + [InlineData(DatePickerRenderStyle.FluentUI, (CalendarViews)999, null, null)] public void FluentDatePicker_GetInputType(DatePickerRenderStyle renderingStyle, CalendarViews view, string? expectedInputType, TextInputMode? expectedInputMode) { // Arrange using var context = new DateTimeProviderContext(System.DateTime.Now); - var picker = Render(@); + var picker = Render(@); // Act - var pickerInstance = picker.FindComponent().Instance; + var pickerInstance = picker.FindComponent>().Instance; var inputType = pickerInstance.GetInputType(); var inputMode = pickerInstance.GetInputMode(); @@ -618,19 +619,19 @@ [InlineData("1999", true, "1/1/1999")] [InlineData("1", true, "1/1/0001")] [InlineData("9999", true, "1/1/9999")] - [InlineData("invalid", true, null)] - [InlineData("", true, null)] - [InlineData(null, true, null)] - [InlineData("2023.5", true, "5/1/2023")] // int.TryParse will parse this as 2023, then new DateTime(2023, 1, 1) gets converted to string and back + [InlineData("invalid", false, null)] + [InlineData("", false, null)] + [InlineData(null, false, null)] + [InlineData("2023.5", true, "5/1/2023")] // Falls back to DateTime.TryParse which parses 2023.5 as May 1, 2023 public void FluentDatePicker_TryParseValueFromString_YearsView(string? input, bool expectedResult, string? expectedDateString) { // Arrange using var context = new DateTimeProviderContext(System.DateTime.Now); - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Use reflection to access the protected TryParseValueFromString method - var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", + var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(method); @@ -642,19 +643,19 @@ // Assert Assert.Equal(expectedResult, result); - - if (expectedDateString != null) + + if (expectedResult && expectedDateString != null) { var expectedDate = DateTime.Parse(expectedDateString, InvariantCulture); Assert.Equal(expectedDate, parsedValue); + Assert.Null(validationErrorMessage); } - else + else if (!expectedResult) { - Assert.Null(parsedValue); + Assert.Equal(default(DateTime), parsedValue); + Assert.NotNull(validationErrorMessage); + Assert.Contains("must be a date", validationErrorMessage); } - - // TryParseValueFromString always returns true and sets validationErrorMessage to null - Assert.Null(validationErrorMessage); } [Theory] @@ -665,17 +666,17 @@ { // Arrange using var context = new DateTimeProviderContext(System.DateTime.Now); - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Use reflection to access the protected TryParseValueFromString method - var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", + var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(method); // Act & Assert var parameters = new object?[] { input, null, null }; - + // These invalid years should throw TargetInvocationException (containing ArgumentOutOfRangeException) when using reflection var ex = Assert.Throws(() => method.Invoke(pickerInstance, parameters)); Assert.IsType(ex.InnerException); @@ -683,18 +684,18 @@ [Theory] [InlineData("3/15/2022", true, "3/15/2022")] - [InlineData("invalid date", true, null)] - [InlineData("", true, null)] - [InlineData(null, true, null)] + [InlineData("invalid date", false, null)] + [InlineData("", false, null)] + [InlineData(null, false, null)] public void FluentDatePicker_TryParseValueFromString_DaysView(string? input, bool expectedResult, string? expectedDateString) { // Arrange using var context = new DateTimeProviderContext(System.DateTime.Now); - var picker = Render(@); - var pickerInstance = picker.FindComponent().Instance; + var picker = Render(@); + var pickerInstance = picker.FindComponent>().Instance; // Use reflection to access the protected TryParseValueFromString method - var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", + var method = typeof(FluentDatePicker).GetMethod("TryParseValueFromString", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(method); @@ -706,18 +707,18 @@ // Assert Assert.Equal(expectedResult, result); - - if (expectedDateString != null) + + if (expectedResult && expectedDateString != null) { var expectedDate = DateTime.Parse(expectedDateString, InvariantCulture); Assert.Equal(expectedDate, parsedValue); + Assert.Null(validationErrorMessage); } - else + else if (!expectedResult) { - Assert.Null(parsedValue); + Assert.Equal(default(DateTime), parsedValue); + Assert.NotNull(validationErrorMessage); + Assert.Contains("must be a date", validationErrorMessage); } - - // TryParseValueFromString always returns true and sets validationErrorMessage to null - Assert.Null(validationErrorMessage); } } diff --git a/tests/Core/Components/DateTimes/Utilities/CalendarExtendedTests.cs b/tests/Core/Components/DateTimes/Utilities/CalendarExtendedTests.cs index b4f8cd8ab1..dc0586ad8b 100644 --- a/tests/Core/Components/DateTimes/Utilities/CalendarExtendedTests.cs +++ b/tests/Core/Components/DateTimes/Utilities/CalendarExtendedTests.cs @@ -1,8 +1,9 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ using System.Globalization; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; using Xunit; namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.DateTimes.Utilities; diff --git a/tests/Core/Components/DateTimes/Utilities/CalendarTValueTests.cs b/tests/Core/Components/DateTimes/Utilities/CalendarTValueTests.cs new file mode 100644 index 0000000000..9ca53b1662 --- /dev/null +++ b/tests/Core/Components/DateTimes/Utilities/CalendarTValueTests.cs @@ -0,0 +1,721 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using Microsoft.FluentUI.AspNetCore.Components.Calendar; +using Xunit; + +namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.DateTimes.Utilities; + +public class CalendarTValueTests +{ + [Theory] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateTime?))] + [InlineData(typeof(DateOnly))] + [InlineData(typeof(DateOnly?))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(DateTimeOffset?))] + public void IsNotDateType_SupportedTypes_ReturnsFalse(Type type) + { + // Act + var result = type.IsNotDateType(); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + [InlineData(typeof(double))] + [InlineData(typeof(bool))] + [InlineData(typeof(object))] + [InlineData(typeof(decimal))] + [InlineData(typeof(float))] + [InlineData(typeof(long))] + public void IsNotDateType_UnsupportedTypes_ReturnsTrue(Type type) + { + // Act + var result = type.IsNotDateType(); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("2023-10-26 14:30:00", typeof(DateTime), true, "2023-10-26 14:30:00")] + [InlineData("2023-10-26 00:00:00", typeof(DateOnly), true, "2023-10-26 00:00:00")] + [InlineData("2023-10-26 14:30:00", typeof(DateTimeOffset), true, "2023-10-26 14:30:00")] + [InlineData("2023-10-26 14:30:00", typeof(DateTime), false, "2023-10-26 14:30:00")] + [InlineData("2023-10-26 00:00:00", typeof(DateOnly), false, "2023-10-26 00:00:00")] + [InlineData("2023-10-26 14:30:00", typeof(DateTimeOffset), false, "2023-10-26 14:30:00")] + public void ConvertToDateTime_ValidValues_ReturnsCorrectDateTime(string inputDate, Type inputType, bool isNullOrDefault, string expectedDate) + { + // Arrange + var expectedDateTime = DateTime.Parse(expectedDate, CultureInfo.InvariantCulture); + object inputValue = inputType switch + { + Type t when t == typeof(DateTime) => DateTime.Parse(inputDate, CultureInfo.InvariantCulture), + Type t when t == typeof(DateOnly) => DateOnly.Parse(inputDate.Split(' ')[0], CultureInfo.InvariantCulture), + Type t when t == typeof(DateTimeOffset) => new DateTimeOffset(DateTime.Parse(inputDate, CultureInfo.InvariantCulture), TimeSpan.FromHours(2)), + _ => throw new ArgumentException($"Unsupported type: {inputType}") + }; + + // Act + var result = inputType switch + { + Type t when t == typeof(DateTime) => ((DateTime)inputValue).ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateOnly) => ((DateOnly)inputValue).ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateTimeOffset) => ((DateTimeOffset)inputValue).ConvertToDateTime(isNullOrDefault), + _ => throw new ArgumentException($"Unsupported type: {inputType}") + }; + + // Assert + Assert.Equal(expectedDateTime, result); + } + + [Theory] + [InlineData(typeof(DateTime?), true, null)] + [InlineData(typeof(DateOnly?), true, null)] + [InlineData(typeof(DateTimeOffset?), true, null)] + [InlineData(typeof(DateTime?), false, null)] + [InlineData(typeof(DateOnly?), false, null)] + [InlineData(typeof(DateTimeOffset?), false, null)] + [InlineData(typeof(DateTime), false, "0001-01-01")] + [InlineData(typeof(DateOnly), false, "0001-01-01")] + [InlineData(typeof(DateTimeOffset), false, "0001-01-01")] + [InlineData(typeof(int), true, null)] + [InlineData(typeof(int), false, null)] + public void ConvertToDateTime_NullValues_ReturnsNull(Type nullableType, bool isNullOrDefault, string? expectedDateTimeString) + { + DateTime? expectedDateTime = expectedDateTimeString != null ? DateTime.Parse(expectedDateTimeString, CultureInfo.InvariantCulture) : null; + + // Arrange & Act + var result = nullableType switch + { + Type t when t == typeof(DateTime?) => ((DateTime?)null).ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateOnly?) => ((DateOnly?)null).ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateTimeOffset?) => ((DateTimeOffset?)null).ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateTime) => DateTime.MinValue.ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateOnly) => DateOnly.MinValue.ConvertToDateTime(isNullOrDefault), + Type t when t == typeof(DateTimeOffset) => DateTimeOffset.MinValue.ConvertToDateTime(isNullOrDefault), + _ => 0.ConvertToDateTime(isNullOrDefault) + }; + + // Assert + Assert.Equal(expectedDateTime, result); + } + + [Theory] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateOnly))] + [InlineData(typeof(DateTimeOffset))] + public void ConvertToDateTime_DefaultValues_ReturnsNull(Type defaultType) + { + // Act & Assert + if (defaultType == typeof(DateTime)) + { + var defaultDateTime = default(DateTime); + var result = defaultDateTime.ConvertToDateTime(); + Assert.Null(result); + } + else if (defaultType == typeof(DateOnly)) + { + var defaultDateOnly = default(DateOnly); + var result = defaultDateOnly.ConvertToDateTime(); + Assert.Null(result); + } + else if (defaultType == typeof(DateTimeOffset)) + { + var defaultDateTimeOffset = default(DateTimeOffset); + var result = defaultDateTimeOffset.ConvertToDateTime(); + Assert.Null(result); + } + } + + #region ConvertToRequiredDateTime Tests + + [Fact] + public void ConvertToRequiredDateTime_DateTime_ReturnsCorrectValue() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + + // Act + var result = dateTime.ConvertToRequiredDateTime(); + + // Assert + Assert.Equal(dateTime, result); + } + + [Fact] + public void ConvertToRequiredDateTime_DateOnly_ReturnsCorrectDateTime() + { + // Arrange + var dateOnly = new DateOnly(2023, 10, 26); + var expectedDateTime = new DateTime(2023, 10, 26); + + // Act + var result = dateOnly.ConvertToRequiredDateTime(); + + // Assert + Assert.Equal(expectedDateTime, result); + } + + [Fact] + public void ConvertToRequiredDateTime_NullValue_ThrowsArgumentNullException() + { + // Arrange + DateTime? nullDateTime = null; + + // Act & Assert + Assert.Throws(() => nullDateTime.ConvertToRequiredDateTime()); + } + + [Fact] + public void ConvertToRequiredDateTime_DefaultDateTime_ReturnsToday() + { + using var context = new DateTimeProviderContext(DateTime.Today); + + // Arrange + var defaultDateTime = default(DateTime); + + // Act + var result = defaultDateTime.ConvertToRequiredDateTime(); + + // Assert + Assert.Equal(DateTimeProvider.Today, result); + } + + #endregion + + #region ConvertToTValue Tests + + [Fact] + public void ConvertToTValue_DateTime_ReturnsCorrectValue() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + + // Act + var result = dateTime.ConvertToTValue(); + + // Assert + Assert.Equal(dateTime, result); + } + + [Fact] + public void ConvertToTValue_NullableDateTime_ReturnsCorrectValue() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + + // Act + var result = dateTime.ConvertToTValue(); + + // Assert + Assert.Equal(dateTime, result); + } + + [Fact] + public void ConvertToTValue_DateOnly_ReturnsCorrectValue() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + var expectedDateOnly = DateOnly.FromDateTime(dateTime); + + // Act + var result = dateTime.ConvertToTValue(); + + // Assert + Assert.Equal(expectedDateOnly, result); + } + + [Fact] + public void ConvertToTValue_NullableDateOnly_ReturnsCorrectValue() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + var expectedDateOnly = DateOnly.FromDateTime(dateTime); + + // Act + var result = dateTime.ConvertToTValue(); + + // Assert + Assert.Equal(expectedDateOnly, result); + } + + [Fact] + public void ConvertToTValue_UnsupportedType_ThrowsArgumentException() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 14, 30, 0); + + // Act & Assert + var exception = Assert.Throws(() => dateTime.ConvertToTValue()); + Assert.Contains("Unsupported type", exception.Message); + } + + #endregion + + #region IsNullOrDefault Tests + + [Fact] + public void IsNullOrDefault_NullValue_ReturnsTrue() + { + // Arrange + DateTime? nullValue = null; + + // Act + var result = nullValue.IsNullOrDefault(); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsNullOrDefault_DefaultDateTime_ReturnsTrue() + { + // Arrange + var defaultValue = default(DateTime); + + // Act + var result = defaultValue.IsNullOrDefault(); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsNullOrDefault_DefaultDateOnly_ReturnsTrue() + { + // Arrange + var defaultValue = default(DateOnly); + + // Act + var result = defaultValue.IsNullOrDefault(); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsNullOrDefault_ValidDateTime_ReturnsFalse() + { + // Arrange + var validValue = new DateTime(2023, 10, 26); + + // Act + var result = validValue.IsNullOrDefault(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsNullOrDefault_ValidDateOnly_ReturnsFalse() + { + // Arrange + var validValue = new DateOnly(2023, 10, 26); + + // Act + var result = validValue.IsNullOrDefault(); + + // Assert + Assert.False(result); + } + + #endregion + + #region IsNotNull Tests + + [Fact] + public void IsNotNull_NullValue_ReturnsFalse() + { + // Arrange + DateTime? nullValue = null; + + // Act + var result = nullValue.IsNotNull(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsNotNull_DefaultValue_ReturnsFalse() + { + // Arrange + var defaultValue = default(DateTime); + + // Act + var result = defaultValue.IsNotNull(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsNotNull_ValidValue_ReturnsTrue() + { + // Arrange + var validValue = new DateTime(2023, 10, 26); + + // Act + var result = validValue.IsNotNull(); + + // Assert + Assert.True(result); + } + + #endregion + + #region AddMonths Tests + + [Fact] + public void AddMonths_DateTime_ReturnsCorrectResult() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2023, 12, 26); + + // Act + var result = dateTime.AddMonths(2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void AddMonths_DateOnly_ReturnsCorrectResult() + { + // Arrange + var dateOnly = new DateOnly(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateOnly(2023, 12, 26); + + // Act + var result = dateOnly.AddMonths(2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void AddMonths_NullValue_ReturnsDefault() + { + // Arrange + DateTime? nullValue = null; + var culture = CultureInfo.InvariantCulture; + + // Act + var result = nullValue.AddMonths(2, culture); + + // Assert + Assert.Null(result); + } + + [Fact] + public void AddMonths_NegativeMonths_ReturnsCorrectResult() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2023, 8, 26); + + // Act + var result = dateTime.AddMonths(-2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + #endregion + + #region AddYears Tests + + [Fact] + public void AddYears_DateTime_ReturnsCorrectResult() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2025, 10, 26); + + // Act + var result = dateTime.AddYears(2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void AddYears_DateOnly_ReturnsCorrectResult() + { + // Arrange + var dateOnly = new DateOnly(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateOnly(2025, 10, 26); + + // Act + var result = dateOnly.AddYears(2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void AddYears_NullValue_ReturnsDefault() + { + // Arrange + DateTime? nullValue = null; + var culture = CultureInfo.InvariantCulture; + + // Act + var result = nullValue.AddYears(2, culture); + + // Assert + Assert.Null(result); + } + + [Fact] + public void AddYears_NegativeYears_ReturnsCorrectResult() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2021, 10, 26); + + // Act + var result = dateTime.AddYears(-2, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + #endregion + + #region GetYear Tests + + [Fact] + public void GetYear_DateTime_ReturnsCorrectYear() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + + // Act + var result = dateTime.GetYear(culture); + + // Assert + Assert.Equal(2023, result); + } + + [Fact] + public void GetYear_DateOnly_ReturnsCorrectYear() + { + // Arrange + var dateOnly = new DateOnly(2023, 10, 26); + var culture = CultureInfo.InvariantCulture; + + // Act + var result = dateOnly.GetYear(culture); + + // Assert + Assert.Equal(2023, result); + } + + [Fact] + public void GetYear_DefaultValue_ReturnsMinValueYear() + { + // Arrange + var defaultDateTime = default(DateTime); + var culture = CultureInfo.InvariantCulture; + + // Act + var result = defaultDateTime.GetYear(culture); + + // Assert + Assert.Equal(DateTime.MinValue.Year, result); + } + + #endregion + + #region MinDateTime Tests + + [Fact] + public void MinDateTime_DateTimeCollection_ReturnsMinimum() + { + // Arrange + var dates = new[] + { + new DateTime(2023, 10, 26), + new DateTime(2023, 5, 15), + new DateTime(2023, 12, 31) + }; + + // Act + var result = dates.MinDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 5, 15), result); + } + + [Fact] + public void MinDateTime_DateOnlyCollection_ReturnsMinimum() + { + // Arrange + var dates = new[] + { + new DateOnly(2023, 10, 26), + new DateOnly(2023, 5, 15), + new DateOnly(2023, 12, 31) + }; + + // Act + var result = dates.MinDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 5, 15), result); + } + + [Fact] + public void MinDateTime_SingleItem_ReturnsThatItem() + { + // Arrange + var dates = new[] { new DateTime(2023, 10, 26) }; + + // Act + var result = dates.MinDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 26), result); + } + + [Fact] + public void MinDateTime_EmptyCollection_ThrowsInvalidOperationException() + { + // Arrange + var dates = Array.Empty(); + + // Act & Assert + Assert.Throws(() => dates.MinDateTime()); + } + + #endregion + + #region MaxDateTime Tests + + [Fact] + public void MaxDateTime_DateTimeCollection_ReturnsMaximum() + { + // Arrange + var dates = new[] + { + new DateTime(2023, 10, 26), + new DateTime(2023, 5, 15), + new DateTime(2023, 12, 31) + }; + + // Act + var result = dates.MaxDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 12, 31), result); + } + + [Fact] + public void MaxDateTime_DateOnlyCollection_ReturnsMaximum() + { + // Arrange + var dates = new[] + { + new DateOnly(2023, 10, 26), + new DateOnly(2023, 5, 15), + new DateOnly(2023, 12, 31) + }; + + // Act + var result = dates.MaxDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 12, 31), result); + } + + [Fact] + public void MaxDateTime_SingleItem_ReturnsThatItem() + { + // Arrange + var dates = new[] { new DateTime(2023, 10, 26) }; + + // Act + var result = dates.MaxDateTime(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 26), result); + } + + [Fact] + public void MaxDateTime_EmptyCollection_ThrowsInvalidOperationException() + { + // Arrange + var dates = Array.Empty(); + + // Act & Assert + Assert.Throws(() => dates.MaxDateTime()); + } + + #endregion + + #region Mixed Type Tests + + [Fact] + public void ConvertToDateTime_MixedDateTimeOffset_HandlesTimeZoneCorrectly() + { + // Arrange + var dateTime = new DateTime(2023, 10, 26, 12, 0, 0, DateTimeKind.Unspecified); + var offset = TimeSpan.FromHours(5); + var dateTimeOffset = new DateTimeOffset(dateTime, offset); + + // Act + var result = dateTimeOffset.ConvertToDateTime(); + + // Assert + Assert.Equal(dateTimeOffset.DateTime, result); + } + + [Fact] + public void AddMonths_CrossYearBoundary_HandlesCorrectly() + { + // Arrange + var dateTime = new DateTime(2023, 11, 15); + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2024, 2, 15); + + // Act + var result = dateTime.AddMonths(3, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void AddYears_LeapYear_HandlesCorrectly() + { + // Arrange + var dateTime = new DateTime(2020, 2, 29); // Leap year + var culture = CultureInfo.InvariantCulture; + var expectedResult = new DateTime(2024, 2, 29); // Another leap year + + // Act + var result = dateTime.AddYears(4, culture); + + // Assert + Assert.Equal(expectedResult, result); + } + + #endregion +} diff --git a/tests/Core/Components/DateTimes/Utilities/CalendarTitlesTests.razor b/tests/Core/Components/DateTimes/Utilities/CalendarTitlesTests.razor index ba1bcedb19..2724752bd5 100644 --- a/tests/Core/Components/DateTimes/Utilities/CalendarTitlesTests.razor +++ b/tests/Core/Components/DateTimes/Utilities/CalendarTitlesTests.razor @@ -1,4 +1,5 @@ -@using System.Globalization +@using System.Globalization +@using Microsoft.FluentUI.AspNetCore.Components.Calendar @using Microsoft.FluentUI.AspNetCore.Components.Extensions @using Xunit @using Microsoft.FluentUI.AspNetCore.Components @@ -25,13 +26,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, new DateTime(2024, 7, 1)) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.ReadOnly); @@ -43,14 +44,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, CalendarViews.Days) .Add(p => p.PickerMonth, new DateTime(2024, 7, 1)) .Add(p => p.Culture, Culture_enUS) .Add(p => p.Disabled, true) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.True(titles.ReadOnly); @@ -67,13 +68,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, new DateTime(2024, 7, 1)) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.Label); @@ -90,13 +91,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, new DateTime(2024, 7, 1)) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.PreviousTitle); @@ -113,14 +114,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var minDate = Culture_enUS.Calendar.MinSupportedDateTime; - var calendar = RenderComponent(parameters => parameters + var minDate = Culture_enUS.Calendar.MinSupportedDateTime.AddMonths(1); + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, minDate) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.PreviousDisabled); @@ -137,13 +138,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, new DateTime(2024, 7, 1)) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.NextTitle); @@ -161,15 +162,15 @@ // Arrange var maxDate = Culture_enUS.Calendar.MaxSupportedDateTime; - var calendar = RenderComponent(parameters => parameters + var calendar = RenderComponent>(parameters => parameters .Add(p => p.View, view) .Add(p => p.PickerMonth, maxDate) .Add(p => p.Culture, Culture_enUS) ); - var titles = new CalendarTitles(calendar.Instance); + var titles = new CalendarTitles(calendar.Instance); // Act & Assert Assert.Equal(expected, titles.NextDisabled); } -} \ No newline at end of file +} diff --git a/tests/Core/Components/DateTimes/Utilities/FluentCalendarDayTests.razor b/tests/Core/Components/DateTimes/Utilities/FluentCalendarDayTests.razor index 340b769e5e..103b40e185 100644 --- a/tests/Core/Components/DateTimes/Utilities/FluentCalendarDayTests.razor +++ b/tests/Core/Components/DateTimes/Utilities/FluentCalendarDayTests.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @using Microsoft.FluentUI.AspNetCore.Components.Extensions @using Xunit @using Microsoft.FluentUI.AspNetCore.Components @@ -20,15 +20,15 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(); + var calendarComponent = RenderComponent>(); var calendar = calendarComponent.Instance; var date = new DateTime(2024, 7, 31); // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert - Assert.Equal(date, calendarDay.Date); + Assert.Equal(date, calendarDay.DateTime); } [Theory] @@ -41,13 +41,13 @@ // Arrange var date = new DateTime(2024, 7, 31); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.DisabledDateFunc, (d) => d == date && isInDisabledList) .Add(p => p.DisabledSelectable, disabledSelectable)); var calendar = calendarComponent.Instance; // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert Assert.Equal(expected, calendarDay.IsDisabled); @@ -63,14 +63,14 @@ // Arrange var date = isOutsideMonth ? new DateTime(2024, 8, 1) : new DateTime(2024, 7, 15); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Value, new DateTime(2024, 7, 10)) .Add(p => p.DisabledDateFunc, (d) => d == date && isInDisabledList) .Add(p => p.DisabledSelectable, false)); var calendar = calendarComponent.Instance; // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert Assert.Equal(expected, calendarDay.IsInactive); @@ -84,12 +84,12 @@ // Arrange var today = DateTime.Today; var notToday = today.AddDays(1); - var calendarComponent = RenderComponent(); + var calendarComponent = RenderComponent>(); var calendar = calendarComponent.Instance; // Act - var calendarDayToday = new FluentCalendarDay(calendar, today); - var calendarDayNotToday = new FluentCalendarDay(calendar, notToday); + var calendarDayToday = new FluentCalendarDay(calendar, today); + var calendarDayNotToday = new FluentCalendarDay(calendar, notToday); // Assert Assert.True(calendarDayToday.IsToday); @@ -104,13 +104,13 @@ // Arrange var selectedDate = new DateTime(2024, 7, 31); var notSelectedDate = new DateTime(2024, 7, 30); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Value, selectedDate)); var calendar = calendarComponent.Instance; // Act - var calendarDaySelected = new FluentCalendarDay(calendar, selectedDate); - var calendarDayNotSelected = new FluentCalendarDay(calendar, notSelectedDate); + var calendarDaySelected = new FluentCalendarDay(calendar, selectedDate); + var calendarDayNotSelected = new FluentCalendarDay(calendar, notSelectedDate); // Assert Assert.True(calendarDaySelected.IsSelected); @@ -126,15 +126,15 @@ var selectedDate1 = new DateTime(2024, 7, 20); var selectedDate2 = new DateTime(2024, 7, 22); var notSelectedDate = new DateTime(2024, 7, 21); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.SelectMode, CalendarSelectMode.Multiple) .Add(p => p.SelectedDates, new List { selectedDate1, selectedDate2 })); var calendar = calendarComponent.Instance; // Act - var calendarDaySelected1 = new FluentCalendarDay(calendar, selectedDate1); - var calendarDaySelected2 = new FluentCalendarDay(calendar, selectedDate2); - var calendarDayNotSelected = new FluentCalendarDay(calendar, notSelectedDate); + var calendarDaySelected1 = new FluentCalendarDay(calendar, selectedDate1); + var calendarDaySelected2 = new FluentCalendarDay(calendar, selectedDate2); + var calendarDayNotSelected = new FluentCalendarDay(calendar, notSelectedDate); // Assert Assert.True(calendarDaySelected1.IsMultiDaySelected); @@ -149,12 +149,12 @@ // Arrange var date = new DateTime(2024, 7, 31); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS)); var calendar = calendarComponent.Instance; // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert Assert.Equal("July 31", calendarDay.Title); @@ -169,13 +169,13 @@ // Arrange var date = new DateTime(2024, 7, 7); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.DayFormat, format) .Add(p => p.Culture, Culture_enUS)); var calendar = calendarComponent.Instance; // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert Assert.Equal(expected, calendarDay.DayNumber); @@ -188,13 +188,13 @@ // Arrange var date = new DateTime(2024, 7, 31); - var calendarComponent = RenderComponent(); + var calendarComponent = RenderComponent>(); var calendar = calendarComponent.Instance; // Act - var calendarDay = new FluentCalendarDay(calendar, date); + var calendarDay = new FluentCalendarDay(calendar, date); // Assert Assert.Equal("2024-07-31", calendarDay.DayIdentifier); } -} \ No newline at end of file +} diff --git a/tests/Core/Components/DateTimes/Utilities/FluentCalendarMonthTests.razor b/tests/Core/Components/DateTimes/Utilities/FluentCalendarMonthTests.razor index 5d73558188..cbb90912b5 100644 --- a/tests/Core/Components/DateTimes/Utilities/FluentCalendarMonthTests.razor +++ b/tests/Core/Components/DateTimes/Utilities/FluentCalendarMonthTests.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @using Microsoft.FluentUI.AspNetCore.Components.Extensions @using Xunit @using Microsoft.FluentUI.AspNetCore.Components @@ -20,13 +20,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 15); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.Equal(new DateTime(2025, 8, 1), calendarMonth.Month); @@ -38,14 +38,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.ReadOnly, true)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.True(calendarMonth.IsReadOnly); @@ -57,14 +57,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.DisabledDateFunc, (dt) => dt.Month == 8)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.True(calendarMonth.IsReadOnly); @@ -76,14 +76,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.DisabledDateFunc, (dt) => dt.Month == 8)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.True(calendarMonth.IsDisabled); @@ -95,7 +95,7 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.DisabledCheckAllDaysOfMonthYear, true) .Add(p => p.DisabledDateFunc, (dt) => dt.Month == 8)); @@ -103,7 +103,7 @@ var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.True(calendarMonth.IsDisabled); @@ -115,14 +115,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.DisabledDateFunc, (dt) => dt.Month == 9)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.False(calendarMonth.IsDisabled); @@ -135,14 +135,14 @@ // Arrange var selectedDate = new DateTime(2025, 8, 10); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.Value, selectedDate)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.True(calendarMonth.IsSelected); @@ -155,14 +155,14 @@ // Arrange var selectedDate = new DateTime(2025, 9, 10); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.Value, selectedDate)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.False(calendarMonth.IsSelected); @@ -174,14 +174,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS) .Add(p => p.Value, null)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.False(calendarMonth.IsSelected); @@ -193,13 +193,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.Equal("August 2025", calendarMonth.Title); @@ -211,15 +211,15 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Culture, Culture_enUS)); var calendar = calendarComponent.Instance; var date = new DateTime(2025, 8, 1); // Act - var calendarMonth = new FluentCalendarMonth(calendar, date); + var calendarMonth = new FluentCalendarMonth(calendar, date); // Assert Assert.Equal("2025-08", calendarMonth.MonthIdentifier); } -} \ No newline at end of file +} diff --git a/tests/Core/Components/DateTimes/Utilities/FluentCalendarYearTests.razor b/tests/Core/Components/DateTimes/Utilities/FluentCalendarYearTests.razor index ea92114767..23433f664b 100644 --- a/tests/Core/Components/DateTimes/Utilities/FluentCalendarYearTests.razor +++ b/tests/Core/Components/DateTimes/Utilities/FluentCalendarYearTests.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @using Microsoft.FluentUI.AspNetCore.Components.Extensions @using Xunit @using Microsoft.FluentUI.AspNetCore.Components @@ -20,12 +20,12 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(); + var calendarComponent = RenderComponent>(); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 6, 15); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.Equal(new DateTime(2023, 1, 1), fluentCalendarYear.Year); @@ -37,13 +37,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.ReadOnly, true)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.True(fluentCalendarYear.IsReadOnly); @@ -55,13 +55,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Disabled, true)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.True(fluentCalendarYear.IsReadOnly); @@ -73,13 +73,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.DisabledDateFunc, (dt) => dt.Year == 2023)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.True(fluentCalendarYear.IsDisabled); @@ -92,14 +92,14 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.DisabledCheckAllDaysOfMonthYear, true) .Add(p => p.DisabledDateFunc, (dt) => true)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.True(fluentCalendarYear.IsDisabled); @@ -113,13 +113,13 @@ // Arrange var selectedDate = new DateTime(2023, 5, 10); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Value, selectedDate)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.True(fluentCalendarYear.IsSelected); @@ -132,13 +132,13 @@ // Arrange var selectedDate = new DateTime(2024, 5, 10); - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Value, selectedDate)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.False(fluentCalendarYear.IsSelected); @@ -150,13 +150,13 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(parameters => parameters + var calendarComponent = RenderComponent>(parameters => parameters .Add(p => p.Value, null)); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); // Assert Assert.False(fluentCalendarYear.IsSelected); @@ -168,15 +168,15 @@ using var context = new DateTimeProviderContext(DateTime.Now); // Arrange - var calendarComponent = RenderComponent(); + var calendarComponent = RenderComponent>(); var calendar = calendarComponent.Instance; var date = new DateTime(2023, 1, 1); // Act - var fluentCalendarYear = new FluentCalendarYear(calendar, date); + var fluentCalendarYear = new FluentCalendarYear(calendar, date); var identifier = fluentCalendarYear.YearIdentifier; // Assert Assert.Equal("2023", identifier); } -} \ No newline at end of file +}