fix(grid): unify data grid scrollbars
This commit is contained in:
parent
60e49128cb
commit
8ee5dd9d35
|
|
@ -1397,16 +1397,25 @@ const gridRef = ref<HTMLDivElement>();
|
|||
const headerRef = ref<HTMLDivElement>();
|
||||
const gridScrollbarGutter = ref(0);
|
||||
const gridHorizontalScrollbarTrackRef = ref<HTMLDivElement>();
|
||||
const gridVerticalScrollbarTrackRef = 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 gridHorizontalScrollbarFrame = 0;
|
||||
let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null;
|
||||
let gridHorizontalScrollbarDragState: {
|
||||
trackRect: DOMRect;
|
||||
thumbOffsetPx: number;
|
||||
} | null = null;
|
||||
let gridVerticalScrollbarDragState: {
|
||||
trackRect: DOMRect;
|
||||
thumbOffsetPx: number;
|
||||
} | null = null;
|
||||
const hiddenColumnIndexes = ref<Set<number>>(new Set());
|
||||
const nullColumnsHidden = ref(false);
|
||||
const autoHiddenNullColumnIndexes = ref<Set<number>>(new Set());
|
||||
|
|
@ -1565,6 +1574,7 @@ function updateGridHorizontalViewport(element: HTMLElement) {
|
|||
gridHorizontalScrollLeft.value = element.scrollLeft;
|
||||
gridViewportWidth.value = element.clientWidth;
|
||||
updateGridHorizontalScrollbar(element);
|
||||
updateGridVerticalScrollbar(element);
|
||||
}
|
||||
|
||||
function gridScrollerElement(): HTMLElement | null {
|
||||
|
|
@ -1589,11 +1599,30 @@ function updateGridHorizontalScrollbar(element: HTMLElement | null = gridScrolle
|
|||
gridHorizontalScrollbarThumbLeftPercent.value = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0;
|
||||
}
|
||||
|
||||
function updateGridVerticalScrollbar(element: HTMLElement | null = gridScrollerElement()) {
|
||||
if (!element) {
|
||||
hasGridVerticalOverflow.value = false;
|
||||
gridVerticalScrollbarThumbTopPercent.value = 0;
|
||||
gridVerticalScrollbarThumbHeightPercent.value = 100;
|
||||
return;
|
||||
}
|
||||
|
||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
hasGridVerticalOverflow.value = 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;
|
||||
}
|
||||
|
||||
function scheduleGridHorizontalScrollbarUpdate() {
|
||||
if (gridHorizontalScrollbarFrame) return;
|
||||
const update = () => {
|
||||
gridHorizontalScrollbarFrame = 0;
|
||||
updateGridHorizontalScrollbar();
|
||||
updateGridVerticalScrollbar();
|
||||
};
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
gridHorizontalScrollbarFrame = requestAnimationFrame(update);
|
||||
|
|
@ -1651,6 +1680,16 @@ function stopGridHorizontalScrollbarDrag() {
|
|||
document.body.style.userSelect = "";
|
||||
}
|
||||
|
||||
function stopGridVerticalScrollbarDrag() {
|
||||
if (!gridVerticalScrollbarDragState) return;
|
||||
gridVerticalScrollbarDragState = null;
|
||||
gridVerticalScrollbarDragging.value = false;
|
||||
window.removeEventListener("pointermove", onGridVerticalScrollbarPointerMove, true);
|
||||
window.removeEventListener("pointerup", stopGridVerticalScrollbarDrag, true);
|
||||
window.removeEventListener("pointercancel", stopGridVerticalScrollbarDrag, true);
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
|
||||
function startGridHorizontalScrollbarDrag(event: PointerEvent) {
|
||||
const scroller = gridScrollerElement();
|
||||
const track = gridHorizontalScrollbarTrackRef.value;
|
||||
|
|
@ -1675,11 +1714,62 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) {
|
|||
applyGridHorizontalScrollbarDrag(event.clientX);
|
||||
}
|
||||
|
||||
function applyGridVerticalScrollbarDrag(clientY: number) {
|
||||
const scroller = gridScrollerElement();
|
||||
const dragState = gridVerticalScrollbarDragState;
|
||||
if (!scroller || !dragState) return;
|
||||
|
||||
const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
||||
if (maxScrollTop <= 1) return;
|
||||
|
||||
const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100);
|
||||
const maxThumbTopPx = Math.max(1, dragState.trackRect.height - thumbHeightPx);
|
||||
const thumbTopPx = Math.min(maxThumbTopPx, Math.max(0, clientY - dragState.trackRect.top - dragState.thumbOffsetPx));
|
||||
scroller.scrollTop = (thumbTopPx / maxThumbTopPx) * maxScrollTop;
|
||||
updateGridVerticalScrollbar(scroller);
|
||||
if (useCanvasGridRows.value) syncCanvasViewport();
|
||||
}
|
||||
|
||||
function onGridVerticalScrollbarPointerMove(event: PointerEvent) {
|
||||
if (!gridVerticalScrollbarDragState) return;
|
||||
event.preventDefault();
|
||||
applyGridVerticalScrollbarDrag(event.clientY);
|
||||
}
|
||||
|
||||
function startGridVerticalScrollbarDrag(event: PointerEvent) {
|
||||
const scroller = gridScrollerElement();
|
||||
const track = gridVerticalScrollbarTrackRef.value;
|
||||
if (!scroller || !track || !hasGridVerticalOverflow.value) return;
|
||||
|
||||
const trackRect = track.getBoundingClientRect();
|
||||
const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent.value / 100);
|
||||
const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100);
|
||||
const pointerY = event.clientY - trackRect.top;
|
||||
const pointerInsideThumb = pointerY >= thumbTopPx && pointerY <= thumbTopPx + thumbHeightPx;
|
||||
|
||||
gridVerticalScrollbarDragState = {
|
||||
trackRect,
|
||||
thumbOffsetPx: pointerInsideThumb ? pointerY - thumbTopPx : thumbHeightPx / 2,
|
||||
};
|
||||
gridVerticalScrollbarDragging.value = true;
|
||||
document.body.style.userSelect = "none";
|
||||
window.addEventListener("pointermove", onGridVerticalScrollbarPointerMove, true);
|
||||
window.addEventListener("pointerup", stopGridVerticalScrollbarDrag, true);
|
||||
window.addEventListener("pointercancel", stopGridVerticalScrollbarDrag, true);
|
||||
event.preventDefault();
|
||||
applyGridVerticalScrollbarDrag(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);
|
||||
}
|
||||
|
|
@ -3585,6 +3675,7 @@ function onCanvasScroll(event: Event) {
|
|||
if (gridHorizontalScrollLeft.value !== scrollLeft || gridViewportWidth.value !== viewportWidth) {
|
||||
updateGridHorizontalViewport(scroller);
|
||||
}
|
||||
updateGridVerticalScrollbar(scroller);
|
||||
const gutter = scrollbarGutterWidth(scroller);
|
||||
if (gridScrollbarGutter.value !== gutter) gridScrollbarGutter.value = gutter;
|
||||
if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft;
|
||||
|
|
@ -3936,6 +4027,7 @@ onUnmounted(() => {
|
|||
pauseCanvasGridWork();
|
||||
gridHorizontalScrollbarResizeObserver?.disconnect();
|
||||
stopGridHorizontalScrollbarDrag();
|
||||
stopGridVerticalScrollbarDrag();
|
||||
if (gridHorizontalScrollbarFrame && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(gridHorizontalScrollbarFrame);
|
||||
}
|
||||
|
|
@ -6908,6 +7000,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
|
|
@ -7705,6 +7800,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
--data-grid-row-number-deleted-bg: rgb(255 244 244);
|
||||
--data-grid-row-number-active-bg: rgb(232 232 232);
|
||||
--data-grid-row-number-selected-bg: rgb(226 226 226);
|
||||
--data-grid-scrollbar-thumb: color-mix(in oklch, var(--foreground) 30%, transparent);
|
||||
--data-grid-scrollbar-thumb-hover: color-mix(in oklch, var(--foreground) 48%, transparent);
|
||||
--data-grid-scrollbar-track: transparent;
|
||||
}
|
||||
|
||||
:global(.dark) [data-grid-root] {
|
||||
|
|
@ -7761,34 +7859,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
scrollbar-gutter: stable;
|
||||
will-change: scroll-position;
|
||||
contain: layout style paint;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in oklch, var(--foreground) 30%, transparent) transparent;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar:horizontal {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar-thumb {
|
||||
border: 3px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 30%, transparent);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in oklch, var(--foreground) 48%, transparent);
|
||||
background-clip: content-box;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-grid-scroller :deep(.vue-recycle-scroller__item-wrapper) {
|
||||
|
|
@ -7821,7 +7896,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
top: 4px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 7%, transparent);
|
||||
background: var(--data-grid-scrollbar-track);
|
||||
}
|
||||
|
||||
.data-grid-horizontal-scrollbar__thumb {
|
||||
|
|
@ -7830,7 +7905,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
height: 4px;
|
||||
min-width: 24px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 24%, transparent);
|
||||
background: var(--data-grid-scrollbar-thumb);
|
||||
transition:
|
||||
height 120ms ease,
|
||||
background-color 120ms ease,
|
||||
|
|
@ -7841,7 +7916,38 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
.data-grid-horizontal-scrollbar--dragging .data-grid-horizontal-scrollbar__thumb {
|
||||
top: 2px;
|
||||
height: 6px;
|
||||
background: color-mix(in oklch, var(--foreground) 42%, transparent);
|
||||
background: var(--data-grid-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.data-grid-vertical-scrollbar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 2px;
|
||||
bottom: 14px;
|
||||
z-index: 30;
|
||||
width: 10px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.data-grid-vertical-scrollbar__thumb {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
width: 4px;
|
||||
min-height: 24px;
|
||||
border-radius: 999px;
|
||||
background: var(--data-grid-scrollbar-thumb);
|
||||
transition:
|
||||
background-color 120ms ease,
|
||||
left 120ms ease,
|
||||
width 120ms ease;
|
||||
}
|
||||
|
||||
.data-grid-vertical-scrollbar:hover .data-grid-vertical-scrollbar__thumb,
|
||||
.data-grid-vertical-scrollbar--dragging .data-grid-vertical-scrollbar__thumb {
|
||||
left: 2px;
|
||||
width: 6px;
|
||||
background: var(--data-grid-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.canvas-grid-surface {
|
||||
|
|
|
|||
Loading…
Reference in New Issue