@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; }
    }
}
