132 lines
5.1 KiB
Plaintext
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));
|
|
}
|
|
}
|