Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<FluentCalendar DisabledDateFunc="@DisabledDay"
@bind-Value="@SelectedDay"
@bind-PickerMonth="@PickerDay"
HeaderInteractive="false"
Style="height: 250px; align-content: start;" />
<p>Selected @(SelectedDay.ToString("yyyy-MM-dd"))</p>
<p>Panel @(PickerDay.ToString("yyyy-MM-dd"))</p>
Expand All @@ -14,6 +15,8 @@
View="CalendarViews.Months"
@bind-Value="@SelectedMonth"
@bind-PickerMonth="@PickerMonth"
MinDate="@(new DateTime(2026, 2, 1))"
MaxDate="@(new DateTime(2027, 7, 30))"
Style="height: 250px; align-content: start;" />
<p>Selected @(SelectedMonth.ToString("yyyy-MM-dd"))</p>
<p>Panel @(PickerMonth.ToString("yyyy-MM-dd"))</p>
Expand All @@ -24,6 +27,7 @@
View="CalendarViews.Years"
@bind-Value="@SelectedYear"
@bind-PickerMonth="@PickerYear"
MaxDate="@(new DateTime(2055, 1, 1))"
Style="height: 250px; align-content: start;" />
<p>Selected @(SelectedYear.ToString("yyyy-MM-dd"))</p>
<p>Panel @(PickerYear.ToString("yyyy-MM-dd"))</p>
Expand Down
116 changes: 116 additions & 0 deletions src/Core/Components/DateTime/CalendarTValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,122 @@ public static TValue AddYears<TValue>(this TValue? value, int years, CultureInfo
return DateTimeExtensions.AddYears(value.ConvertToRequiredDateTime(), years, culture).ConvertToTValue<TValue>();
}

internal static TValue AdaptedForMinDate<TValue>(this TValue value, TValue? minDate, CultureInfo culture)
=> AdaptedForMinDate(value, minDate, CalendarViews.Days, culture);

internal static TValue AdaptedForMinDate<TValue>(
this TValue value,
TValue? minDate,
CalendarViews view,
CultureInfo culture,
Func<DateTime, (DateTime PeriodStart, DateTime PeriodEnd)>? getDisplayedYearsPeriod = null)
{
var minDateTime = minDate.ConvertToDateTime()?.Date;
var candidateDateTime = value.ConvertToDateTime();

if (!minDateTime.HasValue || !candidateDateTime.HasValue)
{
return value;
}

// Allow navigation as long as the *displayed period* still contains at least one date >= MinDate.
// This prevents blocking navigation to the month/year that contains MinDate even if it starts earlier.
var adaptedCandidate = candidateDateTime.Value;
const int maxIterations = 500;

for (var i = 0; i < maxIterations; i++)
{
var candidatePeriodEnd = view switch
{
CalendarViews.Days
=> adaptedCandidate
.StartOfMonth(culture)
.AddMonths(1, culture)
.AddDays(-1),

CalendarViews.Months
=> adaptedCandidate
.StartOfYear(culture)
.AddYears(1, culture)
.AddDays(-1),

CalendarViews.Years when getDisplayedYearsPeriod is not null
=> getDisplayedYearsPeriod(adaptedCandidate).PeriodEnd,

CalendarViews.Years => adaptedCandidate.EndOfYear(culture),

_ => adaptedCandidate,
};

if (candidatePeriodEnd.Date >= minDateTime.Value)
{
return adaptedCandidate.ConvertToTValue<TValue>();
}

adaptedCandidate = view switch
{
CalendarViews.Days => adaptedCandidate.AddMonths(1, culture),
CalendarViews.Months => adaptedCandidate.AddYears(1, culture),
CalendarViews.Years => adaptedCandidate.AddYears(12, culture),
_ => adaptedCandidate,
};
}

return value;
}

internal static TValue AdaptedForMaxDate<TValue>(this TValue value, TValue? maxDate, CultureInfo culture)
=> AdaptedForMaxDate(value, maxDate, CalendarViews.Days, culture);

internal static TValue AdaptedForMaxDate<TValue>(
this TValue value,
TValue? maxDate,
CalendarViews view,
CultureInfo culture,
Func<DateTime, (DateTime PeriodStart, DateTime PeriodEnd)>? getDisplayedYearsPeriod = null)
{
var maxDateTime = maxDate.ConvertToDateTime()?.Date;
var candidateDateTime = value.ConvertToDateTime();

if (!maxDateTime.HasValue || !candidateDateTime.HasValue)
{
return value;
}

// Allow navigation as long as the *displayed period* still contains at least one date <= MaxDate.
// This prevents blocking navigation to the month/year that contains MaxDate even if it ends later.
var adaptedCandidate = candidateDateTime.Value;
const int maxIterations = 500;

for (var i = 0; i < maxIterations; i++)
{
var candidatePeriodStart = view switch
{
CalendarViews.Days => adaptedCandidate.StartOfMonth(culture),
CalendarViews.Months => adaptedCandidate.StartOfYear(culture),
CalendarViews.Years when getDisplayedYearsPeriod is not null
=> getDisplayedYearsPeriod(adaptedCandidate).PeriodStart,
CalendarViews.Years => adaptedCandidate.StartOfYear(culture),
_ => adaptedCandidate,
};

if (candidatePeriodStart.Date <= maxDateTime.Value)
{
return adaptedCandidate.ConvertToTValue<TValue>();
}

adaptedCandidate = view switch
{
CalendarViews.Days => adaptedCandidate.AddMonths(-1, culture),
CalendarViews.Months => adaptedCandidate.AddYears(-1, culture),
CalendarViews.Years => adaptedCandidate.AddYears(-12, culture),
_ => adaptedCandidate,
};
}

return value;
}

/// <summary>
/// Determines whether the specified date represents today's date.
/// </summary>
Expand Down
90 changes: 78 additions & 12 deletions src/Core/Components/DateTime/CalendarTitles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using Microsoft.FluentUI.AspNetCore.Components.Extensions;

namespace Microsoft.FluentUI.AspNetCore.Components.Calendar;

/// <summary>
Expand Down Expand Up @@ -55,7 +57,7 @@ public bool ReadOnly
CalendarViews.Days => false,
CalendarViews.Months => false,
CalendarViews.Years => true,
_ => true
_ => true,
};
}
}
Expand All @@ -74,7 +76,7 @@ public string Label
CalendarViews.Months => CalendarExtended.GetYear(),
CalendarViews.Years => CalendarExtended.GetYearsRangeLabel(Date.GetYear(_calendar.Culture) - CalendarExtended.YearShiftCentered),
#pragma warning restore MA0011
_ => string.Empty
_ => string.Empty,
};
}
}
Expand All @@ -91,7 +93,7 @@ public string PreviousTitle
CalendarViews.Days => CalendarExtended.GetMonthName(Date.AddMonths(-1, _calendar.Culture)),
CalendarViews.Months => CalendarExtended.GetYear(Date.AddYears(-1, _calendar.Culture)),
CalendarViews.Years => CalendarExtended.GetYearsRangeLabel(Date.GetYear(_calendar.Culture) - 12 - CalendarExtended.YearShiftCentered),
_ => string.Empty
_ => string.Empty,
};
}
}
Expand All @@ -107,12 +109,47 @@ public bool PreviousDisabled
var minDate = _calendar.Culture.Calendar.MinSupportedDateTime.AddMonths(1);
#pragma warning restore MA0011

var rangeMinDate = _calendar.MinDate.ConvertToDateTime()?.Date;
var rangePreviousDisabled = false;
if (rangeMinDate.HasValue)
{
var candidate = View switch
{
CalendarViews.Days => Date.AddMonths(-1, _calendar.Culture),
CalendarViews.Months => Date.AddYears(-1, _calendar.Culture),
CalendarViews.Years => Date.AddYears(-12, _calendar.Culture),
_ => Date,
};

var candidatePeriodEnd = View switch
{
CalendarViews.Days
=> candidate.StartOfMonth(_calendar.Culture)
.AddMonths(1, _calendar.Culture)
.AddDays(-1),

CalendarViews.Months
=> candidate.StartOfYear(_calendar.Culture)
.AddYears(1, _calendar.Culture)
.AddDays(-1),

CalendarViews.Years
=> candidate.StartOfYear(_calendar.Culture)
.AddYears(12, _calendar.Culture)
.AddDays(-1),

_ => candidate,
};

rangePreviousDisabled = candidatePeriodEnd.Date < rangeMinDate.Value;
}

return View switch
{
CalendarViews.Days => Date.Year == minDate.Year && Date.Month == minDate.Month,
CalendarViews.Months => Date.Year == minDate.Year,
CalendarViews.Years => Date.Year - CalendarExtended.YearShiftCentered <= minDate.Year + 12,
_ => false
CalendarViews.Days => (Date.Year == minDate.Year && Date.Month == minDate.Month) || rangePreviousDisabled,
CalendarViews.Months => Date.Year == minDate.Year || rangePreviousDisabled,
CalendarViews.Years => Date.Year - CalendarExtended.YearShiftCentered <= minDate.Year + 12 || rangePreviousDisabled,
_ => false,
};
}
}
Expand All @@ -129,7 +166,7 @@ public string NextTitle
CalendarViews.Days => CalendarExtended.GetMonthName(Date.AddMonths(+1, _calendar.Culture)),
CalendarViews.Months => CalendarExtended.GetYear(Date.AddYears(+1, _calendar.Culture)),
CalendarViews.Years => CalendarExtended.GetYearsRangeLabel(Date.GetYear(_calendar.Culture) + 12 - CalendarExtended.YearShiftCentered),
_ => string.Empty
_ => string.Empty,
};
}
}
Expand All @@ -143,12 +180,41 @@ public bool NextDisabled
{
var maxDate = _calendar.Culture.Calendar.MaxSupportedDateTime;

var rangeMaxDate = _calendar.MaxDate.ConvertToDateTime()?.Date;
var rangeNextDisabled = false;
if (rangeMaxDate.HasValue)
{
var candidate = View switch
{
CalendarViews.Days => Date.AddMonths(+1, _calendar.Culture),
CalendarViews.Months => Date.AddYears(+1, _calendar.Culture),
CalendarViews.Years => Date.AddYears(+12, _calendar.Culture),
_ => Date,
};

var candidatePeriodStart = View switch
{
CalendarViews.Days
=> candidate.StartOfMonth(_calendar.Culture),

CalendarViews.Months
=> candidate.StartOfYear(_calendar.Culture),

CalendarViews.Years
=> candidate.StartOfYear(_calendar.Culture),

_ => candidate,
};

rangeNextDisabled = candidatePeriodStart.Date > rangeMaxDate.Value;
}

return View switch
{
CalendarViews.Days => Date.Year == maxDate.Year && Date.Month == maxDate.Month,
CalendarViews.Months => Date.Year == maxDate.Year,
CalendarViews.Years => Date.Year + 12 - CalendarExtended.YearShiftCentered >= maxDate.Year,
_ => false
CalendarViews.Days => (Date.Year == maxDate.Year && Date.Month == maxDate.Month) || rangeNextDisabled,
CalendarViews.Months => Date.Year == maxDate.Year || rangeNextDisabled,
CalendarViews.Years => Date.Year + 12 - CalendarExtended.YearShiftCentered >= maxDate.Year || rangeNextDisabled,
_ => false,
};
}
}
Expand Down
72 changes: 43 additions & 29 deletions src/Core/Components/DateTime/FluentCalendar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,45 @@

@* Title bar (label, previous and next buttons) *@
<div class="title" part="title" aria-label="@titles.Label">
<div part="label" class="@GetAnimationClass("label")" readonly="@ReadOnly"
role="button" tabindex="0"
@onclick="@(e => TitleClickHandlerAsync(titles))">
@titles.Label
</div>
<div part="move" class="change-period">
@if (titles.PreviousDisabled)
{
<div class="previous" disabled />
}
else
{
<div class="previous" title="@titles.PreviousTitle" @onclick="@OnPreviousButtonHandlerAsync"
role="button" tabindex="0">
@((MarkupString)ArrowUp)
</div>
}
@if (titles.NextDisabled)
{
<div class="next" disabled />
}
else
{
<div class="next" title="@titles.NextTitle" @onclick="@OnNextButtonHandlerAsync"
role="button" tabindex="0">
@((MarkupString)ArrowDown)
</div>
}
</div>
@if (HeaderInteractive)
{
<div part="label" class="@GetAnimationClass("label")" readonly="@ReadOnly"
role="button" tabindex="0"
@onclick="@(e => TitleClickHandlerAsync(titles))">
@titles.Label
</div>
<div part="move" class="change-period">
@if (titles.PreviousDisabled)
{
<div class="previous" disabled />
}
else
{
<div class="previous" title="@titles.PreviousTitle" @onclick="@OnPreviousButtonHandlerAsync"
role="button" tabindex="0">
@((MarkupString)ArrowUp)
</div>
}
@if (titles.NextDisabled)
{
<div class="next" disabled />
}
else
{
<div class="next" title="@titles.NextTitle" @onclick="@OnNextButtonHandlerAsync"
role="button" tabindex="0">
@((MarkupString)ArrowDown)
</div>
}
</div>

}
else
{
<div part="label" class="label" style="cursor: default;">
@titles.Label
</div>
}
</div>

@switch (View)
Expand Down Expand Up @@ -177,6 +187,8 @@
ValueChanged="@PickerMonthSelectAsync"
ReadOnly="@ReadOnly"
Disabled="@Disabled"
MinDate="@(MinDate.ConvertToDateTime() ?? default)"
MaxDate="@(MaxDate.ConvertToDateTime() ?? default)"
Culture="@Culture"
DisabledSelectable="@DisabledSelectable"
AnimatePeriodChanges="@AnimatePeriodChanges"
Expand All @@ -193,6 +205,8 @@
ValueChanged="@PickerYearSelectAsync"
ReadOnly="@ReadOnly"
Disabled="@Disabled"
MinDate="@(MinDate.ConvertToDateTime() ?? default)"
MaxDate="@(MaxDate.ConvertToDateTime() ?? default)"
Culture="@Culture"
DisabledSelectable="@DisabledSelectable"
AnimatePeriodChanges="@AnimatePeriodChanges"
Expand Down
Loading
Loading