Skip to content

fix(Table): prevent column width test from restoring pre-drag width while resizing - #8357

Open
h2ls wants to merge 1 commit into
dotnetcore:mainfrom
h2ls:lee/fix-table-resize-width
Open

fix(Table): prevent column width test from restoring pre-drag width while resizing#8357
h2ls wants to merge 1 commit into
dotnetcore:mainfrom
h2ls:lee/fix-table-resize-width

Conversation

@h2ls

@h2ls h2ls commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

问题

fixes #8323

固定表头表格在列内容超宽时,拖动调整列宽会导致 colgroup > col 上的宽度值闪烁(偶尔有、偶尔无),且列宽测试会把拖动前测量的宽度恢复。

根因

#8126 引入的列宽自适应测量(setColSize / applyColumnMinWidth)与手动拖拽调宽相互冲突:

  1. table.body 上的 ResizeObserver 在拖动过程中因横向滚动条出现/消失反复触发 applyColumnMinWidth,把拖动前测量的 minWidth 写回 colgroup > col,与 mousemove 设置的拖动宽度互相覆盖,表现为 col 上的值偶尔有、偶尔无;
  2. 拖动结束时采集的列宽状态是被干扰后的瞬时值,错误值被持久化并回传服务端冻结;
  3. 拖动/双击自适应后的列仍保留在自动测量集合 autoColumns 中,后续任何 body 尺寸变化都会再次把测量值写回。

修复

仅修改 src/BootstrapBlazor/Components/Table/Table.razor.js

  • 拖动期间暂停列宽测试applyColumnMinWidth 入口增加 table.resizing 守卫,统一覆盖 ResizeObserver 与拖动中查询重渲染(setColSize)两条触发路径 —— 对应期望行为"不应再将拖动调整前的各列宽恢复"
  • 拖动结束后列宽测试只执行一次:先将被拖列移出 autoColumns 并同步 options.columnStates 宽度(防止 reset()setColSize 将该列重新纳入测量),再补偿执行一次 applyColumnMinWidth 让其余自动列落定,最后才采集状态回传服务端 —— 对应期望行为"列宽测试应该只执行一次"
  • 双击自适应(autoFit)路径同样修复:该路径与拖拽共用回调,存在相同缺陷
  • 未实际移动位置的点击不再触发 resizeColumnCallback:避免误触 resizer(想点排序)导致全表列宽被冻结、自适应功能静默失效;双击自适应也因此只回调一次而非三次
  • 拖动过程中表格重置/销毁时丢弃过期回调:防止无效状态写入 localStorage 与服务端,且 resizing 标志不泄漏

兼容性

  • 零 C# 改动、零公共 API 变化、localStorage 存储结构不变
  • 非固定表头表格无 ResizeObserver,applyColumnMinWidththead 早退保护,行为与旧版完全一致
  • 服务端 ResizeColumnCallbackUpdateTableColumnState 冻结列宽的既有契约不变;OnTableColumnClientStatusChanged 仅不再收到零位移的空回调

测试

客户端 JS 拖拽交互无法通过 bUnit 覆盖,使用与 issue 附件一致的独立复现工程手工验证(.NET 10 + Interactive Server):BootstrapBlazorApp1.Server.ziprelease 页面

复现条件:IsFixedHeader + AllowResizing + 列未设置 Width + 列内超宽内容。

  • 修复前(NuGet 10.9.2):F12 观察 colgroup > col 上的值,来回反复拖动不松开鼠标时值偶尔有、偶尔无,且拖动前宽度被反复恢复
  • 修复后(本 PR 分支):拖动全程值稳定,松手后列宽测试只执行一次,被拖列宽度不再被恢复;双击自适应、窗口缩放、重新查询、列显隐切换等路径行为一致

Summary by Sourcery

Prevent fixed-header table column resizing from restoring stale widths or persisting transient measurements.

Bug Fixes:

  • Prevent automatic column-width measurement from overwriting user-resized or auto-fitted widths during table resizing.
  • Avoid persisting stale column widths or invoking resize callbacks for clicks that do not move a resizer.
  • Discard resize state safely when the table is reset or destroyed.

Enhancements:

  • Ensure automatic width measurement runs once after resizing and excludes columns with explicit user sizing.

…hile resizing (dotnetcore#8323)

* fix: 拖动调整列宽期间暂停列宽测试,防止 ResizeObserver 恢复拖动前宽度导致 colgroup > col 宽度值闪烁
* fix: 拖动结束或双击自适应后将该列移出自动测量集合,用户显式设定的列宽不再被后续列宽测试覆盖
* fix: 未实际拖动的点击不再触发列宽回调,避免误触 resizer 导致全表列宽被冻结
* fix: 拖动过程中表格重置或销毁时丢弃过期回调,防止写入无效列宽状态

close dotnetcore#8323
Copilot AI lite review requested due to automatic review settings August 25, 2026 01:25
@bb-auto

bb-auto Bot commented Aug 25, 2026

Copy link
Copy Markdown

Thanks for your PR, @h2ls. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@bb-auto bb-auto Bot added the bug Something isn't working label Aug 25, 2026
@bb-auto
bb-auto Bot requested a review from ArgoZhang August 25, 2026 01:25
@bb-auto bb-auto Bot added this to the v10.9.0 milestone Aug 25, 2026
@sourcery-ai

sourcery-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Updates the table’s client-side resize lifecycle so automatic width measurement is paused during drag, excluded for explicitly sized columns, and applied once afterward before persisting state. It also prevents no-op or stale resize callbacks, covers auto-fit, and resets resize state safely without changing public APIs or storage formats.

Sequence diagram for the guarded table column resize lifecycle

sequenceDiagram
    participant User
    participant ResizeHandler
    participant Table
    participant ResizeObserver
    participant Server

    User->>ResizeHandler: drag(col)
    ResizeHandler->>Table: table.resizing = true
    ResizeObserver->>Table: applyColumnMinWidth(table)
    Table-->>ResizeObserver: return while resizing
    ResizeHandler->>Table: update column width
    User->>ResizeHandler: release drag
    ResizeHandler->>Table: table.resizing = false
    ResizeHandler->>Table: removeAutoColumn(table, col, colIndex)
    ResizeHandler->>Table: applyColumnMinWidth(table)
    ResizeHandler->>Table: getColumnStateObject(table)
    ResizeHandler->>Server: resizeColumnCallback(field, state)
Loading

Flow diagram for valid and stale resize completion

flowchart TD
    A[Resize interaction ends] --> B{Actual movement?}
    B -- No --> C[Skip resizeColumnCallback]
    B -- Yes --> D{Header still attached?}
    D -- No --> E[Discard stale callback]
    D -- Yes --> F[removeAutoColumn]
    F --> G[applyColumnMinWidth once]
    G --> H[getColumnStateObject]
    H --> I[saveColumnStateToLocalstorage]
    I --> J[resizeColumnCallback]
Loading

File-Level Changes

Change Details Files
Prevent automatic column-width measurement from overwriting user-driven resizing.
  • Track active resize state and make min-width application a no-op during drag.
  • Remove manually resized or auto-fit columns from automatic measurement tracking.
  • Synchronize the resized column’s persisted option state before reset or state collection.
  • Run one compensating measurement for remaining automatic columns after resizing completes.
src/BootstrapBlazor/Components/Table/Table.razor.js
Make resize and auto-fit callbacks robust against no-op interactions and stale table lifecycle events.
  • Invoke resize callbacks only after an actual movement and while the header remains mounted.
  • Apply the same auto-column removal behavior to double-click auto-fit.
  • Clear the resizing flag when the table is reset or destroyed.
src/BootstrapBlazor/Components/Table/Table.razor.js

Assessment against linked issues

Issue Objective Addressed Explanation
#8323 Prevent automatic column-width measurement from restoring or overwriting column widths while a column is being manually resized, so that colgroup > col width values remain stable during dragging.
#8323 Ensure that after resizing, column-width testing runs only once for the remaining automatic columns and does not re-enroll the manually resized column or restore its pre-drag width before state is persisted.
#8323 Prevent related resize and auto-fit edge cases from producing stale or duplicate callbacks, including zero-distance clicks and table reset or destruction during a resize.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/BootstrapBlazor/Components/Table/Table.razor.js" line_range="506-509" />
<code_context>
                 originalX = e.clientX ?? e.touches[0].clientX
             },
             e => {
+                resized = true;
                 const eventX = e.clientX ?? e.changedTouches[0].clientX
                 const marginX = eventX - originalX
</code_context>
<issue_to_address>
**issue (bug_risk):** `resized` becomes true for every `mousemove`/`touchmove` event, even when `eventX - originalX` is zero, so moving the pointer without changing the column width still persists column state and invokes `resizeColumnCallback`, freezing a column that was not actually resized.

**Triggers:** When a pointer move event is delivered over the resizer without horizontal displacement, such as vertical pointer movement or duplicate events with the same clientX.

**Suggested fix:** Set `resized` only when the computed horizontal margin is nonzero and the resulting width differs from the starting width.

```suggestion
                const eventX = e.clientX ?? e.changedTouches[0].clientX
                const marginX = eventX - originalX
                let calcColWidth = colWidth + marginX;
                if (calcColWidth < 5) {
                    calcColWidth = 5;
                }
                resized = marginX !== 0 && calcColWidth !== colWidth;
                table.tables.forEach(t => {
                    const group = [...t.children].find(i => i.nodeName === 'COLGROUP')
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if the resizing logic is wrong, the table can save an incorrect column width to local storage and invoke the resize callback with that state, so the bad width can persist after the code is reverted. The impact is bounded to affected table users and can be repaired by resetting or clearing the saved column state; otherwise this is an ordinary UI behavior bug.

Blocking findings: src/BootstrapBlazor/Components/Table/Table.razor.js:509


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +506 to 509
resized = true;
const eventX = e.clientX ?? e.changedTouches[0].clientX
const marginX = eventX - originalX
table.tables.forEach(t => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): resized becomes true for every mousemove/touchmove event, even when eventX - originalX is zero, so moving the pointer without changing the column width still persists column state and invokes resizeColumnCallback, freezing a column that was not actually resized.

Triggers: When a pointer move event is delivered over the resizer without horizontal displacement, such as vertical pointer movement or duplicate events with the same clientX.

Suggested fix: Set resized only when the computed horizontal margin is nonzero and the resulting width differs from the starting width.

Suggested change
resized = true;
const eventX = e.clientX ?? e.changedTouches[0].clientX
const marginX = eventX - originalX
table.tables.forEach(t => {
const eventX = e.clientX ?? e.changedTouches[0].clientX
const marginX = eventX - originalX
let calcColWidth = colWidth + marginX;
if (calcColWidth < 5) {
calcColWidth = 5;
}
resized = marginX !== 0 && calcColWidth !== colWidth;
table.tables.forEach(t => {
const group = [...t.children].find(i => i.nodeName === 'COLGROUP')

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a regression in the Table fixed-header resizing experience where the auto column-width measurement (ResizeObserver-driven applyColumnMinWidth) can interfere with manual drag resizing / auto-fit, causing colgroup > col widths to flicker and stale widths to be persisted.

Changes:

  • Add a table.resizing guard so applyColumnMinWidth won’t run during drag resizing.
  • On drag end / auto-fit, remove the affected column from autoColumns and sync options.columnStates.width so later re-measures/resets don’t restore stale widths.
  • Avoid calling the resize callback when the user clicks a resizer but doesn’t actually move it.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +552 to +556
// 拖动的列由用户显式设定宽度,移出自动测量集合,防止列宽测试恢复拖动前宽度
removeAutoColumn(table, col, colIndex);

const field = getColumnName(col);
table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, field, state);
// 拖动结束后列宽测试只执行一次
applyColumnMinWidth(table);
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c28e98a) to head (cedf23a).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #8357   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          771       771           
  Lines        34579     34579           
=========================================
  Hits         34579     34579           
Flag Coverage Δ
BB 100.00% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(table): 当列中存在超宽内容时,拖动改变其中一个列宽时,会导致 colgroup > col 上的值应该有的没有了

3 participants