Add KebabMenu component with JavaScript handler and styles for dropdown actions
This commit is contained in:
94
src/ASTRAIN.Client/Components/KebabMenu.razor
Normal file
94
src/ASTRAIN.Client/Components/KebabMenu.razor
Normal file
@@ -0,0 +1,94 @@
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="kebab-menu" @ref="_menuRef">
|
||||
<button class="kebab-button" type="button" @onclick="Toggle" aria-label="@AriaLabel" aria-haspopup="menu" aria-expanded="@_isOpen">⋮</button>
|
||||
@if (_isOpen)
|
||||
{
|
||||
<div class="kebab-menu__items" role="menu">
|
||||
@ChildContent(_context)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string AriaLabel { get; set; } = "Menu";
|
||||
[Parameter] public RenderFragment<KebabMenuContext> ChildContent { get; set; } = default!;
|
||||
|
||||
private bool _isOpen;
|
||||
private ElementReference _menuRef;
|
||||
private DotNetObjectReference<KebabMenu>? _dotNetRef;
|
||||
private IJSObjectReference? _jsRef;
|
||||
private readonly KebabMenuContext _context;
|
||||
|
||||
public KebabMenu()
|
||||
{
|
||||
_context = new KebabMenuContext(Close);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the open state of the menu.
|
||||
/// </summary>
|
||||
private void Toggle()
|
||||
{
|
||||
_isOpen = !_isOpen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the menu if it is open.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public void Close()
|
||||
{
|
||||
if (!_isOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isOpen = false;
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the component has been rendered. Registers the JavaScript event handler on first render.
|
||||
/// </summary>
|
||||
/// <param name="firstRender">True if this is the first render; otherwise, false.</param>
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
_jsRef = await JS.InvokeAsync<IJSObjectReference>("kebabMenu.register", _menuRef, _dotNetRef);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the JavaScript object reference and event handler.
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_jsRef is not null)
|
||||
{
|
||||
await _jsRef.InvokeVoidAsync("dispose");
|
||||
await _jsRef.DisposeAsync();
|
||||
}
|
||||
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the KebabMenuContext class.
|
||||
/// </summary>
|
||||
/// <param name="close">The action to close the menu.</param>
|
||||
public sealed class KebabMenuContext
|
||||
{
|
||||
public KebabMenuContext(Action close)
|
||||
{
|
||||
Close = close;
|
||||
}
|
||||
|
||||
public Action Close { get; }
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using ASTRAIN.Client
|
||||
@using ASTRAIN.Client.Components
|
||||
@using ASTRAIN.Client.Layout
|
||||
@using ASTRAIN.Client.Services
|
||||
@using ASTRAIN.Shared.Dtos
|
||||
|
||||
@@ -154,6 +154,74 @@ p {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.kebab-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kebab-button {
|
||||
list-style: none;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.kebab-button::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kebab-menu__items {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 0.4rem);
|
||||
min-width: 140px;
|
||||
background: #151515;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 0.35rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.kebab-menu__item {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.kebab-menu__item:hover {
|
||||
background: #222222;
|
||||
}
|
||||
|
||||
.kebab-menu__item.danger {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -216,6 +284,8 @@ p {
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem 1.25rem 6rem;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
@@ -305,7 +375,7 @@ p {
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
padding: 2rem 2.5rem 7rem;
|
||||
max-width: 960px;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="js/kebabMenu.js"></script>
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
28
src/ASTRAIN.Client/wwwroot/js/kebabMenu.js
Normal file
28
src/ASTRAIN.Client/wwwroot/js/kebabMenu.js
Normal file
@@ -0,0 +1,28 @@
|
||||
window.kebabMenu = {
|
||||
/**
|
||||
* Registers the kebab menu event handler for closing on outside clicks.
|
||||
* @param {HTMLElement} element - The menu element.
|
||||
* @param {object} dotNetRef - The .NET object reference.
|
||||
* @returns {object} An object with a dispose method.
|
||||
*/
|
||||
register: function (element, dotNetRef) {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handler = (event) => {
|
||||
if (!element.contains(event.target)) {
|
||||
dotNetRef.invokeMethodAsync('Close');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handler, true);
|
||||
|
||||
return {
|
||||
// Disposes the event listener.
|
||||
dispose: function () {
|
||||
document.removeEventListener('click', handler, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user