From 3263576943a491bcad3aff6c18c166fb1c874619 Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Thu, 11 Jun 2026 00:57:51 +1000 Subject: Added Button Razor component --- Pages/Component/Button.razor | 136 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Pages/Component/Button.razor (limited to 'Pages/Component/Button.razor') 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 + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public Func 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 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(); +} -- cgit v1.3