Grid => InternalGridContext.Grid;
+ protected bool AnyColumnActionEnabled => Sortable is true || IsDefaultSortColumn || ColumnOptions != null || Grid.ResizableColumns;
+
///
/// Event callback for when the row is clicked.
///
@@ -218,4 +224,65 @@ public ColumnBase()
{
HeaderContent = RenderDefaultHeaderContent;
}
+
+ private async Task HandleColumnHeaderClickedAsync()
+ {
+ if ((Sortable is true || IsDefaultSortColumn) && (Grid.ResizableColumns || ColumnOptions is not null))
+ {
+ _isMenuOpen = !_isMenuOpen;
+ }
+ else if ((Sortable is true || IsDefaultSortColumn) && !Grid.ResizableColumns && ColumnOptions is null)
+ {
+ await Grid.SortByColumnAsync(this);
+ }
+ else if (Sortable is not true && !IsDefaultSortColumn && ColumnOptions is null && Grid.ResizableColumns)
+ {
+ await Grid.ShowColumnResizeAsync(this);
+ }
+ }
+
+ private async Task HandleSortMenuKeyDownAsync(KeyboardEventArgs args)
+ {
+ if (KEYBOARD_MENU_SELECT_KEYS.Contains(args.Key))
+ {
+ await Grid.SortByColumnAsync(this);
+ StateHasChanged();
+ _isMenuOpen = false;
+ }
+ }
+
+ private async Task HandleResizeMenuKeyDownAsync(KeyboardEventArgs args)
+ {
+ if (KEYBOARD_MENU_SELECT_KEYS.Contains(args.Key))
+ {
+ await Grid.ShowColumnResizeAsync(this);
+ _isMenuOpen = false;
+ }
+ }
+
+ private async Task HandleOptionsMenuKeyDownAsync(KeyboardEventArgs args)
+ {
+ if (KEYBOARD_MENU_SELECT_KEYS.Contains(args.Key))
+ {
+ await Grid.ShowColumnOptionsAsync(this);
+ _isMenuOpen = false;
+ }
+ }
+
+ private string GetSortOptionText()
+ {
+ if (Grid.SortByAscending.HasValue && ShowSortIcon)
+ {
+ if (Grid.SortByAscending is true)
+ {
+ return Grid.ColumnSortLabels.SortMenuAscendingLabel;
+ }
+ else
+ {
+ return Grid.ColumnSortLabels.SortMenuDescendingLabel;
+ }
+ }
+
+ return Grid.ColumnSortLabels.SortMenu;
+ }
}
diff --git a/src/Core/Components/DataGrid/Columns/ColumnOptionsLabels.cs b/src/Core/Components/DataGrid/Columns/ColumnOptionsLabels.cs
new file mode 100644
index 0000000000..6779270951
--- /dev/null
+++ b/src/Core/Components/DataGrid/Columns/ColumnOptionsLabels.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+public record ColumnOptionsLabels
+{
+ ///
+ /// Gets or sets the text shown in the column menu
+ ///
+ public string OptionsMenu { get; set; } = "Filter";
+
+ ///
+ /// Gets the default labels for the options UI.
+ ///
+ public static ColumnOptionsLabels Default => new();
+
+}
+
diff --git a/src/Core/Components/DataGrid/Columns/ColumnResizeLabels.cs b/src/Core/Components/DataGrid/Columns/ColumnResizeLabels.cs
new file mode 100644
index 0000000000..638dec6983
--- /dev/null
+++ b/src/Core/Components/DataGrid/Columns/ColumnResizeLabels.cs
@@ -0,0 +1,48 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+public record ColumnResizeLabels
+{
+ ///
+ /// Gets or sets the text shown in the column menu
+ ///
+ public string ResizeMenu { get; set; } = "Resize";
+
+ ///
+ /// Gets or sets the label in the discrete mode resize UI
+ ///
+ public string DiscreteLabel { get; set; } = "Column width";
+
+ ///
+ /// Gets or sets the label in the exact mode resize UI
+ ///
+ public string ExactLabel { get; set; } = "Column width (in pixels)";
+
+ ///
+ /// Gets or sets the aria label for the grow button in the discrete resize UI
+ ///
+ public string? GrowAriaLabel { get; set; } = "Grow column width";
+
+ ///
+ /// Gets or sets the aria label for the shrink button in the discrete resize UI
+ ///
+ public string? ShrinkAriaLabel { get; set; } = "Shrink column width";
+
+ ///
+ /// Gets or sets the aria label for the reset button in the resize UI
+ ///
+ public string? ResetAriaLabel { get; set; } = "Reset column widths";
+
+ ///
+ /// Gets or sets the aria label for the submit button in the resize UI
+ ///
+ public string? SubmitAriaLabel { get; set; } = "Set column widths";
+
+ ///
+ /// Gets the default labels for the resize UI.
+ ///
+ public static ColumnResizeLabels Default => new();
+}
diff --git a/src/Core/Components/DataGrid/Columns/ColumnSortLabels.cs b/src/Core/Components/DataGrid/Columns/ColumnSortLabels.cs
new file mode 100644
index 0000000000..706541f3a8
--- /dev/null
+++ b/src/Core/Components/DataGrid/Columns/ColumnSortLabels.cs
@@ -0,0 +1,29 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+public record ColumnSortLabels
+{
+ ///
+ /// Gets or sets the text shown in the column menu
+ ///
+ public string SortMenu { get; set; } = "Sort";
+
+ ///
+ /// Gets or sets the text shown in the column menu when in ascending order
+ ///
+ public string SortMenuAscendingLabel { get; set; } = "Sort (ascending)";
+
+ ///
+ /// Gets or sets the text shown in the column menu when in descending order
+ ///
+ public string SortMenuDescendingLabel { get; set; } = "Sort (descending)";
+
+ ///
+ /// Gets the default labels for the sort UI.
+ ///
+ public static ColumnSortLabels Default => new();
+
+}
+
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor
index b204808cc6..733199fe8f 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor
@@ -27,6 +27,7 @@
aria-rowcount="@(_internalGridContext.TotalItemCount + 1)"
@onrowfocus=HandleOnRowFocusAsync
@onclosecolumnoptions="CloseColumnOptions"
+ @onclosecolumnresize="CloseColumnResize"
@attributes="AdditionalAttributes">
@if (GenerateHeader != GenerateHeaderOption.None)
{
@@ -150,21 +151,21 @@
@if (col == _displayOptionsForColumn)
{
-
@col.ColumnOptions
- @if (ResizeType is not null )
- {
- @if (@col.ColumnOptions is not null)
- {
-
- }
+
+ }
+ @if (ResizableColumns && col == _displayResizeForColumn)
+ {
+
-
- }
+ @if (ResizeType is not null)
+ {
+
+ }
-
}
+
@if (ResizableColumns)
{
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
index e1abd62601..9aa3dfffc5 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
@@ -104,7 +104,25 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve
/// (Aria) Labels used in the column resize UI.
///
[Parameter]
- public ColumnResizeLabels ColumnResizeLabels { get; set; } = new ColumnResizeLabels();
+ public ColumnResizeLabels ColumnResizeLabels { get; set; } = ColumnResizeLabels.Default;
+
+ ///
+ /// Labels used in the column sort UI.
+ ///
+ [Parameter]
+ public ColumnSortLabels ColumnSortLabels { get; set; } = ColumnSortLabels.Default;
+
+ ///
+ /// Labels used in the column options UI.
+ ///
+ [Parameter]
+ public ColumnOptionsLabels ColumnOptionsLabels { get; set; } = ColumnOptionsLabels.Default;
+
+ ///
+ /// If true, enables the new style of header cell that includes a button to display all column options through a menu.
+ ///
+ [Parameter]
+ public bool HeaderCellAsButtonWithMenu { get; set; }
///
/// Optionally defines a value for @key on each rendered row. Typically this should be used to specify a
@@ -245,9 +263,11 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve
// Tracking state for options and sorting
private ColumnBase? _displayOptionsForColumn;
+ private ColumnBase? _displayResizeForColumn;
private ColumnBase? _sortByColumn;
private bool _sortByAscending;
private bool _checkColumnOptionsPosition;
+ private bool _checkColumnResizePosition;
private bool _manualGrid;
// The associated ES6 module, which uses document-level event listeners
@@ -370,7 +390,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (_checkColumnOptionsPosition && _displayOptionsForColumn is not null)
{
_checkColumnOptionsPosition = false;
- _ = Module?.InvokeVoidAsync("checkColumnOptionsPosition", _gridReference).AsTask();
+ _ = Module?.InvokeVoidAsync("checkColumnPopupPosition", _gridReference, ".col-options").AsTask();
+ }
+
+ if (_checkColumnResizePosition && _displayResizeForColumn is not null)
+ {
+ _checkColumnResizePosition = false;
+ _ = Module?.InvokeVoidAsync("checkColumnPopupPosition", _gridReference, ".col-resize").AsTask();
}
}
@@ -491,6 +517,19 @@ public Task ShowColumnOptionsAsync(ColumnBase column)
return Task.CompletedTask;
}
+ ///
+ /// Displays the column resize UI for the specified column, closing any other column
+ /// resize UI that was previously displayed.
+ ///
+ /// The column whose resize UI is to be displayed.
+ public Task ShowColumnResizeAsync(ColumnBase column)
+ {
+ _displayResizeForColumn = column;
+ _checkColumnResizePosition = true; // Triggers a call to JSRuntime to position the options element, apply autofocus, and any other setup
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
public void SetLoadingState(bool loading)
{
Loading = loading;
@@ -689,6 +728,12 @@ private void CloseColumnOptions()
StateHasChanged();
}
+ private void CloseColumnResize()
+ {
+ _displayResizeForColumn = null;
+ StateHasChanged();
+ }
+
private async Task HandleOnRowFocusAsync(DataGridRowFocusEventArgs args)
{
var rowId = args.RowId;
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.css b/src/Core/Components/DataGrid/FluentDataGrid.razor.css
index 28c17768ab..d371ef5f67 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.css
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.css
@@ -16,7 +16,7 @@ fluent-data-grid {
font-weight: 600;
}
-.col-options {
+.col-options, .col-resize {
position: absolute;
min-width: 250px;
top: 2.2rem;
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.js b/src/Core/Components/DataGrid/FluentDataGrid.razor.js
index 1606a5f195..d6c3c6eadc 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.js
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.js
@@ -16,6 +16,10 @@ export function init(gridElement) {
if (columnOptionsElement && event.composedPath().indexOf(columnOptionsElement) < 0) {
gridElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
}
+ const columnResizeElement = gridElement?.querySelector('.col-resize');
+ if (columnResizeElement && event.composedPath().indexOf(columnResizeElement) < 0) {
+ gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
+ }
};
const keyDownHandler = event => {
const columnOptionsElement = gridElement?.querySelector('.col-options');
@@ -33,6 +37,21 @@ export function init(gridElement) {
}
);
}
+ const columnResizeElement = gridElement?.querySelector('.col-resize');
+ if (columnResizeElement) {
+ if (event.key === "Escape") {
+ gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
+ gridElement.focus();
+ }
+ columnResizeElement.addEventListener(
+ "keydown",
+ (event) => {
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "ArrowUp") {
+ event.stopPropagation();
+ }
+ }
+ );
+ }
};
const cells = gridElement.querySelectorAll('[role="gridcell"]');
@@ -92,6 +111,53 @@ export function checkColumnOptionsPosition(gridElement) {
}
}
+export function checkColumnResizePosition(gridElement) {
+ const colOptions = gridElement?._rowElements[0] && gridElement?.querySelector('.col-resize'); // Only match within *our* thead, not nested tables
+ if (colResize) {
+ // We want the options popup to be positioned over the grid, not overflowing on either side, because it's possible that
+ // beyond either side is off-screen or outside the scroll range of an ancestor
+ const gridRect = gridElement.getBoundingClientRect();
+ const resizeRect = colResize.getBoundingClientRect();
+ const leftOverhang = Math.max(0, gridRect.left - resizeRect.left);
+ const rightOverhang = Math.max(0, resizeRect.right - gridRect.right);
+ if (leftOverhang || rightOverhang) {
+ // In the unlikely event that it overhangs both sides, we'll center it
+ const applyOffset = leftOverhang && rightOverhang ? (leftOverhang - rightOverhang) / 2 : (leftOverhang - rightOverhang);
+ colResize.style.transform = `translateX(${applyOffset}px)`;
+ }
+
+ colResize.scrollIntoViewIfNeeded();
+
+ const autoFocusElem = colResize.querySelector('[autofocus]');
+ if (autoFocusElem) {
+ autoFocusElem.focus();
+ }
+ }
+}
+
+export function checkColumnPopupPosition(gridElement, selector) {
+ const colPopup = gridElement?._rowElements[0] && gridElement?.querySelector(selector); // Only match within *our* thead, not nested tables
+ if (colPopup) {
+ // We want the options popup to be positioned over the grid, not overflowing on either side, because it's possible that
+ // beyond either side is off-screen or outside the scroll range of an ancestor
+ const gridRect = gridElement.getBoundingClientRect();
+ const popupRect = colPopup.getBoundingClientRect();
+ const leftOverhang = Math.max(0, gridRect.left - popupRect.left);
+ const rightOverhang = Math.max(0, popupRect.right - gridRect.right);
+ if (leftOverhang || rightOverhang) {
+ // In the unlikely event that it overhangs both sides, we'll center it
+ const applyOffset = leftOverhang && rightOverhang ? (leftOverhang - rightOverhang) / 2 : (leftOverhang - rightOverhang);
+ colPopup.style.transform = `translateX(${applyOffset}px)`;
+ }
+
+ colPopup.scrollIntoViewIfNeeded();
+
+ const autoFocusElem = colPopup.querySelector('[autofocus]');
+ if (autoFocusElem) {
+ autoFocusElem.focus();
+ }
+ }
+}
export function enableColumnResizing(gridElement) {
if (gridElement === latestGridElement)
return;
@@ -276,5 +342,6 @@ export function resizeColumnExact(gridElement, column, width) {
.join(' ');
gridElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
+ gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
gridElement.focus();
}
diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
index 3bb056b485..e866d970d3 100644
--- a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
+++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
@@ -80,6 +80,10 @@ fluent-data-grid-cell {
text-overflow: ellipsis;
}
+::deep .col-sort-button.disabled::part(control) {
+ opacity: 1 !important;
+}
+
.col-justify-center ::deep .col-sort-button::part(control) {
justify-content: center;
}
@@ -88,6 +92,10 @@ fluent-data-grid-cell {
justify-content: end;
}
+.col-justify-end ::deep .col-sort-button::part(start), .col-justify-right ::deep .col-sort-button::part(start) {
+ margin-inline-end: 0 !important;
+}
+
.col-justify-center {
justify-self: center;
}
diff --git a/src/Core/Components/DataGrid/Infrastructure/ColumnResizeLabels.cs b/src/Core/Components/DataGrid/Infrastructure/ColumnResizeLabels.cs
deleted file mode 100644
index bd700d3461..0000000000
--- a/src/Core/Components/DataGrid/Infrastructure/ColumnResizeLabels.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// ------------------------------------------------------------------------
-// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
-// ------------------------------------------------------------------------
-
-namespace Microsoft.FluentUI.AspNetCore.Components;
-
-public record ColumnResizeLabels(string DiscreteLabel = "Column width",
- string ExactLabel = "Column width (in pixels)",
- string GrowAriaLabel = "Grow column width",
- string ShrinkAriaLabel = "Shrink column width",
- string ResetAriaLabel = "Reset column widths",
- string SubmitAriaLabel = "Set column widths");
diff --git a/src/Core/Events/EventHandlers.cs b/src/Core/Events/EventHandlers.cs
index 675da23599..07a5936d81 100644
--- a/src/Core/Events/EventHandlers.cs
+++ b/src/Core/Events/EventHandlers.cs
@@ -17,6 +17,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
[EventHandler("oncellfocus", typeof(DataGridCellFocusEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onrowfocus", typeof(DataGridRowFocusEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onclosecolumnoptions", typeof(EventArgs), enableStopPropagation: true, enablePreventDefault: true)]
+[EventHandler("onclosecolumnresize", typeof(EventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("ontooltipdismiss", typeof(EventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onsplitterresized", typeof(SplitterResizedEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onsplittercollapsed", typeof(SplitterCollapsedEventArgs), enableStopPropagation: true, enablePreventDefault: true)]