diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs
index e40e046d90b..7e64231d0de 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs
@@ -638,7 +638,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
try
{
- await _js.BitCalendarsFocusDay(GetDayButtonId(_focusedDate.Value));
+ await _js.BitCalendarsFocusCell(GetDayButtonId(_focusedDate.Value));
}
catch (JSDisconnectedException) { } // we can ignore this exception here
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor
index 2353f4a3ce7..7aa105f369c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor
@@ -47,6 +47,7 @@
+ title="@ClearButtonTitle"
+ aria-label="@ClearButtonTitle"
+ disabled="@ReadOnly">
@@ -108,37 +109,58 @@
readonly="@(AllowTextInput is false || ReadOnly)" />
}
-
-
-@if (Standalone is false)
-{
+
+
+@if (Standalone is false)
+{
-}
-
+}
+
+@* The callout is rendered outside of the root element - and reparented to the body while it is open -
+ so it inherits nothing of its direction and has to carry it itself. The explicit Dir is set here as
+ it is on the root; a direction that only the culture implies rides on the bit-dtp-rtl class. *@
-
-
+ @* The callout is a modal dialog only when it floats over the page; standalone it is part of the
+ page itself, where role="dialog" would announce a dialog the user can never leave. *@
+
@{
- var todayYear = _culture.Calendar.GetYear(DateTime.Now);
- var todayMonth = _culture.Calendar.GetMonth(DateTime.Now);
- var todayDay = _culture.Calendar.GetDayOfMonth(DateTime.Now);
+ var today = GetToday();
+ var todayYear = _culture.Calendar.GetYear(today);
+ var todayMonth = _culture.Calendar.GetMonth(today);
var showTimePicker = ShowTimePicker && ((_showTimePickerAsOverlayInternal && _isTimePickerOverlayOnTop) || _showTimePickerAsOverlayInternal is false);
}
@if (ShowDayPicker())
{
+ @* Only the day picker has days to focus, so the scan for one is scoped to it. *@
+ var focusableDay = GetFocusableDay();
+
- @if (_showMonthPickerAsOverlayInternal || (ShowTimePicker && ShowTimePickerAsOverlay is false))
+ @* The month title is only a button where there is a month picker for it to reveal;
+ with the month picker turned off it would toggle a panel that never comes. *@
+ @if (IsMonthPickerVisible && (_showMonthPickerAsOverlayInternal || (ShowTimePicker && ShowTimePickerAsOverlay is false)))
{
var title = string.Format(MonthPickerToggleTitle, _monthTitle);
@@ -176,12 +198,14 @@
class="bit-dtp-nbt @Classes?.PrevMonthNavButton"
title="@GoToPrevMonthTitle"
disabled="@prevDisabled"
- aria-disabled="@prevDisabled">
+ aria-disabled="@(prevDisabled ? "true" : null)">
- @if (ShowGoToToday && (_showMonthPickerAsOverlayInternal || (ShowTimePicker && _showTimePickerAsOverlayInternal is false)))
+ @* GoToToday normally lives in the header of the month picker, so the day picker
+ takes it over whenever that header is not on screen. *@
+ @if (ShowGoToToday && (_showMonthPickerAsOverlayInternal || ShowMonthPicker() is false || (ShowTimePicker && _showTimePickerAsOverlayInternal is false)))
{
var goToTodayDisabled = IsGoToTodayButtonDisabled(todayYear, todayMonth);
@@ -191,7 +215,7 @@
class="bit-dtp-gtb @Classes?.GoToTodayButton"
title="@GoToTodayTitle"
disabled="@goToTodayDisabled"
- aria-disabled="@goToTodayDisabled">
+ aria-disabled="@(goToTodayDisabled ? "true" : null)">
@@ -203,14 +227,16 @@
class="bit-dtp-nbt @Classes?.NextMonthNavButton"
title="@GoToNextMonthTitle"
disabled="@nextDisabled"
- aria-disabled="@nextDisabled">
+ aria-disabled="@(nextDisabled ? "true" : null)">
@if (ShowCloseButton && Standalone is false)
{
-
}
- @if (_showMonthPickerAsOverlayInternal && _showTimePickerAsOverlayInternal && ShowTimePicker)
+ @* The clock button belongs to the header of the month picker; the day picker
+ takes it over whenever that header is not on screen. *@
+ @if (ShowTimePicker && _showTimePickerAsOverlayInternal && (_showMonthPickerAsOverlayInternal || ShowMonthPicker() is false))
{
-
- @if (ShowWeekNumbers)
- {
-
- }
-
- @for (var index = 0; index < 7; index++)
- {
- var dayOfWeekName = _culture.DateTimeFormat.GetShortestDayName(GetDayOfWeek(index));
-
- @dayOfWeekName[0]
-
- }
-
-
- @for (var week = 0; week < 6; week++)
- {
- //to ignore the last empty week out of month || to ignore the first whole week out of month
- if (_daysOfCurrentMonth[week, 0].HasValue is false) continue;
-
-
+
+
@if (ShowWeekNumbers)
{
- var weekNumber = GetWeekNumber(week);
- var title = string.Format(WeekNumberTitle, weekNumber);
-
- @weekNumber
-
+
}
- @for (var day = 0; day < 7; day++)
+ @for (var index = 0; index < 7; index++)
{
- var date = _daysOfCurrentMonth[week, day]!.Value;
- var disabled = IsEnabled is false || IsWeekDayOutOfMinAndMaxDate(date);
- var isSelected = IsSelectedDate(date);
- var (style, klass) = GetDayButtonCss(date);
-
+ var dayOfWeek = GetDayOfWeek(index);
+ var dayOfWeekName = _culture.DateTimeFormat.GetShortestDayName(dayOfWeek);
+
+ @* A culture is free to leave the shortest day names empty, and the
+ header of the column would then be an index out of range. *@
+ @(dayOfWeekName.HasValue() ? dayOfWeekName[0].ToString() : string.Empty)
+
}
- }
+
+ @for (var week = 0; week < 6; week++)
+ {
+ @* The weeks past the end of the month (FixedWeeks off) hold no days at all.
+ The check spans the whole row: at the very edge of the calendar's supported
+ range a row can start with empty cells and still hold days to show. *@
+ if (IsWeekRowEmpty(week)) continue;
+
+
+ @if (ShowWeekNumbers)
+ {
+ var weekNumber = GetWeekNumber(week);
+ var title = string.Format(WeekNumberTitle, weekNumber);
+
+ @weekNumber
+
+ }
+
+ @for (var day = 0; day < 7; day++)
+ {
+ var nullableDate = _daysOfCurrentMonth[week, day];
+
+ @* A day out of the calendar's supported range has no date to offer,
+ so it stays an empty cell keeping the columns of the week aligned,
+ exactly like an outside day hidden by ShowOutsideDays. *@
+ if (nullableDate.HasValue is false ||
+ (ShowOutsideDays is false && IsInCurrentMonth(nullableDate.Value) is false))
+ {
+
+ continue;
+ }
+
+ var date = nullableDate.Value;
+
+ var disabled = IsEnabled is false || IsDayDisabled(date);
+ var isSelected = IsSelectedDate(date);
+ var isToday = IsInCurrentMonth(date) && date == today.Date;
+ var (style, klass) = GetDayButtonCss(date);
+ @* A read-only picker still browses, so its days stay focusable - but they
+ no longer select anything, which aria-disabled is what reports. *@
+
+ }
+
+ @* Not every calendar has twelve months: a leap year of the Hebrew calendar has thirteen. *@
+ var monthsInYear = GetMonthsInCurrentYear();
+ var monthRowCount = (monthsInYear + 3) / 4;
+ var focusableMonth = GetFocusableMonth();
@for (var cellIndex = 1; cellIndex <= 4; cellIndex++)
{
var month = (rowIndex * 4) + cellIndex;
+
+ if (month > monthsInYear) continue;
+
var monthName = _culture.DateTimeFormat.GetMonthName(month);
var disabled = IsEnabled is false || IsMonthOutOfMinAndMaxDate(month);
var selected = month == _currentMonth;
+ @* The displayed year is not always inside the range on screen - browsing the
+ ranges moves the range alone - so the roving tabindex has its own fallback. *@
+ var focusableYear = GetFocusableYear();
@for (var rowIndex = 0; rowIndex <= 2; rowIndex++)
{
@@ -511,12 +579,14 @@
var disabled = IsEnabled is false || IsYearOutOfMinAndMaxDate(year);
var selected = year == _currentYear;
}
+ @if (CalloutFooterTemplate is not null)
+ {
+
+ }
\ No newline at end of file
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs
index ece5780ec60..243f353c8bc 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs
@@ -6,6 +6,7 @@ namespace Bit.BlazorUI;
///
/// A BitDatePicker offers a drop-down control that’s optimized for picking a single date from a calendar view where contextual information like the day of the week or fullness of the calendar is important.
+/// It offers day, month, and year views, an optional time picker, flexible day and week rules such as disabled and highlighted dates, any culture and time zone, typed input, and complete keyboard accessibility.
///
public partial class BitDatePicker : BitInputBase
{
@@ -29,13 +30,25 @@ public partial class BitDatePicker : BitInputBase
private TimeZoneInfo _timeZone = TimeZoneInfo.Local;
private CultureInfo _culture = CultureInfo.CurrentUICulture;
private CancellationTokenSource _cancellationTokenSource = new();
- private DotNetObjectReference _dotnetObj = default!;
+ private DotNetObjectReference? _dotnetObj;
private readonly DateTime?[,] _daysOfCurrentMonth = new DateTime?[DEFAULT_WEEK_COUNT, DEFAULT_DAY_COUNT_PER_WEEK];
+ private bool _focusDayOnOpen;
+ private bool _focusTimePickerAfterRender;
+ private int? _focusedYearCell;
+ private int? _focusedMonthCell;
+ private DateTime? _focusedDate;
+ private string? _focusElementIdAfterRender;
+ private HashSet _disabledDates = [];
+ private HashSet _highlightedDates = [];
+ private HashSet _disabledDaysOfWeek = [];
+
private string? _labelId;
private string? _inputId;
private string _calloutId = string.Empty;
private string _overlayId = string.Empty;
+ private string _headerId = string.Empty;
+ private string _footerId = string.Empty;
private string _datePickerId = string.Empty;
private ElementReference _inputTimeHourRef = default!;
private ElementReference _inputTimeMinuteRef = default!;
@@ -64,17 +77,17 @@ private int _hourView
}
set
{
- if (value > 23)
- {
- _hour = 23;
- }
- else if (value < 0)
+ if (TimeFormat == BitTimeFormat.TwelveHours)
{
- _hour = 0;
+ // The input of a 12-hour clock carries no meridiem of its own, so the one already
+ // selected is kept: typing 5 while the time reads 15:30 gives 17:30, not 05:30.
+ var isPm = _hour >= 12;
+
+ _hour = (Math.Clamp(value, 0, 12) % 12) + (isPm ? 12 : 0);
}
else
{
- _hour = value;
+ _hour = Math.Clamp(value, 0, 23);
}
_ = UpdateCurrentValue();
@@ -110,6 +123,12 @@ private int _minuteView
+ ///
+ /// Whether selecting the already selected date deselects it, clearing the value.
+ /// The callout stays open after a deselection, so another date can be picked right away.
+ ///
+ [Parameter] public bool AllowDeselect { get; set; }
+
///
/// Whether or not the DatePicker allows a string date input.
///
@@ -117,6 +136,8 @@ private int _minuteView
///
/// Whether the DatePicker closes automatically after selecting the date.
+ /// It has no effect while the time picker is shown, where the callout stays open so the time of the
+ /// selected day can be set as well.
///
[Parameter] public bool AutoClose { get; set; } = true;
@@ -125,6 +146,17 @@ private int _minuteView
///
[Parameter] public string CalloutAriaLabel { get; set; } = "Calendar";
+ ///
+ /// Custom template to render at the bottom of the DatePicker's callout, below the pickers
+ /// (e.g. preset buttons that set the value from the code).
+ ///
+ [Parameter] public RenderFragment? CalloutFooterTemplate { get; set; }
+
+ ///
+ /// Custom template to render at the top of the DatePicker's callout, above the pickers.
+ ///
+ [Parameter] public RenderFragment? CalloutHeaderTemplate { get; set; }
+
///
/// Capture and render additional html attributes for the DatePicker's callout.
///
@@ -146,6 +178,11 @@ private int _minuteView
///
[Parameter] public string? ClearButtonIconName { get; set; }
+ ///
+ /// The title (tooltip) and the accessible name of the clear button.
+ ///
+ [Parameter] public string ClearButtonTitle { get; set; } = "Clear date";
+
///
/// The icon to display inside the close button.
/// Takes precedence over when both are set.
@@ -162,6 +199,13 @@ private int _minuteView
///
[Parameter] public string CloseDatePickerTitle { get; set; } = "Close date picker";
+ ///
+ /// The general color of the DatePicker that applies to the today day button, the highlighted current month,
+ /// and the selected AM/PM button.
+ ///
+ [Parameter, ResetClassBuilder]
+ public BitColor? Color { get; set; }
+
///
/// CultureInfo for the DatePicker.
///
@@ -179,6 +223,66 @@ private int _minuteView
///
[Parameter] public RenderFragment? DayCellTemplate { get; set; }
+ ///
+ /// The custom validation error message for a typed value that the DatePicker does not allow to be
+ /// selected, through , or
+ /// .
+ ///
+ [Parameter] public string? DisabledDateErrorMessage { get; set; }
+
+ ///
+ /// The list of dates that are disabled (not selectable) in the DatePicker, in addition to
+ /// and . Only the date part of each value is considered.
+ ///
+ [Parameter] public IEnumerable? DisabledDates { get; set; }
+
+ ///
+ /// The days of the week that are disabled (not selectable) in the DatePicker (e.g. weekends).
+ ///
+ [Parameter] public IEnumerable? DisabledDaysOfWeek { get; set; }
+
+ ///
+ /// Disables all days after today, exactly as a of today would.
+ /// When both are set, the earlier of the two bounds wins.
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public bool DisableFuture { get; set; }
+
+ ///
+ /// Disables all days before today, exactly as a of today would.
+ /// When both are set, the later of the two bounds wins.
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public bool DisablePast { get; set; }
+
+ ///
+ /// Determines the allowed drop directions of the callout.
+ ///
+ [Parameter] public BitDropDirection DropDirection { get; set; } = BitDropDirection.TopAndBottom;
+
+ ///
+ /// Overrides the first day of the week of the day picker. If not set, the first day of the week
+ /// of the is used.
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public DayOfWeek? FirstDayOfWeek { get; set; }
+
+ ///
+ /// Whether the day picker should always render six weeks, filling the extra rows with the days of the
+ /// adjacent months, to keep the height of the calendar fixed while navigating between the months.
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public bool FixedWeeks { get; set; }
+
+ ///
+ /// Custom function to provide additional CSS classes for each day button of the DatePicker.
+ ///
+ [Parameter] public Func? GetDayClass { get; set; }
+
///
/// The title of the Go to next month button (tooltip).
///
@@ -268,11 +372,22 @@ private int _minuteView
///
[Parameter] public bool HighlightCurrentMonth { get; set; }
+ ///
+ /// The list of dates that are highlighted (marked) in the day picker.
+ ///
+ [Parameter] public IEnumerable? HighlightedDates { get; set; }
+
///
/// Whether the month picker should highlight the selected month.
///
[Parameter] public bool HighlightSelectedMonth { get; set; }
+ ///
+ /// Whether the day picker should highlight today's day. It only affects the visual style of the
+ /// day cell; the accessibility attributes still report the day as the current date.
+ ///
+ [Parameter] public bool HighlightToday { get; set; } = true;
+
///
/// Determines increment/decrement steps for date-picker's hour.
///
@@ -319,14 +434,22 @@ private int _minuteView
[Parameter] public string? InvalidErrorMessage { get; set; }
///
- /// Whether the month picker is shown or hidden.
+ /// Custom function to determine if a specific date is disabled (not selectable) in the DatePicker.
+ ///
+ [Parameter] public Func? IsDateDisabled { get; set; }
+
+ ///
+ /// Whether the month picker is shown next to the day picker or hidden.
+ /// It has no effect in the MonthPicker mode, where the month picker is the only view.
///
- [Parameter] public bool IsMonthPickerVisible { get; set; } = true;
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public bool IsMonthPickerVisible { get; set; } = true;
///
/// Whether or not the DatePicker's callout is open
///
- [Parameter, TwoWayBound]
+ [Parameter, ResetClassBuilder, TwoWayBound]
public bool IsOpen { get; set; }
///
@@ -408,6 +531,11 @@ private int _minuteView
///
[Parameter] public string? NextYearRangeNavIconName { get; set; }
+ ///
+ /// The callback that is called when the value gets cleared by the clear button.
+ ///
+ [Parameter] public EventCallback OnClear { get; set; }
+
///
/// The callback for clicking on the DatePicker's input.
///
@@ -428,6 +556,23 @@ private int _minuteView
///
[Parameter] public EventCallback OnFocusOut { get; set; }
+ ///
+ /// The callback for when the displayed month of the day picker changes.
+ /// The argument is the first day of the newly displayed month.
+ ///
+ [Parameter] public EventCallback OnMonthChange { get; set; }
+
+ ///
+ /// The callback for when the user selects a date.
+ ///
+ [Parameter] public EventCallback OnSelectDate { get; set; }
+
+ ///
+ /// The custom validation error message for a typed value that falls outside of the
+ /// and range.
+ ///
+ [Parameter] public string? OutOfRangeErrorMessage { get; set; }
+
///
/// The placeholder text of the DatePicker's input.
///
@@ -503,6 +648,11 @@ private int _minuteView
[CallOnSet(nameof(OnSetParameters))]
public bool ShowMonthPickerAsOverlay { get; set; }
+ ///
+ /// Whether the days of the previous and next months should be shown in the day picker.
+ ///
+ [Parameter] public bool ShowOutsideDays { get; set; } = true;
+
///
/// Whether or not render the time-picker.
///
@@ -538,6 +688,12 @@ private int _minuteView
///
[Parameter] public bool ShowWeekNumbers { get; set; }
+ ///
+ /// The size of the DatePicker.
+ ///
+ [Parameter, ResetClassBuilder]
+ public BitSize? Size { get; set; }
+
///
/// Whether the date-picker is rendered standalone or with the input component and callout.
///
@@ -584,6 +740,11 @@ private int _minuteView
///
[Parameter] public string? TimePickerDecreaseMinuteIconName { get; set; }
+ ///
+ /// The title (tooltip) and the accessible name of the time-picker's hour input.
+ ///
+ [Parameter] public string TimePickerHourTitle { get; set; } = "Hour";
+
///
/// The icon to display inside the time-picker's increase-hour button.
/// Takes precedence over when both are set.
@@ -606,6 +767,11 @@ private int _minuteView
///
[Parameter] public string? TimePickerIncreaseMinuteIconName { get; set; }
+ ///
+ /// The title (tooltip) and the accessible name of the time-picker's minute input.
+ ///
+ [Parameter] public string TimePickerMinuteTitle { get; set; } = "Minute";
+
///
/// TimeZone for the DatePicker.
///
@@ -613,12 +779,25 @@ private int _minuteView
[CallOnSet(nameof(OnSetParameters))]
public TimeZoneInfo? TimeZone { get; set; }
+ ///
+ /// Overrides the current date and time considered as "today" and "now" in the DatePicker
+ /// (useful for testing or custom time providers).
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetParameters))]
+ public DateTimeOffset? Today { get; set; }
+
///
/// Whether or not the text field of the DatePicker is underlined.
///
[Parameter, ResetClassBuilder]
public bool Underlined { get; set; }
+ ///
+ /// The rule used to calculate the week numbers. Defaults to the FirstFullWeek rule.
+ ///
+ [Parameter] public CalendarWeekRule? WeekNumberRule { get; set; }
+
///
/// The title of the week number (tooltip).
///
@@ -652,23 +831,16 @@ public async Task _CloseCalloutBeforeAnotherCalloutIsOpened()
StateHasChanged();
}
+ // The swipe of the responsive mode only reports its end, but the JS side of it calls the whole set,
+ // so the three that carry nothing here still have to be there to be called.
[JSInvokable("OnStart")]
- public async Task _OnStart(decimal startX, decimal startY)
- {
-
- }
+ public Task _OnStart(decimal startX, decimal startY) => Task.CompletedTask;
[JSInvokable("OnMove")]
- public async Task _OnMove(decimal diffX, decimal diffY)
- {
-
- }
+ public Task _OnMove(decimal diffX, decimal diffY) => Task.CompletedTask;
[JSInvokable("OnEnd")]
- public async Task _OnEnd(decimal diffX, decimal diffY)
- {
-
- }
+ public Task _OnEnd(decimal diffX, decimal diffY) => Task.CompletedTask;
[JSInvokable("OnClose")]
public async Task _OnClose()
@@ -679,9 +851,26 @@ public async Task _OnClose()
+ ///
+ /// Opens the callout of the DatePicker exactly as clicking its input would.
+ ///
public Task OpenCallout()
{
- return HandleOnClick();
+ // Called from application code, which may well be off the renderer's dispatcher, so the whole
+ // body - state mutations and JS interop alike - runs through InvokeAsync.
+ return InvokeAsync(HandleOnClick);
+ }
+
+ ///
+ /// Closes the callout of the DatePicker and moves the focus back to its input.
+ ///
+ public Task CloseCalloutAndFocus()
+ {
+ return InvokeAsync(async () =>
+ {
+ await CloseCalloutAndRestoreFocus();
+ StateHasChanged();
+ });
}
@@ -694,6 +883,10 @@ protected override void RegisterCssClasses()
ClassBuilder.Register(() => (Dir is null && _culture.TextInfo.IsRightToLeft) ? "bit-rtl" : string.Empty);
+ ClassBuilder.Register(GetColorClass);
+
+ ClassBuilder.Register(GetSizeClass);
+
ClassBuilder.Register(() => IconLocation is BitIconLocation.Left ? "bit-dtp-lic" : string.Empty);
ClassBuilder.Register(() => Underlined ? "bit-dtp-und" : string.Empty);
@@ -702,6 +895,10 @@ protected override void RegisterCssClasses()
ClassBuilder.Register(() => Standalone ? "bit-dtp-sta" : string.Empty);
+ // The callout takes the focus with it when it opens, leaving the input with no focus ring to
+ // show which control the callout belongs to - so the input carries the open state itself.
+ ClassBuilder.Register(() => (Standalone is false && IsOpen) ? "bit-dtp-opn" : string.Empty);
+
ClassBuilder.Register(() => _hasFocus ? $"bit-dtp-foc {Classes?.Focused}" : string.Empty);
ClassBuilder.Register(() => IsEnabled && Required ? "bit-dtp-req" : string.Empty);
@@ -720,6 +917,8 @@ protected override void OnInitialized()
_labelId = $"{_datePickerId}-label";
_calloutId = $"{_datePickerId}-callout";
_overlayId = $"{_datePickerId}-overlay";
+ _headerId = $"{_datePickerId}-header";
+ _footerId = $"{_datePickerId}-footer";
_inputId = $"{_datePickerId}-input";
SetDefaultValue();
@@ -731,17 +930,59 @@ protected override void OnInitialized()
base.OnInitialized();
}
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+
+ BuildDatesLookups();
+ }
+
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
- if (firstRender is false) return;
+ if (firstRender)
+ {
+ _dotnetObj = DotNetObjectReference.Create(this);
+
+ try
+ {
+ // Prevents the default behavior (scrolling) of the navigation keys handled by the grid
+ // cells' keydown handlers, since Blazor cannot conditionally preventDefault per key, and
+ // keeps the focus inside the callout while it is the modal dialog it reports itself to be.
+ await _js.BitCalendarsSetup(_calloutId, Standalone is false);
+
+ // The swipe dismisses the callout, and standalone there is no callout to dismiss.
+ if (Responsive && Standalone is false)
+ {
+ await _js.BitSwipesSetup(_calloutId, 0.25m, BitPanelPosition.Top, IsRtl(), BitSwipeOrientation.Vertical, _dotnetObj);
+ }
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
+ }
+
+ if (_focusElementIdAfterRender.HasValue())
+ {
+ var elementId = _focusElementIdAfterRender!;
+ _focusElementIdAfterRender = null;
- _dotnetObj = DotNetObjectReference.Create(this);
+ try
+ {
+ await _js.BitCalendarsFocusCell(elementId);
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
+ }
- if (Responsive is false) return;
+ if (_focusTimePickerAfterRender)
+ {
+ _focusTimePickerAfterRender = false;
- await _js.BitSwipesSetup(_calloutId, 0.25m, BitPanelPosition.Top, Dir is BitDir.Rtl, BitSwipeOrientation.Vertical, _dotnetObj);
+ try
+ {
+ await _inputTimeHourRef.FocusAsync();
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
+ }
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out DateTimeOffset? result, [NotNullWhen(false)] out string? validationErrorMessage)
@@ -771,6 +1012,37 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa
if (parsed)
{
+ // A typed month is a whole month, whose first day can fall before MinDate (or whose last day
+ // after MaxDate) while the month itself is still selectable, so it is pulled into the range the
+ // same way SelectMonth pulls a month clicked in the calendar. The typed time of day survives it.
+ if (Mode == BitDatePickerMode.MonthPicker)
+ {
+ parsedValue = ClampToRange(parsedValue.Date, _culture.Calendar.GetMonth(parsedValue)) + parsedValue.TimeOfDay;
+ }
+
+ // A date typed by hand is the only way a value outside of the allowed range can reach the
+ // component (the calendar disables those days), so it is rejected here rather than silently
+ // accepted - which would leave the input showing a date the calendar refuses to select.
+ if (IsWeekDayOutOfMinAndMaxDate(parsedValue.Date))
+ {
+ result = default;
+ validationErrorMessage = OutOfRangeErrorMessage.HasValue()
+ ? OutOfRangeErrorMessage!
+ : $"The {DisplayName ?? FieldIdentifier.FieldName} field is out of the allowed range.";
+ return false;
+ }
+
+ // The same goes for the days the calendar disables one by one: typing one of them would
+ // otherwise be the way around a rule the picker itself enforces.
+ if (IsDayDisabled(parsedValue.Date))
+ {
+ result = default;
+ validationErrorMessage = DisabledDateErrorMessage.HasValue()
+ ? DisabledDateErrorMessage!
+ : $"The {DisplayName ?? FieldIdentifier.FieldName} field is not an allowed date.";
+ return false;
+ }
+
result = new DateTimeOffset(parsedValue, _timeZone.GetUtcOffset(parsedValue));
validationErrorMessage = null;
return true;
@@ -785,7 +1057,9 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa
{
if (value.HasValue is false) return null;
- return value.Value.ToString(DateFormat ?? GetDefaultDateFormat(), _culture);
+ // The text of the input is the same wall clock the calendar shows, so it is read in the TimeZone
+ // of the component rather than in whatever offset the value happens to carry.
+ return GetDateTime(value.Value).ToString(DateFormat ?? GetDefaultDateFormat(), _culture);
}
private string GetDefaultDateFormat()
@@ -903,7 +1177,13 @@ private async Task HandleOnClick()
if (Standalone) return;
if (IsEnabled is false) return;
- if (await AssignIsOpen(true) is false) return;
+ var wasOpen = IsOpen;
+
+ if (await AssignIsOpen(true) is false)
+ {
+ _focusDayOnOpen = false;
+ return;
+ }
ResetPickersState();
@@ -937,9 +1217,80 @@ private async Task HandleOnClick()
await ToggleCallout();
+ // The callout is a modal dialog, so it takes the focus with it and the user browses and picks
+ // the date from inside it. The exception is a pointer press on a picker whose input accepts
+ // text: there the user is about to type the date, and the focus has to stay where they typed.
+ if (wasOpen is false && (_focusDayOnOpen || AllowTextInput is false || ReadOnly))
+ {
+ if (ShowDayPicker())
+ {
+ _focusedDate = GetFocusableDay();
+ _focusElementIdAfterRender = GetDayButtonId(_focusedDate.Value);
+ }
+ else if (ShowMonthPicker())
+ {
+ // With no day picker on screen (the MonthPicker mode, or the month picker as an overlay)
+ // the month grid is what the callout opens onto.
+ _focusedMonthCell = GetFocusableMonth();
+ _focusElementIdAfterRender = GetMonthButtonId(_focusedMonthCell.Value);
+ }
+ }
+
+ _focusDayOnOpen = false;
+
await OnClick.InvokeAsync();
}
+ // The keys the input answers itself, per the APG combobox pattern: the popup opens with
+ // ArrowDown/ArrowUp (with or without Alt) and is dismissed with Escape. Enter and the space bar
+ // are deliberately left alone - the first submits the form the input sits in and the second types
+ // a space where text input is allowed, and Blazor cannot prevent one default without the other.
+ private async Task HandleOnInputKeyDown(KeyboardEventArgs e)
+ {
+ if (IsEnabled is false) return;
+
+ if (e.Key is "Escape")
+ {
+ await CloseCalloutAndRestoreFocus();
+ return;
+ }
+
+ if (IsOpen) return;
+
+ if (e.Key is not ("ArrowDown" or "ArrowUp")) return;
+
+ _focusDayOnOpen = true;
+
+ await HandleOnClick();
+ }
+
+ // Escape dismisses the callout from anywhere inside it and hands the focus back to the input,
+ // which is where the modal dialog pattern requires the focus to return.
+ private async Task HandleOnCalloutKeyDown(KeyboardEventArgs e)
+ {
+ if (IsEnabled is false) return;
+ if (e.Key is not "Escape") return;
+
+ await CloseCalloutAndRestoreFocus();
+ }
+
+ private async Task CloseCalloutAndRestoreFocus()
+ {
+ if (Standalone) return;
+ if (IsOpen is false) return;
+
+ await CloseCallout();
+
+ // A refused close (a one-way bound IsOpen) leaves the callout open, so the focus stays in it.
+ if (IsOpen) return;
+
+ try
+ {
+ await InputElement.FocusAsync();
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
+ }
+
private async Task HandleOnFocusIn()
{
if (IsEnabled is false) return;
@@ -970,25 +1321,30 @@ private async Task HandleOnFocus()
await OnFocus.InvokeAsync();
}
- private async Task HandleOnChange(ChangeEventArgs e)
+ private void HandleOnChange(ChangeEventArgs e)
{
if (IsEnabled is false || InvalidValueBinding()) return;
+ if (ReadOnly) return;
if (AllowTextInput is false) return;
- var oldValue = CurrentValue.GetValueOrDefault(DateTimeOffset.Now);
+ var oldValue = CurrentValue;
CurrentValueAsString = e.Value?.ToString();
- var curValue = CurrentValue.GetValueOrDefault(DateTimeOffset.Now);
+ // The comparison is on the nullable values themselves: text that fails to parse leaves
+ // CurrentValue null, and the calendar has nothing to synchronize with in that case.
+ if (IsOpen is false || oldValue == CurrentValue || CurrentValue.HasValue is false) return;
+
+ var previousYear = _currentYear;
- if (IsOpen && oldValue != curValue)
+ CheckCurrentCalendarMatchesCurrentValue();
+
+ // The year range shown by the year picker is anchored on the year of the value, so a typed date
+ // that lands in another year has to move that range along with the calendar. The comparison runs
+ // on the year of the culture's calendar, which is what the picker itself counts in.
+ if (_currentYear != previousYear)
{
- CheckCurrentCalendarMatchesCurrentValue();
- if (curValue.Year != oldValue.Year)
- {
- _currentYear = curValue.Year;
- ChangeYearRanges(_currentYear - 1);
- }
+ ChangeYearRanges(_currentYear - 1);
}
}
@@ -1001,8 +1357,15 @@ private async Task HandleOnClearButtonClick()
_hour = 0;
_minute = 0;
+ _focusedDate = null;
+
+ try
+ {
+ await InputElement.FocusAsync();
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
- await InputElement.FocusAsync();
+ await OnClear.InvokeAsync();
}
private void HandleOnValueChanged(object? sender, EventArgs args)
@@ -1015,22 +1378,28 @@ private void OnSetParameters()
_timeZone = TimeZone ?? TimeZoneInfo.Local;
_culture = Culture ?? CultureInfo.CurrentUICulture;
- var dateTime = CurrentValue.GetValueOrDefault(StartingValue.GetValueOrDefault(DateTimeOffset.Now));
+ var value = CurrentValue.GetValueOrDefault(StartingValue.GetValueOrDefault(GetNow()));
- if (MinDate.HasValue && MinDate > dateTime)
+ var minDate = GetMinDate();
+ if (minDate.HasValue && minDate > value)
{
- dateTime = MinDate.Value;
+ value = minDate.Value;
}
- if (MaxDate.HasValue && MaxDate < dateTime)
+ var maxDate = GetMaxDate();
+ if (maxDate.HasValue && maxDate < value)
{
- dateTime = MaxDate.Value;
+ value = maxDate.Value;
}
+ // Everything the calendar shows - the month it opens on, the time in the time picker - belongs
+ // to the TimeZone of the component, not to the offset the value happens to carry.
+ var dateTime = GetDateTime(value);
+
_hour = CurrentValue.HasValue || StartingValue.HasValue ? dateTime.Hour : 0;
_minute = CurrentValue.HasValue || StartingValue.HasValue ? dateTime.Minute : 0;
- GenerateCalendarData(dateTime.DateTime);
+ GenerateCalendarData(dateTime);
if (Standalone)
{
@@ -1057,24 +1426,71 @@ private async Task SelectDate(DateTime selectedDate)
{
if (ReadOnly) return;
if (IsEnabled is false || InvalidValueBinding()) return;
- if (IsOpenHasBeenSet && IsOpenChanged.HasDelegate is false) return;
- if (IsWeekDayOutOfMinAndMaxDate(selectedDate)) return;
+ if (IsDayDisabled(selectedDate)) return;
+
+ // Selecting the selected day again deselects it (AllowDeselect). The callout stays open - the
+ // user just emptied the value, so the calendar is exactly what they need to pick another one.
+ if (AllowDeselect && IsSelectedDate(selectedDate))
+ {
+ var year = _culture.Calendar.GetYear(selectedDate);
+ var month = _culture.Calendar.GetMonth(selectedDate);
+
+ _focusedDate = selectedDate;
+
+ CurrentValue = null;
+
+ // Clearing the value resets the calendar onto today (OnSetParameters), but the user is
+ // still looking at the month of the day they just deselected, so it is put back on screen.
+ _currentYear = year;
+ _currentMonth = month;
+ GenerateMonthData(_currentYear, _currentMonth);
+
+ await OnSelectDate.InvokeAsync(null);
+
+ return;
+ }
+
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
+ _focusedDate = selectedDate;
selectedDate = selectedDate.AddHours(_hour);
selectedDate = selectedDate.AddMinutes(_minute);
- if (AutoClose && Standalone is false)
+ // With the time picker on screen, picking a day is only half of the value: closing right away
+ // would send the user back to reopen the callout to set the time they were about to set.
+ // A one-way bound IsOpen cannot be closed by the selection either, so the callout stays open on
+ // the date that was just picked instead - the selection itself still goes through.
+ if (AutoClose && Standalone is false && ShowTimePicker is false &&
+ (IsOpenHasBeenSet is false || IsOpenChanged.HasDelegate))
{
await AssignIsOpen(false);
await ToggleCallout();
+
+ // The day that was activated is inside the callout that just closed, so the focus has to be
+ // handed back to the input - otherwise a keyboard selection drops the focus onto the body.
+ if (IsOpen is false)
+ {
+ try
+ {
+ await InputElement.FocusAsync();
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
+ }
}
CurrentValue = new DateTimeOffset(selectedDate, _timeZone.GetUtcOffset(selectedDate));
+ _currentYear = _culture.Calendar.GetYear(selectedDate);
_currentMonth = _culture.Calendar.GetMonth(selectedDate);
GenerateMonthData(_currentYear, _currentMonth);
+
+ await OnSelectDate.InvokeAsync(CurrentValue);
+
+ await NotifyMonthChange(previousYear, previousMonth);
}
private async Task SelectMonth(int month)
@@ -1082,13 +1498,20 @@ private async Task SelectMonth(int month)
if (IsEnabled is false) return;
if (IsMonthOutOfMinAndMaxDate(month)) return;
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
_currentMonth = month;
GenerateMonthData(_currentYear, _currentMonth);
if (Mode == BitDatePickerMode.MonthPicker)
{
- var selectedDate = _culture.Calendar.ToDateTime(_currentYear, _currentMonth, 1, 0, 0, 0, 0);
+ var selectedDate = GetFirstDayOfMonthOrClamp(_currentYear, _currentMonth);
+
+ // The first of the month can fall before MinDate (or after MaxDate) while the month itself is
+ // still selectable, so the selection is pulled to the first day of it the range allows.
+ selectedDate = ClampToRange(selectedDate, month);
await SelectDate(selectedDate);
}
@@ -1096,20 +1519,55 @@ private async Task SelectMonth(int month)
{
ToggleMonthPickerOverlay();
}
+
+ await NotifyMonthChange(previousYear, previousMonth);
+ }
+
+ private DateTime ClampToRange(DateTime date, int month)
+ {
+ // The bounds are truncated to their day: what this returns is a day of the calendar, and a time of
+ // day carried over from MinDate would both miss the day cell _focusedDate is matched against and
+ // be added on top of the hour and minute the time picker contributes in SelectDate.
+ var min = GetMinDate();
+ if (min.HasValue)
+ {
+ var minDate = GetDateTime(min.Value).Date;
+ if (date < minDate && _culture.Calendar.GetMonth(minDate) == month) return minDate;
+ }
+
+ var max = GetMaxDate();
+ if (max.HasValue)
+ {
+ var maxDate = GetDateTime(max.Value).Date;
+ if (date > maxDate && _culture.Calendar.GetMonth(maxDate) == month) return maxDate;
+ }
+
+ return date;
}
- private void SelectYear(int year)
+ private async Task SelectYear(int year)
{
if (IsEnabled is false) return;
if (IsYearOutOfMinAndMaxDate(year)) return;
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
_currentYear = year;
ChangeYearRanges(_currentYear - 1);
+ ClampCurrentMonthToYear();
+
GenerateMonthData(_currentYear, _currentMonth);
ToggleBetweenMonthAndYearPicker();
+
+ // The year that was activated goes away with the year grid, so the focus is handed to the month
+ // grid that replaces it - otherwise a keyboard selection drops the focus onto the body.
+ FocusMonthCell(GetFocusableMonth());
+
+ await NotifyMonthChange(previousYear, previousMonth);
}
private void ToggleBetweenMonthAndYearPicker()
@@ -1117,16 +1575,24 @@ private void ToggleBetweenMonthAndYearPicker()
if (IsEnabled is false) return;
_showMonthPicker = !_showMonthPicker;
+
+ // The grid that comes into view starts its roving tabindex over, on the month or the year the
+ // calendar is actually displaying, rather than on wherever the keyboard left it last time.
+ _focusedYearCell = null;
+ _focusedMonthCell = null;
}
- private void HandleMonthChange(bool isNext)
+ private async Task HandleMonthChange(bool isNext)
{
if (IsEnabled is false) return;
if (CanChangeMonth(isNext) is false) return;
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
if (isNext)
{
- if (_currentMonth < 12)
+ if (_currentMonth < GetMonthsInCurrentYear())
{
_currentMonth++;
}
@@ -1145,21 +1611,30 @@ private void HandleMonthChange(bool isNext)
else
{
_currentYear--;
- _currentMonth = 12;
+ _currentMonth = GetMonthsInCurrentYear();
}
}
GenerateMonthData(_currentYear, _currentMonth);
+
+ await NotifyMonthChange(previousYear, previousMonth);
}
- private void HandleYearChange(bool isNext)
+ private async Task HandleYearChange(bool isNext)
{
if (IsEnabled is false) return;
if (CanChangeYear(isNext) is false) return;
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
_currentYear += isNext ? +1 : -1;
+ ClampCurrentMonthToYear();
+
GenerateMonthData(_currentYear, _currentMonth);
+
+ await NotifyMonthChange(previousYear, previousMonth);
}
private void HandleYearRangeChange(bool isNext)
@@ -1172,11 +1647,26 @@ private void HandleYearRangeChange(bool isNext)
ChangeYearRanges(fromYear);
}
- private void HandleGoToToday()
+ private async Task HandleGoToToday()
{
if (IsEnabled is false) return;
- GenerateCalendarData(DateTime.Now);
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
+ GenerateCalendarData(GetToday());
+
+ await NotifyMonthChange(previousYear, previousMonth);
+ }
+
+ private async Task NotifyMonthChange(int previousYear, int previousMonth)
+ {
+ if (previousYear == _currentYear && previousMonth == _currentMonth) return;
+ if (OnMonthChange.HasDelegate is false) return;
+
+ var date = GetFirstDayOfMonthOrClamp(_currentYear, _currentMonth);
+
+ await OnMonthChange.InvokeAsync(new(date, _timeZone.GetUtcOffset(date)));
}
private void GenerateCalendarData(DateTime dateTime)
@@ -1195,42 +1685,63 @@ private void GenerateMonthData(int year, int month)
_monthTitle = $"{_culture.DateTimeFormat.GetMonthName(month)} {year}";
var calendar = _culture.Calendar;
- var firstDayOfMonth = new DateTime(year, month, 1, calendar);
int daysInMonth = calendar.GetDaysInMonth(year, month);
- int dayOfWeek = (int)calendar.GetDayOfWeek(firstDayOfMonth);
- int firstDayOfWeek = (int)_culture.DateTimeFormat.FirstDayOfWeek;
-
- // Adjust dayOfWeek to match the culture's first day of week
- dayOfWeek = (dayOfWeek - firstDayOfWeek + 7) % 7;
+ int firstDayOfWeek = (int)GetFirstDayOfWeek();
+ int dayOfWeek;
- DateTime previousMonth;
- if (month == 1)
+ var firstDayOfMonth = TryCreateDate(year, month, 1);
+ if (firstDayOfMonth.HasValue)
{
- previousMonth = new(year - 1, 12, 1);
+ dayOfWeek = (int)calendar.GetDayOfWeek(firstDayOfMonth.Value);
}
else
{
- previousMonth = new(year, month - 1, 1);
+ // The first supported month of a calendar does not have to start at its first day (the
+ // minimum of the Hebrew calendar falls in the middle of a month), so the weekday of the
+ // unrepresentable first day is walked back from the first day the calendar does support.
+ var minDate = calendar.MinSupportedDateTime;
+ dayOfWeek = ((int)calendar.GetDayOfWeek(minDate) - (calendar.GetDayOfMonth(minDate) - 1)) % 7;
+ if (dayOfWeek < 0)
+ {
+ dayOfWeek += 7;
+ }
}
- int daysInPreviousMonth = calendar.GetDaysInMonth(previousMonth.Year, previousMonth.Month);
- DateTime nextMonth;
- if (month == 12)
- {
- nextMonth = new(year + 1, 1, 1);
- }
+ // Adjust dayOfWeek to match the culture's first day of week
+ dayOfWeek = (dayOfWeek - firstDayOfWeek + 7) % 7;
+
+ // The adjacent months are kept as plain year/month numbers of the culture's own calendar: a
+ // DateTime built out of them would be a Gregorian date of a year that calendar never had, and a
+ // thirteenth month - which a leap year of the Hebrew calendar does have - has no Gregorian
+ // counterpart to build at all.
+ int monthsInYear = calendar.GetMonthsInYear(year);
+
+ int previousYear = month == 1 ? year - 1 : year;
+ int previousMonth;
+ int daysInPreviousMonth;
+ if (previousYear < GetMinCalendarYearMonth().Year)
+ {
+ // The year before the calendar's first year cannot be asked anything - every one of its
+ // days comes out as an empty cell anyway, so any day numbers at all do for the counting.
+ previousMonth = 1;
+ daysInPreviousMonth = dayOfWeek;
+ }
else
{
- nextMonth = new(year, month + 1, 1);
+ previousMonth = month == 1 ? calendar.GetMonthsInYear(previousYear) : month - 1;
+ daysInPreviousMonth = calendar.GetDaysInMonth(previousYear, previousMonth);
}
+ int nextYear = month == monthsInYear ? year + 1 : year;
+ int nextMonth = month == monthsInYear ? 1 : month + 1;
+
int day = daysInPreviousMonth - dayOfWeek + 1;
for (int i = 0; i < 1; i++)
{
for (int j = 0; j < dayOfWeek; j++)
{
- _daysOfCurrentMonth[i, j] = new(previousMonth.Year, previousMonth.Month, day, calendar);
+ _daysOfCurrentMonth[i, j] = TryCreateDate(previousYear, previousMonth, day);
day++;
}
}
@@ -1245,22 +1756,105 @@ private void GenerateMonthData(int year, int month)
if (day <= daysInMonth)
{
- _daysOfCurrentMonth[i, j] = new(year, month, day, calendar);
+ _daysOfCurrentMonth[i, j] = TryCreateDate(year, month, day);
day++;
}
else
{
- if (j == 0)
+ if (j == 0 && FixedWeeks is false)
{
ended = true;
}
- _daysOfCurrentMonth[i, j] = ended ? null : new(nextMonth.Year, nextMonth.Month, day - daysInMonth, calendar);
+ _daysOfCurrentMonth[i, j] = ended ? null : TryCreateDate(nextYear, nextMonth, day - daysInMonth);
day++;
}
}
}
}
+ // A day at the very edge of the calendar - the days around its first and last supported months -
+ // cannot be represented as a DateTime at all, so it becomes an empty cell instead of an exception.
+ private DateTime? TryCreateDate(int year, int month, int day)
+ {
+ try
+ {
+ return new DateTime(year, month, day, _culture.Calendar);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ }
+
+ // The first day of a month at the very edge of the calendar's supported range does not have to be
+ // representable (the range of the Hebrew calendar starts in the middle of a month), so the nearest
+ // day the calendar does support stands in for it.
+ private DateTime GetFirstDayOfMonthOrClamp(int year, int month)
+ {
+ var date = TryCreateDate(year, month, 1);
+ if (date.HasValue) return date.Value;
+
+ var calendar = _culture.Calendar;
+
+ return year == GetMinCalendarYearMonth().Year && month <= GetMinCalendarYearMonth().Month
+ ? calendar.MinSupportedDateTime.Date
+ : calendar.MaxSupportedDateTime.Date;
+ }
+
+ // DateTime is bounded to the years 1 through 9999 of the Gregorian calendar, and some calendars
+ // support even less, so everything the navigation can reach is bounded by the calendar's own range
+ // the same way MinDate and MaxDate bound it.
+ private (int Year, int Month) GetMinCalendarYearMonth()
+ {
+ var calendar = _culture.Calendar;
+ var minDate = calendar.MinSupportedDateTime;
+
+ return (calendar.GetYear(minDate), calendar.GetMonth(minDate));
+ }
+
+ private (int Year, int Month) GetMaxCalendarYearMonth()
+ {
+ var calendar = _culture.Calendar;
+ var maxDate = calendar.MaxSupportedDateTime;
+
+ return (calendar.GetYear(maxDate), calendar.GetMonth(maxDate));
+ }
+
+ private bool IsWeekRowEmpty(int weekIndex)
+ {
+ for (var day = 0; day < DEFAULT_DAY_COUNT_PER_WEEK; day++)
+ {
+ if (_daysOfCurrentMonth[weekIndex, day].HasValue) return false;
+ }
+
+ return true;
+ }
+
+ // Moving to another year of a calendar whose years do not all have the same number of months (the
+ // Hebrew one) can leave the displayed month past the end of the year that is now displayed - and
+ // moving to the first or the last supported year can leave it past the supported part of that year.
+ private void ClampCurrentMonthToYear()
+ {
+ var monthsInYear = GetMonthsInCurrentYear();
+
+ if (_currentMonth > monthsInYear)
+ {
+ _currentMonth = monthsInYear;
+ }
+
+ var (minCalendarYear, minCalendarMonth) = GetMinCalendarYearMonth();
+ if (_currentYear == minCalendarYear && _currentMonth < minCalendarMonth)
+ {
+ _currentMonth = minCalendarMonth;
+ }
+
+ var (maxCalendarYear, maxCalendarMonth) = GetMaxCalendarYearMonth();
+ if (_currentYear == maxCalendarYear && _currentMonth > maxCalendarMonth)
+ {
+ _currentMonth = maxCalendarMonth;
+ }
+ }
+
private void ChangeYearRanges(int fromYear)
{
_yearPickerStartYear = fromYear;
@@ -1290,9 +1884,14 @@ private bool IsGoToTodayButtonDisabled(int todayYear, int todayMonth, bool showY
}
}
+ private DayOfWeek GetFirstDayOfWeek()
+ {
+ return FirstDayOfWeek ?? _culture.DateTimeFormat.FirstDayOfWeek;
+ }
+
private DayOfWeek GetDayOfWeek(int index)
{
- int dayOfWeek = (int)_culture.DateTimeFormat.FirstDayOfWeek + index;
+ int dayOfWeek = (int)GetFirstDayOfWeek() + index;
if (dayOfWeek > 6)
{
@@ -1304,38 +1903,91 @@ private DayOfWeek GetDayOfWeek(int index)
private int GetWeekNumber(int weekIndex)
{
- return _culture.Calendar.GetWeekOfYear(_daysOfCurrentMonth[weekIndex, 0]!.Value, CalendarWeekRule.FirstFullWeek, _culture.DateTimeFormat.FirstDayOfWeek);
+ // The first cells of the week can be empty at the very edge of the calendar's supported range,
+ // so the number of the week is read off the first day of it that actually exists.
+ var date = _daysOfCurrentMonth[weekIndex, 0];
+ for (var day = 1; date.HasValue is false && day < DEFAULT_DAY_COUNT_PER_WEEK; day++)
+ {
+ date = _daysOfCurrentMonth[weekIndex, day];
+ }
+
+ return _culture.Calendar.GetWeekOfYear(date!.Value, WeekNumberRule ?? CalendarWeekRule.FirstFullWeek, GetFirstDayOfWeek());
}
private void ToggleMonthPickerOverlay()
{
_isMonthPickerOverlayOnTop = !_isMonthPickerOverlayOnTop;
+
+ // Each toggle swaps one whole picker for another, taking the button that was activated out of
+ // the DOM with it, so the focus has to be handed over to the picker that takes its place.
+ _focusedYearCell = null;
+ _focusedMonthCell = null;
+
+ MoveFocusToTheVisiblePicker();
}
private void ToggleTimePickerOverlay()
{
_isTimePickerOverlayOnTop = !_isTimePickerOverlayOnTop;
+
+ MoveFocusToTheVisiblePicker();
+ }
+
+ private void MoveFocusToTheVisiblePicker()
+ {
+ if (ShowDayPicker())
+ {
+ _focusedDate = GetFocusableDay();
+ _focusElementIdAfterRender = GetDayButtonId(_focusedDate.Value);
+ }
+ else if (ShowMonthPicker() && _showMonthPicker)
+ {
+ FocusMonthCell(GetFocusableMonth());
+ }
+ else if (ShowMonthPicker())
+ {
+ FocusYearCell(GetFocusableYear());
+ }
+ else if (ShowTimePicker)
+ {
+ // Nothing but the time picker is left on screen, so its hour input is where the focus goes.
+ _focusTimePickerAfterRender = true;
+ }
}
private bool CanChangeMonth(bool isNext)
{
if (IsEnabled is false) return false;
- if (isNext && MaxDate.HasValue)
+ if (isNext)
{
- var MaxDateYear = _culture.Calendar.GetYear(MaxDate.Value.DateTime);
- var MaxDateMonth = _culture.Calendar.GetMonth(MaxDate.Value.DateTime);
-
- if (MaxDateYear == _currentYear && MaxDateMonth == _currentMonth) return false;
- }
+ var (maxCalendarYear, maxCalendarMonth) = GetMaxCalendarYearMonth();
+ if (_currentYear == maxCalendarYear && _currentMonth >= maxCalendarMonth) return false;
+ var max = GetMaxDate();
+ if (max.HasValue)
+ {
+ var maxDate = GetDateTime(max.Value);
+ var maxDateYear = _culture.Calendar.GetYear(maxDate);
+ var maxDateMonth = _culture.Calendar.GetMonth(maxDate);
- if (isNext is false && MinDate.HasValue)
+ if (maxDateYear == _currentYear && maxDateMonth == _currentMonth) return false;
+ }
+ }
+ else
{
- var MinDateYear = _culture.Calendar.GetYear(MinDate.Value.DateTime);
- var MinDateMonth = _culture.Calendar.GetMonth(MinDate.Value.DateTime);
+ var (minCalendarYear, minCalendarMonth) = GetMinCalendarYearMonth();
+ if (_currentYear == minCalendarYear && _currentMonth <= minCalendarMonth) return false;
+
+ var min = GetMinDate();
+ if (min.HasValue)
+ {
+ var minDate = GetDateTime(min.Value);
+ var minDateYear = _culture.Calendar.GetYear(minDate);
+ var minDateMonth = _culture.Calendar.GetMonth(minDate);
- if (MinDateYear == _currentYear && MinDateMonth == _currentMonth) return false;
+ if (minDateYear == _currentYear && minDateMonth == _currentMonth) return false;
+ }
}
return true;
@@ -1345,9 +1997,15 @@ private bool CanChangeYear(bool isNext)
{
if (IsEnabled is false) return false;
+ if (isNext && _currentYear >= GetMaxCalendarYearMonth().Year) return false;
+ if (isNext is false && _currentYear <= GetMinCalendarYearMonth().Year) return false;
+
+ var maxDate = GetMaxDate();
+ var minDate = GetMinDate();
+
return (
- (isNext && MaxDate.HasValue && _culture.Calendar.GetYear(MaxDate.Value.DateTime) == _currentYear) ||
- (isNext is false && MinDate.HasValue && _culture.Calendar.GetYear(MinDate.Value.DateTime) == _currentYear)
+ (isNext && maxDate.HasValue && _culture.Calendar.GetYear(GetDateTime(maxDate.Value)) == _currentYear) ||
+ (isNext is false && minDate.HasValue && _culture.Calendar.GetYear(GetDateTime(minDate.Value)) == _currentYear)
) is false;
}
@@ -1355,22 +2013,52 @@ private bool CanChangeYearRange(bool isNext)
{
if (IsEnabled is false) return false;
+ if (isNext && GetMaxCalendarYearMonth().Year < _yearPickerStartYear + 12) return false;
+ if (isNext is false && GetMinCalendarYearMonth().Year >= _yearPickerStartYear) return false;
+
+ var maxDate = GetMaxDate();
+ var minDate = GetMinDate();
+
return (
- (isNext && MaxDate.HasValue && _culture.Calendar.GetYear(MaxDate.Value.DateTime) < _yearPickerStartYear + 12) ||
- (isNext is false && MinDate.HasValue && _culture.Calendar.GetYear(MinDate.Value.DateTime) >= _yearPickerStartYear)
+ (isNext && maxDate.HasValue && _culture.Calendar.GetYear(GetDateTime(maxDate.Value)) < _yearPickerStartYear + 12) ||
+ (isNext is false && minDate.HasValue && _culture.Calendar.GetYear(GetDateTime(minDate.Value)) >= _yearPickerStartYear)
) is false;
}
+ // DisablePast and DisableFuture bound the selectable days by today exactly the way MinDate and
+ // MaxDate do, so every consumer of the allowed range reads the bounds through these two accessors.
+ private DateTimeOffset? GetMinDate()
+ {
+ if (DisablePast is false) return MinDate;
+
+ var now = GetNow();
+
+ return MinDate.HasValue && MinDate.Value > now ? MinDate : now;
+ }
+
+ private DateTimeOffset? GetMaxDate()
+ {
+ if (DisableFuture is false) return MaxDate;
+
+ var now = GetNow();
+
+ return MaxDate.HasValue && MaxDate.Value < now ? MaxDate : now;
+ }
+
+ // Every caller weighs a day of the calendar, so the comparison is day against day: a MinDate that
+ // carries a time of day rules out the days before it, not the day it itself falls on.
private bool IsWeekDayOutOfMinAndMaxDate(DateTime date)
{
- if (MaxDate.HasValue)
+ var maxDate = GetMaxDate();
+ if (maxDate.HasValue)
{
- if (date > GetDateTime(MaxDate.Value)) return true;
+ if (date.Date > GetDateTime(maxDate.Value).Date) return true;
}
- if (MinDate.HasValue)
+ var minDate = GetMinDate();
+ if (minDate.HasValue)
{
- if (date < GetDateTime(MinDate.Value)) return true;
+ if (date.Date < GetDateTime(minDate.Value).Date) return true;
}
return false;
@@ -1378,20 +2066,32 @@ private bool IsWeekDayOutOfMinAndMaxDate(DateTime date)
private bool IsMonthOutOfMinAndMaxDate(int month)
{
- if (MaxDate.HasValue)
+ // The supported range of the calendar itself bounds the selection the same way MinDate and
+ // MaxDate do: a month past its edge has no representable days at all.
+ var (minCalendarYear, minCalendarMonth) = GetMinCalendarYearMonth();
+ if (_currentYear < minCalendarYear || (_currentYear == minCalendarYear && month < minCalendarMonth)) return true;
+
+ var (maxCalendarYear, maxCalendarMonth) = GetMaxCalendarYearMonth();
+ if (_currentYear > maxCalendarYear || (_currentYear == maxCalendarYear && month > maxCalendarMonth)) return true;
+
+ var max = GetMaxDate();
+ if (max.HasValue)
{
- var MaxDateYear = _culture.Calendar.GetYear(MaxDate.Value.DateTime);
- var MaxDateMonth = _culture.Calendar.GetMonth(MaxDate.Value.DateTime);
+ var maxDate = GetDateTime(max.Value);
+ var maxDateYear = _culture.Calendar.GetYear(maxDate);
+ var maxDateMonth = _culture.Calendar.GetMonth(maxDate);
- if (_currentYear > MaxDateYear || (_currentYear == MaxDateYear && month > MaxDateMonth)) return true;
+ if (_currentYear > maxDateYear || (_currentYear == maxDateYear && month > maxDateMonth)) return true;
}
- if (MinDate.HasValue)
+ var min = GetMinDate();
+ if (min.HasValue)
{
- var MinDateYear = _culture.Calendar.GetYear(MinDate.Value.DateTime);
- var MinDateMonth = _culture.Calendar.GetMonth(MinDate.Value.DateTime);
+ var minDate = GetDateTime(min.Value);
+ var minDateYear = _culture.Calendar.GetYear(minDate);
+ var minDateMonth = _culture.Calendar.GetMonth(minDate);
- if (_currentYear < MinDateYear || (_currentYear == MinDateYear && month < MinDateMonth)) return true;
+ if (_currentYear < minDateYear || (_currentYear == minDateYear && month < minDateMonth)) return true;
}
return false;
@@ -1399,16 +2099,25 @@ private bool IsMonthOutOfMinAndMaxDate(int month)
private bool IsYearOutOfMinAndMaxDate(int year)
{
- return (MaxDate.HasValue && year > _culture.Calendar.GetYear(MaxDate.Value.DateTime))
- || (MinDate.HasValue && year < _culture.Calendar.GetYear(MinDate.Value.DateTime));
+ var maxDate = GetMaxDate();
+ var minDate = GetMinDate();
+
+ // The years outside of the calendar's own supported range are as unselectable as the ones
+ // outside of MinDate and MaxDate - the year picker can show them at the edges of its ranges.
+ return year < GetMinCalendarYearMonth().Year
+ || year > GetMaxCalendarYearMonth().Year
+ || (maxDate.HasValue && year > _culture.Calendar.GetYear(GetDateTime(maxDate.Value)))
+ || (minDate.HasValue && year < _culture.Calendar.GetYear(GetDateTime(minDate.Value)));
}
private void CheckCurrentCalendarMatchesCurrentValue()
{
- var currentValue = CurrentValue.GetValueOrDefault(DateTimeOffset.Now);
- var currentValueYear = _culture.Calendar.GetYear(currentValue.DateTime);
- var currentValueMonth = _culture.Calendar.GetMonth(currentValue.DateTime);
- var currentValueDay = _culture.Calendar.GetDayOfMonth(currentValue.DateTime);
+ // The day cells are rendered in the TimeZone of the component, so the month the calendar opens
+ // on has to be read in it as well - otherwise a value near midnight opens the month next to the
+ // one holding the day that is actually marked as selected.
+ var currentValue = GetDateTime(CurrentValue.GetValueOrDefault(GetNow()));
+ var currentValueYear = _culture.Calendar.GetYear(currentValue);
+ var currentValueMonth = _culture.Calendar.GetMonth(currentValue);
if (currentValueYear != _currentYear || currentValueMonth != _currentMonth)
{
@@ -1432,10 +2141,7 @@ private void CheckCurrentCalendarMatchesCurrentValue()
klass.Append(' ').Append(Classes?.SelectedDayButton);
}
- if (Styles?.SelectedDayButton is not null)
- {
- style.Append(Styles?.SelectedDayButton);
- }
+ AppendStyle(style, Styles?.SelectedDayButton);
}
var month = _culture.Calendar.GetMonth(date);
@@ -1446,8 +2152,21 @@ private void CheckCurrentCalendarMatchesCurrentValue()
klass.Append(" bit-dtp-dbo");
}
+ //Is highlighted
+ if (_highlightedDates.Contains(date.Date))
+ {
+ klass.Append(" bit-dtp-dhl");
+
+ if (Classes?.HighlightedDayButton is not null)
+ {
+ klass.Append(' ').Append(Classes?.HighlightedDayButton);
+ }
+
+ AppendStyle(style, Styles?.HighlightedDayButton);
+ }
+
//Is today
- if (month == _currentMonth && date == GetDateTime(DateTimeOffset.Now).Date)
+ if (HighlightToday && month == _currentMonth && date == GetToday().Date)
{
klass.Append(" bit-dtp-dtd");
@@ -1456,15 +2175,37 @@ private void CheckCurrentCalendarMatchesCurrentValue()
klass.Append(' ').Append(Classes?.TodayDayButton);
}
- if (Styles?.TodayDayButton is not null)
- {
- style.Append(' ').Append(Styles?.TodayDayButton);
- }
+ AppendStyle(style, Styles?.TodayDayButton);
}
+ var customClass = GetDayClass?.Invoke(GetDateTimeOfDayCell(date));
+ if (customClass.HasValue())
+ {
+ klass.Append(' ').Append(customClass);
+ }
+
+ // The style of every day comes last so it wins over the state specific ones, and it goes
+ // through the same appender so it is separated from them by a semicolon.
+ AppendStyle(style, Styles?.DayButton);
+
return (style.ToString(), klass.ToString());
}
+ // The styles of a day come from more than one state at a time (a selected day that is also today),
+ // so each one is closed with a semicolon before the next is appended - without it the last
+ // declaration of one and the first of the next would run together into a single invalid one.
+ private static void AppendStyle(StringBuilder builder, string? style)
+ {
+ if (style.HasNoValue()) return;
+
+ if (builder.Length > 0 && builder[^1] != ';')
+ {
+ builder.Append(';');
+ }
+
+ builder.Append(style);
+ }
+
private string GetMonthCellCssClass(int monthIndex, int todayYear, int todayMonth)
{
var className = new StringBuilder();
@@ -1479,8 +2220,9 @@ private string GetMonthCellCssClass(int monthIndex, int todayYear, int todayMont
}
else if (Mode == BitDatePickerMode.MonthPicker && CurrentValue.HasValue)
{
- var selectedYear = _culture.Calendar.GetYear(CurrentValue.Value.DateTime);
- var selectedMonth = _culture.Calendar.GetMonth(CurrentValue.Value.DateTime);
+ var selectedValue = GetDateTime(CurrentValue.Value);
+ var selectedYear = _culture.Calendar.GetYear(selectedValue);
+ var selectedMonth = _culture.Calendar.GetMonth(selectedValue);
if (selectedYear == _currentYear && selectedMonth == monthIndex)
{
@@ -1498,7 +2240,7 @@ private DateTimeOffset GetDateTimeOfDayCell(DateTime date)
private DateTimeOffset GetDateTimeOfMonthCell(int monthIndex)
{
- var date = _culture.Calendar.ToDateTime(_currentYear, monthIndex, 1, 0, 0, 0, 0);
+ var date = GetFirstDayOfMonthOrClamp(_currentYear, monthIndex);
return new(date, _timeZone.GetUtcOffset(date));
}
@@ -1509,9 +2251,451 @@ private bool IsSelectedDate(DateTime date)
return date == GetDateTime(CurrentValue.Value).Date;
}
- private async Task UpdateCurrentValue()
+ private void BuildDatesLookups()
+ {
+ // Only the date part of each value counts, as supplied by the caller - converting the value
+ // into the picker's time zone first could shift it to the adjacent day and disable (or
+ // highlight) a different date than the one that was configured.
+ _disabledDates = DisabledDates is null ? [] : DisabledDates.Select(d => d.Date).ToHashSet();
+ _highlightedDates = HighlightedDates is null ? [] : HighlightedDates.Select(d => d.Date).ToHashSet();
+ _disabledDaysOfWeek = DisabledDaysOfWeek is null ? [] : DisabledDaysOfWeek.ToHashSet();
+ }
+
+ private bool IsDayDisabled(DateTime date)
+ {
+ // A day the calendar itself cannot represent is not selectable (nor focusable) at all - the
+ // supported range of a calendar does not have to cover the whole range of DateTime.
+ if (date < _culture.Calendar.MinSupportedDateTime.Date || date > _culture.Calendar.MaxSupportedDateTime.Date) return true;
+
+ if (IsWeekDayOutOfMinAndMaxDate(date)) return true;
+
+ if (_disabledDaysOfWeek.Contains(date.DayOfWeek)) return true;
+
+ if (_disabledDates.Contains(date.Date)) return true;
+
+ if (IsDateDisabled is not null && IsDateDisabled(GetDateTimeOfDayCell(date))) return true;
+
+ return false;
+ }
+
+ private DateTimeOffset GetNow()
+ {
+ return Today ?? DateTimeOffset.Now;
+ }
+
+ private DateTime GetToday()
+ {
+ return GetDateTime(GetNow());
+ }
+
+ private bool IsInCurrentMonth(DateTime date)
+ {
+ return _culture.Calendar.GetYear(date) == _currentYear && _culture.Calendar.GetMonth(date) == _currentMonth;
+ }
+
+ private string GetDayButtonId(DateTime date)
+ {
+ return FormattableString.Invariant($"{_datePickerId}-day-{date.Year:D4}-{date.Month:D2}-{date.Day:D2}");
+ }
+
+ private string GetMonthButtonId(int month)
+ {
+ return FormattableString.Invariant($"{_datePickerId}-month-{month:D2}");
+ }
+
+ private string GetYearButtonId(int year)
+ {
+ return FormattableString.Invariant($"{_datePickerId}-year-{year:D4}");
+ }
+
+ private int GetMonthsInCurrentYear()
+ {
+ // Not every calendar has twelve months: a leap year of the Hebrew calendar has thirteen.
+ return _culture.Calendar.GetMonthsInYear(_currentYear);
+ }
+
+ // The single month of the month grid that is in the tab sequence (the roving tabindex of the APG
+ // grid pattern): the one the keyboard last landed on, otherwise the month the calendar displays,
+ // and as a last resort the first month the Min/Max range allows - the grid must never be
+ // unreachable, so a month is always returned even when every one of them is disabled.
+ private int GetFocusableMonth()
+ {
+ var monthsInYear = GetMonthsInCurrentYear();
+
+ if (_focusedMonthCell.HasValue &&
+ _focusedMonthCell.Value >= 1 && _focusedMonthCell.Value <= monthsInYear &&
+ IsMonthOutOfMinAndMaxDate(_focusedMonthCell.Value) is false) return _focusedMonthCell.Value;
+
+ if (_currentMonth >= 1 && _currentMonth <= monthsInYear &&
+ IsMonthOutOfMinAndMaxDate(_currentMonth) is false) return _currentMonth;
+
+ for (var month = 1; month <= monthsInYear; month++)
+ {
+ if (IsMonthOutOfMinAndMaxDate(month) is false) return month;
+ }
+
+ return Math.Clamp(_currentMonth, 1, monthsInYear);
+ }
+
+ // The same roving tabindex for the year grid. The displayed year is not always inside the range the
+ // year picker shows (browsing the ranges moves the range alone), so the first year of the range is
+ // what the tab sequence falls back to.
+ private int GetFocusableYear()
+ {
+ if (_focusedYearCell.HasValue &&
+ _focusedYearCell.Value >= _yearPickerStartYear && _focusedYearCell.Value <= _yearPickerEndYear &&
+ IsYearOutOfMinAndMaxDate(_focusedYearCell.Value) is false) return _focusedYearCell.Value;
+
+ if (_currentYear >= _yearPickerStartYear && _currentYear <= _yearPickerEndYear &&
+ IsYearOutOfMinAndMaxDate(_currentYear) is false) return _currentYear;
+
+ for (var year = _yearPickerStartYear; year <= _yearPickerEndYear; year++)
+ {
+ if (IsYearOutOfMinAndMaxDate(year) is false) return year;
+ }
+
+ return _yearPickerStartYear;
+ }
+
+ // The month grid answers the same keys as the day grid, one row being four months wide, and
+ // PageUp/PageDown moving to the same month of the adjacent year.
+ private async Task HandleMonthKeyDown(KeyboardEventArgs e, int month)
+ {
+ if (IsEnabled is false) return;
+
+ if (e.Key is "Escape")
+ {
+ await CloseCalloutAndRestoreFocus();
+ return;
+ }
+
+ if (e.Key is "PageUp" or "PageDown")
+ {
+ var isNext = e.Key is "PageDown";
+
+ if (CanChangeYear(isNext) is false) return;
+
+ await HandleYearChange(isNext);
+
+ FocusMonthCell(GetFocusableMonth());
+ return;
+ }
+
+ var isRtl = IsRtl();
+
+ int? target = e.Key switch
+ {
+ "ArrowLeft" => FindEnabledMonth(month, isRtl ? 1 : -1),
+ "ArrowRight" => FindEnabledMonth(month, isRtl ? -1 : 1),
+ "ArrowUp" => FindEnabledMonth(month, -4),
+ "ArrowDown" => FindEnabledMonth(month, 4),
+ "Home" => FindEnabledMonthFrom(1, 1),
+ "End" => FindEnabledMonthFrom(GetMonthsInCurrentYear(), -1),
+ _ => null
+ };
+
+ if (target.HasValue is false) return;
+
+ FocusMonthCell(target.Value);
+ }
+
+ private void FocusMonthCell(int month)
+ {
+ _focusedMonthCell = month;
+ _focusElementIdAfterRender = GetMonthButtonId(month);
+ }
+
+ private int? FindEnabledMonth(int from, int step)
+ {
+ var monthsInYear = GetMonthsInCurrentYear();
+ var month = from + step;
+
+ while (month >= 1 && month <= monthsInYear)
+ {
+ if (IsMonthOutOfMinAndMaxDate(month) is false) return month;
+
+ month += step;
+ }
+
+ return null;
+ }
+
+ private int? FindEnabledMonthFrom(int from, int step)
+ {
+ var monthsInYear = GetMonthsInCurrentYear();
+ var month = from;
+
+ while (month >= 1 && month <= monthsInYear)
+ {
+ if (IsMonthOutOfMinAndMaxDate(month) is false) return month;
+
+ month += step;
+ }
+
+ return null;
+ }
+
+ // The year grid answers the same keys, one row being four years wide, and PageUp/PageDown moving to
+ // the adjacent range of years.
+ private async Task HandleYearKeyDown(KeyboardEventArgs e, int year)
+ {
+ if (IsEnabled is false) return;
+
+ if (e.Key is "Escape")
+ {
+ await CloseCalloutAndRestoreFocus();
+ return;
+ }
+
+ if (e.Key is "PageUp" or "PageDown")
+ {
+ var isNext = e.Key is "PageDown";
+
+ if (CanChangeYearRange(isNext) is false) return;
+
+ HandleYearRangeChange(isNext);
+
+ _focusedYearCell = null;
+ FocusYearCell(GetFocusableYear());
+ return;
+ }
+
+ var isRtl = IsRtl();
+
+ int? target = e.Key switch
+ {
+ "ArrowLeft" => FindEnabledYear(year, isRtl ? 1 : -1),
+ "ArrowRight" => FindEnabledYear(year, isRtl ? -1 : 1),
+ "ArrowUp" => FindEnabledYear(year, -4),
+ "ArrowDown" => FindEnabledYear(year, 4),
+ "Home" => FindEnabledYearFrom(_yearPickerStartYear, 1),
+ "End" => FindEnabledYearFrom(_yearPickerEndYear, -1),
+ _ => null
+ };
+
+ if (target.HasValue is false) return;
+
+ FocusYearCell(target.Value);
+ }
+
+ private void FocusYearCell(int year)
+ {
+ _focusedYearCell = year;
+ _focusElementIdAfterRender = GetYearButtonId(year);
+ }
+
+ private int? FindEnabledYear(int from, int step)
+ {
+ var year = from + step;
+
+ while (year >= _yearPickerStartYear && year <= _yearPickerEndYear)
+ {
+ if (IsYearOutOfMinAndMaxDate(year) is false) return year;
+
+ year += step;
+ }
+
+ return null;
+ }
+
+ private int? FindEnabledYearFrom(int from, int step)
+ {
+ var year = from;
+
+ while (year >= _yearPickerStartYear && year <= _yearPickerEndYear)
+ {
+ if (IsYearOutOfMinAndMaxDate(year) is false) return year;
+
+ year += step;
+ }
+
+ return null;
+ }
+
+ private bool IsRtl()
+ {
+ return Dir == BitDir.Rtl || (Dir is null && _culture.TextInfo.IsRightToLeft);
+ }
+
+ // The single day of the grid that is in the tab sequence (the roving tabindex of the APG grid
+ // pattern): the one the keyboard last landed on, otherwise the selection, otherwise today, and as a
+ // last resort the first day that can actually be selected - the grid must never be unreachable.
+ private DateTime GetFocusableDay()
+ {
+ if (_focusedDate.HasValue && IsInCurrentMonth(_focusedDate.Value) && IsDayDisabled(_focusedDate.Value) is false) return _focusedDate.Value;
+
+ if (CurrentValue.HasValue)
+ {
+ var selectedDate = GetDateTime(CurrentValue.Value).Date;
+ if (IsInCurrentMonth(selectedDate) && IsDayDisabled(selectedDate) is false) return selectedDate;
+ }
+
+ var today = GetToday().Date;
+ if (IsInCurrentMonth(today) && IsDayDisabled(today) is false) return today;
+
+ for (var week = 0; week < DEFAULT_WEEK_COUNT; week++)
+ {
+ for (var day = 0; day < DEFAULT_DAY_COUNT_PER_WEEK; day++)
+ {
+ var date = _daysOfCurrentMonth[week, day];
+ if (date.HasValue && IsInCurrentMonth(date.Value) && IsDayDisabled(date.Value) is false) return date.Value;
+ }
+ }
+
+ // A month can be disabled from end to end, and a disabled day is not focusable anyway - but the
+ // tabindex still has to land on a day of the month itself: with ShowOutsideDays turned off the
+ // days around it are not rendered as buttons at all, so pointing at one loses the grid entirely.
+ for (var week = 0; week < DEFAULT_WEEK_COUNT; week++)
+ {
+ for (var day = 0; day < DEFAULT_DAY_COUNT_PER_WEEK; day++)
+ {
+ var date = _daysOfCurrentMonth[week, day];
+ if (date.HasValue && IsInCurrentMonth(date.Value)) return date.Value;
+ }
+ }
+
+ return today;
+ }
+
+ private async Task HandleDayKeyDown(KeyboardEventArgs e, DateTime date)
+ {
+ if (IsEnabled is false) return;
+
+ if (e.Key is "Escape")
+ {
+ await CloseCalloutAndRestoreFocus();
+ return;
+ }
+
+ var isRtl = IsRtl();
+
+ DateTime? target;
+ try
+ {
+ target = e.Key switch
+ {
+ "ArrowLeft" => FindEnabledDay(date, isRtl ? 1 : -1),
+ "ArrowRight" => FindEnabledDay(date, isRtl ? -1 : 1),
+ "ArrowUp" => FindEnabledDay(date, -7),
+ "ArrowDown" => FindEnabledDay(date, 7),
+ "Home" => FindEnabledDayTowards(GetStartOfWeek(date), date),
+ "End" => FindEnabledDayTowards(GetStartOfWeek(date).AddDays(6), date),
+ "PageUp" => FindEnabledDayTowards(e.ShiftKey ? _culture.Calendar.AddYears(date, -1) : _culture.Calendar.AddMonths(date, -1), date),
+ "PageDown" => FindEnabledDayTowards(e.ShiftKey ? _culture.Calendar.AddYears(date, 1) : _culture.Calendar.AddMonths(date, 1), date),
+ _ => null
+ };
+ }
+ catch (ArgumentException)
+ {
+ // Stepping over the edge of the calendar's supported range (the days around its first and
+ // last representable dates) throws instead of wrapping, and the focus simply stays put.
+ return;
+ }
+
+ if (target.HasValue is false) return;
+
+ await MoveFocusToDay(target.Value);
+ }
+
+ private DateTime? FindEnabledDay(DateTime from, int stepDays)
+ {
+ var date = from;
+
+ for (var i = 0; i < 366; i++)
+ {
+ date = date.AddDays(stepDays);
+
+ if (IsWeekDayOutOfMinAndMaxDate(date)) return null;
+
+ if (IsDayDisabled(date) is false) return date;
+ }
+
+ return null;
+ }
+
+ private DateTime? FindEnabledDayTowards(DateTime target, DateTime origin)
+ {
+ var step = target < origin ? 1 : -1;
+ var date = target;
+
+ while (date != origin)
+ {
+ if (IsDayDisabled(date) is false) return date;
+
+ date = date.AddDays(step);
+ }
+
+ return null;
+ }
+
+ private DateTime GetStartOfWeek(DateTime date)
+ {
+ var diff = ((int)date.DayOfWeek - (int)GetFirstDayOfWeek() + 7) % 7;
+
+ return date.AddDays(-diff);
+ }
+
+ private async Task MoveFocusToDay(DateTime target)
+ {
+ var previousYear = _currentYear;
+ var previousMonth = _currentMonth;
+
+ var year = _culture.Calendar.GetYear(target);
+ var month = _culture.Calendar.GetMonth(target);
+
+ if (year != _currentYear || month != _currentMonth)
+ {
+ _currentYear = year;
+ _currentMonth = month;
+
+ GenerateMonthData(_currentYear, _currentMonth);
+ }
+
+ _focusedDate = target;
+ _focusElementIdAfterRender = GetDayButtonId(target);
+
+ await NotifyMonthChange(previousYear, previousMonth);
+ }
+
+ private string GetColorClass()
+ {
+ return Color switch
+ {
+ BitColor.Primary => "bit-dtp-pri",
+ BitColor.Secondary => "bit-dtp-sec",
+ BitColor.Tertiary => "bit-dtp-ter",
+ BitColor.Info => "bit-dtp-inf",
+ BitColor.Success => "bit-dtp-suc",
+ BitColor.Warning => "bit-dtp-wrn",
+ BitColor.SevereWarning => "bit-dtp-swr",
+ BitColor.Error => "bit-dtp-err",
+ BitColor.PrimaryBackground => "bit-dtp-pbg",
+ BitColor.SecondaryBackground => "bit-dtp-sbg",
+ BitColor.TertiaryBackground => "bit-dtp-tbg",
+ BitColor.PrimaryForeground => "bit-dtp-pfg",
+ BitColor.SecondaryForeground => "bit-dtp-sfg",
+ BitColor.TertiaryForeground => "bit-dtp-tfg",
+ BitColor.PrimaryBorder => "bit-dtp-pbr",
+ BitColor.SecondaryBorder => "bit-dtp-sbr",
+ BitColor.TertiaryBorder => "bit-dtp-tbr",
+ _ => "bit-dtp-pri"
+ };
+ }
+
+ private string GetSizeClass()
+ {
+ return Size switch
+ {
+ BitSize.Small => "bit-dtp-sm",
+ BitSize.Medium => "bit-dtp-md",
+ BitSize.Large => "bit-dtp-lg",
+ _ => string.Empty
+ };
+ }
+
+ private Task UpdateCurrentValue()
{
- if (CurrentValue.HasValue is false) return;
+ if (CurrentValue.HasValue is false) return Task.CompletedTask;
var currentValue = GetDateTime(CurrentValue.Value);
var currentValueYear = _culture.Calendar.GetYear(currentValue);
@@ -1520,6 +2704,8 @@ private async Task UpdateCurrentValue()
var dateTime = _culture.Calendar.ToDateTime(currentValueYear, currentValueMonth, currentValueDay, _hour, _minute, 0, 0);
CurrentValue = new(dateTime, _timeZone.GetUtcOffset(dateTime));
+
+ return Task.CompletedTask;
}
private DateTime GetDateTime(DateTimeOffset dateTimeOffset)
@@ -1531,14 +2717,22 @@ private async Task HandleOnTimeHourFocus()
{
if (IsEnabled is false || ShowTimePicker is false || ReadOnly) return;
- await _js.BitUtilsSelectText(_inputTimeHourRef);
+ try
+ {
+ await _js.BitUtilsSelectText(_inputTimeHourRef);
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
}
private async Task HandleOnTimeMinuteFocus()
{
if (IsEnabled is false || ShowTimePicker is false || ReadOnly) return;
- await _js.BitUtilsSelectText(_inputTimeMinuteRef);
+ try
+ {
+ await _js.BitUtilsSelectText(_inputTimeMinuteRef);
+ }
+ catch (JSDisconnectedException) { } // we can ignore this exception here
}
private async Task HandleOnAmClick()
@@ -1555,12 +2749,13 @@ private async Task HandleOnPmClick()
if (ReadOnly) return;
if (IsEnabled is false) return;
- if (_hour <= 12) // "12:-- pm" is "12:--" in 24h
+ // "12:-- pm" is already "12:--" in 24h, so only the hours before noon move forward -
+ // otherwise clicking pm at 12:-- pm would wrap the hour around to 12:-- am.
+ if (_hour < 12)
{
_hour += 12;
}
- _hour %= 24;
await UpdateCurrentValue();
}
@@ -1691,6 +2886,10 @@ private bool ShowMonthPicker()
{
if (Mode == BitDatePickerMode.MonthPicker) return true;
+ // The month picker of the MonthPicker mode is the whole component, so only the one that sits
+ // next to (or on top of) the day picker can be turned off.
+ if (IsMonthPickerVisible is false) return false;
+
if (ShowTimePicker)
{
if (ShowTimePickerAsOverlay)
@@ -1712,15 +2911,22 @@ private void ResetPickersState()
{
_showMonthPicker = true;
_isMonthPickerOverlayOnTop = Mode == BitDatePickerMode.MonthPicker;
- _showMonthPickerAsOverlayInternal = Mode == BitDatePickerMode.MonthPicker || ShowMonthPickerAsOverlay;
+ // A hidden month picker must not take the day picker's place as an overlay either, so the
+ // overlay mode is only entered while the month picker is actually rendered.
+ _showMonthPickerAsOverlayInternal = Mode == BitDatePickerMode.MonthPicker ||
+ (ShowMonthPickerAsOverlay && IsMonthPickerVisible);
_isTimePickerOverlayOnTop = false;
_showTimePickerAsOverlayInternal = ShowTimePickerAsOverlay;
+ _focusedYearCell = null;
+ _focusedMonthCell = null;
}
private async Task ToggleCallout()
{
if (Standalone) return false;
if (IsEnabled is false || IsDisposed) return false;
+ // The reference is created on the first render, so nothing can toggle the callout before it.
+ if (_dotnetObj is null) return false;
return await _js.BitCalloutToggleCallout(
dotnetObj: _dotnetObj,
@@ -1731,12 +2937,14 @@ private async Task ToggleCallout()
overlayId: _overlayId,
isCalloutOpen: IsOpen,
responsiveMode: Responsive ? BitResponsiveMode.Top : BitResponsiveMode.None,
- dropDirection: BitDropDirection.TopAndBottom,
- isRtl: Dir is BitDir.Rtl,
+ dropDirection: DropDirection,
+ // The same direction the callout renders in (bit-dtp-rtl covers the culture-implied RTL
+ // as well), so the positioning matches the layout.
+ isRtl: IsRtl(),
scrollContainerId: "",
scrollOffset: 0,
- headerId: "",
- footerId: "",
+ headerId: CalloutHeaderTemplate is not null ? _headerId : "",
+ footerId: CalloutFooterTemplate is not null ? _footerId : "",
setCalloutWidth: false,
fixedCalloutWidth: false,
maxWindowWidth: MAX_WIDTH);
@@ -1744,7 +2952,16 @@ private async Task ToggleCallout()
private string GetCalloutCssClasses()
{
- List classes = ["bit-dtp-cal"];
+ // The callout is rendered outside of the root element (and is reparented to the body while it is
+ // open), so the custom properties of the color and the size have to be declared on it as well -
+ // nothing of the root cascades down to it.
+ List classes = ["bit-dtp-cal", GetColorClass()];
+
+ var sizeClass = GetSizeClass();
+ if (sizeClass.HasValue())
+ {
+ classes.Add(sizeClass);
+ }
if (Classes?.Callout is not null)
{
@@ -1761,7 +2978,7 @@ private string GetCalloutCssClasses()
classes.Add("bit-dtp-res");
}
- if (Dir is BitDir.Rtl || (Dir is null && _culture.TextInfo.IsRightToLeft))
+ if (IsRtl())
{
classes.Add("bit-dtp-rtl");
}
@@ -1774,8 +2991,10 @@ private async Task HandleGoToNow()
if (ReadOnly) return;
if (IsEnabled is false) return;
- _hour = DateTime.Now.Hour;
- _minute = DateTime.Now.Minute;
+ var now = GetToday();
+
+ _hour = now.Hour;
+ _minute = now.Minute;
await UpdateCurrentValue();
}
@@ -1796,7 +3015,10 @@ protected override async ValueTask DisposeAsync(bool disposing)
{
await _js.BitCalloutClearCallout(_calloutId);
await _js.BitSwipesDispose(_calloutId);
+ await _js.BitCalendarsDispose(_calloutId);
}
catch (JSDisconnectedException) { } // we can ignore this exception here
+
+ _dotnetObj?.Dispose();
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
index 85ad663298d..4f57767c0b2 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
@@ -1,5 +1,16 @@
@import "../../../Styles/functions.scss";
@import "../../../Styles/media-queries.scss";
+@import "../../../Styles/color-role-maps.scss";
+
+// The default size, declared as a mixin so both the root element and the callout - which is rendered
+// outside of it and therefore inherits nothing from it - can fall back to it.
+@mixin dtp-size-medium {
+ --bit-dtp-inp-h: #{spacing(4)};
+ --bit-dtp-inp-fs: #{spacing(1.75)};
+ --bit-dtp-cell-size: #{spacing(3.5)};
+ --bit-dtp-cell-fs: #{spacing(1.5)};
+ --bit-dtp-lbl-fs: #{spacing(1.75)};
+}
.bit-dtp {
margin: 0;
@@ -11,6 +22,8 @@
font-size: spacing(2.25);
font-family: $tg-font-family;
+ @include dtp-size-medium;
+
&.bit-dis {
.bit-dtp-lbl {
color: $clr-fg-dis;
@@ -66,11 +79,15 @@
}
}
- &.bit-rtl {
- }
}
+// The callout does not sit inside the root element, so a direction the culture implies - which leaves
+// no dir attribute behind to inherit - has to be restated here. It mirrors the whole callout: the day
+// picker leads on the right, the month picker follows on its left, and the days of a week run from the
+// right edge of the grid, which is the order the arrow keys already move in.
.bit-dtp-cal.bit-dtp-rtl {
+ direction: rtl;
+
.bit-dtp-am-pm {
order: -1;
direction: rtl;
@@ -84,7 +101,7 @@
font-weight: 600;
box-sizing: border-box;
padding: spacing(0.5) 0;
- font-size: spacing(1.75);
+ font-size: var(--bit-dtp-lbl-fs);
overflow-wrap: break-word;
color: $clr-fg-pri;
}
@@ -99,7 +116,7 @@
gap: spacing(1);
cursor: pointer;
box-shadow: none;
- height: spacing(4);
+ height: var(--bit-dtp-inp-h);
position: relative;
align-items: center;
padding: 0 spacing(1);
@@ -155,7 +172,7 @@
border-radius: 0;
box-sizing: border-box;
text-overflow: ellipsis;
- font-size: spacing(1.75);
+ font-size: var(--bit-dtp-inp-fs);
background: none transparent;
color: $clr-fg-pri;
font-family: $tg-font-family;
@@ -171,8 +188,8 @@
}
.bit-dtp-ico {
- padding-left: 0;
- padding-right: spacing(1);
+ padding-inline-start: 0;
+ padding-inline-end: spacing(1);
}
}
@@ -182,6 +199,19 @@
}
}
+// The focus itself moves into the callout, so the input is left with no focus ring to show which
+// control the callout belongs to. Its own color marks it as the active one for as long as the
+// callout is on screen.
+.bit-dtp-opn {
+ .bit-dtp-icn {
+ border-color: var(--bit-dtp-clr);
+ }
+
+ .bit-dtp-ico {
+ color: var(--bit-dtp-clr);
+ }
+}
+
.bit-dtp-cal {
display: none;
position: fixed;
@@ -197,6 +227,8 @@
border-radius: $shp-border-radius;
animation-name: bit-fade-show, bit-slide-down;
animation-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1);
+
+ @include dtp-size-medium;
}
.bit-dtp-cac {
@@ -238,15 +270,9 @@
flex-flow: column nowrap;
}
-.bit-dtp-dgd {
- border-spacing: 0;
- position: relative;
- text-align: center;
- font-size: inherit;
- table-layout: fixed;
- margin-top: spacing(0.5);
- border-collapse: collapse;
- margin-bottom: spacing(1.25);
+.bit-dtp-grd {
+ display: flex;
+ flex-flow: column nowrap;
}
.bit-dtp-dgh,
@@ -267,8 +293,8 @@
color: $clr-fg-pri;
position: relative;
text-align: center;
- font-size: spacing(1.5);
- line-height: spacing(3.5);
+ font-size: var(--bit-dtp-cell-fs);
+ line-height: var(--bit-dtp-cell-size);
animation-fill-mode: both;
// A step shorter than the calendar's own entry above, so the days still resolve inside the
// container rather than with it.
@@ -284,13 +310,13 @@
cursor: pointer;
overflow: visible;
position: relative;
- width: spacing(3.5);
align-items: center;
- height: spacing(3.5);
outline: transparent;
font-weight: inherit;
box-sizing: border-box;
- font-size: spacing(1.5);
+ width: var(--bit-dtp-cell-size);
+ height: var(--bit-dtp-cell-size);
+ font-size: var(--bit-dtp-cell-fs);
line-height: spacing(3);
justify-content: center;
background-color: transparent;
@@ -312,6 +338,25 @@
background-color: transparent;
color: $clr-fg-dis;
}
+
+ // The day grid moves the focus with the arrow keys, so the focused day has to be visible as such -
+ // and above its neighbors, whose backgrounds would otherwise clip the ring.
+ &:focus-visible {
+ z-index: 1;
+ outline: $shp-border-width $shp-border-style $clr-brd-pri;
+ }
+}
+
+// The placeholder of a day of an adjacent month while ShowOutsideDays is off: it keeps the columns of
+// the week aligned without rendering a day the user cannot pick.
+.bit-dtp-dbe {
+ width: var(--bit-dtp-cell-size);
+ height: var(--bit-dtp-cell-size);
+}
+
+.bit-dtp-dhl {
+ font-weight: 600;
+ background-color: $clr-bg-ter;
}
.bit-dtp-dbs {
@@ -333,28 +378,28 @@
.bit-dtp-dtd {
font-weight: 600;
border-radius: 50%;
- color: $clr-pri-text;
- background-color: $clr-pri;
+ color: var(--bit-dtp-clr-txt);
+ background-color: var(--bit-dtp-clr);
@media(hover: hover) {
&:hover {
- background-color: $clr-pri-hover;
+ background-color: var(--bit-dtp-clr-hover);
}
}
&:active {
- background-color: $clr-pri-active;
+ background-color: var(--bit-dtp-clr-active);
}
}
.bit-dtp-wnm {
display: flex;
font-weight: 400;
- width: spacing(3.5);
align-items: center;
- height: spacing(3.5);
box-sizing: border-box;
- font-size: spacing(1.5);
+ width: var(--bit-dtp-cell-size);
+ height: var(--bit-dtp-cell-size);
+ font-size: var(--bit-dtp-cell-fs);
justify-content: center;
padding: spacing(0.375) 0 0 0;
color: $clr-fg-sec;
@@ -365,9 +410,9 @@
.bit-dtp-pkh {
width: 100%;
position: relative;
- height: spacing(3.5);
+ height: var(--bit-dtp-cell-size);
display: inline-flex;
- line-height: spacing(5.5);
+ line-height: var(--bit-dtp-cell-size);
}
.bit-dtp-tph {
@@ -392,7 +437,7 @@
text-overflow: ellipsis;
padding: 0 spacing(1.25);
font-size: spacing(1.75);
- line-height: spacing(3.5);
+ line-height: var(--bit-dtp-cell-size);
background-color: transparent;
border-radius: $shp-border-radius;
}
@@ -432,13 +477,13 @@
overflow: visible;
position: relative;
text-align: center;
- width: spacing(3.5);
- height: spacing(3.5);
outline: transparent;
- min-width: spacing(3.5);
- font-size: spacing(1.5);
- min-height: spacing(3.5);
- line-height: spacing(3.5);
+ width: var(--bit-dtp-cell-size);
+ height: var(--bit-dtp-cell-size);
+ min-width: var(--bit-dtp-cell-size);
+ min-height: var(--bit-dtp-cell-size);
+ line-height: var(--bit-dtp-cell-size);
+ font-size: var(--bit-dtp-cell-fs);
background-color: transparent;
color: $clr-fg-pri;
border-radius: $shp-border-radius;
@@ -512,7 +557,7 @@
.bit-dtp-dvd {
top: 0;
- border-right: $shp-border-width $shp-border-style $clr-brd-sec;
+ border-inline-end: $shp-border-width $shp-border-style $clr-brd-sec;
}
.bit-dtp-mwp {
@@ -593,13 +638,13 @@
}
.bit-dtp-pcm {
- color: $clr-pri-text;
- background-color: $clr-pri;
+ color: var(--bit-dtp-clr-txt);
+ background-color: var(--bit-dtp-clr);
@media (hover: hover) {
&:hover {
- color: $clr-sec-hover;
- background-color: $clr-pri-hover;
+ color: var(--bit-dtp-clr-txt);
+ background-color: var(--bit-dtp-clr-hover);
}
}
}
@@ -683,8 +728,8 @@
}
.bit-dtp-bns {
- color: $clr-pri-text;
- background-color: $clr-pri;
+ color: var(--bit-dtp-clr-txt);
+ background-color: var(--bit-dtp-clr);
&:disabled {
background-color: $clr-bg-dis;
@@ -692,12 +737,12 @@
@media (hover: hover) {
&:hover {
- background-color: $clr-pri-hover;
+ background-color: var(--bit-dtp-clr-hover);
}
}
&:active {
- background-color: $clr-pri-active;
+ background-color: var(--bit-dtp-clr-active);
}
}
@@ -722,7 +767,7 @@
.bit-dtp-icn {
border: none;
flex: 1 1 0px;
- text-align: left;
+ text-align: start;
}
&.bit-dis {
@@ -736,6 +781,12 @@
@include focus-underline-ring;
}
}
+
+ // The underlined variant has no border of its own to color, so the open state is carried by the
+ // underline the wrapper draws.
+ &.bit-dtp-opn .bit-dtp-wrp {
+ border-bottom-color: var(--bit-dtp-clr);
+ }
}
.bit-dtp-nbd {
@@ -805,6 +856,7 @@
.bit-dtp-gtn,
.bit-dtp-wlb,
.bit-dtp-dbt,
+ .bit-dtp-dbe,
.bit-dtp-pkb,
.bit-dtp-wnm {
width: 100%;
@@ -814,7 +866,7 @@
max-width: unset;
align-items: center;
justify-content: center;
- font-size: spacing(1.75);
+ font-size: var(--bit-dtp-cell-fs);
}
.bit-dtp-nbt,
@@ -823,37 +875,60 @@
width: spacing(5.5);
}
- .bit-dtp-wlb {
- //font-weight: bold;
- }
-
.bit-dtp-grp {
justify-content: center;
}
- .bit-dtp-tdv {
- //font-size: spacing(7);
- }
-
.bit-dtp-tin {
width: 100%;
min-width: spacing(7);
max-width: spacing(12);
- //font-size: spacing(5);
}
.bit-dtp-tbt {
width: 100%;
min-width: spacing(7);
max-width: spacing(12);
- //font-size: spacing(3);
}
.bit-dtp-bam,
.bit-dtp-bpm {
width: 100%;
- //font-size: spacing(4);
margin: spacing(0.25);
}
}
}
+
+
+// Role classes are generated from the shared $bit-color-roles map (see color-role-maps.scss).
+// They are applied to the root element and to the callout alike, since the callout is rendered
+// outside of the root and inherits nothing from it.
+@each $role, $tokens in $bit-color-roles {
+ .bit-dtp-#{$role} {
+ --bit-dtp-clr: #{role($tokens, main)};
+ --bit-dtp-clr-txt: #{role($tokens, on)};
+ --bit-dtp-clr-hover: #{role($tokens, hover)};
+ --bit-dtp-clr-active: #{role($tokens, active)};
+ }
+}
+
+
+.bit-dtp-sm {
+ --bit-dtp-inp-h: #{spacing(3.25)};
+ --bit-dtp-inp-fs: #{spacing(1.5)};
+ --bit-dtp-cell-size: #{spacing(3)};
+ --bit-dtp-cell-fs: #{spacing(1.375)};
+ --bit-dtp-lbl-fs: #{spacing(1.5)};
+}
+
+.bit-dtp-md {
+ @include dtp-size-medium;
+}
+
+.bit-dtp-lg {
+ --bit-dtp-inp-h: #{spacing(5)};
+ --bit-dtp-inp-fs: #{spacing(2)};
+ --bit-dtp-cell-size: #{spacing(4.25)};
+ --bit-dtp-cell-fs: #{spacing(1.75)};
+ --bit-dtp-lbl-fs: #{spacing(2)};
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePickerClassStyles.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePickerClassStyles.cs
index cb626523718..d9fb680713d 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePickerClassStyles.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePickerClassStyles.cs
@@ -52,6 +52,18 @@ public class BitDatePickerClassStyles
///
public string? CalloutContainer { get; set; }
+ ///
+ /// Custom CSS classes/styles for the header wrapper of the callout of the BitDatePicker,
+ /// rendered when a CalloutHeaderTemplate is provided.
+ ///
+ public string? CalloutHeader { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the footer wrapper of the callout of the BitDatePicker,
+ /// rendered when a CalloutFooterTemplate is provided.
+ ///
+ public string? CalloutFooter { get; set; }
+
///
/// Custom CSS classes/styles for the group of the BitDatePicker.
///
@@ -137,6 +149,11 @@ public class BitDatePickerClassStyles
///
public string? NextMonthNavIcon { get; set; }
+ ///
+ /// Custom CSS classes/styles for the grid of the days of the BitDatePicker.
+ ///
+ public string? DaysGrid { get; set; }
+
///
/// Custom CSS classes/styles for the header row of the days of the BitDatePicker.
///
@@ -147,6 +164,11 @@ public class BitDatePickerClassStyles
///
public string? WeekNumbersHeader { get; set; }
+ ///
+ /// Custom CSS classes/styles for the header cells of the day names of the BitDatePicker.
+ ///
+ public string? DayNameHeader { get; set; }
+
///
/// Custom CSS classes/styles for each row of the days of the BitDatePicker.
///
@@ -172,6 +194,11 @@ public class BitDatePickerClassStyles
///
public string? SelectedDayButton { get; set; }
+ ///
+ /// Custom CSS classes/styles for the highlighted day buttons of the BitDatePicker.
+ ///
+ public string? HighlightedDayButton { get; set; }
+
///
/// Custom CSS classes/styles for the time-picker's input container of the BitDatePicker.
///
diff --git a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalendarsJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalendarsJsRuntimeExtensions.cs
index 89541de580c..0dc295d12fa 100644
--- a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalendarsJsRuntimeExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalendarsJsRuntimeExtensions.cs
@@ -2,9 +2,9 @@ namespace Bit.BlazorUI;
internal static class CalendarsJsRuntimeExtensions
{
- internal static ValueTask BitCalendarsSetup(this IJSRuntime jsRuntime, string id)
+ internal static ValueTask BitCalendarsSetup(this IJSRuntime jsRuntime, string id, bool trapFocus = false)
{
- return jsRuntime.InvokeVoid("BitBlazorUI.Calendars.setup", id);
+ return jsRuntime.InvokeVoid("BitBlazorUI.Calendars.setup", id, trapFocus);
}
internal static ValueTask BitCalendarsDispose(this IJSRuntime jsRuntime, string id)
@@ -12,8 +12,8 @@ internal static ValueTask BitCalendarsDispose(this IJSRuntime jsRuntime, string
return jsRuntime.InvokeVoid("BitBlazorUI.Calendars.dispose", id);
}
- internal static ValueTask BitCalendarsFocusDay(this IJSRuntime jsRuntime, string dayId)
+ internal static ValueTask BitCalendarsFocusCell(this IJSRuntime jsRuntime, string cellId)
{
- return jsRuntime.InvokeVoid("BitBlazorUI.Calendars.focusDay", dayId);
+ return jsRuntime.InvokeVoid("BitBlazorUI.Calendars.focusCell", cellId);
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/Calendars.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/Calendars.ts
index cccc8d5193b..d011c49b5ad 100644
--- a/src/BlazorUI/Bit.BlazorUI/Scripts/Calendars.ts
+++ b/src/BlazorUI/Bit.BlazorUI/Scripts/Calendars.ts
@@ -4,20 +4,35 @@ namespace BitBlazorUI {
private static _navKeys = ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'];
- // Attaches a keydown listener that only prevents the default behavior (page scrolling) of the
- // navigation keys pressed on the day buttons. The actual keyboard logic runs in the Blazor
- // keydown handlers, which cannot conditionally preventDefault per key.
- public static setup(id: string) {
+ // The cells of every calendar grid that navigates with the keyboard: the day buttons of the
+ // standalone BitCalendar and of the day picker inside the callout of a BitDatePicker, plus the
+ // month and year buttons of that same callout.
+ private static _cells = '.bit-cal-dbt, .bit-dtp-dbt, .bit-dtp-pkb';
+
+ // Everything that can hold the focus inside a calendar. The roving tabindex of the grids takes
+ // every cell but one out of the tab sequence, which is why tabindex="-1" is excluded here.
+ private static _focusables = 'button:not([disabled]):not([tabindex="-1"]), input:not([disabled]):not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])';
+
+ // Attaches a keydown listener that prevents the default behavior (page scrolling) of the
+ // navigation keys pressed on the grid cells - the actual keyboard logic runs in the Blazor
+ // keydown handlers, which cannot conditionally preventDefault per key - and, for a calendar that
+ // is a modal dialog, keeps Tab and Shift+Tab cycling inside it as the dialog pattern requires.
+ public static setup(id: string, trapFocus: boolean) {
Calendars.dispose(id);
const root = document.getElementById(id);
if (!root) return;
const handler = (e: KeyboardEvent) => {
+ if (trapFocus && e.key === 'Tab') {
+ Calendars.wrapFocus(root, e);
+ return;
+ }
+
if (Calendars._navKeys.indexOf(e.key) === -1) return;
const target = e.target as HTMLElement | null;
- if (!target || !target.closest('.bit-cal-dbt')) return;
+ if (!target || !target.closest(Calendars._cells)) return;
e.preventDefault();
};
@@ -34,8 +49,27 @@ namespace BitBlazorUI {
Calendars._handlers.delete(id);
}
- public static focusDay(dayId: string) {
- document.getElementById(dayId)?.focus();
+ public static focusCell(cellId: string) {
+ document.getElementById(cellId)?.focus();
+ }
+
+ private static wrapFocus(root: HTMLElement, e: KeyboardEvent) {
+ const focusables = Array.from(root.querySelectorAll(Calendars._focusables))
+ .filter(el => el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0);
+
+ if (focusables.length === 0) return;
+
+ const first = focusables[0];
+ const last = focusables[focusables.length - 1];
+ const active = document.activeElement;
+
+ if (e.shiftKey && active === first) {
+ last.focus();
+ e.preventDefault();
+ } else if (!e.shiftKey && active === last) {
+ first.focus();
+ e.preventDefault();
+ }
}
}
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor
index 2c37d74b792..f554ac8370f 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor
@@ -1,20 +1,28 @@
-@page "/components/datepicker"
+@page "/components/datepicker"
@page "/components/date-picker"
@using Bit.BlazorUI.Demo.Client.Core.Helpers
+ Description="A BitDatePicker offers a drop-down calendar for picking a date, or a date and time, with any culture and time zone and full keyboard accessibility." />
-
Explore basic configurations of the BitDatePicker, including labels, placeholders, week numbers, and highlighting features.
+
+ The essential BitDatePicker configurations: the default picker, the disabled and required states,
+ a placeholder, week numbers, the highlighting of the current and the selected month, the built-in
+ time picker, the clear and close buttons, keeping the callout open after a selection
+ (AutoClose), clearing the value by selecting the selected day again (AllowDeselect),
+ the date the calendar opens on when there is no value yet
+ (StartingValue), and overriding the current date with Today.
+
@@ -33,12 +41,26 @@
+
+
+
+
+
+
+
+
-
Set minimum and maximum selectable dates to restrict user input to a specific range of days, months, or years.
+
+ Limit the selectable dates using MinDate and MaxDate. Days outside of the range are
+ disabled, the month, year, and year-range navigation stops at the boundaries, and a date typed into
+ the input outside of the range (when AllowTextInput is enabled) is reported as a validation error.
+ The DisablePast and DisableFuture shortcuts bound the range by today, exactly as a
+ MinDate or MaxDate of today would, and combine with them (the narrower bound wins).
+
Min: Now.AddDays(-5)
@@ -52,11 +74,114 @@
Min: Now.AddYears(-5)
Max: Now.AddYears(+1)
+
+
+
+
+
+
+
+
+
+ Disable specific dates beyond MinDate and MaxDate: pass an explicit list using DisabledDates,
+ disable whole days of the week using DisabledDaysOfWeek (e.g. weekends), or provide any custom
+ rule using the IsDateDisabled function. Disabled dates cannot be selected and are skipped by
+ the keyboard navigation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mark important dates using HighlightedDates, or provide custom CSS classes for any day using
+ the GetDayClass function, which receives each rendered day and returns the classes to add to it.
+ The highlight of today's day can also be turned off using HighlightToday; the day keeps its
+ accessibility semantics and only loses the visual accent.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customize how the weeks are rendered: override the first day of the week using FirstDayOfWeek,
+ choose the week numbering rule using WeekNumberRule (ISO 8601 is the FirstFourDayWeek rule with
+ Monday as the first day of the week), hide the days of the adjacent months using ShowOutsideDays,
+ and keep the height of the calendar fixed using FixedWeeks.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Control the month and year picker that sits beside the day picker: hide it using
+ IsMonthPickerVisible, or render it as an overlay on top of the day picker (reached through the
+ month title button) using ShowMonthPickerAsOverlay. On screens too narrow for both panels the
+ overlay mode is turned on automatically.
+
+
+
+
+
+
+
+
+
+
-
-
Adjust the increment step for hours and minutes when using the BitDatePicker's time selection feature.
+
+
+ Enable the built-in time picker using ShowTimePicker to let users pick the time of day along with
+ the date. Switch between the 24-hour and the 12-hour clock using TimeFormat, render the time
+ picker as an overlay (reached through the clock button) using ShowTimePickerAsOverlay, and hide
+ the GoToNow button using ShowGoToNow. While the time picker is on screen, selecting a day keeps
+ the callout open so the time can be set as well.
+
+
+
+
+
Selected DateTime: @selectedDateTime.ToString()
+
+
+
+
+
+
+
+
+
+
+
+ Customize the increment/decrement step of the hour and minute buttons of the time picker using
+ HourStep and MinuteStep.
+
-
+
- Applications can customize how dates are formatted and parsed. Formatted dates can be ambiguous, so the control will avoid parsing the formatted strings of dates selected using the UI when text input is allowed.
- In this example, we are formatting and parsing dates as dd=MM(yy).
+ Applications can customize how dates are formatted and parsed using DateFormat. The same pattern
+ is used to render the value in the input and to parse what the user types, so a custom format takes
+ effect in both directions. Without it, the short date pattern of the culture is used (plus the time
+ pattern when the time picker is enabled).
-
+
- The input field will open the BitDatePicker, and clicking the field again will dismiss the BitDatePicker and
- allow text input. Please note to use this feature, you must enter the date in the exact DateFormat provided for the BitDatePicker.
+ AllowTextInput lets the date be typed into the input instead of being picked from the calendar.
+ The typed text has to match the DateFormat of the component; anything else, or a date
+ outside of the Min/Max range, is reported as a validation error (customizable through
+ InvalidErrorMessage and OutOfRangeErrorMessage).
+
+
-
-
Bind a selected date to a model or view, allowing two-way data binding with the BitDatePicker.
+
+
+ Two-way bind the selected date using @@bind-Value to keep the model and the picker in sync, or,
+ in uncontrolled mode, set the initial date using DefaultValue and follow the changes through the
+ OnChange callback.
+
-
+
Selected date: @selectedDate.ToString()
+
+
+
+
+
+
Changed date: @(changedDate?.ToString() ?? "-")
-
+
- By default, BitDatePicker picks the current culture. But you can provide your own instance of CultureInfo for any custom culture.
+ By default, BitDatePicker uses the current culture of the app, but any custom CultureInfo can be
+ provided to fully localize the calendar: the month and day names, the first day of the week, the date
+ formats, and even non-Gregorian calendars such as the Persian calendar.
You also can use our
@@ -125,18 +277,20 @@
-
+
- Specifies the timezone used to interpret and display the selected date/time.
+ Specifies the time zone used to interpret and display the selected date and time. Everything the
+ calendar shows - the highlighted day, the month it opens on, and the time in the time picker - is
+ resolved in that zone.
- Remeber using this feature in different runtimes needs more investigations, for example,
- there are different data available based on the OS the code is running on or different settings
+ Remember that using this feature across different runtimes needs more investigation; for example,
+ different time zone data is available based on the OS the code is running on, or different settings
enabled for the project (like InvariantTimezone in the project file,
more info).
-
Defalt (local TimeZone):
+
Default (local TimeZone):
@@ -154,7 +308,7 @@
@if (timeZoneInfo is not null) {
-
"@timeZoneInfo.Id" TimeZone:
+
"@timeZoneInfo.Id" TimeZone:
@@ -163,8 +317,12 @@
}
-
-
Use the BitDatePicker in a standalone mode, allowing it to function independently without a surrounding form or container.
+
+
+ The Standalone mode renders the calendar inline, without the input and the callout around it,
+ which suits a form or a panel that has room for the calendar itself. The value still takes part in the
+ surrounding EditForm through a hidden input.
+
@@ -179,41 +337,49 @@
-
-
Use the BitDatePicker in MonthPicker mode to allow users to select only month and year. The day is automatically set to the 1st of the selected month.
+
+
+ The MonthPicker mode turns the component into a month and year picker: the day picker is not
+ rendered, the input shows the year/month pattern of the culture, and picking a month selects its first
+ day (or the first day the Min/Max range allows).
+
The ReadOnly parameter makes the date picker input non-editable, preventing users from manually changing the time value.
+
+
+ The ReadOnly parameter shows the value and lets the calendar be browsed, but refuses every change
+ to it: the days, the time picker, and the clear button are all inert, and the input cannot be typed into
+ even where AllowTextInput is set.
+
@@ -228,8 +394,14 @@
-
-
Utilize various templates within the BitDatePicker, such as custom label templates, day cell templates, and year cell templates.
+
+
+ Take full control of the rendering using LabelTemplate for the label, IconTemplate for the
+ icon of the input, and DayCellTemplate, MonthCellTemplate, and YearCellTemplate for
+ the content of the day, month, and year cells. Arbitrary content can also be rendered above and below
+ the pickers of the callout using CalloutHeaderTemplate and CalloutFooterTemplate - the
+ footer is a natural place for preset buttons that set the value from the code.
+
Enable responsive design for the BitDatePicker, allowing it to adjust its layout and appearance based on the screen size.
+
+
+ The Responsive mode turns the callout into a full-width panel on small screens, sliding in from
+ the top of the viewport and dismissible with a swipe, which gives the calendar the room it needs on a
+ phone. Resize the window to see it in action.
+
-
-
-
Implement form validation with BitDatePicker using data annotations, ensuring the user selects a valid date before submitting the form.
+
+
+ OnSelectDate reports every date the user picks, OnMonthChange reports the first day of the
+ month the calendar navigates to (through the nav buttons, the month/year pickers, GoToToday, or the
+ keyboard), which is ideal for lazy-loading the data of the visible month, OnClick,
+ OnFocusIn, and OnFocusOut report the interactions with the input, and OnClear
+ reports the value getting cleared by the clear button.
+
+ BitDatePicker follows the WAI-ARIA combobox and grid patterns. From the input, ↓ or ↑
+ opens the calendar and moves the focus into the grid it opens on; Esc closes it and brings the
+ focus back to the input. Inside the day grid:
+
+
+
← / →: previous/next day
+
↑ / ↓: same day of the previous/next week
+
Home / End: first/last day of the week
+
PageUp / PageDown: same day of the previous/next month
+
Shift+PageUp / Shift+PageDown: same day of the previous/next year
+
Enter / Space: select the focused day
+
+
+ The month and the year grids answer the same keys, four cells to a row: the arrows move between the
+ cells, Home and End jump to the first and the last of them, and
+ PageUp/PageDown move to the adjacent year (in the month grid) or to the adjacent range
+ of years (in the year grid).
+
+ Crossing a month boundary navigates the calendar and keeps the focus on the day, switching between the
+ pickers takes the focus along with it, and disabled days, months, and years are all skipped.
+
+
+
+
+
+
+
+
+
+
+
+ Use BitDatePicker inside an EditForm and validate the selected date with data annotations, just
+ like any other input component. The invalid state is announced through aria-invalid and is
+ styled on the input as well.
+
@@ -306,7 +561,92 @@
-
+
+
+ The callout can be driven from the outside as well: two-way bind IsOpen to open and close
+ it from the code, or call the OpenCallout and CloseCalloutAndFocus methods on a
+ reference to the component - the first opens the callout exactly as clicking the input would, and
+ the second closes it and hands the focus back to the input. A one-way bound IsOpen keeps
+ the callout under the control of the application: the component reports the change but never
+ closes on its own.
+
+
+
+
+
+
IsOpen: @isCalloutOpen
+
+ OpenCallout()
+
+ CloseCalloutAndFocus()
+
+
+
+
+
+
+ Adapt the input to the surrounding form: Underlined replaces the box with a single
+ underline, HasBorder removes the border altogether, IconLocation moves the calendar
+ icon to the other side of the input, and IconName replaces it with any icon of the built-in
+ Fluent UI set.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Offering a range of specialized color variants with Primary being the default, providing visual cues for
+ specific actions or states within your application. The color applies to the today day button, the
+ highlighted current month, and the selected AM/PM button.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Use icons from external libraries like FontAwesome, Material Icons, and Bootstrap Icons with the Icon parameter.
@@ -374,8 +714,27 @@
-
-
Explore styling and class customization for BitDatePicker, including component styles, custom classes, and detailed styles.
+
+
+ The Size parameter scales the input and the whole calendar - the label, the day cells, and the
+ navigation buttons - between three sizes, with Medium being the default.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customize the appearance of BitDatePicker: use Style and Class on the root element, or
+ target every individual part of the component - the input, the callout, the day cells, the time picker -
+ using the Styles and Classes parameters.
+
Component's Style & Class:
@@ -403,6 +762,7 @@
-
-
Use BitDatePicker in right-to-left (RTL).
+
+
+ Render BitDatePicker in right-to-left (RTL) using the Dir parameter. Providing an RTL culture
+ (like fa-IR) enables it automatically, and the keyboard navigation of the day grid follows the direction
+ as well.
+
@@ -430,4 +795,4 @@
-
\ No newline at end of file
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.cs
index 12c1087cefe..12ea04c8e06 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.cs
@@ -2,8 +2,31 @@
public partial class BitDatePickerDemo
{
+ private readonly List componentPublicMembers =
+ [
+ new()
+ {
+ Name = "OpenCallout",
+ Type = "Task",
+ Description = "Opens the callout of the DatePicker exactly as clicking its input would."
+ },
+ new()
+ {
+ Name = "CloseCalloutAndFocus",
+ Type = "Task",
+ Description = "Closes the callout of the DatePicker and moves the focus back to its input."
+ },
+ ];
+
private readonly List componentParameters =
[
+ new()
+ {
+ Name = "AllowDeselect",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Whether selecting the already selected date deselects it, clearing the value. The callout stays open after a deselection, so another date can be picked right away."
+ },
new()
{
Name = "AllowTextInput",
@@ -16,7 +39,7 @@ public partial class BitDatePickerDemo
Name = "AutoClose",
Type = "bool",
DefaultValue = "true",
- Description = "Whether the DatePicker closes automatically after selecting the date."
+ Description = "Whether the DatePicker closes automatically after selecting the date. It has no effect while the time picker is shown, where the callout stays open so the time of the selected day can be set as well."
},
new()
{
@@ -26,6 +49,20 @@ public partial class BitDatePickerDemo
Description = "Aria label of the DatePicker's callout for screen readers."
},
new()
+ {
+ Name = "CalloutFooterTemplate",
+ Type = "RenderFragment?",
+ DefaultValue = "null",
+ Description = "Custom template to render at the bottom of the DatePicker's callout, below the pickers (e.g. preset buttons that set the value from the code)."
+ },
+ new()
+ {
+ Name = "CalloutHeaderTemplate",
+ Type = "RenderFragment?",
+ DefaultValue = "null",
+ Description = "Custom template to render at the top of the DatePicker's callout, above the pickers."
+ },
+ new()
{
Name = "CalloutHtmlAttributes",
Type = "Dictionary",
@@ -58,6 +95,13 @@ public partial class BitDatePickerDemo
Description = "The name of the clear button's icon from the built-in Fluent UI icon set."
},
new()
+ {
+ Name = "ClearButtonTitle",
+ Type = "string",
+ DefaultValue = "Clear date",
+ Description = "The title (tooltip) and the accessible name of the clear button."
+ },
+ new()
{
Name = "CloseButtonIcon",
Type = "BitIconInfo?",
@@ -81,6 +125,15 @@ public partial class BitDatePickerDemo
Description = "The title of the CloseDatePicker button (tooltip).",
},
new()
+ {
+ Name = "Color",
+ Type = "BitColor?",
+ DefaultValue = "null",
+ Description = "The general color of the DatePicker that applies to the today day button, the highlighted current month, and the selected AM/PM button.",
+ LinkType = LinkType.Link,
+ Href = "#color-enum"
+ },
+ new()
{
Name = "Culture",
Type = "CultureInfo",
@@ -102,6 +155,71 @@ public partial class BitDatePickerDemo
Description = "Custom template to render the day cells of the DatePicker."
},
new()
+ {
+ Name = "DisabledDateErrorMessage",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The custom validation error message for a typed value that the DatePicker does not allow to be selected, through DisabledDates, DisabledDaysOfWeek or IsDateDisabled."
+ },
+ new()
+ {
+ Name = "DisabledDates",
+ Type = "IEnumerable?",
+ DefaultValue = "null",
+ Description = "The list of dates that are disabled (not selectable) in the DatePicker, in addition to MinDate and MaxDate."
+ },
+ new()
+ {
+ Name = "DisabledDaysOfWeek",
+ Type = "IEnumerable?",
+ DefaultValue = "null",
+ Description = "The days of the week that are disabled (not selectable) in the DatePicker (e.g. weekends)."
+ },
+ new()
+ {
+ Name = "DisableFuture",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Disables all days after today, exactly as a MaxDate of today would. When both are set, the earlier of the two bounds wins."
+ },
+ new()
+ {
+ Name = "DisablePast",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Disables all days before today, exactly as a MinDate of today would. When both are set, the later of the two bounds wins."
+ },
+ new()
+ {
+ Name = "DropDirection",
+ Type = "BitDropDirection",
+ DefaultValue = "BitDropDirection.TopAndBottom",
+ Description = "Determines the allowed drop directions of the callout.",
+ LinkType = LinkType.Link,
+ Href = "#drop-direction-enum"
+ },
+ new()
+ {
+ Name = "FirstDayOfWeek",
+ Type = "DayOfWeek?",
+ DefaultValue = "null",
+ Description = "Overrides the first day of the week of the day picker. If not set, the first day of the week of the Culture is used."
+ },
+ new()
+ {
+ Name = "FixedWeeks",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Whether the day picker should always render six weeks, filling the extra rows with the days of the adjacent months, to keep the height of the calendar fixed while navigating between the months."
+ },
+ new()
+ {
+ Name = "GetDayClass",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Custom function to provide additional CSS classes for each day button of the DatePicker."
+ },
+ new()
{
Name = "GoToNextMonthTitle",
Type = "string",
@@ -193,7 +311,7 @@ public partial class BitDatePickerDemo
{
Name = "HasBorder",
Type = "bool",
- DefaultValue = "false",
+ DefaultValue = "true",
Description = "Determines if the DatePicker has a border."
},
new()
@@ -227,6 +345,13 @@ public partial class BitDatePickerDemo
Description = "Whether the month picker should highlight the current month."
},
new()
+ {
+ Name = "HighlightedDates",
+ Type = "IEnumerable?",
+ DefaultValue = "null",
+ Description = "The list of dates that are highlighted (marked) in the day picker."
+ },
+ new()
{
Name = "HighlightSelectedMonth",
Type = "bool",
@@ -234,6 +359,13 @@ public partial class BitDatePickerDemo
Description = "Whether the month picker should highlight the selected month."
},
new()
+ {
+ Name = "HighlightToday",
+ Type = "bool",
+ DefaultValue = "true",
+ Description = "Whether the day picker should highlight today's day. It only affects the visual style of the day cell; the accessibility attributes still report the day as the current date."
+ },
+ new()
{
Name = "HourStep",
Type = "int",
@@ -280,11 +412,18 @@ public partial class BitDatePickerDemo
Description = "The custom validation error message for the invalid value."
},
new()
+ {
+ Name = "IsDateDisabled",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Custom function to determine if a specific date is disabled (not selectable) in the DatePicker."
+ },
+ new()
{
Name = "IsMonthPickerVisible",
Type = "bool",
DefaultValue = "true",
- Description = "Whether the month picker is shown or hidden."
+ Description = "Whether the month picker is shown next to the day picker or hidden. It has no effect in the MonthPicker mode, where the month picker is the only view."
},
new()
{
@@ -400,6 +539,12 @@ public partial class BitDatePickerDemo
Description = "The name of the next-year-range navigation button's icon from the built-in Fluent UI icon set."
},
new()
+ {
+ Name = "OnClear",
+ Type = "EventCallback",
+ Description = "The callback that is called when the value gets cleared by the clear button."
+ },
+ new()
{
Name = "OnClick",
Type = "EventCallback",
@@ -424,6 +569,25 @@ public partial class BitDatePickerDemo
Description = "The callback for when the focus moves out of the DatePicker's input."
},
new()
+ {
+ Name = "OnMonthChange",
+ Type = "EventCallback",
+ Description = "The callback for when the displayed month of the day picker changes. The argument is the first day of the newly displayed month."
+ },
+ new()
+ {
+ Name = "OnSelectDate",
+ Type = "EventCallback",
+ Description = "The callback for when the user selects a date."
+ },
+ new()
+ {
+ Name = "OutOfRangeErrorMessage",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The custom validation error message for a typed value that falls outside of the MinDate and MaxDate range."
+ },
+ new()
{
Name = "Placeholder",
Type = "string",
@@ -528,6 +692,13 @@ public partial class BitDatePickerDemo
Description = "Show month picker on top of date picker when visible."
},
new()
+ {
+ Name = "ShowOutsideDays",
+ Type = "bool",
+ DefaultValue = "true",
+ Description = "Whether the days of the previous and next months should be shown in the day picker."
+ },
+ new()
{
Name = "ShowTimePicker",
Type = "bool",
@@ -572,6 +743,15 @@ public partial class BitDatePickerDemo
Description = "Whether the week number (weeks 1 to 53) should be shown before each week row."
},
new()
+ {
+ Name = "Size",
+ Type = "BitSize?",
+ DefaultValue = "null",
+ Description = "The size of the DatePicker.",
+ LinkType = LinkType.Link,
+ Href = "#size-enum"
+ },
+ new()
{
Name = "Standalone",
Type = "bool",
@@ -636,6 +816,13 @@ public partial class BitDatePickerDemo
Description = "The name of the time-picker's decrease-minute button icon from the built-in Fluent UI icon set."
},
new()
+ {
+ Name = "TimePickerHourTitle",
+ Type = "string",
+ DefaultValue = "Hour",
+ Description = "The title (tooltip) and the accessible name of the time-picker's hour input."
+ },
+ new()
{
Name = "TimePickerIncreaseHourIcon",
Type = "BitIconInfo?",
@@ -668,6 +855,13 @@ public partial class BitDatePickerDemo
Description = "The name of the time-picker's increase-minute button icon from the built-in Fluent UI icon set."
},
new()
+ {
+ Name = "TimePickerMinuteTitle",
+ Type = "string",
+ DefaultValue = "Minute",
+ Description = "The title (tooltip) and the accessible name of the time-picker's minute input."
+ },
+ new()
{
Name = "TimeZone",
Type = "TimeZoneInfo?",
@@ -675,6 +869,13 @@ public partial class BitDatePickerDemo
Description = "TimeZone for the DatePicker."
},
new()
+ {
+ Name = "Today",
+ Type = "DateTimeOffset?",
+ DefaultValue = "null",
+ Description = "Overrides the current date and time considered as \"today\" and \"now\" in the DatePicker (useful for testing or custom time providers)."
+ },
+ new()
{
Name = "Underlined",
Type = "bool",
@@ -682,6 +883,13 @@ public partial class BitDatePickerDemo
Description = "Whether or not the Text field of the DatePicker is underlined."
},
new()
+ {
+ Name = "WeekNumberRule",
+ Type = "CalendarWeekRule?",
+ DefaultValue = "null",
+ Description = "The rule used to calculate the week numbers. Defaults to the FirstFullWeek rule."
+ },
+ new()
{
Name = "WeekNumberTitle",
Type = "string",
@@ -738,7 +946,7 @@ public partial class BitDatePickerDemo
Name = "Label",
Type = "string?",
DefaultValue = "null",
- Description = "Custom CSS classes/styles for the Label of the BitDatePicker."
+ Description = "Custom CSS classes/styles for the label of the BitDatePicker."
},
new()
{
@@ -755,13 +963,6 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the input container of the BitDatePicker."
},
new()
- {
- Name = "InputContainer",
- Type = "string?",
- DefaultValue = "null",
- Description = "Custom CSS classes/styles for the input container of the BitDatePicker."
- },
- new()
{
Name = "Input",
Type = "string?",
@@ -797,6 +998,20 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the callout container of the BitDatePicker."
},
new()
+ {
+ Name = "CalloutHeader",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the header wrapper of the callout of the BitDatePicker, rendered when a CalloutHeaderTemplate is provided."
+ },
+ new()
+ {
+ Name = "CalloutFooter",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the footer wrapper of the callout of the BitDatePicker, rendered when a CalloutFooterTemplate is provided."
+ },
+ new()
{
Name = "Group",
Type = "string?",
@@ -853,6 +1068,27 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the Go to today button of the BitDatePicker."
},
new()
+ {
+ Name = "GoToNowButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the Go to now button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "HideTimePickerButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the hide time-picker button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "HideTimePickerIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the hide time-picker icon of the BitDatePicker."
+ },
+ new()
{
Name = "GoToTodayIcon",
Type = "string?",
@@ -860,6 +1096,13 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the Go to today icon of the BitDatePicker."
},
new()
+ {
+ Name = "GoToNowIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the Go to now icon of the BitDatePicker."
+ },
+ new()
{
Name = "CloseButton",
Type = "string?",
@@ -888,6 +1131,13 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the Go to next month icon of the BitDatePicker."
},
new()
+ {
+ Name = "DaysGrid",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the grid of the days of the BitDatePicker."
+ },
+ new()
{
Name = "DaysHeaderRow",
Type = "string?",
@@ -902,6 +1152,13 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the header of the week numbers of the BitDatePicker."
},
new()
+ {
+ Name = "DayNameHeader",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the header cells of the day names of the BitDatePicker."
+ },
+ new()
{
Name = "DaysRow",
Type = "string?",
@@ -938,10 +1195,31 @@ public partial class BitDatePickerDemo
},
new()
{
- Name = "TimePickerContainer",
+ Name = "HighlightedDayButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the highlighted day buttons of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimeInputContainer",
Type = "string?",
DefaultValue = "null",
- Description = "Custom CSS classes/styles for the time-picker's main container of the BitDatePicker."
+ Description = "Custom CSS classes/styles for the time-picker's input container of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "HourInputContainer",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's hour input container of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "MinuteInputContainer",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's minute input container of the BitDatePicker."
},
new()
{
@@ -952,17 +1230,17 @@ public partial class BitDatePickerDemo
},
new()
{
- Name = "TimePickerHourMinuteSeparator",
+ Name = "TimePickerHourInput",
Type = "string?",
DefaultValue = "null",
- Description = "Custom CSS classes/styles for the time-picker's hour/minute separator of the BitDatePicker."
+ Description = "Custom CSS classes/styles for the time-picker's hour input of the BitDatePicker."
},
new()
{
- Name = "TimePickerDivider",
+ Name = "TimePickerHourMinuteSeparator",
Type = "string?",
DefaultValue = "null",
- Description = "Custom CSS classes/styles for the time-picker's divider of the BitDatePicker."
+ Description = "Custom CSS classes/styles for the time-picker's hour/minute separator of the BitDatePicker."
},
new()
{
@@ -972,6 +1250,62 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the time-picker's minute input of the BitDatePicker."
},
new()
+ {
+ Name = "TimePickerIncreaseHourButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's increase hour button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerIncreaseHourIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's increase hour icon of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerDecreaseHourButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's decrease hour button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerDecreaseHourIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's decrease hour icon of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerIncreaseMinuteButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's increase minute button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerIncreaseMinuteIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's increase minute icon of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerDecreaseMinuteButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's decrease minute button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "TimePickerDecreaseMinuteIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's decrease minute icon of the BitDatePicker."
+ },
+ new()
{
Name = "TimePickerAmPmContainer",
Type = "string?",
@@ -1014,6 +1348,13 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the month-picker's header of the BitDatePicker."
},
new()
+ {
+ Name = "TimePickerHeader",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the time-picker's header of the BitDatePicker."
+ },
+ new()
{
Name = "YearPickerToggleButton",
Type = "string?",
@@ -1021,6 +1362,20 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the year-picker's toggle button of the BitDatePicker."
},
new()
+ {
+ Name = "ShowTimePickerButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the show time-picker button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "ShowTimePickerIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the show time-picker icon of the BitDatePicker."
+ },
+ new()
{
Name = "MonthPickerNavWrapper",
Type = "string?",
@@ -1028,6 +1383,13 @@ public partial class BitDatePickerDemo
Description = "Custom CSS classes/styles for the wrapper of the month-picker's nav buttons of the BitDatePicker."
},
new()
+ {
+ Name = "TimePickerNavWrapper",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the wrapper of the time-picker's nav buttons of the BitDatePicker."
+ },
+ new()
{
Name = "PrevYearNavButton",
Type = "string?",
@@ -1130,7 +1492,7 @@ public partial class BitDatePickerDemo
Name = "YearsContainer",
Type = "string?",
DefaultValue = "null",
- Description = "Custom CSS classes/styles for the years container of the BitDatePicker."
+ Description = "Custom CSS classes/styles of the years container of the BitDatePicker."
},
new()
{
@@ -1145,6 +1507,20 @@ public partial class BitDatePickerDemo
Type = "string?",
DefaultValue = "null",
Description = "Custom CSS classes/styles for each year button of the BitDatePicker."
+ },
+ new()
+ {
+ Name = "ClearButton",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the BitDatePicker's clear button."
+ },
+ new()
+ {
+ Name = "ClearButtonIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the BitDatePicker's clear button icon."
}
]
},
@@ -1181,6 +1557,44 @@ public partial class BitDatePickerDemo
private readonly List componentSubEnums =
[
+ new()
+ {
+ Id = "color-enum",
+ Name = "BitColor",
+ Description = "Defines the general colors available in the bit BlazorUI.",
+ Items =
+ [
+ new() { Name = "Primary", Description = "Primary general color.", Value = "0" },
+ new() { Name = "Secondary", Description = "Secondary general color.", Value = "1" },
+ new() { Name = "Tertiary", Description = "Tertiary general color.", Value = "2" },
+ new() { Name = "Info", Description = "Info general color.", Value = "3" },
+ new() { Name = "Success", Description = "Success general color.", Value = "4" },
+ new() { Name = "Warning", Description = "Warning general color.", Value = "5" },
+ new() { Name = "SevereWarning", Description = "SevereWarning general color.", Value = "6" },
+ new() { Name = "Error", Description = "Error general color.", Value = "7" },
+ new() { Name = "PrimaryBackground", Description = "Primary background color.", Value = "8" },
+ new() { Name = "SecondaryBackground", Description = "Secondary background color.", Value = "9" },
+ new() { Name = "TertiaryBackground", Description = "Tertiary background color.", Value = "10" },
+ new() { Name = "PrimaryForeground", Description = "Primary foreground color.", Value = "11" },
+ new() { Name = "SecondaryForeground", Description = "Secondary foreground color.", Value = "12" },
+ new() { Name = "TertiaryForeground", Description = "Tertiary foreground color.", Value = "13" },
+ new() { Name = "PrimaryBorder", Description = "Primary border color.", Value = "14" },
+ new() { Name = "SecondaryBorder", Description = "Secondary border color.", Value = "15" },
+ new() { Name = "TertiaryBorder", Description = "Tertiary border color.", Value = "16" }
+ ]
+ },
+ new()
+ {
+ Id = "size-enum",
+ Name = "BitSize",
+ Description = "Defines the sizes available in the bit BlazorUI.",
+ Items =
+ [
+ new() { Name = "Small", Description = "The small size DatePicker.", Value = "0" },
+ new() { Name = "Medium", Description = "The medium size DatePicker.", Value = "1" },
+ new() { Name = "Large", Description = "The large size DatePicker.", Value = "2" }
+ ]
+ },
new()
{
Id = "component-visibility-enum",
@@ -1251,6 +1665,27 @@ public partial class BitDatePickerDemo
]
},
new()
+ {
+ Id = "drop-direction-enum",
+ Name = "BitDropDirection",
+ Description = "",
+ Items =
+ [
+ new()
+ {
+ Name = "All",
+ Description = "The direction determined automatically based on the available spaces in all directions.",
+ Value = "0"
+ },
+ new()
+ {
+ Name = "TopAndBottom",
+ Description = "Show the callout at the top or bottom side.",
+ Value = "1"
+ }
+ ]
+ },
+ new()
{
Id = "datepicker-mode-enum",
Name = "BitDatePickerMode",
@@ -1278,12 +1713,47 @@ public partial class BitDatePickerDemo
private DateTimeOffset? readOnlyDate = DateTimeOffset.Now;
private DateTimeOffset? selectedDate = new DateTimeOffset(2020, 1, 17, 0, 0, 0, DateTimeOffset.Now.Offset);
private DateTimeOffset? startingValue = new DateTimeOffset(2020, 12, 4, 20, 45, 0, DateTimeOffset.Now.Offset);
+ private DateTimeOffset? customToday = new DateTimeOffset(2021, 3, 15, 0, 0, 0, DateTimeOffset.Now.Offset);
private DateTimeOffset? timeZoneDate1;
private DateTimeOffset? timeZoneDate2;
private DateTimeOffset? classesValue;
private DateTimeOffset? monthPickerDate;
+ private DateTimeOffset? selectedDateTime;
+ private DateTimeOffset? changedDate;
+
+ private bool isMonthPickerVisible = true;
+ private bool showMonthPickerAsOverlay;
+
+ private bool isCalloutOpen;
+ private BitDatePicker? programmaticPicker;
+
+ private DateTimeOffset? presetsValue;
+ private BitDatePicker? presetsPicker;
+
+ private int clickCount;
+ private int clearCount;
+ private int focusInCount;
+ private int focusOutCount;
+ private DateTimeOffset? displayedMonth;
+ private DateTimeOffset? selectedDateEvent;
+
+ private readonly DayOfWeek[] weekendDays = [DayOfWeek.Friday, DayOfWeek.Saturday];
+
+ private readonly DateTimeOffset[] disabledDates =
+ [
+ DateTimeOffset.Now.AddDays(2),
+ DateTimeOffset.Now.AddDays(3),
+ DateTimeOffset.Now.AddDays(7)
+ ];
+
+ private readonly DateTimeOffset[] highlightedDates =
+ [
+ DateTimeOffset.Now.AddDays(1),
+ DateTimeOffset.Now.AddDays(5),
+ DateTimeOffset.Now.AddDays(10)
+ ];
private CultureInfo culture = CultureInfo.CurrentUICulture;
@@ -1303,4 +1773,14 @@ private void HandleInvalidSubmit()
{
SuccessMessage = string.Empty;
}
+
+ private async Task SelectPreset(int days)
+ {
+ presetsValue = DateTimeOffset.Now.Date.AddDays(days);
+
+ if (presetsPicker is not null)
+ {
+ await presetsPicker.CloseCalloutAndFocus();
+ }
+ }
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.samples.cs
index 24b24ed1ab8..215cb9d8828 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/DatePicker/BitDatePickerDemo.razor.samples.cs
@@ -1,4 +1,4 @@
-namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Inputs.DatePicker;
+namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Inputs.DatePicker;
public partial class BitDatePickerDemo
{
@@ -11,17 +11,97 @@ public partial class BitDatePickerDemo
-";
+
+
+
+
+";
private readonly string example1CsharpCode = @"
+private DateTimeOffset? customToday = new DateTimeOffset(2021, 3, 15, 0, 0, 0, DateTimeOffset.Now.Offset);
private DateTimeOffset? startingValue = new DateTimeOffset(2020, 12, 4, 20, 45, 0, DateTimeOffset.Now.Offset);";
private readonly string example2RazorCode = @"
-";
+
+
+
+";
private readonly string example3RazorCode = @"
+
+
+
+
+ d.Day % 2 == 1)"" />";
+ private readonly string example3CsharpCode = @"
+private readonly DayOfWeek[] weekendDays = [DayOfWeek.Friday, DayOfWeek.Saturday];
+
+private readonly DateTimeOffset[] disabledDates =
+[
+ DateTimeOffset.Now.AddDays(2),
+ DateTimeOffset.Now.AddDays(3),
+ DateTimeOffset.Now.AddDays(7)
+];";
+
+ private readonly string example4RazorCode = @"
+
+
+
+
+
+
+
+";
+ private readonly string example4CsharpCode = @"
+private readonly DateTimeOffset[] highlightedDates =
+[
+ DateTimeOffset.Now.AddDays(1),
+ DateTimeOffset.Now.AddDays(5),
+ DateTimeOffset.Now.AddDays(10)
+];";
+
+ private readonly string example5RazorCode = @"
+
+
+
+
+
+
+";
+
+ private readonly string example6RazorCode = @"
+
+
+
+
+";
+ private readonly string example6CsharpCode = @"
+private bool isMonthPickerVisible = true;
+private bool showMonthPickerAsOverlay;";
+
+ private readonly string example7RazorCode = @"
+
+