fix(grid): update scrollbar thumbs outside Vue render

This commit is contained in:
t8y2 2026-07-09 20:54:44 +08:00
parent d725face37
commit d25b142cf8
1 changed files with 86 additions and 45 deletions

View File

@ -2467,15 +2467,15 @@ const dataGridTopbarRef = ref<HTMLDivElement>();
const headerRef = ref<HTMLDivElement>();
const gridScrollbarGutter = ref(0);
const gridHorizontalScrollbarTrackRef = ref<HTMLDivElement>();
const gridHorizontalScrollbarThumbRef = ref<HTMLDivElement>();
const gridVerticalScrollbarTrackRef = ref<HTMLDivElement>();
const gridVerticalScrollbarThumbRef = ref<HTMLDivElement>();
const hasGridHorizontalOverflow = ref(false);
const hasGridVerticalOverflow = ref(false);
const gridHorizontalScrollbarThumbLeftPercent = ref(0);
const gridHorizontalScrollbarThumbWidthPercent = ref(100);
const gridVerticalScrollbarThumbTopPercent = ref(0);
const gridVerticalScrollbarThumbHeightPercent = ref(100);
const gridHorizontalScrollbarDragging = ref(false);
const gridVerticalScrollbarDragging = ref(false);
let gridHorizontalScrollbarThumbLeftPercent = 0;
let gridHorizontalScrollbarThumbWidthPercent = 100;
let gridVerticalScrollbarThumbTopPercent = 0;
let gridVerticalScrollbarThumbHeightPercent = 100;
let gridHorizontalScrollbarFrame = 0;
let gridHorizontalScrollbarDragFrame = 0;
let gridHorizontalScrollbarPendingClientX = 0;
@ -2497,6 +2497,8 @@ let gridVerticalScrollbarDragState: {
thumbOffsetPx: number;
maxScrollTop: number;
} | null = null;
const GRID_HORIZONTAL_SCROLLBAR_DRAGGING_CLASS = "data-grid-horizontal-scrollbar--dragging";
const GRID_VERTICAL_SCROLLBAR_DRAGGING_CLASS = "data-grid-vertical-scrollbar--dragging";
const hiddenColumnIndexes = ref<Set<number>>(new Set());
const nullColumnsHidden = ref(false);
const autoHiddenNullColumnIndexes = ref<Set<number>>(new Set());
@ -2728,9 +2730,12 @@ const renderedColumnOffsets = computed(() => {
});
function updateGridHorizontalViewport(element: HTMLElement) {
gridHorizontalScrollLeft.value = element.scrollLeft;
gridViewportWidth.value = element.clientWidth;
updateGridHorizontalScrollbar(element);
const nextScrollLeft = element.scrollLeft;
const nextViewportWidth = element.clientWidth;
const horizontalChanged = gridHorizontalScrollLeft.value !== nextScrollLeft || gridViewportWidth.value !== nextViewportWidth;
if (gridHorizontalScrollLeft.value !== nextScrollLeft) gridHorizontalScrollLeft.value = nextScrollLeft;
if (gridViewportWidth.value !== nextViewportWidth) gridViewportWidth.value = nextViewportWidth;
if (horizontalChanged) updateGridHorizontalScrollbar(element);
updateGridVerticalScrollbar(element);
}
@ -2740,38 +2745,84 @@ function gridScrollerElement(): HTMLElement | null {
function updateGridHorizontalScrollbar(element: HTMLElement | null = gridScrollerElement()) {
if (!element) {
hasGridHorizontalOverflow.value = false;
gridHorizontalScrollbarThumbLeftPercent.value = 0;
gridHorizontalScrollbarThumbWidthPercent.value = 100;
setGridHorizontalOverflow(false);
gridHorizontalScrollbarThumbLeftPercent = 0;
gridHorizontalScrollbarThumbWidthPercent = 100;
applyGridHorizontalScrollbarThumbStyle();
return;
}
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
hasGridHorizontalOverflow.value = maxScrollLeft > 1;
setGridHorizontalOverflow(maxScrollLeft > 1);
const rawThumbWidth = element.scrollWidth > 0 ? (element.clientWidth / element.scrollWidth) * 100 : 100;
const thumbWidth = Math.min(100, Math.max(6, rawThumbWidth));
const thumbTravel = Math.max(0, 100 - thumbWidth);
gridHorizontalScrollbarThumbWidthPercent.value = thumbWidth;
gridHorizontalScrollbarThumbLeftPercent.value = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0;
gridHorizontalScrollbarThumbWidthPercent = thumbWidth;
gridHorizontalScrollbarThumbLeftPercent = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0;
if (!applyGridHorizontalScrollbarThumbStyle() && hasGridHorizontalOverflow.value) {
nextTick(applyGridHorizontalScrollbarThumbStyle);
}
}
function updateGridVerticalScrollbar(element: HTMLElement | null = gridScrollerElement()) {
if (!element) {
hasGridVerticalOverflow.value = false;
gridVerticalScrollbarThumbTopPercent.value = 0;
gridVerticalScrollbarThumbHeightPercent.value = 100;
setGridVerticalOverflow(false);
gridVerticalScrollbarThumbTopPercent = 0;
gridVerticalScrollbarThumbHeightPercent = 100;
applyGridVerticalScrollbarThumbStyle();
return;
}
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
hasGridVerticalOverflow.value = maxScrollTop > 1;
setGridVerticalOverflow(maxScrollTop > 1);
const rawThumbHeight = element.scrollHeight > 0 ? (element.clientHeight / element.scrollHeight) * 100 : 100;
const thumbHeight = Math.min(100, Math.max(6, rawThumbHeight));
const thumbTravel = Math.max(0, 100 - thumbHeight);
gridVerticalScrollbarThumbHeightPercent.value = thumbHeight;
gridVerticalScrollbarThumbTopPercent.value = maxScrollTop > 0 ? (element.scrollTop / maxScrollTop) * thumbTravel : 0;
gridVerticalScrollbarThumbHeightPercent = thumbHeight;
gridVerticalScrollbarThumbTopPercent = maxScrollTop > 0 ? (element.scrollTop / maxScrollTop) * thumbTravel : 0;
if (!applyGridVerticalScrollbarThumbStyle() && hasGridVerticalOverflow.value) {
nextTick(applyGridVerticalScrollbarThumbStyle);
}
}
function setGridHorizontalOverflow(overflow: boolean) {
if (hasGridHorizontalOverflow.value === overflow) return;
hasGridHorizontalOverflow.value = overflow;
if (overflow) nextTick(applyGridHorizontalScrollbarThumbStyle);
}
function setGridVerticalOverflow(overflow: boolean) {
if (hasGridVerticalOverflow.value === overflow) return;
hasGridVerticalOverflow.value = overflow;
if (overflow) nextTick(applyGridVerticalScrollbarThumbStyle);
}
function applyGridHorizontalScrollbarThumbStyle(): boolean {
const thumb = gridHorizontalScrollbarThumbRef.value;
if (!thumb) return false;
// Scroll thumb position changes on every drag frame; update it outside Vue's
// render path so large result grids do not re-render while the user drags.
thumb.style.width = `${gridHorizontalScrollbarThumbWidthPercent}%`;
thumb.style.left = `${gridHorizontalScrollbarThumbLeftPercent}%`;
return true;
}
function applyGridVerticalScrollbarThumbStyle(): boolean {
const thumb = gridVerticalScrollbarThumbRef.value;
if (!thumb) return false;
thumb.style.height = `${gridVerticalScrollbarThumbHeightPercent}%`;
thumb.style.top = `${gridVerticalScrollbarThumbTopPercent}%`;
return true;
}
function setGridHorizontalScrollbarDragging(dragging: boolean) {
gridHorizontalScrollbarTrackRef.value?.classList.toggle(GRID_HORIZONTAL_SCROLLBAR_DRAGGING_CLASS, dragging);
}
function setGridVerticalScrollbarDragging(dragging: boolean) {
gridVerticalScrollbarTrackRef.value?.classList.toggle(GRID_VERTICAL_SCROLLBAR_DRAGGING_CLASS, dragging);
}
function scheduleGridHorizontalScrollbarUpdate() {
@ -2822,7 +2873,7 @@ function applyPendingGridHorizontalScrollbarDrag() {
const dragState = gridHorizontalScrollbarDragState;
if (!dragState) return;
const thumbWidthPx = dragState.trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100);
const thumbWidthPx = dragState.trackRect.width * (gridHorizontalScrollbarThumbWidthPercent / 100);
const maxThumbLeftPx = Math.max(1, dragState.trackRect.width - thumbWidthPx);
const thumbLeftPx = Math.min(maxThumbLeftPx, Math.max(0, gridHorizontalScrollbarPendingClientX - dragState.trackRect.left - dragState.thumbOffsetPx));
const scroller = dragState.scroller;
@ -2859,7 +2910,7 @@ function stopGridHorizontalScrollbarDrag() {
if (!gridHorizontalScrollbarDragState) return;
flushGridHorizontalScrollbarDrag();
gridHorizontalScrollbarDragState = null;
gridHorizontalScrollbarDragging.value = false;
setGridHorizontalScrollbarDragging(false);
window.removeEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true);
window.removeEventListener("pointerup", stopGridHorizontalScrollbarDrag, true);
window.removeEventListener("pointercancel", stopGridHorizontalScrollbarDrag, true);
@ -2870,7 +2921,7 @@ function stopGridVerticalScrollbarDrag() {
if (!gridVerticalScrollbarDragState) return;
flushGridVerticalScrollbarDrag();
gridVerticalScrollbarDragState = null;
gridVerticalScrollbarDragging.value = false;
setGridVerticalScrollbarDragging(false);
window.removeEventListener("pointermove", onGridVerticalScrollbarPointerMove, true);
window.removeEventListener("pointerup", stopGridVerticalScrollbarDrag, true);
window.removeEventListener("pointercancel", stopGridVerticalScrollbarDrag, true);
@ -2885,8 +2936,8 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) {
const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
if (maxScrollLeft <= 1) return;
const trackRect = track.getBoundingClientRect();
const thumbLeftPx = trackRect.width * (gridHorizontalScrollbarThumbLeftPercent.value / 100);
const thumbWidthPx = trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100);
const thumbLeftPx = trackRect.width * (gridHorizontalScrollbarThumbLeftPercent / 100);
const thumbWidthPx = trackRect.width * (gridHorizontalScrollbarThumbWidthPercent / 100);
const pointerX = event.clientX - trackRect.left;
const pointerInsideThumb = pointerX >= thumbLeftPx && pointerX <= thumbLeftPx + thumbWidthPx;
@ -2896,7 +2947,7 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) {
thumbOffsetPx: pointerInsideThumb ? pointerX - thumbLeftPx : thumbWidthPx / 2,
maxScrollLeft,
};
gridHorizontalScrollbarDragging.value = true;
setGridHorizontalScrollbarDragging(true);
document.body.style.userSelect = "none";
window.addEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true);
window.addEventListener("pointerup", stopGridHorizontalScrollbarDrag, true);
@ -2910,7 +2961,7 @@ function applyPendingGridVerticalScrollbarDrag() {
const dragState = gridVerticalScrollbarDragState;
if (!dragState) return;
const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100);
const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent / 100);
const maxThumbTopPx = Math.max(1, dragState.trackRect.height - thumbHeightPx);
const thumbTopPx = Math.min(maxThumbTopPx, Math.max(0, gridVerticalScrollbarPendingClientY - dragState.trackRect.top - dragState.thumbOffsetPx));
const scroller = dragState.scroller;
@ -2951,8 +3002,8 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) {
const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
if (maxScrollTop <= 1) return;
const trackRect = track.getBoundingClientRect();
const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent.value / 100);
const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100);
const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent / 100);
const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent / 100);
const pointerY = event.clientY - trackRect.top;
const pointerInsideThumb = pointerY >= thumbTopPx && pointerY <= thumbTopPx + thumbHeightPx;
@ -2962,7 +3013,7 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) {
thumbOffsetPx: pointerInsideThumb ? pointerY - thumbTopPx : thumbHeightPx / 2,
maxScrollTop,
};
gridVerticalScrollbarDragging.value = true;
setGridVerticalScrollbarDragging(true);
document.body.style.userSelect = "none";
window.addEventListener("pointermove", onGridVerticalScrollbarPointerMove, true);
window.addEventListener("pointerup", stopGridVerticalScrollbarDrag, true);
@ -2971,16 +3022,6 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) {
scheduleGridVerticalScrollbarDrag(event.clientY);
}
const gridHorizontalScrollbarThumbStyle = computed<CSSProperties>(() => ({
width: `${gridHorizontalScrollbarThumbWidthPercent.value}%`,
left: `${gridHorizontalScrollbarThumbLeftPercent.value}%`,
}));
const gridVerticalScrollbarThumbStyle = computed<CSSProperties>(() => ({
height: `${gridVerticalScrollbarThumbHeightPercent.value}%`,
top: `${gridVerticalScrollbarThumbTopPercent.value}%`,
}));
function updateGridScrollbarGutter(element: HTMLElement) {
gridScrollbarGutter.value = scrollbarGutterWidth(element);
}
@ -9913,11 +9954,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<Loader2 class="w-3 h-3 animate-spin mr-1" />
{{ t("grid.loadingMore") }}
</div>
<div v-if="hasGridHorizontalOverflow" ref="gridHorizontalScrollbarTrackRef" class="data-grid-horizontal-scrollbar" :class="{ 'data-grid-horizontal-scrollbar--dragging': gridHorizontalScrollbarDragging }" @pointerdown="startGridHorizontalScrollbarDrag">
<div class="data-grid-horizontal-scrollbar__thumb" :style="gridHorizontalScrollbarThumbStyle" />
<div v-if="hasGridHorizontalOverflow" ref="gridHorizontalScrollbarTrackRef" class="data-grid-horizontal-scrollbar" @pointerdown="startGridHorizontalScrollbarDrag">
<div ref="gridHorizontalScrollbarThumbRef" class="data-grid-horizontal-scrollbar__thumb" />
</div>
<div v-if="hasGridVerticalOverflow" ref="gridVerticalScrollbarTrackRef" class="data-grid-vertical-scrollbar" :class="{ 'data-grid-vertical-scrollbar--dragging': gridVerticalScrollbarDragging }" @pointerdown="startGridVerticalScrollbarDrag">
<div class="data-grid-vertical-scrollbar__thumb" :style="gridVerticalScrollbarThumbStyle" />
<div v-if="hasGridVerticalOverflow" ref="gridVerticalScrollbarTrackRef" class="data-grid-vertical-scrollbar" @pointerdown="startGridVerticalScrollbarDrag">
<div ref="gridVerticalScrollbarThumbRef" class="data-grid-vertical-scrollbar__thumb" />
</div>
<div v-if="loading" class="absolute inset-0 z-20 bg-background/50 flex items-center justify-center">
<div class="flex items-center gap-2 px-3 py-1.5 rounded-md bg-background border shadow-sm text-xs text-muted-foreground">