From 30d8c74c1788e224d01c3883910844d0c2a756a6 Mon Sep 17 00:00:00 2001
From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:23:39 -0700
Subject: [PATCH] fix: playhead location offset issues in piano roll window;
z-index issue in `bar-numbers` and `top-left-spacer`
---
src/components/MainContent.css | 4 +-
.../piano-roll/PianoGridHeader.test.tsx | 121 ++++++++++++++++++
src/components/piano-roll/PianoGridHeader.tsx | 22 ++--
3 files changed, 132 insertions(+), 15 deletions(-)
create mode 100644 src/components/piano-roll/PianoGridHeader.test.tsx
diff --git a/src/components/MainContent.css b/src/components/MainContent.css
index 46e964a..685b2d4 100644
--- a/src/components/MainContent.css
+++ b/src/components/MainContent.css
@@ -26,7 +26,7 @@
background-color: #2d2d2d;
border-bottom: 1px solid #3a3a3a;
border-right: 1px solid #3a3a3a;
- z-index: 1002;
+ z-index: 1005;
/* Higher than other elements to ensure it's always visible */
display: flex;
flex-direction: row;
@@ -107,7 +107,7 @@
display: flex;
border-bottom: 1px solid #3a3a3a;
background-color: #2d2d2d;
- z-index: 20;
+ z-index: 1004;
margin-left: var(--track-info-panel-width);
/* Offset for info-container */
width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
diff --git a/src/components/piano-roll/PianoGridHeader.test.tsx b/src/components/piano-roll/PianoGridHeader.test.tsx
new file mode 100644
index 0000000..35375e5
--- /dev/null
+++ b/src/components/piano-roll/PianoGridHeader.test.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import PianoGridHeader from './PianoGridHeader';
+
+const setPlayheadPosition = vi.fn();
+const requestMainContentScroll = vi.fn();
+const getPlayheadPosition = vi.fn(() => 0);
+
+const storeState = {
+ setPlayheadPosition,
+ requestMainContentScroll,
+};
+
+vi.mock('../../stores/projectStore', () => ({
+ useProjectStore: () => storeState,
+}));
+
+vi.mock('../../core/KGCore', () => ({
+ KGCore: {
+ instance: () => ({
+ getPlayheadPosition,
+ }),
+ },
+}));
+
+function renderHeader({
+ hasPianoKeys = true,
+ scrollLeft = 0,
+ paddingLeft = hasPianoKeys ? '60px' : '0px',
+}: {
+ hasPianoKeys?: boolean;
+ scrollLeft?: number;
+ paddingLeft?: string;
+} = {}) {
+ const view = render(
+
+ );
+
+ const scrollContainer = view.container.querySelector('.piano-roll-note-scroll') as HTMLDivElement;
+ const header = view.container.querySelector('.piano-grid-header') as HTMLDivElement;
+
+ Object.defineProperty(scrollContainer, 'scrollLeft', {
+ configurable: true,
+ value: scrollLeft,
+ writable: true,
+ });
+
+ header.style.paddingLeft = paddingLeft;
+
+ Object.defineProperty(header, 'getBoundingClientRect', {
+ configurable: true,
+ value: () => ({
+ left: 100,
+ top: 0,
+ right: 600,
+ bottom: 20,
+ width: 500,
+ height: 20,
+ x: 100,
+ y: 0,
+ toJSON: () => ({}),
+ }),
+ });
+
+ return { ...view, header };
+}
+
+describe('PianoGridHeader', () => {
+ beforeEach(() => {
+ document.documentElement.style.setProperty('--region-grid-beat-width', '40px');
+ setPlayheadPosition.mockClear();
+ requestMainContentScroll.mockClear();
+ getPlayheadPosition.mockClear();
+ getPlayheadPosition.mockReturnValue(0);
+ });
+
+ it('seeks to the expected beat without horizontal scroll', () => {
+ const { header } = renderHeader();
+
+ fireEvent.click(header, { clientX: 280 });
+
+ expect(setPlayheadPosition).toHaveBeenCalledTimes(1);
+ expect(setPlayheadPosition).toHaveBeenCalledWith(3);
+ expect(requestMainContentScroll).toHaveBeenCalledTimes(1);
+ expect(requestMainContentScroll).toHaveBeenCalledWith(3);
+ });
+
+ it('includes the note scroll offset when seeking after horizontal scroll', () => {
+ const { header } = renderHeader({ scrollLeft: 160 });
+
+ fireEvent.click(header, { clientX: 280 });
+
+ expect(setPlayheadPosition).toHaveBeenCalledTimes(1);
+ expect(setPlayheadPosition).toHaveBeenCalledWith(7);
+ expect(requestMainContentScroll).toHaveBeenCalledTimes(1);
+ expect(requestMainContentScroll).toHaveBeenCalledWith(7);
+ });
+
+ it('does not subtract a gutter when piano keys are hidden', () => {
+ const { header } = renderHeader({ hasPianoKeys: false });
+
+ fireEvent.click(header, { clientX: 180 });
+
+ expect(setPlayheadPosition).toHaveBeenCalledTimes(1);
+ expect(setPlayheadPosition).toHaveBeenCalledWith(2);
+ expect(requestMainContentScroll).toHaveBeenCalledWith(2);
+ });
+
+ it('uses the same corrected math on mousedown for drag-to-seek', () => {
+ const { header } = renderHeader({ scrollLeft: 80 });
+
+ fireEvent.mouseDown(header, { button: 0, clientX: 280 });
+
+ expect(setPlayheadPosition).toHaveBeenCalledTimes(1);
+ expect(setPlayheadPosition).toHaveBeenCalledWith(5);
+ expect(requestMainContentScroll).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/components/piano-roll/PianoGridHeader.tsx b/src/components/piano-roll/PianoGridHeader.tsx
index 702a0ff..a8f9c4b 100644
--- a/src/components/piano-roll/PianoGridHeader.tsx
+++ b/src/components/piano-roll/PianoGridHeader.tsx
@@ -26,20 +26,16 @@ const PianoGridHeader: React.FC = ({
// Utility function to calculate playhead position from mouse coordinates
const calculatePlayheadFromMouse = useCallback((clientX: number): number | null => {
if (!headerElementRef.current) return null;
-
- const rect = headerElementRef.current.getBoundingClientRect();
+
+ const headerElement = headerElementRef.current;
+ const rect = headerElement.getBoundingClientRect();
const relativeX = clientX - rect.left;
-
- // Account for the piano keys width offset
- const pianoKeysWidth = hasPianoKeys
- ? (parseInt(
- getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
- ) || 60)
- : 0;
-
- const adjustedX = relativeX - pianoKeysWidth;
-
- // If the click is in the piano keys area (left side), ignore it
+ const scrollContainer = headerElement.closest('.piano-roll-note-scroll') as HTMLElement | null;
+ const scrollLeft = scrollContainer?.scrollLeft ?? 0;
+ const leftGutter = parseFloat(getComputedStyle(headerElement).paddingLeft) || 0;
+ const adjustedX = relativeX + scrollLeft - leftGutter;
+
+ // Ignore clicks inside the visual left gutter reserved for piano keys.
if (adjustedX < 0) {
return null;
}