Skip to content

Commit

Permalink
[DataGrid] Add remove sort capability to columns (#1826)
Browse files Browse the repository at this point in the history
* Add remove sort for datagrid

* Undo remove sort method returning bool

* A11y fixes: add keydown handler to remove column sort

* Update WhatsNew
Update DataGrid a11y docs
  • Loading branch information
vnbaaij committed Apr 29, 2024
1 parent 7f7e350 commit 28ab7f9
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 35 deletions.
12 changes: 12 additions & 0 deletions examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
</table>
</p>

<h2 id="a11y">Accessibility</h2>
<p>
You can use the <kbd>Arrow</kbd> keys to navigate through a DataGrid. When a header cell is focused and the column is sortable, you can use the <kbd>Tab</kbd> key to select the sort button.
Pressing the <kbd>Enter</kbd> key will toggle the sorting direction. Pressing <kbd>Ctrl+Enter</kbd> removes the column sorting and restores the default/start situation with regards to sorting.
<em>You cannot not remove the default grid sorting with this key combination.</em>
</p>
<p>
When a header cell is focused and the column allows setting options, you can use the <kbd>Tab</kbd> key to select the options button.
Pressing the <kbd>Enter</kbd> key then will toggle the options popover. Pressing <kbd>Esc</kbd> closes the popover
.
</p>

<h2 id="example">Examples</h2>

<DemoSection Title="Get started" Component="@typeof(DataGridGetStarted)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
.ByDescending(x => x.Medals.Gold)
.ThenDescending(x => x.Medals.Silver)
.ThenDescending(x => x.Medals.Bronze);

Func<Country, string?> rowClass = x => x.Name.StartsWith("A") ? "highlighted-row" : null;
Func<Country, string?> rowStyle = x => x.Name.StartsWith("Au") ? "background-color: var(--highlight-bg);" : null;

Expand All @@ -56,9 +56,12 @@
}
}

private void HandleClear(string? value)
private void HandleClear()
{
nameFilter = value ?? string.Empty;
if (string.IsNullOrWhiteSpace(nameFilter))
{
nameFilter = string.Empty;
}
}

private async Task ToggleItemsAsync()
Expand Down
34 changes: 19 additions & 15 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.Fast.Components.FluentUI.DataGrid.Infrastructure
@namespace Microsoft.Fast.Components.FluentUI
@typeparam TGridItem
Expand All @@ -20,7 +20,7 @@
@if (ColumnOptions is not null && (Align == Align.Start || Align == Align.Center))
{
<FluentButton Appearance="Appearance.Stealth" class="col-options-button" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))" aria-label="Filter this column">
@if(Filtered.GetValueOrDefault())
@if (Filtered.GetValueOrDefault())
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.FilterDismiss())" />
}
Expand All @@ -33,22 +33,26 @@

if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault())
{
<FluentButton Appearance="Appearance.Stealth" class="col-sort-button" @onclick="@(() => Grid.SortByColumnAsync(this))" aria-label="@tooltip" title="@tooltip">
<div class="col-title-text" title="@tooltip">@Title</div>
<FluentKeyCode Only="new [] { KeyCode.Ctrl, KeyCode.Enter}" OnKeyDown="HandleKeyDown" class="keycapture">
<span class="col-sort-container" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(this))" @oncontextmenu:preventDefault>
<FluentButton Appearance="Appearance.Stealth" class="col-sort-button" @onclick="@(() => Grid.SortByColumnAsync(this))" aria-label="@tooltip" title="@tooltip">
<div class="col-title-text" title="@tooltip">@Title</div>

@if (Grid.SortByAscending.HasValue && ShowSortIcon)
{
if (Grid.SortByAscending == true)
@if (Grid.SortByAscending.HasValue && ShowSortIcon)
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.ArrowSortUp())" Slot="@(Align == Align.End ? "start" : "end")" />
if (Grid.SortByAscending == true)
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.ArrowSortUp())" Slot="@(Align == Align.End ? "start" : "end")" />
}
else
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.ArrowSortDown())" Slot="@(Align == Align.End ? "start" : "end")" />
}
}
else
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.ArrowSortDown())" Slot="@(Align == Align.End ? "start" : "end")" />
}
}

</FluentButton>
</FluentButton>
</span>
</FluentKeyCode>
}
else
{
Expand All @@ -60,7 +64,7 @@
@if (ColumnOptions is not null && Align == Align.End)
{
<FluentButton Appearance="Appearance.Stealth" class="col-options-button" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))" aria-label="Filter this column">
@if(Filtered.GetValueOrDefault())
@if (Filtered.GetValueOrDefault())
{
<FluentIcon Value="@(new CoreIcons.Regular.Size24.FilterDismiss())" />
}
Expand Down
29 changes: 20 additions & 9 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ public abstract partial class ColumnBase<TGridItem>
[CascadingParameter] internal InternalGridContext<TGridItem> InternalGridContext { get; set; } = default!;

/// <summary>
/// Title text for the column. This is rendered automatically if <see cref="HeaderCellItemTemplate" /> is not used.
/// Gets or sets the title text for the column.
/// This is rendered automatically if <see cref="HeaderCellItemTemplate" /> is not used.
/// </summary>
[Parameter] public string? Title { get; set; }

/// <summary>
/// An optional CSS class name. If specified, this is included in the class attribute of header and grid cells
/// Gets or sets the an optional CSS class name.
/// If specified, this is included in the class attribute of header and grid cells
/// for this column.
/// </summary>
[Parameter] public string? Class { get; set; }

/// <summary>
/// An optional CSS style specification. If specified, this is included in the style attribute of header and grid cells
/// Gets or sets an optional CSS style specification.
/// If specified, this is included in the style attribute of header and grid cells
/// for this column.
/// </summary>
[Parameter] public string? Style { get; set; }
Expand All @@ -36,18 +39,18 @@ public abstract partial class ColumnBase<TGridItem>
[Parameter] public Align Align { get; set; }

/// <summary>
/// If true, generate a title and aria-label attribute for the cell contents
/// If true, generates a title and aria-label attribute for the cell contents
/// </summary>
[Parameter] public bool Tooltip { get; set; } = false;

/// <summary>
/// Defines the value to be used as the tooltip and aria-label in this column's cells
/// Gets or sets the value to be used as the tooltip and aria-label in this column's cells
/// </summary>
[Parameter] public Func<TGridItem, string?>? TooltipText { get; set; }

/// <summary>
/// An optional template for this column's header cell. If not specified, the default header template
/// includes the <see cref="Title" /> along with any applicable sort indicators and options buttons.
/// Gets or sets an optional template for this column's header cell.
/// If not specified, the default header template includes the <see cref="Title" /> along with any applicable sort indicators and options buttons.
/// </summary>
[Parameter] public RenderFragment<ColumnBase<TGridItem>>? HeaderCellItemTemplate { get; set; }

Expand Down Expand Up @@ -81,7 +84,7 @@ public abstract partial class ColumnBase<TGridItem>
public abstract GridSort<TGridItem>? SortBy { get; set; }

/// <summary>
/// Indicates which direction to sort in
/// Gets or sets the initial sort direction.
/// if <see cref="IsDefaultSortColumn"/> is true.
/// </summary>
[Parameter] public SortDirection InitialSortDirection { get; set; } = default;
Expand Down Expand Up @@ -123,7 +126,7 @@ public abstract partial class ColumnBase<TGridItem>
protected internal RenderFragment HeaderContent { get; protected set; }

/// <summary>
/// Get a value indicating whether this column should act as sortable if no value was set for the
/// Gets a value indicating whether this column should act as sortable if no value was set for the
/// <see cref="ColumnBase{TGridItem}.Sortable" /> parameter. The default behavior is not to be
/// sortable unless <see cref="ColumnBase{TGridItem}.Sortable" /> is true.
///
Expand All @@ -132,6 +135,14 @@ public abstract partial class ColumnBase<TGridItem>
/// <returns>True if the column should be sortable by default, otherwise false.</returns>
protected virtual bool IsSortableByDefault() => false;

protected void HandleKeyDown(FluentKeyCodeEventArgs e)
{
if (e.CtrlKey && e.Key == KeyCode.Enter)
{
Grid.RemoveSortByColumnAsync(this);
}
}

public bool ShowSortIcon;

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ internal void AddColumn(ColumnBase<TGridItem> column, SortDirection? initialSort
{
_sortByColumn = column;
_sortByAscending = initialSortDirection.Value != SortDirection.Descending;
_internalGridContext.DefaultSortColumn = (column, initialSortDirection.Value);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function init(gridElement) {
if (columnOptionsElement) {
if (event.key === "Escape") {
gridElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
gridElement.focus();
}
columnOptionsElement.addEventListener(
"keydown",
Expand Down
30 changes: 23 additions & 7 deletions src/Core/Components/DataGrid/FluentDataGridCell.razor.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fluent-data-grid-cell {
fluent-data-grid-cell {
text-overflow: ellipsis;
}

Expand All @@ -10,28 +10,44 @@

.column-header {
display: flex;
width: 100%;
height: 100%;
align-self: center;
padding-inline: 0;
padding-right: 1px;
padding-left: 1px;
}

.column-header.col-justify-end, .column-header.col-justify-right {
width: 100%;
padding: 0;

padding-right: 1px;
padding-left: 1px;
justify-content: end;
}

.column-header.col-justify-center {
width: 100%;
padding: 0;

padding-left: 1px;
padding-right: 1px;
justify-content: center;
}

.column-header > ::deep .col-sort-button {
.column-header > ::deep .keycapture {
display: flex;
flex-grow: 1;
}

.column-header > ::deep .keycapture .col-sort-container {
display: flex;
flex-grow: 1;
}

.column-header > ::deep .keycapture .col-sort-container .col-sort-button {
flex-grow: 1;
padding-inline-end: 5px;
}

.column-header > ::deep .col-sort-button::part(content) {
.column-header > ::deep .keycapture .col-sort-container .col-sort-button::part(content) {
overflow: hidden;
text-overflow: ellipsis;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Core/Components/DataGrid/Infrastructure/InternalGridContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ internal sealed class InternalGridContext<TGridItem>
private int _index = 0;
public Dictionary<string, FluentDataGridRow<TGridItem>> Rows { get; set; } = new();

public (ColumnBase<TGridItem>? Column, SortDirection? Direction) DefaultSortColumn { get; set; }
//public SortDirection? DefaultSortDirection { get; set; }

public FluentDataGrid<TGridItem> Grid { get; }
public EventCallbackSubscribable<object?> ColumnsFirstCollected { get; } = new();

Expand All @@ -17,6 +20,18 @@ public InternalGridContext(FluentDataGrid<TGridItem> grid)
Grid = grid;
}

public int GetNextRowId()
{
Interlocked.Increment(ref _rowId);
return _rowId;
}

public int GetNextCellId()
{
Interlocked.Increment(ref _cellId);
return _cellId;
}

internal void ResetRowIndexes(int start)
{
_index = start;
Expand Down
8 changes: 7 additions & 1 deletion src/Core/Components/Dialog/Services/DialogService-Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ public async Task<IDialogReference> ShowPanelAsync<TDialog>(DialogParameters par
return await ShowDialogAsync<object>(typeof(TDialog), default!, FixPanelParameters(parameters));
}

private DialogParameters FixPanelParameters(DialogParameters value)
/// <inheritdoc cref="IDialogService.ShowDialogAsync(RenderFragment, DialogParameters)"/>
public async Task<IDialogReference> ShowDialogAsync(RenderFragment renderFragment, DialogParameters dialogParameters)
{
return await ShowDialogAsync(typeof(RenderFragmentDialog), renderFragment, dialogParameters);
}

private static DialogParameters FixPanelParameters(DialogParameters value)
{
value.DialogType = DialogType.Panel;

Expand Down

0 comments on commit 28ab7f9

Please sign in to comment.