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:
1 parent
4b0fd2062b
commit
8629b4e771
2 files changed
+193
-98
No files matched your search
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user