Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fdaa067
add new BitDataGrid component #12502
msynk Jun 21, 2026
8316057
resolve review comments
msynk Jun 21, 2026
8acd2aa
fix some issues
msynk Jun 22, 2026
b9ba019
resolve review comments II
msynk Jun 22, 2026
a44c223
resovle review comments III
msynk Jun 22, 2026
8a11d7c
resolve review comments IV
msynk Jun 23, 2026
08a958f
resolve review comments V
msynk Jun 23, 2026
5ccebf5
resolve review comments VI
msynk Jun 23, 2026
f359f8e
resolve review comments VII
msynk Jun 23, 2026
994fb00
resolve review comments VIII
msynk Jun 26, 2026
2bc4693
resolve review comments IX
msynk Jun 26, 2026
58170be
resolve review comments X
msynk Jun 26, 2026
ad74781
resolve review comments XI
msynk Jun 26, 2026
1803009
resolve review comments XII
msynk Jun 26, 2026
c3a5f87
resolve review comments XIII
msynk Jun 27, 2026
f1e27e0
resolve review comments XIV
msynk Jun 27, 2026
512597d
fix blazor wasm issues
msynk Jun 27, 2026
469c04a
resolve review comments XV
msynk Jun 27, 2026
f0c5945
resolve review comments XVI
msynk Jun 27, 2026
c834f34
resolve review comments XVII
msynk Jun 27, 2026
d2651ed
resolve review comments XVIII
msynk Jun 27, 2026
b790687
resolve review comments XIX
msynk Jun 27, 2026
0306768
resolve review comments XX
msynk Jun 28, 2026
30ce88d
resolve review comments XXI
msynk Jun 28, 2026
6261e51
resolve review comments XXII
msynk Jun 28, 2026
e04eac9
resolve review comments XXIII
msynk Jun 28, 2026
54822f6
resolve review comments XXIV
msynk Jun 28, 2026
70b1704
resolve review comments XXV
msynk Jun 28, 2026
32c2f34
resovle review comments XXVI
msynk Jun 28, 2026
334d539
resolve review comments XXVII
msynk Jun 28, 2026
f3a3b26
resolve review comments XXVIII
msynk Jun 28, 2026
a1456d7
resolve review comments XXIX
msynk Jun 28, 2026
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
486 changes: 383 additions & 103 deletions src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor

Large diffs are not rendered by default.

1,421 changes: 1,060 additions & 361 deletions src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs

Large diffs are not rendered by default.

384 changes: 285 additions & 99 deletions src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.scss

Large diffs are not rendered by default.

113 changes: 25 additions & 88 deletions src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,38 @@
namespace BitBlazorUI {
export class DataGrid {
public static init(tableElement: any) {
DataGrid.enableColumnResizing(tableElement);

const bodyClickHandler = (event: any) => {
const columnOptionsElement = tableElement.tHead.querySelector('.bit-dtg-cop');
if (columnOptionsElement && event.composedPath().indexOf(columnOptionsElement) < 0) {
tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
// Infinite scrolling is the one feature that genuinely needs to read scroll
// position (which Blazor's scroll EventArgs do not expose), so this watches
// the viewport and notifies .NET when the user nears the end.
public static initInfiniteScroll(viewport: HTMLElement, dotNetRef: DotNetObject, threshold: number) {
const distance = threshold ?? 200;
let ticking = false;
let disposed = false;

const check = () => {
ticking = false;
if (disposed || !viewport) return;
const remaining = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
if (remaining <= distance) {
dotNetRef.invokeMethodAsync('OnInfiniteScrollNearEndAsync');
}
};
Comment thread
msynk marked this conversation as resolved.
const keyDownHandler = (event: any) => {
const columnOptionsElement = tableElement.tHead.querySelector('.bit-dtg-cop');
if (columnOptionsElement && event.key === "Escape") {
tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));

const onScroll = () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(check);
}
};

document.body.addEventListener('click', bodyClickHandler);
document.body.addEventListener('mousedown', bodyClickHandler); // Otherwise it seems strange that it doesn't go away until you release the mouse button
document.body.addEventListener('keydown', keyDownHandler);
viewport.addEventListener('scroll', onScroll, { passive: true });
// Initial check so a first batch that doesn't fill the viewport keeps loading.
setTimeout(check, 0);

return {
stop: () => {
document.body.removeEventListener('click', bodyClickHandler);
document.body.removeEventListener('mousedown', bodyClickHandler);
document.body.removeEventListener('keydown', keyDownHandler);
}
check: () => check(),
scrollToTop: () => { if (viewport) viewport.scrollTop = 0; },
dispose: () => { disposed = true; viewport.removeEventListener('scroll', onScroll); }
};
}

public static checkColumnOptionsPosition(tableElement: any) {
const colOptions = tableElement.tHead && tableElement.tHead.querySelector('.bit-dtg-cop'); // Only match within *our* thead, not nested tables
if (colOptions) {
// 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 = tableElement.getBoundingClientRect();
const optionsRect = colOptions.getBoundingClientRect();
const leftOverhang = Math.max(0, gridRect.left - optionsRect.left);
const rightOverhang = Math.max(0, optionsRect.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);
colOptions.style.transform = `translateX(${applyOffset}px)`;
}

colOptions.scrollIntoViewIfNeeded();

const autoFocusElem = colOptions.querySelector('[autofocus]');
if (autoFocusElem) {
autoFocusElem.focus();
}
}
}

private static enableColumnResizing(tableElement: any) {
tableElement.tHead.querySelectorAll('.bit-dtg-drg').forEach((handle: any) => {
handle.addEventListener('mousedown', handleMouseDown);
if ('ontouchstart' in window) {
handle.addEventListener('touchstart', handleMouseDown);
}

function handleMouseDown(evt: any) {
evt.preventDefault();
evt.stopPropagation();

const th = handle.parentElement;
const startPageX = evt.touches ? evt.touches[0].pageX : evt.pageX;
const originalColumnWidth = th.offsetWidth;
const rtlMultiplier = window.getComputedStyle(th, null).getPropertyValue('direction') === 'rtl' ? -1 : 1;
let updatedColumnWidth = 0;

function handleMouseMove(evt: any) {
evt.stopPropagation();
const newPageX = evt.touches ? evt.touches[0].pageX : evt.pageX;
const nextWidth = originalColumnWidth + (newPageX - startPageX) * rtlMultiplier;
if (Math.abs(nextWidth - updatedColumnWidth) > 0) {
updatedColumnWidth = nextWidth;
th.style.width = `${updatedColumnWidth}px`;
}
}

function handleMouseUp() {
document.body.removeEventListener('mousemove', handleMouseMove);
document.body.removeEventListener('mouseup', handleMouseUp);
document.body.removeEventListener('touchmove', handleMouseMove);
document.body.removeEventListener('touchend', handleMouseUp);
}

if (window.TouchEvent && evt instanceof TouchEvent) {
document.body.addEventListener('touchmove', handleMouseMove, { passive: true });
document.body.addEventListener('touchend', handleMouseUp, { passive: true });
} else {
document.body.addEventListener('mousemove', handleMouseMove, { passive: true });
document.body.addEventListener('mouseup', handleMouseUp, { passive: true });
}
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@typeparam TItem
@namespace Bit.BlazorUI

@* A single data cell. Each cell owns one stable element with an unconditional @@ref so
keyboard navigation can move DOM focus via Blazor's FocusAsync without conditional
reference-capture frames (which break render-tree diffing). *@
<div @ref="_el" class="@CssClass" role="gridcell" style="@Style" tabindex="@TabIndex"
@onclick="HandleClick"
@ondblclick="HandleDoubleClick"
@oncontextmenu="HandleContextMenu"
@oncontextmenu:preventDefault="Grid.OnCellContextMenu.HasDelegate"
@onfocusin="HandleFocusIn"
@onkeydown="HandleKeyDown">
Comment thread
msynk marked this conversation as resolved.
@ChildContent
</div>

@code {
[Parameter, EditorRequired] public BitDataGrid<TItem> Grid { get; set; } = default!;
[Parameter, EditorRequired] public TItem Item { get; set; } = default!;
[Parameter, EditorRequired] public BitDataGridColumn<TItem> Column { get; set; } = default!;
[Parameter] public int ColIndex { get; set; }
[Parameter] public string? CssClass { get; set; }
[Parameter] public string? Style { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }

private ElementReference _el;

private int? TabIndex => Grid.CellNavigation ? Grid.CellTabIndex(Item, ColIndex) : (int?)null;
private bool Editing => Grid.IsEditing(Item);

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (Grid.CellNavigation && Grid.ShouldFocusCell(Item, ColIndex))
{
Grid.ClearFocusPending();
try { await _el.FocusAsync(); } catch { /* element may have been removed */ }
}
}

private Task HandleClick(MouseEventArgs e) => Grid.HandleCellClickAsync(Column, Item, e);
private Task HandleDoubleClick(MouseEventArgs e) => Grid.HandleCellDoubleClickAsync(Column, Item, e);
private Task HandleContextMenu(MouseEventArgs e) => Grid.HandleCellContextMenuAsync(Column, Item, e);

private void HandleFocusIn()
{
if (Grid.CellNavigation) Grid.SetFocusedCell(Item, ColIndex);
}

private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (!Grid.CellNavigation) return;

// While inline-editing, the cell only handles the edit lifecycle keys; everything
// else (typing, caret movement) belongs to the editor input.
if (Editing)
{
if (e.Key == "Escape")
{
await Grid.CancelEditAsync();
Grid.RefocusFocusedCell();
}
else if (e.Key == "Enter")
{
await Grid.CommitEditAsync();
Grid.RefocusFocusedCell();
}
return;
}

await Grid.HandleCellKeyDownAsync(Item, ColIndex, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@typeparam TItem
@namespace Bit.BlazorUI

@switch (Column.EffectiveDataType)
{
case BitDataGridColumnDataType.Boolean:
<input type="checkbox" class="bit-dtg-editor bit-dtg-editor-check"
checked="@(GetBool())"
@onchange="e => Set(e.Value)" />
break;
Comment thread
msynk marked this conversation as resolved.

case BitDataGridColumnDataType.Number:
<input type="number" class="bit-dtg-editor" step="any"
value="@(GetString())"
@oninput="e => Set(e.Value)" />
Comment thread
msynk marked this conversation as resolved.
Outdated
break;

case BitDataGridColumnDataType.Date:
<input type="date" class="bit-dtg-editor"
value="@(GetDate())"
@onchange="e => Set(e.Value)" />
break;

case BitDataGridColumnDataType.Enum:
<select class="bit-dtg-editor" value="@(GetString())" @onchange="e => Set(e.Value)">
@foreach (var name in Enum.GetNames(Column.Accessor!.UnderlyingType))
{
Comment thread
msynk marked this conversation as resolved.
Outdated
<option value="@name" selected="@(name == GetString())">@name</option>
}
</select>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
break;

default:
<input type="text" class="bit-dtg-editor"
value="@(GetString())"
@oninput="e => Set(e.Value)" />
Comment on lines +10 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give inline editor controls accessible names.

These inputs/selects focus directly but don’t expose the column name. Add an aria-label such as Edit @Column.DisplayTitle`` to each built-in editor control.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGridCellEditor.razor`
around lines 10 - 65, The built-in editors in BitDataGridCellEditor.razor
currently have no accessible name, so screen readers can’t identify the column
being edited. Update each editor branch in the switch (select/input for Boolean,
Number, Date, DateTime, DateTimeOffset, Enum, and default text) to include an
aria-label based on the column title, using Column.DisplayTitle in the label
text. Keep the change localized to these editor controls so focusable elements
announce the correct field name.

break;
}

@code {
[Parameter, EditorRequired] public BitDataGrid<TItem> Grid { get; set; } = default!;
[Parameter, EditorRequired] public BitDataGridColumn<TItem> Column { get; set; } = default!;
[Parameter, EditorRequired] public TItem Item { get; set; } = default!;

private object? Value => Column.GetValue(Item);
private string GetString() => Value?.ToString() ?? string.Empty;
private bool GetBool() => Value is bool b && b;

private string GetDate()
{
return Value switch
{
DateTime dt => dt.ToString("yyyy-MM-dd"),
DateOnly d => d.ToString("yyyy-MM-dd"),
DateTimeOffset dto => dto.ToString("yyyy-MM-dd"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
_ => string.Empty
};
}

private void Set(object? raw) => Grid.SetEditValue(Column, raw);
}
Loading
Loading