Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/Core.Scripts/src/Components/Dialog/FluentDialog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export namespace Microsoft.FluentUI.Blazor.Components.Dialog {

const getDeepActiveElement = (): HTMLElement | null => {
let activeElement: Element | null = document.activeElement;
while (activeElement instanceof HTMLElement && activeElement.shadowRoot?.activeElement) {
activeElement = activeElement.shadowRoot.activeElement;
}

return activeElement instanceof HTMLElement ? activeElement : null;
};

/**
* Tag names of non-modal, transient elements (e.g. toasts) that reuse the
* dialog toggle plumbing but must never restore focus when they open or close.
Expand Down Expand Up @@ -90,4 +99,25 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog {
dialog.addEventListener('keydown', handler, true);
}
}

/**
* Returns whether dialog keyboard shortcuts should be handled for the current focused element.
* Shortcuts are limited to dialog action areas so interactive content inside drawers/dialogs
* can use Enter/Space/Arrow keys without being intercepted.
* @param id The id of the fluent-dialog/fluent-drawer element
*/
export function ShouldHandleShortcut(id: string): boolean {
const dialog = document.getElementById(id) as HTMLElement | null;
if (!dialog) {
return false;
}

const activeElement = getDeepActiveElement();
if (!activeElement || !dialog.contains(activeElement)) {
return false;
}

// Keep shortcuts active for explicit dialog action surfaces.
return !!activeElement.closest('[slot="action"], [slot="footer"], [slot="close"], [slot="title-action"]');
}
}
43 changes: 23 additions & 20 deletions src/Core.Scripts/src/Components/Menu/FluentMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,34 @@ export namespace Microsoft.FluentUI.Blazor.Components.Menu {
* @param triggerId The id of the trigger element that will open the menu when clicked.
*/
export function Initialize(id: string, triggerId: string) {
const trigger = document.getElementById(triggerId) as HTMLElement | null;
if (!trigger) return;
const initWithRetry = (attempt: number = 0) => {
const trigger = document.getElementById(triggerId) as HTMLElement | null;
const menu = document.getElementById(id) as Menu | null;
if (!trigger || !menu) {
if (attempt < 10) {
requestAnimationFrame(() => initWithRetry(attempt + 1));
}
return;
}

trigger.style["anchor-name" as any] = `--anchor-${triggerId}`;
const menuElement = document.getElementById(id) as Menu | null;
if (!menuElement) return;
trigger.style["anchor-name" as any] = `--anchor-${triggerId}`;

// Set the anchor name for the menu list to position it relative to the trigger
const doInit = () => {
const menu = document.getElementById(id) as Menu | null;
if (menu && menu.slottedMenuList?.length) {
menu.slottedMenuList[0].style["position-anchor" as any] = `--anchor-${triggerId}`;
menu.slottedTriggersChanged(menu.slottedTriggers, [trigger]);
// Keep trigger wiring explicit for hosted surfaces (drawer/dialog/shadow-heavy layouts).
menu.setAttribute("trigger", triggerId);

const menuList = menu.slottedMenuList?.[0] as MenuList | undefined;
if (!menuList) {
if (attempt < 10) {
requestAnimationFrame(() => initWithRetry(attempt + 1));
}
return;
}
};

// already populated (e.g. hot-reload / re-render)
if (menuElement.slottedMenuList?.length) {
doInit();
}
menuList.style["position-anchor" as any] = `--anchor-${triggerId}`;
menu.slottedTriggersChanged(menu.slottedTriggers ?? [], [trigger]);
};

// wait for slotchange macrotask
else {
requestAnimationFrame(doInit);
}
initWithRetry();
}

/**
Expand Down
7 changes: 3 additions & 4 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@namespace Microsoft.FluentUI.AspNetCore.Components
@typeparam TGridItem
Expand Down Expand Up @@ -35,7 +34,7 @@

@if (headerCapabilities.HasAnyAction)
{
<FluentButton Id="@_columnId" Appearance="ButtonAppearance.Subtle" Class="col-sort-button" Style="@headerButtonStyle" @onclick="@HandleColumnHeaderClickedAsync" aria-label="@tooltip" title="@tooltip" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(this))">
<FluentButton Id="@HeaderButtonId" Appearance="ButtonAppearance.Subtle" Class="col-sort-button" Style="@headerButtonStyle" @onclick="@HandleColumnHeaderClickedAsync" StopPropagation="true" @onkeydown="@HandleHeaderButtonKeyDownAsync" aria-label="@tooltip" title="@tooltip" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(this))">
<div class="col-title-text" title="@tooltip">
@HeaderTitleContent
</div>
Expand Down Expand Up @@ -65,7 +64,7 @@
</div>
</div>
}
<FluentMenu @key="@(_columnId + ":" + Index)" @ref="@_menu" Trigger="@_columnId" Style="@headerPopupLayerStyle">
<FluentMenu @key="@HeaderMenuId" Id="@HeaderMenuId" @ref="@_menu" Trigger="@HeaderButtonId" Style="@headerPopupLayerStyle">
<FluentMenuList>
@if (headerCapabilities.CanSort)
{
Expand Down Expand Up @@ -188,7 +187,7 @@
private RenderFragment OptionsButton()
{
return
@<FluentButton IconOnly="true" Appearance="ButtonAppearance.Subtle" class="col-options-button" @onclick="@(() => Grid.ShowAllHeaderUIAsync(this))" aria-label="@Localizer[Localization.LanguageResource.DataGrid_OptionsMenu]">
@<FluentButton IconOnly="true" Appearance="ButtonAppearance.Subtle" class="col-options-button" @onclick="@(() => Grid.ShowAllHeaderUIAsync(this))" @onkeydown="@HandleOptionsButtonKeyDownAsync" aria-label="@Localizer[Localization.LanguageResource.DataGrid_OptionsMenu]">
<FluentIcon Value="@(new CoreIcons.Regular.Size20.ChevronDown())" Color="Color.Default" Width="20px" Style="opacity: var(--fluent-data-grid-header-opacity);" />
</FluentButton>;
}
Expand Down
61 changes: 57 additions & 4 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;

namespace Microsoft.FluentUI.AspNetCore.Components;

Expand All @@ -19,8 +18,13 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
public abstract partial class ColumnBase<TGridItem>
{
private static readonly string[] KEYBOARD_MENU_SELECT_KEYS = ["Enter", "NumpadEnter"];
private readonly string _columnId = Identifier.NewId();
private FluentMenu? _menu;
private bool _suppressNextHeaderSyntheticClick;
private bool _openHeaderMenuAfterRender;

private string HeaderButtonId => $"{Grid.Id}-col-{Index}";

private string HeaderMenuId => $"{HeaderButtonId}-menu";

/// <summary />
[Inject]
Expand Down Expand Up @@ -345,7 +349,29 @@ protected internal virtual Task OnCellKeyDownAsync(FluentDataGridCell<TGridItem>

internal bool CanSortFromHeader() => Sortable ?? IsSortableByDefault();

private async Task HandleColumnHeaderClickedAsync()
/// <summary />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_openHeaderMenuAfterRender && _menu is not null)
{
_openHeaderMenuAfterRender = false;
await _menu.OpenMenuAsync();
}
}

private async Task HandleColumnHeaderClickedAsync(MouseEventArgs args)
{
if (_suppressNextHeaderSyntheticClick && args.Detail == 0)
{
_suppressNextHeaderSyntheticClick = false;
return;
}

_suppressNextHeaderSyntheticClick = false;
await HandleColumnHeaderActivatedAsync();
}

private async Task HandleColumnHeaderActivatedAsync()
{
var headerCapabilities = HeaderCapabilities;
var hasSorting = headerCapabilities.CanSort;
Expand All @@ -362,8 +388,8 @@ private async Task HandleColumnHeaderClickedAsync()

if (hasMultiple)
{
_openHeaderMenuAfterRender = true;
return;
//StateHasChanged();
}

if (hasSorting)
Expand All @@ -384,6 +410,33 @@ private async Task HandleColumnHeaderClickedAsync()
}
}

private async Task HandleHeaderButtonKeyDownAsync(KeyboardEventArgs args)
{
if (!string.Equals(args.Code, "Enter", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(args.Code, "Space", StringComparison.OrdinalIgnoreCase))
{
return;
}

if (HeaderCapabilities.HasAnyAction && Grid.HeaderCellAsButtonWithMenu)
{
_suppressNextHeaderSyntheticClick = true;
_openHeaderMenuAfterRender = true;
return;
}

await HandleColumnHeaderActivatedAsync();
}

private async Task HandleOptionsButtonKeyDownAsync(KeyboardEventArgs args)
{
if (string.Equals(args.Code, "Enter", StringComparison.OrdinalIgnoreCase) ||
string.Equals(args.Code, "Space", StringComparison.OrdinalIgnoreCase))
{
await Grid.ShowAllHeaderUIAsync(this);
}
}

private async Task HandleSortMenuKeyDownAsync(KeyboardEventArgs args)
{
if (KEYBOARD_MENU_SELECT_KEYS.Contains(args.Key, StringComparer.OrdinalIgnoreCase))
Expand Down
Loading
Loading