fix(usage): prevent table clipping after column changes

- recalculate the top scrollbar from the actual table overflow
- clamp and synchronize scroll positions when visible columns change
- preserve horizontal scrolling while hiding the bottom scrollbar
- remove forced table width expansion for smaller column sets
- improve toolbar and display settings responsiveness
This commit is contained in:
lzt404 committed 2026-08-26 01:56:46 +08:00
1 parent 4b0fd2062b
commit 8629b4e771
2 files changed
+193 -98

No files matched your search

+47 -11
View File
@@ -1092,12 +1092,15 @@ const getInitialColumnWidths = (): Record<EventColumnKey, number> => {
function TableTopScrollbar({
tableWrapRef,
totalWidth,
visibleColumnKeys,
}: {
tableWrapRef: React.RefObject<HTMLDivElement | null>;
totalWidth: number;
visibleColumnKeys: EventColumnKey[];
}) {
const scrollbarRef = useRef<HTMLDivElement | null>(null);
const [hasOverflow, setHasOverflow] = useState(false);
const [scrollWidth, setScrollWidth] = useState(totalWidth);
useEffect(() => {
const scrollbar = scrollbarRef.current;
@@ -1105,6 +1108,7 @@ function TableTopScrollbar({
if (!scrollbar || !tableWrap) return;
let syncing = false;
const syncTable = () => {
if (syncing) return;
syncing = true;
@@ -1113,6 +1117,7 @@ function TableTopScrollbar({
syncing = false;
});
};
const syncScrollbar = () => {
if (syncing) return;
syncing = true;
@@ -1121,22 +1126,50 @@ function TableTopScrollbar({
syncing = false;
});
};
const updateOverflow = () => {
setHasOverflow(tableWrap.scrollWidth > tableWrap.clientWidth + 1);
scrollbar.scrollLeft = tableWrap.scrollLeft;
const updateLayout = () => {
const clientWidth = tableWrap.clientWidth;
const wrapScrollWidth = tableWrap.scrollWidth;
const maxScroll = Math.max(0, wrapScrollWidth - clientWidth);
const isOverflowing = maxScroll > 1;
setHasOverflow(isOverflowing);
if (isOverflowing) {
const scrollbarClientWidth = scrollbar.clientWidth || clientWidth;
const targetInnerWidth = scrollbarClientWidth + maxScroll;
setScrollWidth(targetInnerWidth);
if (tableWrap.scrollLeft > maxScroll) {
tableWrap.scrollLeft = maxScroll;
}
scrollbar.scrollLeft = tableWrap.scrollLeft;
} else {
tableWrap.scrollLeft = 0;
scrollbar.scrollLeft = 0;
setScrollWidth(clientWidth);
}
};
updateOverflow();
updateLayout();
const frameId = window.requestAnimationFrame(updateLayout);
scrollbar.addEventListener('scroll', syncTable, { passive: true });
tableWrap.addEventListener('scroll', syncScrollbar, { passive: true });
const resizeObserver = new ResizeObserver(updateOverflow);
const resizeObserver = new ResizeObserver(() => {
updateLayout();
});
resizeObserver.observe(tableWrap);
resizeObserver.observe(scrollbar);
return () => {
window.cancelAnimationFrame(frameId);
scrollbar.removeEventListener('scroll', syncTable);
tableWrap.removeEventListener('scroll', syncScrollbar);
resizeObserver.disconnect();
};
}, [tableWrapRef, totalWidth]);
}, [tableWrapRef, totalWidth, visibleColumnKeys]);
return (
<div
@@ -1144,7 +1177,7 @@ function TableTopScrollbar({
className={`usage-table-top-scrollbar ${hasOverflow ? '' : 'is-hidden'}`}
aria-hidden="true"
>
<div style={{ width: `${totalWidth}px`, height: '1px' }} />
<div style={{ width: `${scrollWidth}px`, height: '1px' }} />
</div>
);
}
@@ -1354,8 +1387,7 @@ function EventsView({
};
const resetVisibleColumns = () => {
const allColumns = getAllEventColumnKeys();
setDraftVisibleColumnKeys(allColumns);
setDraftVisibleColumnKeys(getAllEventColumnKeys());
};
const resetSingleColumn = (key: EventColumnKey, e: React.MouseEvent) => {
@@ -1498,12 +1530,16 @@ function EventsView({
</div>
{events.items.length > 0 ? (
<TableTopScrollbar tableWrapRef={tableWrapRef} totalWidth={totalTableWidth} />
<TableTopScrollbar
tableWrapRef={tableWrapRef}
totalWidth={totalTableWidth}
visibleColumnKeys={visibleColumnKeys}
/>
) : null}
{events.items.length ? (
<div ref={tableWrapRef} className="usage-table-wrap">
<table className="usage-events-table" style={{ width: `${totalTableWidth}px`, minWidth: '100%' }}>
<table className="usage-events-table" style={{ width: `${totalTableWidth}px` }}>
<colgroup>
{visibleColumns.map((col) => (
<col key={col.key} style={{ width: `${widths[col.key]}px` }} />
+146 -87
View File
@@ -7253,7 +7253,7 @@ textarea:focus-visible {
.usage-events-summary {
display: flex;
min-height: 54px;
min-height: 52px;
align-items: center;
justify-content: space-between;
gap: 12px;
@@ -7270,6 +7270,7 @@ textarea:focus-visible {
align-items: center;
gap: 10px;
flex-wrap: wrap;
min-width: 0;
}
.usage-events-count-badge {
@@ -7282,6 +7283,15 @@ textarea:focus-visible {
color: var(--theme-2d2a26);
font-weight: 600;
font-size: var(--font-size-caption);
white-space: nowrap;
flex-shrink: 0;
}
.usage-pagination-summary {
font-size: var(--font-size-caption);
color: var(--theme-7a746c);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.usage-events-summary-right {
@@ -7289,6 +7299,7 @@ textarea:focus-visible {
align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.usage-col-hint {
@@ -7297,14 +7308,83 @@ textarea:focus-visible {
opacity: 0.9;
}
.usage-col-reset-btn,
.usage-page-size-select {
height: 30px;
padding: 4px 8px;
border: 1px solid var(--theme-e3e1db);
border-radius: 5px;
background: var(--theme-faf8f4);
color: var(--theme-2d2a26);
font-size: var(--font-size-caption);
cursor: pointer;
flex-shrink: 0;
}
.usage-page-size-select:focus {
border-color: var(--theme-3f6f98);
outline: none;
}
.usage-pagination-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.usage-pagination-top {
display: inline-flex;
align-items: center;
gap: 4px;
}
.usage-page-nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 30px;
padding: 0 8px;
border: 1px solid var(--theme-e3e1db);
border-radius: 5px;
background: var(--theme-fffdf8);
color: var(--theme-5f5a53);
cursor: pointer;
font-size: var(--font-size-caption);
font-weight: 500;
transition: all 0.15s ease;
gap: 3px;
flex-shrink: 0;
white-space: nowrap;
}
.usage-page-nav-btn:hover:not(:disabled) {
background: var(--theme-f4f1ea);
border-color: var(--theme-d5d2cb);
color: var(--theme-2d2a26);
}
.usage-page-nav-btn:disabled {
opacity: 0.35;
cursor: default;
}
.usage-pagination-info {
padding: 0 6px;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--theme-7a746c);
white-space: nowrap;
font-size: var(--font-size-caption);
}
.usage-col-settings-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 30px;
padding: 5px 10px;
height: 30px;
padding: 0 10px;
font-size: var(--font-size-caption);
font-weight: 600;
background: var(--theme-f4f1ea);
@@ -7314,22 +7394,55 @@ textarea:focus-visible {
cursor: pointer;
line-height: 1.2;
transition: all 0.15s ease;
flex-shrink: 0;
white-space: nowrap;
}
.usage-col-settings-btn:hover {
background: var(--theme-e8e5dc);
color: var(--theme-2d2a26);
border-color: var(--theme-d5d2cb);
}
.usage-col-reset-btn svg,
.usage-col-settings-btn svg {
width: 14px;
height: 14px;
flex: 0 0 auto;
}
.usage-col-reset-btn:hover,
.usage-col-settings-btn:hover {
.usage-col-reset-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 30px;
width: 30px;
padding: 0;
background: var(--theme-f4f1ea);
border: 1px solid var(--theme-e3e1db);
border-radius: 5px;
color: var(--theme-7a746c);
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
}
.usage-col-reset-btn:hover {
background: var(--theme-e8e5dc);
color: var(--theme-2d2a26);
border-color: var(--theme-d5d2cb);
}
.usage-col-reset-btn svg {
width: 13px;
height: 13px;
flex: 0 0 auto;
}
.usage-col-reset-btn.icon-only {
width: 30px;
padding: 0;
}
/* Column Settings Dialog */
.usage-column-dialog {
display: flex;
@@ -7377,7 +7490,9 @@ textarea:focus-visible {
min-height: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
overflow: auto;
overflow-y: auto;
max-height: 380px;
padding: 2px;
}
.usage-column-option {
@@ -7469,7 +7584,7 @@ textarea:focus-visible {
/* Main Table Wrap & Table */
.usage-table-wrap {
position: relative;
overflow-x: hidden;
overflow-x: auto;
overflow-y: auto;
height: clamp(480px, 60vh, 720px);
min-height: 0;
@@ -7480,12 +7595,18 @@ textarea:focus-visible {
scrollbar-gutter: stable;
}
/* Hide horizontal scrollbar on the table wrap so only the top scrollbar is shown */
.usage-table-wrap::-webkit-scrollbar:horizontal {
display: none;
height: 0;
width: 0;
}
.usage-events-table {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
font-size: var(--font-size-caption);
width: 100%;
}
.usage-events-table th,
@@ -7698,81 +7819,24 @@ body.table-col-resizing * {
font-size: 11px;
}
.usage-pagination-summary {
font-size: var(--font-size-caption);
color: var(--theme-7a746c);
font-variant-numeric: tabular-nums;
@media (max-width: 768px) {
.usage-events-summary {
gap: 8px;
padding: 8px 12px;
}
.usage-events-summary-left,
.usage-events-summary-right {
width: 100%;
justify-content: space-between;
}
}
.usage-page-size-select {
height: 30px;
padding: 4px 8px;
border: 1px solid var(--theme-e3e1db);
border-radius: 5px;
background: var(--theme-faf8f4);
color: var(--theme-2d2a26);
font-size: var(--font-size-caption);
cursor: pointer;
}
@media (max-width: 540px) {
.usage-pagination-summary {
display: none;
}
.usage-page-size-select:focus {
border-color: var(--theme-3f6f98);
outline: none;
}
.usage-pagination-right {
display: flex;
align-items: center;
gap: 6px;
}
.usage-pagination-top {
padding-right: 8px;
border-right: 1px solid var(--theme-e3e1db);
}
.usage-page-nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 30px;
height: 30px;
padding: 0 6px;
border: 1px solid var(--theme-e3e1db);
border-radius: 5px;
background: var(--theme-fffdf8);
color: var(--theme-5f5a53);
cursor: pointer;
font-size: var(--font-size-caption);
font-weight: 500;
transition: all 0.15s ease;
gap: 3px;
}
.usage-col-reset-btn.icon-only {
width: 30px;
padding: 0;
}
.usage-page-nav-btn:hover:not(:disabled) {
background: var(--theme-f4f1ea);
border-color: var(--theme-d5d2cb);
color: var(--theme-2d2a26);
}
.usage-page-nav-btn:disabled {
opacity: 0.35;
cursor: default;
}
.usage-pagination-info {
padding: 0 6px;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--theme-7a746c);
}
@media (max-width: 640px) {
.usage-column-options {
grid-template-columns: 1fr;
}
@@ -7785,11 +7849,6 @@ body.table-col-resizing * {
.usage-column-dialog-actions button {
flex: 1;
}
.usage-events-summary-right {
width: 100%;
justify-content: flex-end;
}
}
.usage-events-panel > .usage-empty {