Files
Enciphered.Blazor.UIComponents/Enciphered.Blazor.UIComponents/Forms/DateTimeInput.razor
T
2026-04-13 15:08:49 +05:00

132 lines
5.1 KiB
Plaintext

@namespace Enciphered.Blazor.UIComponents
@inherits InputBase<DateTime?>
@* Hidden native input preserves form semantics, name, id, and data-testid for tests *@
<input type="datetime-local"
id="@Id"
name="@Name"
value="@FormatValue()"
class="sr-only"
tabindex="-1"
aria-hidden="true"
disabled="@Disabled"
@attributes="AdditionalAttributes" />
@* ── Two side-by-side triggers: date field + time field ── *@
<div class="flex gap-2">
@* ── Date portion ── *@
<Popover @ref="_datePopover">
<Trigger>
<button type="button"
disabled="@Disabled"
data-testid="@($"trigger-{Id}-date")"
class="@TriggerClass"
@onclick="() => _datePopover?.Toggle()">
@* Lucide calendar icon *@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="mr-2 shrink-0 text-muted-foreground">
<path d="M8 2v4" /><path d="M16 2v4" />
<rect width="18" height="18" x="3" y="4" rx="2" />
<path d="M3 10h18" />
</svg>
<span class="@(SelectedDateOnly.HasValue ? "" : "text-muted-foreground")">
@(SelectedDateOnly.HasValue ? SelectedDateOnly.Value.ToString("MMM d, yyyy") : (Placeholder ?? "Select date"))
</span>
</button>
</Trigger>
<Content>
<Calendar SelectedDate="@SelectedDateOnly" SelectedDateChanged="OnDatePartChanged" />
</Content>
</Popover>
@* ── Time portion ── *@
<Popover @ref="_timePopover">
<Trigger>
<button type="button"
disabled="@Disabled"
data-testid="@($"trigger-{Id}-time")"
class="@TriggerClass"
@onclick="() => _timePopover?.Toggle()">
@* Lucide clock icon *@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="mr-2 shrink-0 text-muted-foreground">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span class="@(SelectedTimeOnly.HasValue ? "" : "text-muted-foreground")">
@(SelectedTimeOnly.HasValue ? SelectedTimeOnly.Value.ToString("hh\\:mm tt") : "Select time")
</span>
</button>
</Trigger>
<Content>
<TimePicker SelectedTime="@SelectedTimeOnly" SelectedTimeChanged="OnTimePartChanged" Use12Hour="true" />
</Content>
</Popover>
</div>
@code {
[Parameter] public string? Min { get; set; }
[Parameter] public string? Max { get; set; }
/// <summary>
/// Step in seconds. Use "1" for second precision, "60" (default) for minutes only.
/// </summary>
[Parameter] public string? Step { get; set; }
private Popover? _datePopover;
private Popover? _timePopover;
private DateOnly? SelectedDateOnly =>
Value.HasValue ? DateOnly.FromDateTime(Value.Value) : null;
private TimeOnly? SelectedTimeOnly =>
Value.HasValue ? TimeOnly.FromDateTime(Value.Value) : null;
private string? FormatValue() =>
Value?.ToString("yyyy-MM-ddTHH:mm");
private string TriggerClass
{
get
{
const string baseClass =
"flex h-9 w-full items-center rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm " +
"transition-colors cursor-pointer text-left " +
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring " +
"disabled:cursor-not-allowed disabled:opacity-50";
var validation = GetTriggerValidationClass();
return string.IsNullOrEmpty(Class)
? $"{baseClass} {validation}"
: $"{baseClass} {validation} {Class}";
}
}
[CascadingParameter] private EditContext? _cascadedEditContext { get; set; }
private string GetTriggerValidationClass()
{
if (_cascadedEditContext is null || FieldId is not { } fi) return "border-input";
return _cascadedEditContext.GetValidationMessages(fi).Any()
? "border-destructive focus-visible:ring-destructive"
: "border-input";
}
private async Task OnDatePartChanged(DateOnly? date)
{
if (date is null) return;
var timePart = SelectedTimeOnly ?? new TimeOnly(0, 0);
await SetValueAsync(date.Value.ToDateTime(timePart));
_datePopover?.Close();
}
private async Task OnTimePartChanged(TimeOnly? time)
{
if (time is null) return;
var datePart = SelectedDateOnly ?? DateOnly.FromDateTime(DateTime.Today);
await SetValueAsync(datePart.ToDateTime(time.Value));
}
}