From 69f65ed585227ecfee50d19b368c808a400100a0 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 23 Jul 2026 22:24:32 +0200 Subject: [PATCH 01/10] feat: enhance UI components with dismiss behavior and new parameters Added `CloseOnScroll` parameter to various UI components (e.g., Combobox, DatePicker, MultiSelect) to control dropdown dismissal on page scroll, aligning with shadcn/Radix behavior. Implemented async disposal and JS interop for managing dismiss events. Updated Drawer and Sheet components to support compositional behavior. Enhanced DataTable with server-side data handling capabilities, including new request and response models for better data management. --- .../Components/Demo/OverlayDemo.razor | 4 +- .../Components/Models/DataTableModels.cs | 17 + .../Components/Services/SonnerService.cs | 51 +- .../Components/UI/Combobox.razor | 75 +- .../Components/UI/Command.razor | 2 +- .../Components/UI/DataPicker.razor | 65 +- .../Components/UI/DataTable.razor | 95 +- .../Components/UI/DatePicker.razor | 79 +- .../Components/UI/DateRangePicker.razor | 79 +- .../Components/UI/DialogContent.razor | 27 +- .../Components/UI/DrawerContent.razor | 27 +- .../Components/UI/MultiSelect.razor | 62 +- .../Components/UI/SheetContent.razor | 27 +- .../Components/UI/Sonner.razor | 5 +- .../Components/UI/Stepper.razor | 17 +- .../Components/UI/StepperStep.razor | 12 +- .../Components/UI/Toast.razor | 4 +- .../Components/UI/TypedSelect.razor | 76 + .../wwwroot/shellui.js | 35 + NET10/ShellUI.Preview/App.razor | 11 - .../Components/DashboardBlockPreview.razor | 32 - .../Components/DialogPreview.razor | 26 - NET10/ShellUI.Preview/Layout/MainLayout.razor | 14 - .../Layout/PreviewLayout.razor | 11 - .../Pages/ComponentPreview.razor | 249 -- NET10/ShellUI.Preview/Pages/Home.razor | 59 - NET10/ShellUI.Preview/Pages/NotFound.razor | 5 - NET10/ShellUI.Preview/Program.cs | 11 - .../Properties/launchSettings.json | 25 - NET10/ShellUI.Preview/ShellUI.Preview.csproj | 20 - NET10/ShellUI.Preview/_Imports.razor | 13 - NET10/ShellUI.Preview/tailwind.config.js | 8 - NET10/ShellUI.Preview/wwwroot/css/app.css | 2721 ----------------- NET10/ShellUI.Preview/wwwroot/index.html | 55 - NET10/ShellUI.Preview/wwwroot/input.css | 188 -- .../wwwroot/sample-data/weather.json | 27 - 36 files changed, 671 insertions(+), 3563 deletions(-) create mode 100644 NET10/BlazorInteractiveServer/Components/UI/TypedSelect.razor delete mode 100644 NET10/ShellUI.Preview/App.razor delete mode 100644 NET10/ShellUI.Preview/Components/DashboardBlockPreview.razor delete mode 100644 NET10/ShellUI.Preview/Components/DialogPreview.razor delete mode 100644 NET10/ShellUI.Preview/Layout/MainLayout.razor delete mode 100644 NET10/ShellUI.Preview/Layout/PreviewLayout.razor delete mode 100644 NET10/ShellUI.Preview/Pages/ComponentPreview.razor delete mode 100644 NET10/ShellUI.Preview/Pages/Home.razor delete mode 100644 NET10/ShellUI.Preview/Pages/NotFound.razor delete mode 100644 NET10/ShellUI.Preview/Program.cs delete mode 100644 NET10/ShellUI.Preview/Properties/launchSettings.json delete mode 100644 NET10/ShellUI.Preview/ShellUI.Preview.csproj delete mode 100644 NET10/ShellUI.Preview/_Imports.razor delete mode 100644 NET10/ShellUI.Preview/tailwind.config.js delete mode 100644 NET10/ShellUI.Preview/wwwroot/css/app.css delete mode 100644 NET10/ShellUI.Preview/wwwroot/index.html delete mode 100644 NET10/ShellUI.Preview/wwwroot/input.css delete mode 100644 NET10/ShellUI.Preview/wwwroot/sample-data/weather.json diff --git a/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor b/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor index 8fb28d7..cb13e82 100644 --- a/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor +++ b/NET10/BlazorInteractiveServer/Components/Demo/OverlayDemo.razor @@ -124,7 +124,7 @@

Drawer & Sheet

- + @@ -134,7 +134,7 @@ - + diff --git a/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs b/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs index 218233e..a7040d8 100644 --- a/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs +++ b/NET10/BlazorInteractiveServer/Components/Models/DataTableModels.cs @@ -24,3 +24,20 @@ public enum SortDirection Ascending, Descending } + +// Passed to OnDataRequest when DataTable is in ServerSide mode. The server-side data +// loader receives filter / sort / page state and returns just the current page. +public class DataTableRequest +{ + public int Skip { get; set; } + public int Take { get; set; } + public string? SortBy { get; set; } + public SortDirection SortDirection { get; set; } = SortDirection.None; + public string SearchQuery { get; set; } = ""; +} + +public class DataTableResponse +{ + public IEnumerable Items { get; set; } = Array.Empty(); + public int TotalCount { get; set; } +} diff --git a/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs b/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs index de05d84..08b0a1f 100644 --- a/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs +++ b/NET10/BlazorInteractiveServer/Components/Services/SonnerService.cs @@ -6,13 +6,20 @@ public class SonnerToastItem public string Message { get; set; } = ""; public string? Description { get; set; } public string Variant { get; set; } = "default"; + // Zero or negative disables auto-dismiss (toast stays until manually closed). + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(4); public DateTime CreatedAt { get; } = DateTime.UtcNow; } public interface ISonnerService { IReadOnlyList Toasts { get; } - void Show(string message, string? description = null, string variant = "default"); + void Show(string message, string? description = null, string variant = "default", TimeSpan? duration = null); + void Success(string message, string? description = null, TimeSpan? duration = null); + void Error(string message, string? description = null, TimeSpan? duration = null); + void Info(string message, string? description = null, TimeSpan? duration = null); + void Warning(string message, string? description = null, TimeSpan? duration = null); + void Remove(string id); } public class SonnerService : ISonnerService @@ -21,15 +28,49 @@ public class SonnerService : ISonnerService public IReadOnlyList Toasts => _toasts; public event Action? OnChange; - public void Show(string message, string? description = null, string variant = "default") + public void Show(string message, string? description = null, string variant = "default", TimeSpan? duration = null) { - _toasts.Add(new SonnerToastItem { Message = message, Description = description, Variant = variant }); + var toast = new SonnerToastItem + { + Message = message, + Description = description, + Variant = variant, + Duration = duration ?? TimeSpan.FromSeconds(4) + }; + _toasts.Add(toast); OnChange?.Invoke(); + + if (toast.Duration > TimeSpan.Zero) + { + _ = AutoDismissAsync(toast.Id, toast.Duration); + } } + public void Success(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "success", duration); + + public void Error(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "destructive", duration); + + public void Info(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "info", duration); + + public void Warning(string message, string? description = null, TimeSpan? duration = null) + => Show(message, description, "warning", duration); + public void Remove(string id) { - _toasts.RemoveAll(t => t.Id == id); - OnChange?.Invoke(); + var removed = _toasts.RemoveAll(t => t.Id == id); + if (removed > 0) OnChange?.Invoke(); + } + + private async Task AutoDismissAsync(string id, TimeSpan delay) + { + try + { + await Task.Delay(delay); + Remove(id); + } + catch { } } } diff --git a/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor b/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor index 010736f..eeb75df 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/Combobox.razor @@ -1,4 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS
} - +
@@ -73,7 +75,7 @@ @if (_isOpen) { -
+
} @code { @@ -84,6 +86,9 @@ [Parameter] public string Placeholder { get; set; } = "Pick a date range"; [Parameter] public bool AllowClear { get; set; } = true; [Parameter] public bool Disabled { get; set; } + /// Dismiss the calendar when the page scrolls. Matches shadcn/Radix behavior. + /// Set false to keep the calendar open across scroll. + [Parameter] public bool CloseOnScroll { get; set; } [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } @@ -91,6 +96,9 @@ private bool _isOpen; private DateTime _currentMonth = DateTime.Now; private bool _selectingStart = true; + private DotNetObjectReference? _selfRef; + private readonly string _dismissHandle = Guid.NewGuid().ToString("N"); + private bool _dismissRegistered; private string GetDisplayText() { @@ -128,7 +136,43 @@ return baseClass; } - private void ToggleCalendar() => _isOpen = !_isOpen; + private async Task ToggleCalendar() + { + _isOpen = !_isOpen; + if (_isOpen) await RegisterDismissAsync(); + else await UnregisterDismissAsync(); + } + + [JSInvokable] + public async Task OnDismissEvent() + { + if (_isOpen) + { + _isOpen = false; + await UnregisterDismissAsync(); + StateHasChanged(); + } + } + + private async Task RegisterDismissAsync() + { + if (!CloseOnScroll || _dismissRegistered) return; + _selfRef ??= DotNetObjectReference.Create(this); + try + { + await JS.InvokeVoidAsync("ShellUI.onDismissEvents", _dismissHandle, _selfRef); + _dismissRegistered = true; + } + catch { } + } + + private async Task UnregisterDismissAsync() + { + if (!_dismissRegistered) return; + try { await JS.InvokeVoidAsync("ShellUI.offDismissEvents", _dismissHandle); } catch { } + _dismissRegistered = false; + } + private void PreviousMonth() => _currentMonth = _currentMonth.AddMonths(-1); private void NextMonth() => _currentMonth = _currentMonth.AddMonths(1); @@ -176,11 +220,22 @@ var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1); var lastDay = firstDay.AddMonths(1).AddDays(-1); var startDayOfWeek = (int)firstDay.DayOfWeek; - + for (int i = 0; i < startDayOfWeek; i++) days.Add(null); for (int day = 1; day <= lastDay.Day; day++) days.Add(new DateTime(_currentMonth.Year, _currentMonth.Month, day)); - + return days; } -} + private async Task CloseBackdrop() + { + _isOpen = false; + await UnregisterDismissAsync(); + } + + public async ValueTask DisposeAsync() + { + await UnregisterDismissAsync(); + _selfRef?.Dispose(); + } +} diff --git a/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor b/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor index 6eb2fc1..ca643c5 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/DialogContent.razor @@ -1,5 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI -@using BlazorInteractiveServer.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS @if (Dialog?.Open == true) { @@ -20,6 +22,8 @@ [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + private bool _isLocked; + private async Task Close() { if (Dialog != null) @@ -27,6 +31,25 @@ await Dialog.SetOpen(false); } } -} + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var shouldBeLocked = Dialog?.Open == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.lockBodyScroll"); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); _isLocked = false; } catch { } + } + } + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); } catch { } + } + } +} diff --git a/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor b/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor index ca38146..206fae0 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/DrawerContent.razor @@ -1,5 +1,7 @@ @namespace BlazorInteractiveServer.Components.UI -@using BlazorInteractiveServer.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS @if (Parent?.Open == true) { @@ -19,8 +21,31 @@ [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } + private bool _isLocked; + private async Task Close() { if (Parent != null) await Parent.SetOpen(false); } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + var shouldBeLocked = Parent?.Open == true; + if (shouldBeLocked && !_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.lockBodyScroll"); _isLocked = true; } catch { } + } + else if (!shouldBeLocked && _isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); _isLocked = false; } catch { } + } + } + + public async ValueTask DisposeAsync() + { + if (_isLocked) + { + try { await JS.InvokeVoidAsync("ShellUI.unlockBodyScroll"); } catch { } + } + } } diff --git a/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor b/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor index 8750345..300c04b 100644 --- a/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor +++ b/NET10/BlazorInteractiveServer/Components/UI/MultiSelect.razor @@ -1,6 +1,9 @@ @namespace BlazorInteractiveServer.Components.UI +@using Microsoft.JSInterop @typeparam TItem @typeparam TKey where TKey : notnull +@implements IAsyncDisposable +@inject IJSRuntime JS
- - - - Dialog Title - Preview for docs embedding. - -

Content goes here.

- - - - - - - - -
- - -@code { - private bool _open; -} diff --git a/NET10/ShellUI.Preview/Layout/MainLayout.razor b/NET10/ShellUI.Preview/Layout/MainLayout.razor deleted file mode 100644 index 41ebcea..0000000 --- a/NET10/ShellUI.Preview/Layout/MainLayout.razor +++ /dev/null @@ -1,14 +0,0 @@ -@inherits LayoutComponentBase -
- -
- @Body -
-
diff --git a/NET10/ShellUI.Preview/Layout/PreviewLayout.razor b/NET10/ShellUI.Preview/Layout/PreviewLayout.razor deleted file mode 100644 index b2b5c2d..0000000 --- a/NET10/ShellUI.Preview/Layout/PreviewLayout.razor +++ /dev/null @@ -1,11 +0,0 @@ -@inherits LayoutComponentBase - -
-
- ← Components -
-
- @Body -
-
- diff --git a/NET10/ShellUI.Preview/Pages/ComponentPreview.razor b/NET10/ShellUI.Preview/Pages/ComponentPreview.razor deleted file mode 100644 index 6ee4409..0000000 --- a/NET10/ShellUI.Preview/Pages/ComponentPreview.razor +++ /dev/null @@ -1,249 +0,0 @@ -@page "/preview/{ComponentName}" -@layout Layout.PreviewLayout -@using ShellUI.Components -@using ShellUI.Components.Models -@using ShellUI.Preview.Components - -
- @switch (_id) - { - case "button": - - break; - case "badge": - Badge - break; - case "switch": - - break; - case "checkbox": - - break; - case "input": - - break; - case "textarea": -