fix: enforce DataGrid cover boundary
This commit is contained in:
@@ -22,15 +22,17 @@ layout, container resize, persisted-layout restore, and pointer resize.
|
|||||||
|
|
||||||
| `initialFit` | `resizeBehavior` | Initial layout | Pointer resize |
|
| `initialFit` | `resizeBehavior` | Initial layout | Pointer resize |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `container` | `cover` | Real columns fill the container. Hard-minimum excess scrolls horizontally. | The active change is compensated by ordinary peer columns. Passive maxima are soft; no blank filler column is created. |
|
| `container` | `cover` | Real columns fill the container. Hard-minimum excess scrolls horizontally. | Growth may create horizontal overflow. Shrink first consumes overflow, then grows resizable columns to the right; it stops before underflow. |
|
||||||
| `content` | `cover` | After measurement, the same cover invariant applies. | Same as cover above. |
|
| `content` | `cover` | After measurement, the same cover invariant applies. | Same as cover above. |
|
||||||
| `container` | `constrained` | Real columns fill the container. | Peer compensation respects every hard min/max and stops the active resize when capacity is exhausted. |
|
| `container` | `constrained` | Real columns fill the container. | Peer compensation respects every hard min/max and stops the active resize when capacity is exhausted. |
|
||||||
| `content` | `free` | Declared content widths are retained. | Only the active column changes, so underflow or horizontal overflow is allowed. |
|
| `content` | `free` | Declared content widths are retained. | Only the active column changes, so underflow or horizontal overflow is allowed. |
|
||||||
| `container` | `free` | The initial layout fills the container. | Later user resizing is free. A right-sticky column promotes this mode to cover so its edge remains stable. |
|
| `container` | `free` | The initial layout fills the container. | Later user resizing is free. A right-sticky column promotes this mode to cover so its edge remains stable. |
|
||||||
|
|
||||||
Sticky columns do not absorb ordinary cover residuals and are not resize
|
Sticky columns do not absorb ordinary cover residuals and are not resize
|
||||||
compensation targets. A last resizable column compensates against columns to
|
compensation targets. A last resizable column may grow into overflow. It may
|
||||||
its left, so it follows the same right-edge invariant as earlier columns.
|
shrink only by the current overflow, because shrinking farther would require a
|
||||||
|
blank filler track. Dragging farther past that stop does not bank width changes:
|
||||||
|
the column remains stopped until the pointer crosses the same boundary again.
|
||||||
|
|
||||||
## Persistence
|
## Persistence
|
||||||
|
|
||||||
@@ -41,8 +43,9 @@ behavior. A changed signature discards the old override and recomputes the
|
|||||||
declared layout.
|
declared layout.
|
||||||
|
|
||||||
Container reconciliation is suspended while a pointer drag is active. On
|
Container reconciliation is suspended while a pointer drag is active. On
|
||||||
release, the already-rendered pixel layout becomes the persisted preference;
|
release, the already-rendered pixel layout becomes the persisted preference.
|
||||||
there is no second fit pass and therefore no drag-end snap.
|
Reconciliation may grow it to prevent underflow, but never shrinks intentional
|
||||||
|
user overflow, so there is no drag-end snap.
|
||||||
|
|
||||||
## Regression Matrix
|
## Regression Matrix
|
||||||
|
|
||||||
@@ -54,7 +57,8 @@ there is no second fit pass and therefore no drag-end snap.
|
|||||||
- fixed-only cover grids;
|
- fixed-only cover grids;
|
||||||
- persisted overrides under growth and viewport pressure;
|
- persisted overrides under growth and viewport pressure;
|
||||||
- stale layout signatures;
|
- stale layout signatures;
|
||||||
- first, middle, and last-column peer compensation;
|
- first and middle-column right-side compensation;
|
||||||
|
- last-resizable-column overflow, underflow stop, and reverse-pointer boundary;
|
||||||
- free, cover, and constrained resizing;
|
- free, cover, and constrained resizing;
|
||||||
- cover-expanded tracks that already exceed preferred maxima; and
|
- cover-expanded tracks that already exceed preferred maxima; and
|
||||||
- preservation of the pointer layout across the commit fit.
|
- preservation of the pointer layout across the commit fit.
|
||||||
|
|||||||
@@ -135,9 +135,11 @@ type DataGridBaseProps<T> = {
|
|||||||
/** @deprecated Use initialFit instead. */
|
/** @deprecated Use initialFit instead. */
|
||||||
fit?: DataGridInitialFit;
|
fit?: DataGridInitialFit;
|
||||||
/**
|
/**
|
||||||
* Cover keeps the right edge fixed by redistributing every resize across
|
* Cover prevents underflow while allowing horizontal overflow. Shrinking
|
||||||
* real columns. Free permits underflow and horizontal growth. Free is
|
* consumes existing overflow and then grows eligible columns to the right;
|
||||||
* automatically promoted to cover when a right-sticky column exists.
|
* it stops when no such capacity remains. Free permits both underflow and
|
||||||
|
* overflow. Free is automatically promoted to cover when a right-sticky
|
||||||
|
* column exists.
|
||||||
*/
|
*/
|
||||||
resizeBehavior?: DataGridResizeBehavior;
|
resizeBehavior?: DataGridResizeBehavior;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -210,15 +210,8 @@ export function fitDataGridColumns(
|
|||||||
resizeTargetForLayout(column, widths, widths[column.id])
|
resizeTargetForLayout(column, widths, widths[column.id])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// A persisted user layout is preferred, not a reason to overflow. Under
|
// Explicit user widths are hard during reconciliation. Cover prevents
|
||||||
// viewport pressure every ordinary track may shrink down to its hard floor.
|
// underflow, but intentional user growth may remain horizontally scrollable.
|
||||||
remaining = applyDataGridDistribution(
|
|
||||||
widths,
|
|
||||||
remaining,
|
|
||||||
coverageColumns.map((column) =>
|
|
||||||
resizeTargetForLayout(column, widths, widths[column.id])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const roundedWidths = roundDataGridWidths(widths);
|
const roundedWidths = roundDataGridWidths(widths);
|
||||||
@@ -268,6 +261,43 @@ export function resizeDataGridColumn(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fitMode === "cover") {
|
||||||
|
if (boundedDelta >= 0) {
|
||||||
|
widths[columnId] = startWidth + boundedDelta;
|
||||||
|
return {
|
||||||
|
widths: roundDataGridWidths(widths),
|
||||||
|
appliedDelta: roundWidth(boundedDelta)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedShrink = -boundedDelta;
|
||||||
|
const freeShrink = Math.min(
|
||||||
|
requestedShrink,
|
||||||
|
Math.max(0, uncompensatedShrinkRoom)
|
||||||
|
);
|
||||||
|
const rightTargets = columns
|
||||||
|
.slice(activeIndex + 1)
|
||||||
|
.filter((column) => column.resizable && !column.sticky)
|
||||||
|
.map((column) =>
|
||||||
|
resizeTargetForLayout(
|
||||||
|
column,
|
||||||
|
widths,
|
||||||
|
dataGridColumnResizeWeight(column)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const compensation = distributeDataGridResizeAmount(
|
||||||
|
requestedShrink - freeShrink,
|
||||||
|
rightTargets
|
||||||
|
);
|
||||||
|
applyDataGridAmounts(widths, compensation.amounts);
|
||||||
|
const appliedShrink = freeShrink + compensation.applied;
|
||||||
|
widths[columnId] = startWidth - appliedShrink;
|
||||||
|
return {
|
||||||
|
widths: roundDataGridWidths(widths),
|
||||||
|
appliedDelta: roundWidth(-appliedShrink)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const freeShrink = boundedDelta < 0
|
const freeShrink = boundedDelta < 0
|
||||||
? Math.min(-boundedDelta, Math.max(0, uncompensatedShrinkRoom))
|
? Math.min(-boundedDelta, Math.max(0, uncompensatedShrinkRoom))
|
||||||
: 0;
|
: 0;
|
||||||
@@ -282,8 +312,7 @@ export function resizeDataGridColumn(
|
|||||||
resizeTargetForLayout(
|
resizeTargetForLayout(
|
||||||
column,
|
column,
|
||||||
widths,
|
widths,
|
||||||
dataGridColumnResizeWeight(column),
|
dataGridColumnResizeWeight(column)
|
||||||
fitMode === "cover" ? DATA_GRID_MAX_TRACK_WIDTH : undefined
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const distribution = distributeDataGridResizeAmount(remainingCompensation, targets);
|
const distribution = distributeDataGridResizeAmount(remainingCompensation, targets);
|
||||||
|
|||||||
@@ -94,13 +94,25 @@ assertWidths(
|
|||||||
const overriddenOverflowLayout = fitDataGridColumns(weightedColumns, 500, {}, { primary: 450 });
|
const overriddenOverflowLayout = fitDataGridColumns(weightedColumns, 500, {}, { primary: 450 });
|
||||||
assertWidths(
|
assertWidths(
|
||||||
overriddenOverflowLayout.widths,
|
overriddenOverflowLayout.widths,
|
||||||
{ fixed: 72, primary: 328, secondary: 100 },
|
{ fixed: 72, primary: 450, secondary: 100 },
|
||||||
"a cover layout treats a persisted user width as preferred under viewport pressure"
|
"a cover layout preserves an explicit user width under viewport pressure"
|
||||||
);
|
);
|
||||||
assertEqual(
|
assertEqual(
|
||||||
overriddenOverflowLayout.overflowWidth,
|
overriddenOverflowLayout.overflowWidth,
|
||||||
|
122,
|
||||||
|
"intentional user width may remain horizontally scrollable"
|
||||||
|
);
|
||||||
|
|
||||||
|
const overriddenUnderflowLayout = fitDataGridColumns(
|
||||||
|
weightedColumns,
|
||||||
|
800,
|
||||||
|
{},
|
||||||
|
{ fixed: 80, primary: 200, secondary: 200 }
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
overriddenUnderflowLayout.underflowWidth,
|
||||||
0,
|
0,
|
||||||
"a persisted user preference does not create avoidable horizontal overflow"
|
"a persisted cover layout is expanded when it would otherwise underflow"
|
||||||
);
|
);
|
||||||
|
|
||||||
const clampedOverrideLayout = fitDataGridColumns(weightedColumns, 800, {}, { primary: 900 }, "free");
|
const clampedOverrideLayout = fitDataGridColumns(weightedColumns, 800, {}, { primary: 900 }, "free");
|
||||||
@@ -176,15 +188,22 @@ const resizeBase = { first: 200, middle: 300, last: 300, actions: 100 };
|
|||||||
const resizedFirst = resizeDataGridColumn(resizeColumns, resizeBase, "first", 120, "cover");
|
const resizedFirst = resizeDataGridColumn(resizeColumns, resizeBase, "first", 120, "cover");
|
||||||
assertWidths(
|
assertWidths(
|
||||||
resizedFirst.widths,
|
resizedFirst.widths,
|
||||||
{ first: 320, middle: 240, last: 240, actions: 100 },
|
{ first: 320, middle: 300, last: 300, actions: 100 },
|
||||||
"right-side peers compensate an earlier cover-column resize"
|
"growing an earlier cover column may create horizontal overflow"
|
||||||
|
);
|
||||||
|
|
||||||
|
const shrunkFirst = resizeDataGridColumn(resizeColumns, resizeBase, "first", -80, "cover");
|
||||||
|
assertWidths(
|
||||||
|
shrunkFirst.widths,
|
||||||
|
{ first: 120, middle: 340, last: 340, actions: 100 },
|
||||||
|
"shrinking an earlier cover column grows resizable columns to its right"
|
||||||
);
|
);
|
||||||
|
|
||||||
const resizedLast = resizeDataGridColumn(resizeColumns, resizeBase, "last", 120, "cover");
|
const resizedLast = resizeDataGridColumn(resizeColumns, resizeBase, "last", 120, "cover");
|
||||||
assertWidths(
|
assertWidths(
|
||||||
resizedLast.widths,
|
resizedLast.widths,
|
||||||
{ first: 140, middle: 240, last: 420, actions: 100 },
|
{ first: 200, middle: 300, last: 420, actions: 100 },
|
||||||
"left-side peers compensate the last resizable cover column"
|
"the last resizable cover column may grow into horizontal overflow"
|
||||||
);
|
);
|
||||||
|
|
||||||
const committedLayout = fitDataGridColumns(
|
const committedLayout = fitDataGridColumns(
|
||||||
@@ -197,7 +216,72 @@ const committedLayout = fitDataGridColumns(
|
|||||||
assertWidths(
|
assertWidths(
|
||||||
committedLayout.widths,
|
committedLayout.widths,
|
||||||
resizedLast.widths,
|
resizedLast.widths,
|
||||||
"committing a cover resize preserves the drag layout without a mouse-up snap"
|
"committing a cover resize preserves intentional overflow without a mouse-up snap"
|
||||||
|
);
|
||||||
|
|
||||||
|
const stoppedLastShrink = resizeDataGridColumn(
|
||||||
|
resizeColumns,
|
||||||
|
resizeBase,
|
||||||
|
"last",
|
||||||
|
-120,
|
||||||
|
"cover"
|
||||||
|
);
|
||||||
|
assertWidths(
|
||||||
|
stoppedLastShrink.widths,
|
||||||
|
resizeBase,
|
||||||
|
"the last resizable column stops before its shrink would require a filler"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
stoppedLastShrink.appliedDelta,
|
||||||
|
0,
|
||||||
|
"the stopped last-column shrink reports no applied movement"
|
||||||
|
);
|
||||||
|
|
||||||
|
const overflowedResizeBase = { first: 200, middle: 300, last: 420, actions: 100 };
|
||||||
|
const consumedLastOverflow = resizeDataGridColumn(
|
||||||
|
resizeColumns,
|
||||||
|
overflowedResizeBase,
|
||||||
|
"last",
|
||||||
|
-180,
|
||||||
|
"cover",
|
||||||
|
120
|
||||||
|
);
|
||||||
|
assertWidths(
|
||||||
|
consumedLastOverflow.widths,
|
||||||
|
{ first: 200, middle: 300, last: 300, actions: 100 },
|
||||||
|
"the last resizable column consumes overflow and stops at exact coverage"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
consumedLastOverflow.appliedDelta,
|
||||||
|
-120,
|
||||||
|
"last-column shrink is clamped to the available overflow"
|
||||||
|
);
|
||||||
|
|
||||||
|
const stillPastLastStop = resizeDataGridColumn(
|
||||||
|
resizeColumns,
|
||||||
|
overflowedResizeBase,
|
||||||
|
"last",
|
||||||
|
-150,
|
||||||
|
"cover",
|
||||||
|
120
|
||||||
|
);
|
||||||
|
assertWidths(
|
||||||
|
stillPastLastStop.widths,
|
||||||
|
consumedLastOverflow.widths,
|
||||||
|
"reversing the pointer while it remains beyond the stop does not move the column"
|
||||||
|
);
|
||||||
|
const crossedLastStop = resizeDataGridColumn(
|
||||||
|
resizeColumns,
|
||||||
|
overflowedResizeBase,
|
||||||
|
"last",
|
||||||
|
-110,
|
||||||
|
"cover",
|
||||||
|
120
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
crossedLastStop.widths.last,
|
||||||
|
310,
|
||||||
|
"the column resumes only after the pointer crosses the cover boundary"
|
||||||
);
|
);
|
||||||
|
|
||||||
const fixedPeerResize = resizeDataGridColumn([
|
const fixedPeerResize = resizeDataGridColumn([
|
||||||
@@ -207,8 +291,8 @@ const fixedPeerResize = resizeDataGridColumn([
|
|||||||
], { editable: 200, fixed: 300, actions: 100 }, "editable", 80, "cover");
|
], { editable: 200, fixed: 300, actions: 100 }, "editable", 80, "cover");
|
||||||
assertWidths(
|
assertWidths(
|
||||||
fixedPeerResize.widths,
|
fixedPeerResize.widths,
|
||||||
{ editable: 280, fixed: 220, actions: 100 },
|
{ editable: 280, fixed: 300, actions: 100 },
|
||||||
"non-resizable tracks may compensate layout without exposing a resize handle"
|
"growth does not resize a fixed peer merely to avoid overflow"
|
||||||
);
|
);
|
||||||
|
|
||||||
const coverBeyondPassiveMax = resizeDataGridColumn([
|
const coverBeyondPassiveMax = resizeDataGridColumn([
|
||||||
@@ -218,8 +302,8 @@ const coverBeyondPassiveMax = resizeDataGridColumn([
|
|||||||
], { first: 200, second: 300, actions: 100 }, "first", -80, "cover");
|
], { first: 200, second: 300, actions: 100 }, "first", -80, "cover");
|
||||||
assertWidths(
|
assertWidths(
|
||||||
coverBeyondPassiveMax.widths,
|
coverBeyondPassiveMax.widths,
|
||||||
{ first: 120, second: 380, actions: 100 },
|
{ first: 200, second: 300, actions: 100 },
|
||||||
"cover compensation may exceed a peer's preferred maximum to retain full coverage"
|
"cover shrinking stops when right-side peers have reached their maximum"
|
||||||
);
|
);
|
||||||
|
|
||||||
const constrainedAtPassiveMax = resizeDataGridColumn([
|
const constrainedAtPassiveMax = resizeDataGridColumn([
|
||||||
|
|||||||
Reference in New Issue
Block a user