Files
prefab-page-detail/Prefab.Web.Client/Layout/NavMenu.razor
2025-10-27 17:39:18 -04:00

410 lines
10 KiB
Plaintext

@rendermode InteractiveAuto
@using System.Linq
@using Prefab.Web.Client.Models.Shared
@using Prefab.Web.Client.Services
@implements IDisposable
<nav class="header__nav main-nav">
<ul class="main-nav__list">
<li class="main-nav__item ">
<NavLink class="main-nav__link" href="/" Match="NavLinkMatch.All">
Home
</NavLink>
</li>
<li class="@ProductsMenuItemClass"
@onmouseenter="HandleMouseEnter"
@onmouseleave="HandleMouseLeave">
<span class="main-nav__trigger"
@onclick="OnProductsMenuLinkClick"
@onclick:preventDefault>
<NavLink id="productsMenuLink"
class="main-nav__link"
href="/catalog"
role="button"
aria-haspopup="true"
aria-expanded="@(_productsMenuOpen.ToString().ToLowerInvariant())"
aria-controls="productsMenuPanel">
<span>Products</span>
<svg class="main-nav__link-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</NavLink>
</span>
<div id="productsMenuPanel"
class="main-nav__sub-megamenu"
role="region"
aria-hidden="@((!_productsMenuOpen).ToString().ToLowerInvariant())"
tabindex="-1"
@ref="_panel">
@if (!_menuLoaded)
{
<div class="megamenu" role="menu" aria-live="polite">
<div class="row g-custom-30">
@for (var i = 0; i < 4; i++)
{
<div class="col-3">
<TelerikSkeleton Width="100%" Height="20px" Class="mb-2" />
<ul class="megamenu__links megamenu__links--sub">
@for (var j = 0; j < 4; j++)
{
<li class="mb-2">
<TelerikSkeleton Height="16px" Width="80%" />
</li>
}
</ul>
</div>
}
</div>
</div>
}
else if (_menu is null || _menu.Columns.Count == 0)
{
<div class="mega-menu__empty" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
}
else
{
<div class="megamenu" role="menu" aria-live="polite">
<div class="row g-custom-30">
@foreach (var column in _menu.Columns.Take(4))
{
<div class="col-3">
<ul class="megamenu__links megamenu__links--root">
<li>
<NavLink href="@column.Root.Url">@column.Root.Name</NavLink>
@if (column.Items is { Count: > 0 })
{
<ul class="megamenu__links megamenu__links--sub">
@foreach (var item in column.Items)
{
<li><NavLink href="@item.Url">@item.Name</NavLink></li>
}
<li class="megamenu__see-all">
<NavLink href="@column.SeeAllUrl">See all @column.Root.Name</NavLink>
</li>
</ul>
}
</li>
</ul>
</div>
}
</div>
</div>
}
</div>
</li>
<li class="@ProductsFlyoutItemClass"
@onmouseenter="HandleFlyoutMouseEnter"
@onmouseleave="HandleFlyoutMouseLeave">
<span class="main-nav__trigger"
@onclick="OnProductsFlyoutLinkClick"
@onclick:preventDefault>
<NavLink class="main-nav__link"
href="/catalog"
role="button"
aria-haspopup="true"
aria-expanded="@(_productsFlyoutOpen.ToString().ToLowerInvariant())"
aria-controls="productsFlyoutMenu">
<span>Products</span>
<svg class="main-nav__link-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</NavLink>
</span>
<div id="productsFlyoutMenu"
class="main-nav__sub-menu"
role="region"
aria-live="polite">
@if (!_menuLoaded)
{
<div class="p-3" role="status">
@for (var i = 0; i < 4; i++)
{
<div class="mb-2">
<TelerikSkeleton Width="180px" Height="16px" />
</div>
}
</div>
}
else if (_menu is null || _menu.Columns.Count == 0)
{
<div class="menu__empty p-3" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
}
else
{
<ul class="menu">
@foreach (var column in GetFlyoutColumns())
{
var hasChildren = column.Items is { Count: > 0 };
var hasSeeAll = !string.IsNullOrWhiteSpace(column.SeeAllUrl);
<li class="menu__item">
<NavLink class="menu__link"
href="@column.Root.Url">
@column.Root.Name
@if (hasChildren)
{
<svg class="menu__link-arrow" width="6px" height="9px">
<use xlink:href="images/sprite.svg#arrow-right-6x9"></use>
</svg>
}
</NavLink>
@if (hasChildren)
{
<div class="menu__sub">
<ul class="menu">
@if (column.Items is { Count: > 0 })
{
@foreach (var item in column.Items)
{
<li class="menu__item">
<NavLink class="menu__link" href="@item.Url">@item.Name</NavLink>
</li>
}
}
@if (hasSeeAll)
{
<li class="menu__item">
<NavLink class="menu__link menu__link--see-all" href="@column.SeeAllUrl">
See all @column.Root.Name
</NavLink>
</li>
}
</ul>
</div>
}
</li>
}
</ul>
}
</div>
</li>
<li class="main-nav__item">
<NavLink class="main-nav__link" href="Contact">
Contact Us
</NavLink>
</li>
</ul>
</nav>
@code {
private const string ProductsMenuCacheKey = "nav-menu/products";
[Inject]
private NavigationManager NavigationManager { get; set; } = default!;
[Inject]
private INavMenuService NavMenuService { get; set; } = default!;
[Inject]
private IServiceProvider ServiceProvider { get; set; } = default!;
private bool _productsMenuOpen;
private bool _productsFlyoutOpen;
private ElementReference _panel;
private NavMenuModel? _menu;
private bool _menuLoaded;
private string? _errorMessage;
private IDisposable? _persistRegistration;
private CancellationTokenSource? _loadCancellationSource;
private string ProductsMenuItemClass =>
$"main-nav__item main-nav__item--with--menu {(_productsMenuOpen ? "main-nav__item--open" : string.Empty)}";
private string ProductsFlyoutItemClass =>
$"main-nav__item main-nav__item--with--menu {(_productsFlyoutOpen ? "main-nav__item--open" : string.Empty)}";
private PersistentComponentState? AppState => ServiceProvider.GetService<PersistentComponentState>();
protected override void OnInitialized()
{
NavigationManager.LocationChanged += OnLocationChanged;
var appState = AppState;
if (appState is not null)
{
_persistRegistration = appState.RegisterOnPersisting(PersistAsync);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || _menuLoaded)
{
return;
}
if (AppState is not null && AppState.TryTakeFromJson(ProductsMenuCacheKey, out NavMenuModel? cached) && cached is not null)
{
_menu = cached;
}
else
{
await LoadAsync(forceReload: true);
}
_menuLoaded = true;
StateHasChanged();
}
private async Task ToggleProductsMenuAsync()
{
if (_productsMenuOpen)
{
await CloseAsync();
return;
}
await OpenAsync(focusPanel: true);
}
private Task OnProductsMenuLinkClick(MouseEventArgs _) => ToggleProductsMenuAsync();
private Task HandleMouseEnter() => OpenAsync(focusPanel: false);
private Task HandleMouseLeave() => CloseAsync();
private async Task HandleFlyoutMouseEnter()
{
await LoadAsync(forceReload: false);
_productsFlyoutOpen = true;
StateHasChanged();
}
private Task HandleFlyoutMouseLeave()
{
if (!_productsFlyoutOpen)
{
return Task.CompletedTask;
}
_productsFlyoutOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
private Task OnProductsFlyoutLinkClick(MouseEventArgs _)
{
if (_productsFlyoutOpen)
{
_productsFlyoutOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
return HandleFlyoutMouseEnter();
}
private async Task OpenAsync(bool focusPanel)
{
if (_productsMenuOpen)
{
return;
}
await LoadAsync(forceReload: false);
_productsMenuOpen = true;
StateHasChanged();
if (focusPanel)
{
await Task.Yield();
await _panel.FocusAsync();
}
}
private Task CloseAsync()
{
if (!_productsMenuOpen)
{
return Task.CompletedTask;
}
_productsMenuOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
_ = InvokeAsync(async () =>
{
await CloseAsync();
_productsFlyoutOpen = false;
await LoadAsync(forceReload: true);
StateHasChanged();
});
}
private async Task LoadAsync(bool forceReload)
{
if (!forceReload && _menu is not null)
{
return;
}
_loadCancellationSource?.Cancel();
_loadCancellationSource?.Dispose();
_loadCancellationSource = new CancellationTokenSource();
var token = _loadCancellationSource.Token;
try
{
var result = await NavMenuService.Get(2, token);
if (result.IsSuccess && result.Value is { } model)
{
_menu = model;
_errorMessage = null;
}
else
{
_menu = null;
_errorMessage = result.Problem?.Detail ?? "Navigation unavailable.";
}
}
catch (OperationCanceledException)
{
// Swallow expected cancellation when a new request supersedes the current one.
}
catch (Exception exception)
{
_menu = null;
_errorMessage = exception.Message;
}
}
private Task PersistAsync()
{
var appState = AppState;
if (_menu is not null && appState is not null)
{
appState.PersistAsJson(ProductsMenuCacheKey, _menu);
}
return Task.CompletedTask;
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
_persistRegistration?.Dispose();
_loadCancellationSource?.Cancel();
_loadCancellationSource?.Dispose();
}
private IEnumerable<NavMenuColumnModel> GetFlyoutColumns()
{
if (_menu is null || _menu.Columns.Count == 0)
{
return Array.Empty<NavMenuColumnModel>();
}
return _menu.Columns;
}
}