| File: Components\Controls\IconCheckbox.razor.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; namespace Aspire.Dashboard.Components; /// <summary> /// An icon-only checkbox that exposes proper checkbox semantics to assistive tech. /// </summary> /// <remarks> /// FluentButton renders a shadow-DOM button that remains exposed as role=button, so the /// checkbox semantics need to live on this focusable element. A small JS helper handles /// the Space key here so Tab/Shift+Tab keep their native focus behavior while Space /// cannot scroll the page or bubble to an enclosing grid's row activation. /// </remarks> public partial class IconCheckbox : ComponentBase, IAsyncDisposable { private const string JsModulePath = "./Components/Controls/IconCheckbox.razor.js"; // The control owns the mapping from state to icon so callers only describe the // checked state via CheckState rather than wiring up icons and aria values themselves. private static readonly Icon s_uncheckedIcon = new Icons.Regular.Size20.CheckboxUnchecked().WithColor(Color.FillInverse); private static readonly Icon s_checkedIcon = new Icons.Filled.Size20.CheckboxChecked(); private static readonly Icon s_indeterminateIcon = new Icons.Filled.Size20.CheckboxIndeterminate(); private ElementReference _element; private IJSObjectReference? _jsModule; private bool _keyboardInitialized; [Inject] public required IJSRuntime JS { get; init; } /// <summary> /// The checked state of the checkbox. Determines the rendered icon and the exposed /// <c>aria-checked</c> value. /// </summary> [Parameter] public required IconCheckboxState CheckState { get; set; } /// <summary> /// The accessible name used for both <c>title</c> and <c>aria-label</c>. /// </summary> [Parameter] public required string AccessibleLabel { get; set; } /// <summary> /// Invoked when the checkbox is activated (click or Space). /// </summary> [Parameter] public EventCallback OnClick { get; set; } /// <summary> /// When <c>true</c>, exposes the checkbox as disabled, removes it from the tab order, and /// skips the Space-key handler. /// </summary> [Parameter] public bool Disabled { get; set; } /// <summary> /// Additional CSS classes appended to the root element. /// </summary> [Parameter] public string? CssClass { get; set; } /// <summary> /// Whether the click event should be prevented from propagating to ancestors. /// Defaults to <c>true</c> so the checkbox does not also activate a surrounding row. /// </summary> [Parameter] public bool StopPropagation { get; set; } = true; private Icon CurrentIcon => CheckState switch { IconCheckboxState.Checked => s_checkedIcon, IconCheckboxState.Unchecked => s_uncheckedIcon, _ => s_indeterminateIcon }; // The indeterminate state maps to aria-checked="mixed" per the ARIA checkbox spec. // See: https://www.w3.org/TR/wai-aria-1.2/#checkbox private string AriaChecked => CheckState switch { IconCheckboxState.Checked => "true", IconCheckboxState.Unchecked => "false", _ => "mixed" }; // Render aria-disabled only when disabled so the attribute is omitted in the common case, // which also keeps the CSS/keyboard handler checks (aria-disabled="true") working. private string? AriaDisabled => Disabled ? "true" : null; // Disabled checkboxes are removed from the tab order but remain focusable via pointer. private string TabIndex => Disabled ? "-1" : "0"; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _jsModule = await JS.InvokeAsync<IJSObjectReference>("import", JsModulePath); await _jsModule.InvokeVoidAsync("initializeIconCheckboxKeyboard", _element); _keyboardInitialized = true; } } private async Task HandleClickAsync() { if (Disabled) { return; } await OnClick.InvokeAsync(); } public async ValueTask DisposeAsync() { if (_jsModule is not null) { if (_keyboardInitialized) { try { await _jsModule.InvokeVoidAsync("disposeIconCheckboxKeyboard", _element); } catch (JSDisconnectedException) { // The browser may already be gone when the component is disposed. } catch (OperationCanceledException) { // The browser may already be gone when the component is disposed. } } await JSInteropHelpers.SafeDisposeAsync(_jsModule); } } } /// <summary> /// The checked state of an <see cref="IconCheckbox"/>. /// </summary> public enum IconCheckboxState { /// <summary> /// The checkbox is unchecked (aria-checked="false"). /// </summary> Unchecked, /// <summary> /// The checkbox is checked (aria-checked="true"). /// </summary> Checked, /// <summary> /// The checkbox is partially checked (aria-checked="mixed"). /// </summary> Indeterminate }