fix: correct piano-roll header seek when horizontally scrolled

This commit is contained in:
Xiaohan-Tian
2026-07-06 18:23:34 -07:00
parent be15983e12
commit 3c93dd76fc
2 changed files with 32 additions and 6 deletions
@@ -28,10 +28,12 @@ function renderHeader({
hasPianoKeys = true, hasPianoKeys = true,
scrollLeft = 0, scrollLeft = 0,
paddingLeft = hasPianoKeys ? '60px' : '0px', paddingLeft = hasPianoKeys ? '60px' : '0px',
scrollContainerLeft = 100,
}: { }: {
hasPianoKeys?: boolean; hasPianoKeys?: boolean;
scrollLeft?: number; scrollLeft?: number;
paddingLeft?: string; paddingLeft?: string;
scrollContainerLeft?: number;
} = {}) { } = {}) {
const view = render( const view = render(
<div className="piano-roll-note-scroll"> <div className="piano-roll-note-scroll">
@@ -48,18 +50,33 @@ function renderHeader({
writable: true, writable: true,
}); });
Object.defineProperty(scrollContainer, 'getBoundingClientRect', {
configurable: true,
value: () => ({
left: scrollContainerLeft,
top: 0,
right: scrollContainerLeft + 500,
bottom: 20,
width: 500,
height: 20,
x: scrollContainerLeft,
y: 0,
toJSON: () => ({}),
}),
});
header.style.paddingLeft = paddingLeft; header.style.paddingLeft = paddingLeft;
Object.defineProperty(header, 'getBoundingClientRect', { Object.defineProperty(header, 'getBoundingClientRect', {
configurable: true, configurable: true,
value: () => ({ value: () => ({
left: 100, left: scrollContainerLeft - scrollLeft,
top: 0, top: 0,
right: 600, right: scrollContainerLeft - scrollLeft + 500,
bottom: 20, bottom: 20,
width: 500, width: 500,
height: 20, height: 20,
x: 100, x: scrollContainerLeft - scrollLeft,
y: 0, y: 0,
toJSON: () => ({}), toJSON: () => ({}),
}), }),
@@ -88,7 +105,7 @@ describe('PianoGridHeader', () => {
expect(requestMainContentScroll).toHaveBeenCalledWith(3); expect(requestMainContentScroll).toHaveBeenCalledWith(3);
}); });
it('includes the note scroll offset when seeking after horizontal scroll', () => { it('seeks to the same beat after horizontal scroll when the header rect already shifts with content', () => {
const { header } = renderHeader({ scrollLeft: 160 }); const { header } = renderHeader({ scrollLeft: 160 });
fireEvent.click(header, { clientX: 280 }); fireEvent.click(header, { clientX: 280 });
@@ -118,4 +135,13 @@ describe('PianoGridHeader', () => {
expect(setPlayheadPosition).toHaveBeenCalledWith(5); expect(setPlayheadPosition).toHaveBeenCalledWith(5);
expect(requestMainContentScroll).not.toHaveBeenCalled(); expect(requestMainContentScroll).not.toHaveBeenCalled();
}); });
it('ignores clicks inside the visible piano-key gutter before it fully scrolls out of view', () => {
const { header } = renderHeader({ scrollLeft: 20 });
fireEvent.click(header, { clientX: 110 });
expect(setPlayheadPosition).not.toHaveBeenCalled();
expect(requestMainContentScroll).not.toHaveBeenCalled();
});
}); });
@@ -28,9 +28,9 @@ const PianoGridHeader: React.FC<PianoGridHeaderProps> = ({
if (!headerElementRef.current) return null; if (!headerElementRef.current) return null;
const headerElement = headerElementRef.current; const headerElement = headerElementRef.current;
const rect = headerElement.getBoundingClientRect();
const relativeX = clientX - rect.left;
const scrollContainer = headerElement.closest('.piano-roll-note-scroll') as HTMLElement | null; const scrollContainer = headerElement.closest('.piano-roll-note-scroll') as HTMLElement | null;
const referenceRect = scrollContainer?.getBoundingClientRect() ?? headerElement.getBoundingClientRect();
const relativeX = clientX - referenceRect.left;
const scrollLeft = scrollContainer?.scrollLeft ?? 0; const scrollLeft = scrollContainer?.scrollLeft ?? 0;
const leftGutter = parseFloat(getComputedStyle(headerElement).paddingLeft) || 0; const leftGutter = parseFloat(getComputedStyle(headerElement).paddingLeft) || 0;
const adjustedX = relativeX + scrollLeft - leftGutter; const adjustedX = relativeX + scrollLeft - leftGutter;