Skip to content

Commit 268f4d1

Browse files
authored
Merge pull request #17 from Cratis/Components:copilot/improve-pivotviewer-features
Improve PivotViewer: faster pinch zoom and fix grouped-mode fly-to animation
2 parents 7ca87e1 + 6934cc7 commit 268f4d1

6 files changed

Lines changed: 61 additions & 19 deletions

File tree

Source/PivotViewer/PivotViewer.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -799,16 +799,22 @@
799799
font-variant-numeric: tabular-nums;
800800
}
801801

802-
/* Fade axis labels when switching views - matched to sprite animation duration (600ms) */
802+
/* Fade and collapse axis labels when switching views */
803803
.pivot-viewer .pv-axis-labels {
804-
transition: opacity 600ms ease;
804+
transition: opacity 600ms ease, max-height 600ms ease, padding 600ms ease, border-color 600ms ease;
805+
overflow: hidden;
805806
}
806807
.pivot-viewer .pv-axis-labels.hidden {
807808
opacity: 0;
809+
max-height: 0;
810+
padding-top: 0;
811+
padding-bottom: 0;
812+
border-top-color: transparent;
808813
pointer-events: none;
809814
}
810815
.pivot-viewer .pv-axis-labels.visible {
811816
opacity: 1;
817+
max-height: 80px;
812818
pointer-events: auto;
813819
}
814820

Source/PivotViewer/PivotViewer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ export function PivotViewer<TItem extends object>({
277277
zoomLevel,
278278
viewMode,
279279
layout,
280+
containerRef,
281+
spacerRef,
280282
containerDimensions,
281283
scrollPosition,
282284
preSelectionState,
@@ -296,6 +298,7 @@ export function PivotViewer<TItem extends object>({
296298
zoomLevel,
297299
viewMode,
298300
layout,
301+
containerRef,
299302
containerDimensions,
300303
grouping,
301304
data,

Source/PivotViewer/hooks/useCardSelection.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
import { useCallback } from 'react';
5+
import type React from 'react';
56
import { handleCardSelection } from '../utils/selection';
67
import type { Layout } from '../utils/cardPosition';
78
import type { ViewMode } from '../components/Toolbar';
@@ -20,6 +21,8 @@ interface UseCardSelectionParams<TItem extends object> {
2021
zoomLevel: number;
2122
viewMode: ViewMode;
2223
layout: Layout;
24+
containerRef: React.RefObject<HTMLDivElement | null>;
25+
spacerRef: React.RefObject<HTMLDivElement | null>;
2326
containerDimensions: { width: number; height: number };
2427
scrollPosition: { x: number; y: number };
2528
preSelectionState: { zoom: number; scrollLeft: number; scrollTop: number } | null;
@@ -39,6 +42,8 @@ export function useCardSelection<TItem extends object>({
3942
zoomLevel,
4043
viewMode,
4144
layout,
45+
containerRef,
46+
spacerRef,
4247
containerDimensions,
4348
scrollPosition,
4449
preSelectionState,
@@ -53,8 +58,8 @@ export function useCardSelection<TItem extends object>({
5358
return useCallback((item: TItem, e: MouseEvent, id?: number | string) => {
5459
if (isPanning) return;
5560

56-
// Get container element from event target
57-
const container = (e.target as Element)?.closest('.pv-main')?.parentElement as HTMLDivElement | null;
61+
// Use the containerRef directly as the scrollable viewport
62+
const container = containerRef.current;
5863
if (!container) return;
5964

6065
// Resolve item ID
@@ -111,7 +116,7 @@ export function useCardSelection<TItem extends object>({
111116
targetCardPosition,
112117
getCardPositionAtZoom: callbacks.getCardPositionAtZoom,
113118
getLayoutSizeAtZoom: callbacks.getLayoutSizeAtZoom,
114-
spacer: container.querySelector('.pv-spacer') as HTMLDivElement,
119+
spacer: spacerRef.current,
115120
preSelectionState,
116121
startScrollPosition: { x: scrollPosition.x, y: scrollPosition.y },
117122
setZoomLevel,
@@ -122,5 +127,5 @@ export function useCardSelection<TItem extends object>({
122127
zoomLevel,
123128
totalHeight: targetTotalHeight,
124129
});
125-
}, [isPanning, selectedItem, zoomLevel, preSelectionState, viewMode, resolveId, setZoomLevel, layout, grouping, containerDimensions, scrollPosition, data, getItemId]);
130+
}, [isPanning, selectedItem, zoomLevel, preSelectionState, viewMode, resolveId, setZoomLevel, layout, grouping, containerRef, spacerRef, containerDimensions, scrollPosition, data, getItemId]);
126131
}

Source/PivotViewer/hooks/useDetailPanelClose.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
import { useCallback } from 'react';
5+
import type React from 'react';
56
import { animateZoomAndScroll, smoothScrollTo } from '../utils/animations';
67
import type { Layout } from '../utils/cardPosition';
78
import type { ViewMode } from '../components/Toolbar';
@@ -18,6 +19,7 @@ interface UseDetailPanelCloseParams<TItem extends object> {
1819
zoomLevel: number;
1920
viewMode: ViewMode;
2021
layout: Layout;
22+
containerRef: React.RefObject<HTMLDivElement | null>;
2123
containerDimensions: { width: number; height: number };
2224
grouping: unknown;
2325
data: TItem[];
@@ -34,6 +36,7 @@ export function useDetailPanelClose<TItem extends object>({
3436
zoomLevel,
3537
viewMode,
3638
layout,
39+
containerRef,
3740
containerDimensions,
3841
grouping,
3942
data,
@@ -44,8 +47,8 @@ export function useDetailPanelClose<TItem extends object>({
4447
setPreSelectionState,
4548
}: UseDetailPanelCloseParams<TItem>) {
4649
return useCallback(() => {
47-
// Get container element
48-
const container = document.querySelector('.pv-main')?.parentElement as HTMLDivElement | null;
50+
// Use the containerRef directly as the scrollable viewport
51+
const container = containerRef.current;
4952
if (!container || !selectedItem) {
5053
setSelectedItem(null);
5154
return;
@@ -116,5 +119,5 @@ export function useDetailPanelClose<TItem extends object>({
116119
setPreSelectionState(null);
117120
},
118121
});
119-
}, [preSelectionState, selectedItem, zoomLevel, viewMode, resolveId, setZoomLevel, layout, grouping, containerDimensions, data, setSelectedItem, setPreSelectionState, setIsZooming]);
122+
}, [preSelectionState, selectedItem, zoomLevel, viewMode, resolveId, setZoomLevel, layout, grouping, containerRef, containerDimensions, data, setSelectedItem, setPreSelectionState, setIsZooming]);
120123
}

Source/PivotViewer/hooks/useWheelZoom.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,17 @@ export function useWheelZoom(
2424
const scrollX = container.scrollLeft;
2525
const scrollY = container.scrollTop;
2626

27-
const delta = -e.deltaY * 0.01;
27+
// Normalize delta based on deltaMode:
28+
// 0 = DOM_DELTA_PIXEL (trackpad pinch — deltaY is in pixels, small values)
29+
// 1 = DOM_DELTA_LINE (mouse wheel — deltaY is in lines, typically 3)
30+
// 2 = DOM_DELTA_PAGE (rare, treated same as pixel for safety)
31+
let factor: number;
32+
if (e.deltaMode === 1) {
33+
factor = 0.12; // line-mode: each "line" gives a noticeable zoom step
34+
} else {
35+
factor = 0.01; // pixel-mode (trackpad pinch) and page-mode: fast zoom step
36+
}
37+
const delta = -e.deltaY * factor;
2838
const newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomLevel + delta));
2939
const zoomRatio = newZoom / zoomLevel;
3040

Source/PivotViewer/utils/selection.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,20 @@ export function handleCardSelection<TItem>({
101101

102102
if (isFirstSelection) {
103103
if (viewMode === 'collection') {
104-
// Collection mode: just smooth scroll to center, no zoom
104+
// Collection mode: animate scroll to center the selected card (no zoom change)
105105
if (cardPosition) {
106-
// In collection mode, we don't have a detail panel width offset because the panel is an overlay or separate
107-
// But if we want to center it, we should consider if the detail panel pushes content
108-
// For now, assume 0 offset as per original code
106+
setIsZooming(true);
109107
const { scrollLeft, scrollTop } = calculateCenterScrollPosition(container, cardPosition, zoomLevel, 0, totalHeight);
110-
smoothScrollTo(container, scrollLeft, scrollTop, true);
108+
animateZoomAndScroll({
109+
container,
110+
cardPosition,
111+
startZoom: zoomLevel,
112+
targetZoom: zoomLevel,
113+
targetScrollLeft: scrollLeft,
114+
targetScrollTop: scrollTop,
115+
onUpdate: setZoomLevel,
116+
onComplete: () => setIsZooming(false),
117+
});
111118
}
112119
} else {
113120
// Grouped mode: animate zoom and scroll
@@ -127,13 +134,21 @@ export function handleCardSelection<TItem>({
127134
});
128135
}
129136
} else {
130-
// Subsequent selections: just center the new card
137+
// Subsequent selections: animate the scroll to center the new card
131138
if (cardPosition) {
132-
// In collection mode, we don't zoom, so we just center.
133-
// In grouped mode, we might be zoomed in, so we center with offset.
139+
setIsZooming(true);
134140
const detailWidth = viewMode === 'collection' ? 0 : DETAIL_PANEL_WIDTH;
135141
const { scrollLeft, scrollTop } = calculateCenterScrollPosition(container, cardPosition, zoomLevel, detailWidth, totalHeight);
136-
smoothScrollTo(container, scrollLeft, scrollTop, true);
142+
animateZoomAndScroll({
143+
container,
144+
cardPosition,
145+
startZoom: zoomLevel,
146+
targetZoom: zoomLevel,
147+
targetScrollLeft: scrollLeft,
148+
targetScrollTop: scrollTop,
149+
onUpdate: setZoomLevel,
150+
onComplete: () => setIsZooming(false),
151+
});
137152
}
138153
}
139154
}

0 commit comments

Comments
 (0)