diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-06-11 00:57:51 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-11 01:40:57 +1000 |
| commit | 3263576943a491bcad3aff6c18c166fb1c874619 (patch) | |
| tree | 750d5ac7234a6b3da622cd1095a5bf1bca084956 /Pages/Component/Button.razor | |
| parent | a547945940de4ee5638a761053797cbbe1e0302f (diff) | |
Added Button Razor component
Diffstat (limited to 'Pages/Component/Button.razor')
| -rw-r--r-- | Pages/Component/Button.razor | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/Pages/Component/Button.razor b/Pages/Component/Button.razor new file mode 100644 index 0000000..4212e50 --- /dev/null +++ b/Pages/Component/Button.razor @@ -0,0 +1,136 @@ +@using System.Timers +@implements IDisposable +@inject IJSRuntime js + +<button + @attributes=Attributes + @onclick=Trigger + @ref=button> + + @if(isLoading) { + <svg + width="15" + height="15" + viewBox="0 0 1000 1000" + fill="none" + class="loading"> + + <path d=" + M 500 0 + A 500 500 0 1 1 0 500 + A 75 75 0 1 1 150 500 + A 350 350 0 1 0 500 150 + A 75 75 0 1 1 500 0 + Z" + fill="currentColor"> + </path> + </svg> + } else if(IconUrl is not null) { + <img src=@IconUrl /> + } + + @ChildContent +</button> + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public Func<Task> OnClick { get; set; } + + [Parameter] + public Color BackgroundColor { get; set; } = Color.ButtonSecondary; + + [Parameter] + public string? IconUrl { get; set; } + + [Parameter] + public char? ShortcutKey { + get => shortcutKey; + set { + if(value is not null && !char.IsLetter((char) value)) + throw new System.ArgumentException("Cannot assign a non-letter to a shortcut"); + shortcutKey = value is null ? null : char.ToLower((char) value); + } + } + + [Parameter] + public int? FontSize { get; set; } + + private Dictionary<string, object?> Attributes => new() { + ["data-keyboard-shortcut"] = ShortcutKey, + ["disabled"] = isLoading, + ["style"] = CssStyle + }; + + private string CssStyle => string.Join("", [ + $"--col-pri:{BackgroundColor.CssString};", + FontSize is not null ? $"font-size:{FontSize}pt;" : null + ]); + + private ElementReference button; + + private Timer loadingTimer; + + private char? shortcutKey; + + private object triggerLock = new(); + private bool isRunning = false; + private bool isLoading = false; + + protected override void OnInitialized() { + loadingTimer = new(50) { + AutoReset = false + }; + loadingTimer.Elapsed += TimerElapsed; + } + + // Most button-click actions (e.g. triggering dialogs, page + // navigation, etc) complete very quickly (under 50ms). + // Triggering a render cycle to show a loading animation is + // usually pointless as doing so would waste resources and + // appear less seemless to the user who will see brief + // flickering while the browser quickly recalculates it's + // layout twice. To that end, a timer is used to only render + // the loading animation on the button if the delegate task + // takes longer than 50ms + private void TimerElapsed(object? sender, EventArgs e) { + lock(triggerLock) { + if(isRunning) + isLoading = true; + InvokeAsync(() => StateHasChanged()); + } + } + + private async Task Trigger() { + lock(triggerLock) { + if(isRunning) + return; + isRunning = true; + loadingTimer.Start(); + } + + bool error = false; + + try { + await Task.Run(OnClick); + } catch { + error = true; + } finally { + lock(triggerLock) { + loadingTimer.Stop(); + isRunning = false; + isLoading = false; + } + } + + await InvokeAsync(() => StateHasChanged()); + + if(error) + await js.InvokeVoidAsync("triggerButtonError", button); + } + + public void Dispose() => + loadingTimer.Dispose(); +} |
