From cba735435ad89dda15963f3838c601736a2d220d Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Wed, 16 Sep 2026 16:23:03 +0800 Subject: [PATCH 1/9] =?UTF-8?q?test:=E5=9B=BA=E5=AE=9A=E5=88=97=E6=8B=96?= =?UTF-8?q?=E5=8A=A8=E5=88=97=E5=AE=BD=E6=B5=8B=E8=AF=95=EF=BC=8C=E8=BF=98?= =?UTF-8?q?=E6=9C=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test:固定列拖动列宽测试,还有问题,拖动列宽时其他列宽也会跟着变,固定列宽调大后,再由大拖动变小,拖动时有点困难 --- src/BootstrapBlazor/Components/Table/Table.razor.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index 6ce02dd3986..15c5d27c57a 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -215,10 +215,12 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( private string? GetHeaderStyleString(ITableColumn col, int margin = 0) { var fixedStyle = GetFixedCellStyleString(col, margin); - var widthStyle = col.Width is > 0 + var columnState = _tableColumnStateCache.Columns.FirstOrDefault(x => x.Name == col.GetFieldName()); + var width = columnState?.Width ?? col.Width; + var widthStyle = width is > 0 ? AllowResizing - ? $"width: {col.Width}px;" - : $"width: {col.Width}px; min-width: {col.Width}px;" + ? $"width: {width}px;" + : $"width: {width}px; min-width: {width}px;" : null; if (fixedStyle == null) { @@ -1609,7 +1611,7 @@ private async Task OnTableRenderAsync(bool firstRender) } // 计算实际宽度 - var width = _tableColumnStateCache.TableWidth; + var width = _tableColumnStateCache.Columns.Sum(x => x.Width); var tableWidth = hasHeader ? width : width - ActualScrollWidth; return $"width: {tableWidth}px;"; From d69e43628383519f635099048d6bfd3eb2ba0b83 Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Thu, 17 Sep 2026 10:00:30 +0800 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=E8=A1=A5=E5=85=85=E6=8B=96?= =?UTF-8?q?=E5=8A=A8=E5=88=97=E5=AE=BD=E5=AD=98=E7=9A=84=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=B7=A6=E5=8F=B3=E5=9B=BA=E5=AE=9A=E5=88=97=E5=B7=B2=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor:补充拖动列宽存的题,左右固定列已测试 --- .../Components/Table/Table.razor.js | 117 ++++++++++++------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.js b/src/BootstrapBlazor/Components/Table/Table.razor.js index a257c1fd4d4..554e393e3cd 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.js +++ b/src/BootstrapBlazor/Components/Table/Table.razor.js @@ -467,7 +467,9 @@ const setResizeListener = table => { } let colWidth = 0 - let tableWidth = 0 + let colMinWidth = 5 + let columnWidths = [] + let tableWidths = [] let colIndex = 0 let originalX = 0 @@ -489,53 +491,76 @@ const setResizeListener = table => { const tableEl = col.closest('table') const currentCol = tableEl.querySelectorAll('colgroup col')[colIndex] const width = currentCol.style.width - if (width) { - colWidth = parseInt(width) + const th = getColumnHeader(col); + const isFixedLayout = getComputedStyle(tableEl).tableLayout === 'fixed'; + if (isFixedLayout) { + colWidth = width ? parseInt(width) : getResizableColumnWidth(col); + colMinWidth = Math.max(table.options.columnMinWidth || 0, 5); } else { colWidth = getResizableColumnWidth(col); + colMinWidth = Math.max( + getCellWidth(th) + getHeaderIconsWidth(th), + getColumnMaxCellWidth(table, colIndex), + table.options.columnMinWidth || 0 + ) | 0; } - tableWidth = getWidth(tableEl); + const headerCells = [...th.parentElement.children]; + columnWidths = [...table.tables[0].querySelectorAll('colgroup col')].map((col, index) => { + const width = parseFloat(col.style.width); + return !isNaN(width) ? width : getWidth(headerCells[index]); + }); + tableWidths = table.tables.map(t => getWidth(t)); originalX = e.clientX ?? e.touches[0].clientX }, e => { const eventX = e.clientX ?? e.changedTouches[0].clientX const marginX = eventX - originalX - table.tables.forEach(t => { + const calcColWidth = Math.max(colWidth + marginX, colMinWidth); + const actualDelta = calcColWidth - colWidth; + columnWidths[colIndex] = calcColWidth; + table.tables.forEach((t, index) => { const group = [...t.children].find(i => i.nodeName === 'COLGROUP') - let calcColWidth = colWidth + marginX; - if (calcColWidth < 5) { - calcColWidth = 5; - } if (group) { const curCol = group.children.item(colIndex) - curCol.style.setProperty('width', `${calcColWidth}px`); - const tableEl = curCol.closest('table') - let width = tableWidth + marginX - if (t.closest('.table-fixed-body')) { - width = width - table.scrollWidth; + if (curCol) { + curCol.style.setProperty('width', `${calcColWidth}px`); + const tableEl = curCol.closest('table') + const width = tableWidths[index] + actualDelta; + tableEl.style.setProperty('width', `${width}px`); + } + } + }); + + table.tables.forEach(t => { + t.querySelectorAll('thead > tr').forEach(row => { + const th = row.children.item(colIndex); + if (th) { + th.style.setProperty('width', `${calcColWidth}px`); } - tableEl.setAttribute('style', `width: ${width}px;`) + }); + }); - resetColumnWidthTips(table, col); + resetColumnWidthTips(table, col); - const header = getColumnHeader(col); - if (header.classList.contains('fixed')) { - resizeNextFixedColumnWidth(header, getWidth(header)); + const header = getColumnHeader(col); + if (header.classList.contains('fixed')) { + resizeNextFixedColumnWidth(header, calcColWidth, columnWidths, colIndex); + table.tables.forEach(t => { + const tbody = [...t.children].find(i => i.nodeName === 'TBODY'); + if (!tbody) { + return; } - } - const tbody = [...t.children].find(i => i.nodeName === 'TBODY'); - if (tbody) { const rows = [...tbody.children].filter(i => i.nodeName === 'TR'); rows.forEach(row => { - const header = row.children.item(colIndex); - if (header !== null && header.classList.contains('fixed')) { - resizeNextFixedColumnWidth(header, getWidth(header)); + const cell = row.children.item(colIndex); + if (cell?.classList.contains('fixed')) { + resizeNextFixedColumnWidth(cell, calcColWidth, columnWidths, colIndex); } }); - } - }) + }); + } }, () => { eff(col, false) @@ -550,21 +575,29 @@ const setResizeListener = table => { }) } -const resizeNextFixedColumnWidth = (col, width) => { +const resizeNextFixedColumnWidth = (col, width, columnWidths, colIndex) => { if (col.classList.contains('fixed-right')) { const nextColumn = col.previousElementSibling; - if (nextColumn.classList.contains('fixed')) { - const right = parseFloat(col.style.getPropertyValue('right')); + if (nextColumn?.classList.contains('fixed')) { + const nextIndex = colIndex - 1; + const right = parseFloat(col.style.getPropertyValue('right')) || 0; nextColumn.style.setProperty('right', `${right + width}px`); - resizeNextFixedColumnWidth(nextColumn, getWidth(nextColumn)); + const nextWidth = columnWidths[nextIndex]; + if (nextWidth !== void 0) { + resizeNextFixedColumnWidth(nextColumn, nextWidth, columnWidths, nextIndex); + } } } else if (col.classList.contains('fixed')) { const nextColumn = col.nextElementSibling; - if (nextColumn.classList.contains('fixed')) { - const left = parseFloat(col.style.getPropertyValue('left')); + if (nextColumn?.classList.contains('fixed')) { + const nextIndex = colIndex + 1; + const left = parseFloat(col.style.getPropertyValue('left')) || 0; nextColumn.style.setProperty('left', `${left + width}px`); - resizeNextFixedColumnWidth(nextColumn, getWidth(nextColumn)); + const nextWidth = columnWidths[nextIndex]; + if (nextWidth !== void 0) { + resizeNextFixedColumnWidth(nextColumn, nextWidth, columnWidths, nextIndex); + } } } } @@ -1018,7 +1051,7 @@ const getColumnStateObject = table => { cols: cols.map(col => { return { name: col.name, - width: getColumnWidth(col, table.columns), + width: getColumnWidth(table, col, table.columns), visible: col.visible } }), @@ -1030,7 +1063,7 @@ const getColumnStateObject = table => { cols: table.columns.map(col => { return { name: getColumnName(col), - width: getResizableColumnWidth(col), + width: getColumnStyleWidth(table, col), visible: true } }), @@ -1038,11 +1071,10 @@ const getColumnStateObject = table => { }; } -const getColumnWidth = (col, columns) => { +const getColumnWidth = (table, col, columns) => { const column = columns.find(i => getColumnName(i) === col.name); if (column) { - const width = getResizableColumnWidth(column); - return width > 0 ? width : null; + return getColumnStyleWidth(table, column); } else if (col.width) { return col.width; @@ -1052,6 +1084,13 @@ const getColumnWidth = (col, columns) => { } } +const getColumnStyleWidth = (table, col) => { + const index = indexOfCol(col); + const colEl = table.tables[0].querySelectorAll('colgroup col')[index]; + const width = parseFloat(colEl?.style.width); + return !isNaN(width) && width > 0 ? width : getResizableColumnWidth(col); +} + const getTableWidth = table => { let tableWidth = 0; const colgroup = [...table.children].find(i => i.nodeName === 'COLGROUP'); From 57081a06cbe0b79afec678d288ea8001490a7bb9 Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Thu, 17 Sep 2026 11:14:26 +0800 Subject: [PATCH 3/9] =?UTF-8?q?refactor:=E5=B0=86=E6=89=80=E6=9C=89?= =?UTF-8?q?=E9=9D=9E=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E5=88=97=E7=9A=84?= =?UTF-8?q?th=E4=B8=8Astyle=E9=87=8C=E9=9D=A2=E5=8A=A0=E4=B8=8Awidth?= =?UTF-8?q?=E5=80=BC=EF=BC=8C=E4=BB=A5=E4=BF=9D=E8=AF=81=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor:将所有非用户数据列的th上style里面加上width值,以保证表格显示 refactor:将表格宽度的值计算全部由表格包含的全部列的值的总合,以保持表格宽度与总列宽一致,免避免出现各种奇怪问题 --- .../Components/Table/Table.razor | 18 +++---- .../Components/Table/Table.razor.Sort.cs | 12 +++-- .../Components/Table/Table.razor.cs | 54 +++++++++++++++++-- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor b/src/BootstrapBlazor/Components/Table/Table.razor index 5b6ef0c9ba9..8da63bd4d0e 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor +++ b/src/BootstrapBlazor/Components/Table/Table.razor @@ -380,7 +380,7 @@ } @if (hasScroll) { - +
 
} @@ -391,25 +391,25 @@ @if (ShowDetails()) { - +
 
} @if (IsMultipleSelect) { - +
 
} @if (ShowLineNo) { - +
 
} @if (ShowExtendButtons && IsExtendButtonsInRowHeader) { - +
 
} @@ -429,13 +429,13 @@ } @if (ShowExtendButtons && !IsExtendButtonsInRowHeader) { - +
 
} @if (hasScroll) { - +
 
} @@ -622,7 +622,7 @@ ; RenderFragment RenderExtendButtonsHeader => - @ + @
@ColumnButtonTemplateHeaderText
@@ -671,7 +671,7 @@ ; RenderFragment RenderRowExtendButtons => item => - @ + @
@{ diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs index c3f95ebc3f3..f4aa3a84c61 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs @@ -175,11 +175,17 @@ private int MultipleSelectColumnLeft() private bool GetFixedLineNoColumn => FixedLineNoColumn && ShowLineNo; - private string? DetailColumnStyleString => GetFixedDetailRowHeaderColumn ? "left: 0;" : null; + private string? DetailColumnStyleString => GetNonDataColumnStyleString( + GetFixedDetailRowHeaderColumn ? "left: 0;" : null, + DetailColumnWidth); - private string? LineNoColumnStyleString => GetFixedLineNoColumn ? $"left: {LineNoColumnLeft()}px;" : null; + private string? LineNoColumnStyleString => GetNonDataColumnStyleString( + GetFixedLineNoColumn ? $"left: {LineNoColumnLeft()}px;" : null, + LineNoColumnWidth); - private string? MultiColumnStyleString => GetFixedMultipleSelectColumn ? $"left: {MultipleSelectColumnLeft()}px;" : null; + private string? MultiColumnStyleString => GetNonDataColumnStyleString( + GetFixedMultipleSelectColumn ? $"left: {MultipleSelectColumnLeft()}px;" : null, + MultiColumnWidth); private int MultiColumnWidth => ShowCheckboxText ? ShowCheckboxTextColumnWidth : TableSize == TableSize.Normal diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index 15c5d27c57a..f60b2a4fae5 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -230,6 +230,29 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( return widthStyle == null ? fixedStyle : $"{fixedStyle} {widthStyle}"; } + private string? GetFixedExtendButtonsStyleString(int margin = 0) + { + var fixedStyle = GetFixedExtendButtonsColumnStyleString(margin); + return GetNonDataColumnStyleString(fixedStyle, ExtendButtonColumnWidth); + } + + private string? GetNonDataColumnStyleString(string? fixedStyle, int width) + { + var widthStyle = width > 0 + ? AllowResizing + ? $"width: {width}px;" + : $"width: {width}px; min-width: {width}px;" + : null; + if (fixedStyle == null) + { + return widthStyle; + } + + return widthStyle == null ? fixedStyle : $"{fixedStyle} {widthStyle}"; + } + + private string? ScrollColumnStyleString => GetNonDataColumnStyleString(null, ActualScrollWidth); + /// /// 获得/设置 滚动条宽度 默认 null 未设置使用 配置类中的 /// Gets or sets Scroll Width. Default null (Use ) @@ -1610,9 +1633,34 @@ private async Task OnTableRenderAsync(bool firstRender) return null; } - // 计算实际宽度 - var width = _tableColumnStateCache.Columns.Sum(x => x.Width); - var tableWidth = hasHeader ? width : width - ActualScrollWidth; + var tableWidth = _tableColumnStateCache.Columns + .Where(x => x.Visible) + .Sum(x => x.Width ?? 0); + + if (ShowDetails()) + { + tableWidth += DetailColumnWidth; + } + + if (IsMultipleSelect) + { + tableWidth += MultiColumnWidth; + } + + if (ShowLineNo) + { + tableWidth += LineNoColumnWidth; + } + + if (ShowExtendButtons) + { + tableWidth += ExtendButtonColumnWidth; + } + + if (IsFixedHeader && hasHeader) + { + tableWidth += ActualScrollWidth; + } return $"width: {tableWidth}px;"; } From a8e39a09b5ceab705e7907091c16ddc3ebbe7b6b Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Thu, 17 Sep 2026 13:05:39 +0800 Subject: [PATCH 4/9] =?UTF-8?q?refacotr:=E6=8B=96=E5=8A=A8=E5=88=97?= =?UTF-8?q?=E5=AE=BD=E6=97=B6=EF=BC=8C=E8=AE=A1=E7=AE=97=E7=9A=84=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=AE=BD=E5=BA=A6=E5=BA=94=E5=BD=93=E4=BB=A5colgroup?= =?UTF-8?q?=E4=B8=AD=E7=9A=84col=E7=9A=84=E5=80=BC=E4=B8=BA=E5=87=86?= =?UTF-8?q?=EF=BC=8C=E5=BC=BA=E5=8C=96=E4=BB=A5=E5=85=8D=E4=BA=A7=E7=94=9F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refacotr:拖动列宽时,计算的表格宽度应当以colgroup中的col的值为准,强化以免产生问题 --- src/BootstrapBlazor/Components/Table/Table.razor.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.js b/src/BootstrapBlazor/Components/Table/Table.razor.js index 554e393e3cd..6ac8e6894f2 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.js +++ b/src/BootstrapBlazor/Components/Table/Table.razor.js @@ -510,7 +510,13 @@ const setResizeListener = table => { const width = parseFloat(col.style.width); return !isNaN(width) ? width : getWidth(headerCells[index]); }); - tableWidths = table.tables.map(t => getWidth(t)); + tableWidths = table.tables.map(t => { + const cols = [...t.querySelectorAll('colgroup col')]; + return cols.reduce((total, col, index) => { + const width = parseFloat(col.style.width); + return total + (!isNaN(width) ? width : columnWidths[index]); + }, 0); + }); originalX = e.clientX ?? e.touches[0].clientX }, e => { From 8804f9a8510c9da2e8e9bf9ca7330e45dcc8435b Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Tue, 22 Sep 2026 19:40:34 +0800 Subject: [PATCH 5/9] =?UTF-8?q?refactor:=E9=87=8D=E6=9E=84=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E6=8B=96=E5=8A=A8=E5=88=97=E5=AE=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、去掉colgroup元素,将width改到thead>th上 2、去掉td上的width,以避免过多渲染导致太复杂 3、固定表头时,body表加上thead>th元素,用来处理width值存放及避免在td上过多渲染,采用aria-hidden="true"将body表上的th不可见 4、固定表头时,初次渲染时将整个head用visibility属性做隐藏占位处理,在自动列宽的列测量完宽度后,临时回写到head表格上,再移除visibility属性让其可见,以防固定表头两个表格渲染时出现抖动 5、接第4点,存在自动列宽的列时,测量好的宽度回写head表格后以保持表格正确显示,但并不进行持久化回写,只有当用户拖动改变列宽时才真正持久化回写 6、去掉固定表头中滑动条那一列里面padding的属性,以确保实际值一直保持5px,以防止列宽变小,整个表格宽度变小后,head表格的滑动列style上width值是5px,但由于padding的原因,实际值不止5px --- .../Components/Table/Table.razor | 52 ++-- .../Components/Table/Table.razor.Sort.cs | 18 +- .../Components/Table/Table.razor.cs | 49 ++-- .../Components/Table/Table.razor.js | 244 +++++++++++------- .../Components/Table/Table.razor.scss | 25 +- test/UnitTest/Components/TableTest.cs | 239 +++++++++++++++-- 6 files changed, 463 insertions(+), 164 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor b/src/BootstrapBlazor/Components/Table/Table.razor index 8da63bd4d0e..32f467c84a0 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor +++ b/src/BootstrapBlazor/Components/Table/Table.razor @@ -87,9 +87,8 @@ @code { RenderFragment RenderFixTableHeader => - @
+ @
- @RenderColGroup(true) @RenderHeader(true)
; @@ -101,11 +100,14 @@ RenderFragment RenderTable => hasHeader => @ - @RenderColGroup(false) @if (hasHeader) { @RenderHeader(false) } + else + { + @RenderSizingHeader(false) + } @if (ScrollMode == ScrollMode.Virtual) { @@ -226,50 +228,56 @@ }
; - RenderFragment RenderColGroup => hasScroll => - @ + RenderFragment RenderSizingHeader => hasScroll => + @ + @RenderSizingRow(hasScroll) + ; + + RenderFragment RenderSizingRow => hasScroll => + @ @if (ShowDetails()) { - + } @if (IsMultipleSelect) { - + } @if (ShowLineNo) { - + } @if (ShowExtendButtons && IsExtendButtonsInRowHeader) { - + } @foreach (var col in _tableColumnStates) { if (col.Visible) { - + } } @if (ShowExtendButtons && !IsExtendButtonsInRowHeader) { - + } @if (hasScroll) { - + } - ; + ; RenderFragment RenderHeader => hasScroll => @ @if (MultiHeaderTemplate != null) { + @RenderSizingRow(hasScroll) @MultiHeaderTemplate } @if (GetShowHeader()) { - + @if (ShowDetails()) { @@ -380,9 +388,7 @@ } @if (hasScroll) { - -
 
- + } } @@ -435,9 +441,7 @@ } @if (hasScroll) { - -
 
- + } } @@ -454,7 +458,7 @@ OnDoubleClick="() => DoubleClickRow(item)"> @if (ShowDetails()) { - +
@if (ShowDetailRow == null || ShowDetailRow(item)) { @@ -466,7 +470,7 @@ } @if (IsMultipleSelect) { - +
@if (GetShowRowCheckbox(item)) { @@ -479,7 +483,7 @@ } @if (ShowLineNo) { - +
@GetLineNo(item)
} @@ -671,7 +675,7 @@
; RenderFragment RenderRowExtendButtons => item => - @ + @
@{ diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs index f4aa3a84c61..2eba5ff1f5b 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs @@ -175,17 +175,17 @@ private int MultipleSelectColumnLeft() private bool GetFixedLineNoColumn => FixedLineNoColumn && ShowLineNo; - private string? DetailColumnStyleString => GetNonDataColumnStyleString( - GetFixedDetailRowHeaderColumn ? "left: 0;" : null, - DetailColumnWidth); + private string? DetailColumnFixedStyleString => GetFixedDetailRowHeaderColumn ? "left: 0;" : null; - private string? LineNoColumnStyleString => GetNonDataColumnStyleString( - GetFixedLineNoColumn ? $"left: {LineNoColumnLeft()}px;" : null, - LineNoColumnWidth); + private string? DetailColumnStyleString => GetNonDataColumnStyleString(DetailColumnFixedStyleString, DetailColumnWidth); - private string? MultiColumnStyleString => GetNonDataColumnStyleString( - GetFixedMultipleSelectColumn ? $"left: {MultipleSelectColumnLeft()}px;" : null, - MultiColumnWidth); + private string? LineNoColumnFixedStyleString => GetFixedLineNoColumn ? $"left: {LineNoColumnLeft()}px;" : null; + + private string? LineNoColumnStyleString => GetNonDataColumnStyleString(LineNoColumnFixedStyleString, LineNoColumnWidth); + + private string? MultiColumnFixedStyleString => GetFixedMultipleSelectColumn ? $"left: {MultipleSelectColumnLeft()}px;" : null; + + private string? MultiColumnStyleString => GetNonDataColumnStyleString(MultiColumnFixedStyleString, MultiColumnWidth); private int MultiColumnWidth => ShowCheckboxText ? ShowCheckboxTextColumnWidth : TableSize == TableSize.Normal diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index f60b2a4fae5..a845bda5b55 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -100,6 +100,12 @@ public partial class Table : ITable, IModelEqualityComparer where .AddClass("table-draggable", AllowDragColumn) .Build(); + private string? FixedHeaderClassString => CssBuilder.Default("table-fixed-header") + .AddClass("is-width-syncing", HasAutomaticColumnWidth) + .Build(); + + private bool HasAutomaticColumnWidth => _tableColumnStates.Any(x => x.Visible && x.Width is null); + /// /// 获得 wrapper 样式表集合 /// Get wrapper CSS Class @@ -210,18 +216,10 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( private string PageInfoLabelString => Localizer[nameof(PageInfoText), PageStartIndex, (PageIndex - 1) * _pageItems + Rows.Count, TotalCount]; - private static string? GetColWidthString(int? width) => (width.HasValue && width.Value > 0) ? $"width: {width.Value}px;" : null; - private string? GetHeaderStyleString(ITableColumn col, int margin = 0) { var fixedStyle = GetFixedCellStyleString(col, margin); - var columnState = _tableColumnStateCache.Columns.FirstOrDefault(x => x.Name == col.GetFieldName()); - var width = columnState?.Width ?? col.Width; - var widthStyle = width is > 0 - ? AllowResizing - ? $"width: {width}px;" - : $"width: {width}px; min-width: {width}px;" - : null; + var widthStyle = GetColumnWidthStyleString(col); if (fixedStyle == null) { return widthStyle; @@ -230,19 +228,31 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( return widthStyle == null ? fixedStyle : $"{fixedStyle} {widthStyle}"; } - private string? GetFixedExtendButtonsStyleString(int margin = 0) + private string? GetColumnWidthStyleString(ITableColumn col) { - var fixedStyle = GetFixedExtendButtonsColumnStyleString(margin); - return GetNonDataColumnStyleString(fixedStyle, ExtendButtonColumnWidth); + var columnState = _tableColumnStateCache.Columns.FirstOrDefault(x => x.Name == col.GetFieldName()); + var width = columnState?.Width ?? col.Width; + return GetColumnWidthStyleString(width); } - private string? GetNonDataColumnStyleString(string? fixedStyle, int width) + private string? GetColumnWidthStyleString(int? width) { - var widthStyle = width > 0 + return width is > 0 ? AllowResizing ? $"width: {width}px;" : $"width: {width}px; min-width: {width}px;" : null; + } + + private string? GetFixedExtendButtonsStyleString(int margin = 0) + { + var fixedStyle = GetFixedExtendButtonsColumnStyleString(margin); + return GetNonDataColumnStyleString(fixedStyle, ExtendButtonColumnWidth); + } + + private string? GetNonDataColumnStyleString(string? fixedStyle, int width) + { + var widthStyle = GetColumnWidthStyleString(width); if (fixedStyle == null) { return widthStyle; @@ -251,7 +261,7 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( return widthStyle == null ? fixedStyle : $"{fixedStyle} {widthStyle}"; } - private string? ScrollColumnStyleString => GetNonDataColumnStyleString(null, ActualScrollWidth); + private string ScrollColumnStyleString => $"width: {ActualScrollWidth}px; min-width: {ActualScrollWidth}px; max-width: {ActualScrollWidth}px;"; /// /// 获得/设置 滚动条宽度 默认 null 未设置使用 配置类中的 @@ -288,8 +298,6 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName( //[Parameter] //public Func? OnGetRowKey { get; set; } - private string ScrollWidthString => $"width: {ActualScrollWidth}px;"; - private string? GetScrollStyleString(bool condition) => condition ? $"--bb-scroll-width: {ActualScrollWidth}px; --bb-scroll-hover-width: {ActualScrollHoverWidth}px;" : null; @@ -1628,6 +1636,13 @@ private async Task OnTableRenderAsync(bool firstRender) private string? GetTableStyleString(bool hasHeader) { + if (HasAutomaticColumnWidth) + { + return IsFixedHeader && hasHeader + ? $"width: calc(100% + {ActualScrollWidth}px);" + : null; + } + if (_tableColumnStateCache.TableWidth <= 0) { return null; diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.js b/src/BootstrapBlazor/Components/Table/Table.razor.js index 6ac8e6894f2..4c79566ebea 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.js +++ b/src/BootstrapBlazor/Components/Table/Table.razor.js @@ -25,6 +25,7 @@ export async function reset(id) { if (table === null) { return; } + let columnSizeInitialized = false; if (table.minWidthObserver) { table.minWidthObserver.disconnect(); @@ -55,6 +56,10 @@ export async function reset(id) { table.tables.push(table.thead.firstElementChild) table.tables.push(table.body.firstElementChild) table.scrollWidth = parseFloat(table.body.style.getPropertyValue('--bb-scroll-width')); + if (isVisible(table.el)) { + setColSize(table, table.options); + columnSizeInitialized = true; + } await fixHeader(table); EventHandler.on(table.body, 'scroll', () => { @@ -115,7 +120,7 @@ export async function reset(id) { return; } - if (table.thead) { + if (table.thead && !columnSizeInitialized) { setColSize(table, table.options); } } @@ -209,6 +214,9 @@ const check = table => { cancelAnimationFrame(table.loopCheckHeightHandler); delete table.loopCheckHeightHandler; } + if (table.thead) { + setColSize(table, table.options); + } } }; @@ -469,11 +477,11 @@ const setResizeListener = table => { let colWidth = 0 let colMinWidth = 5 let columnWidths = [] - let tableWidths = [] + let tableColumnWidths = [] let colIndex = 0 let originalX = 0 - const columns = [...table.tables[0].querySelectorAll('.col-resizer')] + const columns = [...table.tables[0].querySelectorAll('[data-bb-header-row] .col-resizer')] columns.forEach(col => { table.columns.push(col) EventHandler.on(col, 'click', e => e.stopPropagation()); @@ -486,15 +494,15 @@ const setResizeListener = table => { setColumnResizingListen(table, col); drag(col, e => { - stopAutoColumnWidth(table); colIndex = eff(col, true) + freezeAutoColumnWidths(table); const tableEl = col.closest('table') - const currentCol = tableEl.querySelectorAll('colgroup col')[colIndex] - const width = currentCol.style.width + const currentCell = getColumnWidthCell(tableEl, colIndex) + const width = currentCell?.style.width const th = getColumnHeader(col); const isFixedLayout = getComputedStyle(tableEl).tableLayout === 'fixed'; if (isFixedLayout) { - colWidth = width ? parseInt(width) : getResizableColumnWidth(col); + colWidth = width ? parseFloat(width) : getResizableColumnWidth(col); colMinWidth = Math.max(table.options.columnMinWidth || 0, 5); } else { @@ -505,46 +513,20 @@ const setResizeListener = table => { table.options.columnMinWidth || 0 ) | 0; } - const headerCells = [...th.parentElement.children]; - columnWidths = [...table.tables[0].querySelectorAll('colgroup col')].map((col, index) => { - const width = parseFloat(col.style.width); - return !isNaN(width) ? width : getWidth(headerCells[index]); - }); - tableWidths = table.tables.map(t => { - const cols = [...t.querySelectorAll('colgroup col')]; - return cols.reduce((total, col, index) => { - const width = parseFloat(col.style.width); - return total + (!isNaN(width) ? width : columnWidths[index]); - }, 0); - }); + tableColumnWidths = table.tables.map(getColumnWidths); + columnWidths = tableColumnWidths[0]; originalX = e.clientX ?? e.touches[0].clientX }, e => { const eventX = e.clientX ?? e.changedTouches[0].clientX const marginX = eventX - originalX - const calcColWidth = Math.max(colWidth + marginX, colMinWidth); - const actualDelta = calcColWidth - colWidth; + const calcColWidth = Math.max(Math.round(colWidth + marginX), colMinWidth); columnWidths[colIndex] = calcColWidth; table.tables.forEach((t, index) => { - const group = [...t.children].find(i => i.nodeName === 'COLGROUP') - if (group) { - const curCol = group.children.item(colIndex) - if (curCol) { - curCol.style.setProperty('width', `${calcColWidth}px`); - const tableEl = curCol.closest('table') - const width = tableWidths[index] + actualDelta; - tableEl.style.setProperty('width', `${width}px`); - } - } - }); - - table.tables.forEach(t => { - t.querySelectorAll('thead > tr').forEach(row => { - const th = row.children.item(colIndex); - if (th) { - th.style.setProperty('width', `${calcColWidth}px`); - } - }); + setColumnWidth(t, colIndex, calcColWidth); + tableColumnWidths[index][colIndex] = calcColWidth; + const width = tableColumnWidths[index].reduce((total, columnWidth) => total + columnWidth, 0); + t.style.setProperty('width', `${width}px`); }); resetColumnWidthTips(table, col); @@ -571,10 +553,11 @@ const setResizeListener = table => { () => { eff(col, false) - const state = getColumnStateObject(table); + const field = getColumnName(col); + const state = getColumnStateObject(table, field); + table.options.columnStates = state.cols; saveColumnStateToLocalstorage(table, state); - const field = getColumnName(col); table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, field, state); } ) @@ -649,6 +632,34 @@ const getColumnName = col => getColumnHeader(col).getAttribute('data-bb-field'); const getResizableColumnWidth = col => getWidth(getColumnHeader(col)) | 0; +const getHeaderCells = table => [...(table.querySelector('[data-bb-header-row]')?.cells ?? [])]; + +const getSizingCells = table => [...(table.querySelector('[data-bb-sizing-row]')?.cells ?? [])]; + +const getWidthCells = table => { + const sizingCells = getSizingCells(table); + return sizingCells.length > 0 ? sizingCells : getHeaderCells(table); +} + +const getColumnWidthCell = (table, index) => getHeaderCells(table)[index] ?? getSizingCells(table)[index]; + +const getColumnWidths = table => getWidthCells(table).map(cell => { + const width = parseFloat(cell.style.width); + return !isNaN(width) ? width : getWidth(cell); +}); + +const getRenderedColumnWidths = table => getWidthCells(table).map(cell => getWidth(cell)); + +const setColumnWidth = (table, index, width) => { + const value = typeof width === 'number' ? `${width}px` : width; + const cells = [getHeaderCells(table)[index], getSizingCells(table)[index]]; + [...new Set(cells)].forEach(cell => { + if (cell) { + cell.style.setProperty('width', value); + } + }); +} + const indexOfCol = col => { const th = getColumnHeader(col); const row = th.parentElement; @@ -656,9 +667,9 @@ const indexOfCol = col => { } const autoFitColumnWidth = async (table, col) => { - stopAutoColumnWidth(table); const field = getColumnName(col); const index = indexOfCol(col); + freezeAutoColumnWidths(table); let rows = null; let maxWidth = getColumnMaxCellWidth(table, index); @@ -668,25 +679,27 @@ const autoFitColumnWidth = async (table, col) => { } if (maxWidth > 0) { - table.tables.forEach(table => { - const colEl = table.querySelectorAll('colgroup col')[index]; - if (colEl) { - colEl.style.setProperty('width', `${maxWidth}px`); - } - - const th = table.querySelectorAll('thead > tr > th')[index]; + table.tables.forEach(tableEl => { + setColumnWidth(tableEl, index, maxWidth); + const th = getHeaderCells(tableEl)[index]; if (th) { const span = th.querySelector('.table-text'); - span.style.removeProperty('width'); + span?.style.removeProperty('width'); } - const tableWidth = getTableWidth(table); - table.style.setProperty('width', `${tableWidth}px`); + if (!table.thead || table.autoColumns.length === 0) { + const tableWidth = getTableWidth(tableEl); + tableEl.style.setProperty('width', `${tableWidth}px`); + } }); + if (table.thead && table.autoColumns.length > 0) { + syncFixedHeaderColumnWidths(table); + } resetColumnWidthTips(table, col); - const state = getColumnStateObject(table); + const state = getColumnStateObject(table, field); + table.options.columnStates = state.cols; saveColumnStateToLocalstorage(table, state); await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, field, state); @@ -886,13 +899,13 @@ const disposeColumnDrag = columns => { const setDraggable = table => { let dragItem = null; let index = 0 - table.dragColumns = [...table.tables[0].querySelectorAll('thead > tr > th')].filter(i => i.draggable) + table.dragColumns = getHeaderCells(table.tables[0]).filter(i => i.draggable) disposeDragColumns(table.dragColumns); table.dragColumns.forEach(col => { EventHandler.on(col, 'dragstart', e => { col.parentNode.classList.add('table-dragging') col.classList.add('table-drag') - table.dragColumns = [...table.tables[0].querySelectorAll('thead > tr > th')].filter(i => i.draggable) + table.dragColumns = getHeaderCells(table.tables[0]).filter(i => i.draggable) index = table.dragColumns.indexOf(col) dragItem = col e.dataTransfer.effectAllowed = 'move' @@ -1050,18 +1063,19 @@ const getLocalStorageValue = key => { return result; } -const getColumnStateObject = table => { +const getColumnStateObject = (table, resizedField) => { const cols = table.options.columnStates; if (cols !== void 0) { + const columnStates = cols.map(col => { + return { + name: col.name, + width: col.name === resizedField ? getColumnWidth(table, col, table.columns) : col.width, + visible: col.visible + } + }); return { - cols: cols.map(col => { - return { - name: col.name, - width: getColumnWidth(table, col, table.columns), - visible: col.visible - } - }), - table: getTableWidth(table.tables[0]) + cols: columnStates, + table: columnStates.some(col => col.visible && col.width == null) ? 0 : getTableWidth(table.tables[0]) }; } @@ -1092,16 +1106,15 @@ const getColumnWidth = (table, col, columns) => { const getColumnStyleWidth = (table, col) => { const index = indexOfCol(col); - const colEl = table.tables[0].querySelectorAll('colgroup col')[index]; - const width = parseFloat(colEl?.style.width); + const cell = getColumnWidthCell(table.tables[0], index); + const width = parseFloat(cell?.style.width); return !isNaN(width) && width > 0 ? width : getResizableColumnWidth(col); } const getTableWidth = table => { let tableWidth = 0; - const colgroup = [...table.children].find(i => i.nodeName === 'COLGROUP'); - for (const col of colgroup.children) { - const width = parseInt(col.style.width); + for (const cell of getWidthCells(table)) { + const width = parseFloat(cell.style.width); if (isNaN(width)) { tableWidth = null; break; @@ -1110,7 +1123,7 @@ const getTableWidth = table => { tableWidth += width; } } - return (tableWidth ?? getWidth(table)) | 0; + return Math.round(tableWidth ?? getWidth(table)); } const getColumnWidthStateObject = table => { @@ -1215,7 +1228,8 @@ const setColSize = (table, options) => { if (!table.tables || table.tables.length === 0) { return; } - const headerCollection = [...table.tables[0].querySelectorAll('thead > tr > th')]; + const headerCollection = getHeaderCells(table.tables[0]); + const widthCells = getWidthCells(table.tables[0]); const columnMinWidth = table.options.columnMinWidth || 0; const autoColumns = []; options.columnStates.forEach(col => { @@ -1223,22 +1237,65 @@ const setColSize = (table, options) => { return; } const th = headerCollection.find(i => i.getAttribute('data-bb-field') === col.name); - if (th === void 0) { + const widthCell = widthCells.find(i => i.getAttribute('data-bb-field') === col.name); + if (widthCell === void 0) { return; } - const colIndex = headerCollection.indexOf(th); - const minWidth = Math.max(getCellWidth(th) + getHeaderIconsWidth(th), getColumnMaxCellWidth(table, colIndex), columnMinWidth) | 0; + const colIndex = widthCells.indexOf(widthCell); + const headerWidth = th ? getCellWidth(th) + getHeaderIconsWidth(th) : 0; + const minWidth = Math.max(headerWidth, getColumnMaxCellWidth(table, colIndex), columnMinWidth) | 0; autoColumns.push({ colIndex, minWidth }); }); table.autoColumns = autoColumns; + table.options.columnStates = options.columnStates; applyColumnMinWidth(table); } -const stopAutoColumnWidth = table => { +const freezeAutoColumnWidths = table => { if (table.minWidthRaf) { cancelAnimationFrame(table.minWidthRaf); table.minWidthRaf = null; } + const sourceTable = table.thead ? table.tables[1] : table.tables[0]; + const sourceCells = getWidthCells(sourceTable); + const measuredColumns = []; + table.options.columnStates.forEach(col => { + if (col.visible === false || col.width != null) { + return; + } + const colIndex = sourceCells.findIndex(cell => cell.getAttribute('data-bb-field') === col.name); + if (colIndex > -1) { + measuredColumns.push({ + name: col.name, + colIndex, + width: getWidth(sourceCells[colIndex]) + }); + } + }); + measuredColumns.sort((x, y) => x.colIndex - y.colIndex); + + let measuredTotal = 0; + let roundedTotal = 0; + const frozenColumns = measuredColumns.map(column => { + measuredTotal += column.width; + const nextRoundedTotal = Math.round(measuredTotal); + const width = nextRoundedTotal - roundedTotal; + roundedTotal = nextRoundedTotal; + return { ...column, width }; + }); + + const autoColumnWidths = new Map(); + frozenColumns.forEach(column => { + autoColumnWidths.set(column.name, column.width); + table.tables.forEach(tableEl => setColumnWidth(tableEl, column.colIndex, column.width)); + }); + + table.options.columnStates = table.options.columnStates.map(col => { + return { + ...col, + width: autoColumnWidths.get(col.name) ?? col.width + } + }); table.autoColumns = []; } @@ -1251,27 +1308,38 @@ const getHeaderIconsWidth = th => { } const applyColumnMinWidth = table => { - if (!table.thead || !table.body || !table.autoColumns || table.autoColumns.length === 0) { + if (!table.thead || !table.body) { return; } - setAutoColWidths(table, true); - const compact = table.body.scrollWidth > table.body.clientWidth + 1; - if (!compact) { - setAutoColWidths(table, false); + if (table.autoColumns && table.autoColumns.length > 0) { + setAutoColWidths(table, true); + const compact = table.body.scrollWidth > table.body.clientWidth + 1; + if (!compact) { + setAutoColWidths(table, false); + } } + syncFixedHeaderColumnWidths(table); } const setAutoColWidths = (table, apply) => { - const colgroups = table.tables.map(t => t.querySelectorAll('colgroup col')); + const bodyTable = table.tables[1]; table.autoColumns.forEach(({ colIndex, minWidth }) => { const width = apply ? `${minWidth}px` : ''; - colgroups.forEach(cols => { - const colEl = cols[colIndex]; - if (colEl) { - colEl.style.width = width; - } - }); + setColumnWidth(bodyTable, colIndex, width); + }); +} + +const syncFixedHeaderColumnWidths = table => { + if (!table.thead || table.tables.length < 2) { + return; + } + const headerTable = table.tables[0]; + const bodyTable = table.tables[1]; + getRenderedColumnWidths(bodyTable).forEach((width, index) => { + setColumnWidth(headerTable, index, width); }); + headerTable.style.setProperty('width', `${getWidth(bodyTable) + (table.scrollWidth || 0)}px`); + table.thead.classList.remove('is-width-syncing'); } const updateSortTooltip = table => { diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.scss b/src/BootstrapBlazor/Components/Table/Table.razor.scss index f95e66ae49d..01314785de7 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.scss +++ b/src/BootstrapBlazor/Components/Table/Table.razor.scss @@ -110,7 +110,8 @@ border: 1px solid var(--bs-border-color); } -.table-wrapper thead tr:first-child { +.table-wrapper thead tr:first-child, +.table-wrapper thead tr.table-sizing-row + tr { border-top: none; } @@ -124,6 +125,10 @@ border-left-width: 0; } +.table th.fixed-scroll { + padding-inline: 0; +} + .table-wrapper th:last-child, .table-wrapper td:last-child, .table-wrapper .table-fixed-header th:nth-last-of-type(2):not(.border-resize) { @@ -152,6 +157,18 @@ overflow: hidden; } +.table .table-sizing-row { + height: 0; + + > th { + height: 0; + padding: 0; + border: 0; + line-height: 0; + pointer-events: none; + } +} + .table thead th.sortable .table-text { cursor: pointer; } @@ -451,6 +468,10 @@ overflow: hidden; border-top-left-radius: var(--bs-border-radius); border-top-right-radius: var(--bs-border-radius); + + &.is-width-syncing { + visibility: hidden; + } } .table-scroll { @@ -752,7 +773,7 @@ tr.active:not(.is-edit):hover { padding-left: 4px; } -.table-bordered thead tr:last-child > th { +.table-bordered thead tr:last-child:not(.table-sizing-row) > th { border-bottom-width: 2px; border-bottom-color: rgba(var(--bs-body-color-rgb), 0.125); } diff --git a/test/UnitTest/Components/TableTest.cs b/test/UnitTest/Components/TableTest.cs index 395953bbebc..9fa5f3f47a9 100644 --- a/test/UnitTest/Components/TableTest.cs +++ b/test/UnitTest/Components/TableTest.cs @@ -286,7 +286,7 @@ await cut.InvokeAsync(() => table.Instance.ResetVisibleColumns( Assert.Equal("地址", labels[1].TextContent); // 检查宽度设置 - Assert.Contains("", table.Markup); + Assert.Contains("width: 120px;", table.Find("th[data-bb-field='Name']").GetAttribute("style")); } [Fact] @@ -1610,6 +1610,13 @@ public void IsFixedHeader_Ok() cut.Contains("table-fixed-header"); cut.Contains("height: 200px;"); cut.Contains("table-layout-fixed"); + cut.Contains("table-fixed-header is-width-syncing"); + Assert.Equal("width: calc(100% + 5px);", cut.Find(".table-fixed-header > table").GetAttribute("style")); + var scrollColumn = cut.Find("th.fixed-scroll"); + Assert.Equal("width: 5px; min-width: 5px; max-width: 5px;", scrollColumn.GetAttribute("style")); + Assert.Empty(scrollColumn.Children); + Assert.Empty(cut.FindAll("colgroup")); + Assert.Single(cut.FindAll("[data-bb-sizing-row]")); } [Theory] @@ -1751,6 +1758,44 @@ public void AllowResizing_Ok(bool resizing) cut.DoesNotContain("table-layout-fixed"); } + [Fact] + public void ColumnWidthStyle_FallbackToColumnWidth_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.AllowResizing, true); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 1)); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", nameof(Foo.Name)); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.AddAttribute(3, "Width", 100); + builder.CloseComponent(); + }); + }); + }); + + var table = cut.FindComponent>().Instance; + var stateField = typeof(Table).GetField("_tableColumnStateCache", BindingFlags.Instance | BindingFlags.NonPublic)!; + var state = (TableColumnClientStatus)stateField.GetValue(table)!; + state.Columns.Clear(); + + var method = typeof(Table).GetMethod( + "GetColumnWidthStyleString", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + [typeof(ITableColumn)], + null)!; + var style = method.Invoke(table, [table.GetVisibleColumns()[0]]); + + Assert.Equal("width: 100px;", style); + } + [Theory] [InlineData(null)] [InlineData(100)] @@ -1850,10 +1895,9 @@ public void ColumnFixed_Ok(bool showExtendButton, bool isFixedHeader) cut.Contains("left: 0px;"); cut.Contains("left: 200px;"); var columns = cut.FindAll("th"); - var col = cut.FindAll("col"); if (columns[0].ClassName == "fixed") { - var fixedWidth = cut.FindAll("col")[0].OuterHtml.Contains("width: 200px"); + var fixedWidth = columns[0].OuterHtml.Contains("width: 200px"); Assert.Equal("fixedWidth:True", $"fixedWidth:{fixedWidth}"); } @@ -1887,6 +1931,7 @@ public void ColumnFixed_Ok(bool showExtendButton, bool isFixedHeader) cut.Contains("right: 8px;"); } } + Assert.Empty(cut.FindAll("td[style*='width']")); } [Fact] @@ -1946,7 +1991,7 @@ public void ColumnFixed_TailColumn_Ok() }); cut.Contains("style=\"left: 0px;\""); - cut.Contains("style=\"left: 100px;\""); + cut.Contains("left: 100px;"); cut.Contains("style=\"right: 200px;\""); cut.Contains("style=\"right: 100px;\""); cut.Contains("style=\"right: 0px;\""); @@ -2091,13 +2136,13 @@ public void FixedColumn_Ok() }); // DetailRow - cut.Contains("style=\"left: 0;\""); + cut.Contains("left: 0;"); // MultipleSelect - cut.Contains("style=\"left: 100px;\""); + cut.Contains("left: 100px;"); // LineNo - cut.Contains("style=\"left: 136px;\""); + cut.Contains("left: 136px;"); // Name - cut.Contains("style=\"left: 236px;\""); + cut.Contains("left: 236px;"); cut.Contains("width: 180px; min-width: 180px;"); var table = cut.FindComponent>(); @@ -2114,27 +2159,27 @@ public void FixedColumn_Ok() pb.Add(a => a.FixedDetailRowHeaderColumn, false); }); // MultipleSelect - cut.Contains("style=\"left: 0px;\""); + cut.Contains("left: 0px;"); // LineNo - cut.Contains("style=\"left: 36px;\""); + cut.Contains("left: 36px;"); // Name - cut.Contains("style=\"left: 136px;\""); + cut.Contains("left: 136px;"); table.Render(pb => { pb.Add(a => a.FixedMultipleColumn, false); }); // LineNo - cut.Contains("style=\"left: 0px;\""); + cut.Contains("left: 0px;"); // Name - cut.Contains("style=\"left: 100px;\""); + cut.Contains("left: 100px;"); table.Render(pb => { pb.Add(a => a.FixedLineNoColumn, false); }); // Name - cut.Contains("style=\"left: 0px;\""); + cut.Contains("left: 0px;"); table.Render(pb => { @@ -2142,11 +2187,11 @@ public void FixedColumn_Ok() pb.Add(a => a.FixedLineNoColumn, true); }); // Detail - cut.Contains("style=\"left: 0;\""); + cut.Contains("left: 0;"); // LineNo - cut.Contains("style=\"left: 100px;\""); + cut.Contains("left: 100px;"); // Name - cut.Contains("style=\"left: 200px;\""); + cut.Contains("left: 200px;"); } [Fact] @@ -2202,7 +2247,7 @@ public void Column_IsFixedDetailColumn() }); }); }); - cut.Contains("style=\"left: 0;\""); + cut.Contains("left: 0;"); } class MockTableColumn : AutoGenerateColumnAttribute @@ -2425,7 +2470,7 @@ public void ShowExtendButtons_Table_Ok() }); }); }); - cut.Contains(""); + Assert.Contains("width: 130px;", cut.Find("[data-bb-header-row] > th:last-child").GetAttribute("style")); } [Fact] @@ -3456,6 +3501,108 @@ public void MultiHeaderTemplate_Ok() }); }); cut.Contains("Test-MultiHeaderTemplate"); + Assert.Empty(cut.FindAll("colgroup")); + Assert.Equal("true", cut.Find("thead > tr:first-child").GetAttribute("data-bb-sizing-row")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SizingHeader_Ok(bool extendButtonsInRowHeader) + { + var state = new TableColumnClientStatus + { + TableWidth = 100, + Columns = + [ + new() { Name = nameof(Foo.Name), Visible = true, Width = 100 } + ] + }; + Context.JSInterop.Setup("getColumnStates", "test_sizing_header").SetResult(state); + + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.ClientTableName, "test_sizing_header"); + pb.Add(a => a.IsFixedHeader, true); + pb.Add(a => a.IsDetails, true); + pb.Add(a => a.IsMultipleSelect, true); + pb.Add(a => a.ShowLineNo, true); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.IsExtendButtonsInRowHeader, extendButtonsInRowHeader); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 1)); + pb.Add(a => a.DetailRowTemplate, foo => builder => builder.AddContent(0, foo.Name)); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", nameof(Foo.Name)); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.CloseComponent(); + }); + pb.Add(a => a.MultiHeaderTemplate, builder => + { + builder.OpenElement(0, "tr"); + builder.OpenElement(1, "th"); + builder.AddAttribute(2, "colspan", 5); + builder.AddContent(3, "Test-MultiHeaderTemplate"); + builder.CloseElement(); + builder.CloseElement(); + }); + }); + }); + + var headerSizingRow = cut.Find(".table-fixed-header [data-bb-sizing-row]"); + var bodySizingRow = cut.Find(".table-fixed-body [data-bb-sizing-row]"); + + Assert.Equal(6, headerSizingRow.Children.Length); + Assert.Equal(5, bodySizingRow.Children.Length); + Assert.Contains(headerSizingRow.Children, cell => cell.GetAttribute("style")?.Contains("width: 100px;") == true); + Assert.Single(bodySizingRow.QuerySelectorAll($"th[data-bb-field='{nameof(Foo.Name)}']")); + Assert.DoesNotContain("is-width-syncing", cut.Find(".table-fixed-header").ClassList); + Assert.Empty(cut.FindAll("td[style*='width']")); + } + + [Fact] + public void FixedHeader_MixedColumnWidth_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.IsFixedHeader, true); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 1)); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", nameof(Foo.Name)); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.AddAttribute(3, "Width", 100); + builder.CloseComponent(); + + builder.OpenComponent>(4); + builder.AddAttribute(5, "Field", nameof(Foo.Address)); + builder.AddAttribute(6, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Address), typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + var headerName = cut.Find($".table-fixed-header th[data-bb-field='{nameof(Foo.Name)}']"); + var headerAddress = cut.Find($".table-fixed-header th[data-bb-field='{nameof(Foo.Address)}']"); + var bodyName = cut.Find($".table-fixed-body th[data-bb-field='{nameof(Foo.Name)}']"); + var bodyAddress = cut.Find($".table-fixed-body th[data-bb-field='{nameof(Foo.Address)}']"); + + Assert.Contains("width: 100px;", headerName.GetAttribute("style")); + Assert.Contains("width: 100px;", bodyName.GetAttribute("style")); + Assert.Null(headerAddress.GetAttribute("style")); + Assert.Null(bodyAddress.GetAttribute("style")); + Assert.Contains("is-width-syncing", cut.Find(".table-fixed-header").ClassList); + Assert.Equal("width: calc(100% + 5px);", cut.Find(".table-fixed-header > table").GetAttribute("style")); } [Fact] @@ -9087,6 +9234,51 @@ public async Task OnTableColumnClientStatusChanged_ResizeColumn_Ok() Assert.NotNull(clientState); } + [Fact] + public async Task ResizeColumnCallback_PersistMeasuredColumns_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.IsFixedHeader, true); + pb.Add(a => a.AllowResizing, true); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 1)); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", nameof(Foo.Name)); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(3); + builder.AddAttribute(4, "Field", nameof(Foo.Address)); + builder.AddAttribute(5, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Address), typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + var table = cut.FindComponent>(); + var state = new TableColumnClientStatus + { + TableWidth = 220, + Columns = + [ + new() { Name = nameof(Foo.Name), Visible = true, Width = 120 }, + new() { Name = nameof(Foo.Address), Visible = true, Width = 100 } + ] + }; + + await cut.InvokeAsync(() => table.Instance.ResizeColumnCallback(nameof(Foo.Name), state)); + + Assert.Contains("width: 120px;", table.Find($"th[data-bb-field='{nameof(Foo.Name)}']").GetAttribute("style")); + Assert.Contains("width: 100px;", table.Find($"th[data-bb-field='{nameof(Foo.Address)}']").GetAttribute("style")); + Assert.Equal("width: 220px;", table.Find(".table-fixed-body > table").GetAttribute("style")); + } + [Fact] public async Task ClearTableColumnClientStatus_Ok() { @@ -9126,14 +9318,13 @@ public async Task ClearTableColumnClientStatus_Ok() // 由于启用了客户端持久化 Name 列宽使用 100 而非 80 var table = cut.FindComponent>(); - var colGroup = table.Find("colgroup"); - Assert.Contains("style=\"width: 100px;\"", colGroup.ToMarkup()); - Assert.Contains("style=\"width: 120px;\"", colGroup.ToMarkup()); + Assert.Contains("width: 100px;", table.Find("th[data-bb-field='Name']").GetAttribute("style")); + Assert.Contains("width: 120px;", table.Find("th[data-bb-field='Address']").GetAttribute("style")); // 清除客户端状态 await cut.InvokeAsync(() => table.Instance.ClearTableColumnClientStatus()); invoker.VerifyInvoke("clearColumnStates"); - Assert.Contains("style=\"width: 80px;\"", colGroup.ToMarkup()); + Assert.Contains("width: 80px;", table.Find("th[data-bb-field='Name']").GetAttribute("style")); } [Fact] @@ -9303,7 +9494,7 @@ public void ReloadColumnWidth_Ok(bool fixedHeader) Assert.Contains("style=\"width: 220px;\"", table.Markup); if (fixedHeader) { - Assert.Contains("style=\"width: 215px;\"", table.Markup); + Assert.Contains("style=\"width: 225px;\"", table.Markup); } } From 1e45ca3e0e626ca66707378e4ad2eb942d18294c Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Wed, 23 Sep 2026 17:26:33 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E4=B8=8E=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E5=88=97=E7=9B=B8=E5=85=B3=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix:修正在扩展列固定在右侧,且右侧只有扩展列时,扩展列左侧的阴影效果没有正确加载 fix:修正扩展列放置行头时,右侧存在固定列时,最右侧固定列的style的right值不正确题 fix:修正扩展列放置行头时,且固定时,行号列不固定时,扩展列style的left值不正确的问题,及其他前置的非数据列固定状态未判断导致left值不正确的问题 fix:修正扩展列放置行头时,且固定时,后续左侧数据固定列的style的left值没有加上扩展列宽度 --- .../Components/Table/Table.razor.Sort.cs | 22 +-- test/UnitTest/Components/TableTest.cs | 128 ++++++++++++++++++ 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs index 2eba5ff1f5b..8f54023464e 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs @@ -138,7 +138,7 @@ protected Func OnClickHeader(ITableColumn col) => async () => private string? LineNoColumnClassString => CssBuilder.Default() .AddClass("fixed", FixedLineNoColumn) - .AddClass("fr", IsLastLineNoColumn()) + .AddClass("fr", FixedLineNoColumn && IsLastLineNoColumn()) .Build(); private int LineNoColumnLeft() @@ -211,7 +211,7 @@ private int MultipleSelectColumnLeft() /// protected string? FixedExtendButtonsColumnClassString => CssBuilder.Default("table-column-button") .AddClass("fixed", FixedExtendButtonsColumn) - .AddClass("fixed-right", !IsExtendButtonsInRowHeader) + .AddClass("fixed-right", FixedExtendButtonsColumn && !IsExtendButtonsInRowHeader) .AddClass("fr", IsLastExtendButtonColumn()) .AddClass("fl", IsFirstExtendButtonColumn()) .Build(); @@ -222,7 +222,7 @@ private int MultipleSelectColumnLeft() /// protected string? ExtendButtonsColumnClass => CssBuilder.Default() .AddClass("fixed", FixedExtendButtonsColumn) - .AddClass("fixed-right", !IsExtendButtonsInRowHeader) + .AddClass("fixed-right", FixedExtendButtonsColumn && !IsExtendButtonsInRowHeader) .AddClass("fr", IsLastExtendButtonColumn()) .AddClass("fl", IsFirstExtendButtonColumn()) .Build(); @@ -258,7 +258,7 @@ private bool IsLastColumn(ITableColumn col) => LastFixedColumnCache.GetOrAdd(col return ret; }); - private bool IsLastExtendButtonColumn() => IsExtendButtonsInRowHeader && !GetVisibleColumns().Any(i => i.Fixed); + private bool IsLastExtendButtonColumn() => FixedExtendButtonsColumn && IsExtendButtonsInRowHeader && !GetVisibleColumns().Any(i => i.Fixed && !IsFixRight(i)); private ConcurrentDictionary FirstFixedColumnCache { get; } = new(ReferenceEqualityComparer.Instance); @@ -277,20 +277,20 @@ private bool IsFirstColumn(ITableColumn col) => FirstFixedColumnCache.GetOrAdd(c return ret; }); - private bool IsFirstExtendButtonColumn() => !IsExtendButtonsInRowHeader && !GetVisibleColumns().Any(i => i.Fixed); + private bool IsFirstExtendButtonColumn() => FixedExtendButtonsColumn && !IsExtendButtonsInRowHeader && !GetVisibleColumns().Any(i => i.Fixed && IsFixRight(i)); private int GetExtendButtonsColumnLeftMargin() { var width = 0; - if (ShowDetails()) + if (GetFixedDetailRowHeaderColumn) { width += DetailColumnWidth; } - if (ShowLineNo) + if (GetFixedLineNoColumn) { width += LineNoColumnWidth; } - if (FixedMultipleColumn) + if (GetFixedMultipleSelectColumn) { width += MultiColumnWidth; } @@ -379,6 +379,10 @@ string GetFixedHeaderStyleString() => IsFixedHeader { width += LineNoColumnWidth; } + if (ShowExtendButtons && FixedExtendButtonsColumn && IsExtendButtonsInRowHeader) + { + width += ExtendButtonColumnWidth; + } while (index > start) { var column = columns[start++]; @@ -399,7 +403,7 @@ string GetFixedHeaderStyleString() => IsFixedHeader var column = columns[i]; width += GetFixedColumnWidth(column); } - if (ShowExtendButtons && FixedExtendButtonsColumn) + if (ShowExtendButtons && FixedExtendButtonsColumn && !IsExtendButtonsInRowHeader) { width += ExtendButtonColumnWidth; } diff --git a/test/UnitTest/Components/TableTest.cs b/test/UnitTest/Components/TableTest.cs index 9fa5f3f47a9..eb4bfff0312 100644 --- a/test/UnitTest/Components/TableTest.cs +++ b/test/UnitTest/Components/TableTest.cs @@ -2417,6 +2417,134 @@ public void IsExtendButtonsInRowHeader_Ok(bool inRowHeader) cut.Contains("test-button"); } + [Theory] + [InlineData(true, false, true, "fr")] + [InlineData(true, true, true, null)] + [InlineData(false, false, true, "fl")] + [InlineData(false, true, true, null)] + [InlineData(true, false, false, null)] + [InlineData(false, false, false, null)] + public void FixedExtendButtonsColumn_BoundaryClass_Ok( + bool inRowHeader, + bool hasAdjacentFixedColumn, + bool fixedExtendButtonsColumn, + string? expectedClass) + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer)); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.FixedExtendButtonsColumn, fixedExtendButtonsColumn); + pb.Add(a => a.IsExtendButtonsInRowHeader, inRowHeader); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", foo.Name); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.AddAttribute(3, nameof(TableColumn<,>.Fixed), inRowHeader && hasAdjacentFixedColumn); + builder.CloseComponent(); + + builder.OpenComponent>(4); + builder.AddAttribute(5, "Field", foo.Count); + builder.AddAttribute(6, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Count), typeof(int))); + builder.AddAttribute(7, nameof(TableColumn<,>.Fixed), !inRowHeader && hasAdjacentFixedColumn); + builder.CloseComponent(); + }); + }); + }); + + var selector = inRowHeader + ? "[data-bb-header-row] > th:first-child" + : "[data-bb-header-row] > th:last-child"; + var column = cut.Find(selector); + Assert.Equal(fixedExtendButtonsColumn, column.ClassList.Contains("fixed")); + Assert.Equal(fixedExtendButtonsColumn && !inRowHeader, column.ClassList.Contains("fixed-right")); + Assert.Equal(expectedClass == "fr", column.ClassList.Contains("fr")); + Assert.Equal(expectedClass == "fl", column.ClassList.Contains("fl")); + + var cellSelector = inRowHeader + ? "tbody tr > td:first-child" + : "tbody tr > td:last-child"; + var cell = cut.Find(cellSelector); + Assert.Equal(fixedExtendButtonsColumn, cell.ClassList.Contains("fixed")); + Assert.Equal(fixedExtendButtonsColumn && !inRowHeader, cell.ClassList.Contains("fixed-right")); + Assert.Equal(expectedClass == "fr", cell.ClassList.Contains("fr")); + Assert.Equal(expectedClass == "fl", cell.ClassList.Contains("fl")); + } + + [Theory] + [InlineData(true, "Name", "left: 130px;")] + [InlineData(false, "Count", "right: 130px;")] + public void FixedExtendButtonsColumn_DataColumnOffset_Ok(bool inRowHeader, string fieldName, string expectedStyle) + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer)); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.FixedExtendButtonsColumn, true); + pb.Add(a => a.IsExtendButtonsInRowHeader, inRowHeader); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", foo.Name); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.AddAttribute(3, nameof(TableColumn<,>.Fixed), inRowHeader); + builder.CloseComponent(); + + builder.OpenComponent>(4); + builder.AddAttribute(5, "Field", foo.Count); + builder.AddAttribute(6, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Count), typeof(int))); + builder.AddAttribute(7, nameof(TableColumn<,>.Fixed), !inRowHeader); + builder.CloseComponent(); + }); + }); + }); + + Assert.Contains(expectedStyle, cut.Find($"[data-bb-field='{fieldName}']").GetAttribute("style")); + } + + [Fact] + public void FixedExtendButtonsColumn_LeftMargin_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer)); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.FixedExtendButtonsColumn, true); + pb.Add(a => a.IsExtendButtonsInRowHeader, true); + pb.Add(a => a.DetailColumnWidth, 70); + pb.Add(a => a.DetailRowTemplate, foo => builder => builder.AddContent(0, foo.Name)); + pb.Add(a => a.FixedDetailRowHeaderColumn, false); + pb.Add(a => a.FixedMultipleColumn, true); + pb.Add(a => a.ShowLineNo, true); + pb.Add(a => a.FixedLineNoColumn, true); + pb.Add(a => a.LineNoColumnWidth, 100); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", foo.Name); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + var columns = cut.FindAll("[data-bb-header-row] > th"); + Assert.Contains("left: 100px;", columns[2].GetAttribute("style")); + } + [Fact] public void RowButtonTemplate_Ok() { From 5e8979aad1c4d2b3cee30505a61eb9b6c07210a2 Mon Sep 17 00:00:00 2001 From: Tony-ST0754 <6914529@qq.com> Date: Wed, 23 Sep 2026 21:30:28 +0800 Subject: [PATCH 7/9] =?UTF-8?q?test:=E8=A1=A5=E5=85=85=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=AE=8C=E5=96=84=E8=A6=86=E7=9B=96=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test:补充单元测试完善覆盖率 --- test/UnitTest/Components/ContextMenuTest.cs | 6 +++ test/UnitTest/Components/TableTest.cs | 43 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/test/UnitTest/Components/ContextMenuTest.cs b/test/UnitTest/Components/ContextMenuTest.cs index 60bbe91cc91..5bca89fa077 100644 --- a/test/UnitTest/Components/ContextMenuTest.cs +++ b/test/UnitTest/Components/ContextMenuTest.cs @@ -88,13 +88,19 @@ public async Task ContextMenu_Ok() return false; }); }); + var beforeShowCallback = false; menu.Render(pb => { pb.Add(a => a.OnBeforeShowCallback, v => { + beforeShowCallback = true; + Assert.Same(foo, v); return Task.CompletedTask; }); }); + await row.ContextMenuAsync(new MouseEventArgs { Button = 2, Buttons = 2 }); + Assert.True(beforeShowCallback); + item = menu.Find(".dropdown-item"); item.Click(); Assert.True(menuCallback); diff --git a/test/UnitTest/Components/TableTest.cs b/test/UnitTest/Components/TableTest.cs index eb4bfff0312..f84c21ffd80 100644 --- a/test/UnitTest/Components/TableTest.cs +++ b/test/UnitTest/Components/TableTest.cs @@ -2511,6 +2511,33 @@ public void FixedExtendButtonsColumn_DataColumnOffset_Ok(bool inRowHeader, strin Assert.Contains(expectedStyle, cut.Find($"[data-bb-field='{fieldName}']").GetAttribute("style")); } + [Fact] + public void FixedExtendButtonsColumn_ZeroWidth_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.Render(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.Items, Foo.GenerateFoo(localizer)); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.FixedExtendButtonsColumn, true); + pb.Add(a => a.ExtendButtonColumnWidth, 0); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", foo.Name); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + Assert.Equal("right: 0px;", cut.Find("[data-bb-header-row] > th:last-child").GetAttribute("style")); + Assert.Equal("right: 0px;", cut.Find("tbody tr > td:last-child").GetAttribute("style")); + } + [Fact] public void FixedExtendButtonsColumn_LeftMargin_Ok() { @@ -2541,8 +2568,20 @@ public void FixedExtendButtonsColumn_LeftMargin_Ok() }); }); - var columns = cut.FindAll("[data-bb-header-row] > th"); - Assert.Contains("left: 100px;", columns[2].GetAttribute("style")); + Assert.Contains("left: 100px;", cut.Find("[data-bb-header-row] > th.fixed.fr").GetAttribute("style")); + + var table = cut.FindComponent>(); + table.Render(pb => + { + pb.Add(a => a.IsMultipleSelect, true); + }); + Assert.Contains("left: 136px;", cut.Find("[data-bb-header-row] > th.fixed.fr").GetAttribute("style")); + + table.Render(pb => + { + pb.Add(a => a.FixedDetailRowHeaderColumn, true); + }); + Assert.Contains("left: 206px;", cut.Find("[data-bb-header-row] > th.fixed.fr").GetAttribute("style")); } [Fact] From 9329d0479313d8dc8508bbf870db5053fa3c5628 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 28 Sep 2026 08:32:18 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=E4=BF=AE=E5=A4=8D=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=AE=BD=E5=BA=A6=E5=90=8E=20toggle=20=E5=88=97?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=8A=B6=E6=80=81=E5=90=8E=E5=AE=BD=E5=BA=A6?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Table/Table.razor.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index a845bda5b55..b9639877335 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -2236,6 +2236,11 @@ private void UpdateTableColumnState(TableColumnClientStatus columnState) if (colState != null) { item.Width = colState.Width; + var column = _tableColumnStates.Find(i => i.Name == item.Name); + if (column != null) + { + column.Width = colState.Width; + } } } _tableColumnStateCache.TableWidth = columnState.TableWidth; From c92756ef73efdf7ca8dc866bb63c4dfc8a5703f1 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 28 Sep 2026 08:35:38 +0800 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=90=8D=E6=8F=90=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Table/Table.razor.Sort.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs index 8f54023464e..f50089bd072 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs @@ -138,7 +138,7 @@ protected Func OnClickHeader(ITableColumn col) => async () => private string? LineNoColumnClassString => CssBuilder.Default() .AddClass("fixed", FixedLineNoColumn) - .AddClass("fr", FixedLineNoColumn && IsLastLineNoColumn()) + .AddClass("fr", IsLastFixedLineNoColumn()) .Build(); private int LineNoColumnLeft() @@ -236,13 +236,13 @@ private int MultipleSelectColumnLeft() .AddClass($"left: {GetExtendButtonsColumnLeftMargin()}px;", FixedExtendButtonsColumn && IsExtendButtonsInRowHeader) .Build(); - private bool IsLastDetailColumn() => !GetFixedMultipleSelectColumn && !GetFixedLineNoColumn && IsNotFixedColumn(); + private bool IsLastDetailColumn() => !GetFixedMultipleSelectColumn && !GetFixedLineNoColumn && HasNoFixedColumnAfterLeadingAuxiliaryColumns(); - private bool IsLastMultiColumn() => !GetFixedLineNoColumn && IsNotFixedColumn(); + private bool IsLastMultiColumn() => !GetFixedLineNoColumn && HasNoFixedColumnAfterLeadingAuxiliaryColumns(); - private bool IsLastLineNoColumn() => IsNotFixedColumn(); + private bool IsLastFixedLineNoColumn() => GetFixedLineNoColumn && HasNoFixedColumnAfterLeadingAuxiliaryColumns(); - private bool IsNotFixedColumn() => !(FixedExtendButtonsColumn && IsExtendButtonsInRowHeader) && !(GetVisibleColumns().FirstOrDefault()?.Fixed ?? false); + private bool HasNoFixedColumnAfterLeadingAuxiliaryColumns() => !(FixedExtendButtonsColumn && IsExtendButtonsInRowHeader) && !(GetVisibleColumns().FirstOrDefault()?.Fixed ?? false); private ConcurrentDictionary LastFixedColumnCache { get; } = new(ReferenceEqualityComparer.Instance);