summaryrefslogtreecommitdiff
path: root/Pages/Component/Button.razor
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2026-06-11 00:57:51 +1000
committerJake Mannens <jake@asger.xyz>2026-06-11 01:40:57 +1000
commit3263576943a491bcad3aff6c18c166fb1c874619 (patch)
tree750d5ac7234a6b3da622cd1095a5bf1bca084956 /Pages/Component/Button.razor
parenta547945940de4ee5638a761053797cbbe1e0302f (diff)
Added Button Razor component
Diffstat (limited to 'Pages/Component/Button.razor')
-rw-r--r--Pages/Component/Button.razor136
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();
+}