Migrate all interactive Blazor components to vanilla JS for full SSR
- Replace server interactivity with vanilla JS (forms.js) for Popover, Calendar, TimePicker, NumberInput, and Counter components - Rewrite all Razor components to static SSR using data-* attributes for JS hooks - Simplify InputBase.cs (remove EventCallback, EditContext, SetValueAsync) - Remove AddInteractiveServerComponents/AddInteractiveServerRenderMode from Program.cs - Update demo pages: remove @rendermode, replace EditForm with native form - Add InteractivityGapTests.cs with 30 scoped E2E tests - Update FormsTests.cs selectors for new static SSR structure - Fix year picker navigation bug and date format mismatch in forms.js - All 126 tests passing
This commit is contained in:
@@ -1,19 +1,50 @@
|
||||
@namespace Enciphered.Blazor.UIComponents
|
||||
|
||||
@* ── shadcn/ui-style time picker with scrollable columns ─────────────── *@
|
||||
<div class="flex items-stretch gap-1 p-4" @attributes="AdditionalAttributes">
|
||||
@* ── shadcn/ui-style time picker with scrollable columns (JS-driven) ── *@
|
||||
@{
|
||||
var use12 = Use12Hour;
|
||||
var minStep = MinuteStep < 1 ? 1 : MinuteStep;
|
||||
var selHour = 0;
|
||||
var selMinute = 0;
|
||||
var isPm = false;
|
||||
|
||||
if (SelectedTime is { } t)
|
||||
{
|
||||
if (use12)
|
||||
{
|
||||
isPm = t.Hour >= 12;
|
||||
selHour = t.Hour % 12;
|
||||
selMinute = t.Minute;
|
||||
}
|
||||
else
|
||||
{
|
||||
selHour = t.Hour;
|
||||
selMinute = t.Minute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<div class="flex items-stretch gap-1 p-4"
|
||||
data-timepicker
|
||||
data-selected-hour="@selHour"
|
||||
data-selected-minute="@selMinute"
|
||||
data-selected-pm="@(isPm ? "true" : "false")"
|
||||
data-use-12-hour="@(use12 ? "true" : "false")"
|
||||
data-linked-input="@LinkedInputId"
|
||||
@attributes="AdditionalAttributes">
|
||||
|
||||
@* ── Hour column ── *@
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-xs font-medium text-muted-foreground mb-2">Hr</span>
|
||||
<div class="h-52 w-16 overflow-y-auto flex flex-col gap-1 scrollbar-thin pr-1">
|
||||
@for (int h = 0; h < (_use12Hour ? 12 : 24); h++)
|
||||
@for (int h = 0; h < (use12 ? 12 : 24); h++)
|
||||
{
|
||||
var hour = _use12Hour ? (h == 0 ? 12 : h) : h;
|
||||
var hour = use12 ? (h == 0 ? 12 : h) : h;
|
||||
var hourValue = h;
|
||||
var isSelected = _selectedHour == hourValue;
|
||||
var isSelected = selHour == hourValue;
|
||||
<button type="button"
|
||||
class="@TimeItemClass(isSelected)"
|
||||
@onclick="() => SelectHour(hourValue)">
|
||||
data-tp-hour="@hourValue">
|
||||
@hour.ToString("D2")
|
||||
</button>
|
||||
}
|
||||
@@ -26,20 +57,20 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-xs font-medium text-muted-foreground mb-2">Min</span>
|
||||
<div class="h-52 w-16 overflow-y-auto flex flex-col gap-1 scrollbar-thin pr-1">
|
||||
@for (int m = 0; m < 60; m += _minuteStep)
|
||||
@for (int m = 0; m < 60; m += minStep)
|
||||
{
|
||||
var minute = m;
|
||||
var isSelected = _selectedMinute == minute;
|
||||
var isSel = selMinute == minute;
|
||||
<button type="button"
|
||||
class="@TimeItemClass(isSelected)"
|
||||
@onclick="() => SelectMinute(minute)">
|
||||
class="@TimeItemClass(isSel)"
|
||||
data-tp-minute="@minute">
|
||||
@minute.ToString("D2")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_use12Hour)
|
||||
@if (use12)
|
||||
{
|
||||
<div class="flex items-center px-1"></div>
|
||||
|
||||
@@ -48,13 +79,13 @@
|
||||
<span class="text-xs font-medium text-muted-foreground mb-2"> </span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<button type="button"
|
||||
class="@TimeItemClass(!_isPm)"
|
||||
@onclick='() => SelectPeriod(false)'>
|
||||
class="@TimeItemClass(!isPm)"
|
||||
data-tp-period="am">
|
||||
AM
|
||||
</button>
|
||||
<button type="button"
|
||||
class="@TimeItemClass(_isPm)"
|
||||
@onclick='() => SelectPeriod(true)'>
|
||||
class="@TimeItemClass(isPm)"
|
||||
data-tp-period="pm">
|
||||
PM
|
||||
</button>
|
||||
</div>
|
||||
@@ -63,9 +94,8 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>The currently selected time (two-way bindable).</summary>
|
||||
/// <summary>The currently selected time (for initial render).</summary>
|
||||
[Parameter] public TimeOnly? SelectedTime { get; set; }
|
||||
[Parameter] public EventCallback<TimeOnly?> SelectedTimeChanged { get; set; }
|
||||
|
||||
/// <summary>Use 12-hour format with AM/PM. Default is false (24-hour).</summary>
|
||||
[Parameter] public bool Use12Hour { get; set; }
|
||||
@@ -73,74 +103,13 @@
|
||||
/// <summary>Minute step interval. Default is 1.</summary>
|
||||
[Parameter] public int MinuteStep { get; set; } = 1;
|
||||
|
||||
/// <summary>The id of the linked hidden input to sync time value to.</summary>
|
||||
[Parameter] public string? LinkedInputId { get; set; }
|
||||
|
||||
/// <summary>Any extra HTML attributes.</summary>
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||
|
||||
private int _selectedHour;
|
||||
private int _selectedMinute;
|
||||
private bool _isPm;
|
||||
private bool _use12Hour;
|
||||
private int _minuteStep = 1;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_use12Hour = Use12Hour;
|
||||
_minuteStep = MinuteStep < 1 ? 1 : MinuteStep;
|
||||
ApplyFromValue();
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_use12Hour = Use12Hour;
|
||||
_minuteStep = MinuteStep < 1 ? 1 : MinuteStep;
|
||||
ApplyFromValue();
|
||||
}
|
||||
|
||||
private void ApplyFromValue()
|
||||
{
|
||||
if (SelectedTime is { } t)
|
||||
{
|
||||
if (_use12Hour)
|
||||
{
|
||||
_isPm = t.Hour >= 12;
|
||||
_selectedHour = t.Hour % 12;
|
||||
_selectedMinute = t.Minute;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedHour = t.Hour;
|
||||
_selectedMinute = t.Minute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectHour(int hour)
|
||||
{
|
||||
_selectedHour = hour;
|
||||
await EmitValue();
|
||||
}
|
||||
|
||||
private async Task SelectMinute(int minute)
|
||||
{
|
||||
_selectedMinute = minute;
|
||||
await EmitValue();
|
||||
}
|
||||
|
||||
private async Task SelectPeriod(bool isPm)
|
||||
{
|
||||
_isPm = isPm;
|
||||
await EmitValue();
|
||||
}
|
||||
|
||||
private async Task EmitValue()
|
||||
{
|
||||
var hour = _use12Hour ? (_selectedHour % 12) + (_isPm ? 12 : 0) : _selectedHour;
|
||||
var time = new TimeOnly(hour, _selectedMinute);
|
||||
SelectedTime = time;
|
||||
await SelectedTimeChanged.InvokeAsync(time);
|
||||
}
|
||||
|
||||
private static string TimeItemClass(bool isSelected)
|
||||
{
|
||||
const string baseClass = "h-9 w-full inline-flex items-center justify-center rounded-md text-sm transition-colors cursor-pointer";
|
||||
|
||||
Reference in New Issue
Block a user