fix: playhead location offset issues in piano roll window; z-index issue in bar-numbers and top-left-spacer
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
border-bottom: 1px solid #3a3a3a;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
border-right: 1px solid #3a3a3a;
|
border-right: 1px solid #3a3a3a;
|
||||||
z-index: 1002;
|
z-index: 1005;
|
||||||
/* Higher than other elements to ensure it's always visible */
|
/* Higher than other elements to ensure it's always visible */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid #3a3a3a;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
z-index: 20;
|
z-index: 1004;
|
||||||
margin-left: var(--track-info-panel-width);
|
margin-left: var(--track-info-panel-width);
|
||||||
/* Offset for info-container */
|
/* Offset for info-container */
|
||||||
width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
|
width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<div className="piano-roll-note-scroll">
|
||||||
|
<PianoGridHeader maxBars={8} hasPianoKeys={hasPianoKeys} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,19 +27,15 @@ const PianoGridHeader: React.FC<PianoGridHeaderProps> = ({
|
|||||||
const calculatePlayheadFromMouse = useCallback((clientX: number): number | null => {
|
const calculatePlayheadFromMouse = useCallback((clientX: number): number | null => {
|
||||||
if (!headerElementRef.current) return null;
|
if (!headerElementRef.current) return null;
|
||||||
|
|
||||||
const rect = headerElementRef.current.getBoundingClientRect();
|
const headerElement = headerElementRef.current;
|
||||||
|
const rect = headerElement.getBoundingClientRect();
|
||||||
const relativeX = clientX - rect.left;
|
const relativeX = clientX - rect.left;
|
||||||
|
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;
|
||||||
|
|
||||||
// Account for the piano keys width offset
|
// Ignore clicks inside the visual left gutter reserved for piano keys.
|
||||||
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
|
|
||||||
if (adjustedX < 0) {
|
if (adjustedX < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user