fix(scroll): gap config (#6828)

This commit is contained in:
Univer
2026-04-22 12:37:02 +08:00
committed by GitHub
parent 79ac910552
commit 48fb4c254d
5 changed files with 223 additions and 17 deletions
@@ -25,7 +25,7 @@ import { ThemeService } from '../../services/theme/theme.service';
import { ColorKit } from '../../shared';
import { BooleanNumber, HorizontalAlign } from '../../types/enum';
import { LocaleType } from '../../types/enum/locale-type';
import { SheetSkeleton } from '../sheet-skeleton';
import { createSheetGapTestConfig, SheetSkeleton } from '../sheet-skeleton';
import { RANGE_TYPE } from '../typedef';
import { createCoreTestBed } from './create-core-test-bed';
@@ -350,6 +350,50 @@ describe('SheetSkeleton integration', () => {
expect(skeleton.getColGapAtPosition(100)).toBe(-1);
});
it('should preserve viewport positions when decomposing offsets that land inside gaps', () => {
const testBed = createCoreTestBed(workbookDataFactory());
const injector = testBed.univer.__getInjector();
const worksheet = testBed.sheet.getActiveSheet()!;
const skeleton = new SheetSkeleton(
worksheet,
testBed.sheet.getStyles(),
injector.get(LocaleService),
injector.get(IContextService),
injector.get(IConfigService),
injector as Injector
).calculate()!;
disposables.push(() => testBed.univer.dispose());
skeleton.setGapConfig({
rowGaps: {
1: { size: 5 },
},
colGaps: {
2: { size: 7 },
},
});
const viewportScrollX = 195;
const viewportScrollY = 30;
const { row, column, rowOffset, columnOffset } = skeleton.getOffsetRelativeToRowCol(viewportScrollX, viewportScrollY);
const { startX, startY } = skeleton.getCellWithCoordByIndex(row, column, false);
expect({ row, column, rowOffset, columnOffset }).toEqual({
row: 1,
column: 2,
rowOffset: -3,
columnOffset: -4,
});
expect({
viewportScrollX: startX + columnOffset,
viewportScrollY: startY + rowOffset,
}).toEqual({
viewportScrollX,
viewportScrollY,
});
});
it('should keep provided gap default colors and return no-gap helpers when row/col gaps are absent', () => {
const testBed = createCoreTestBed(workbookDataFactory());
const injector = testBed.univer.__getInjector();
@@ -377,6 +421,52 @@ describe('SheetSkeleton integration', () => {
expect(skeleton.getColGapAtPosition(20)).toBe(-1);
});
it('should expose a reusable mixed gap test fixture', () => {
const testBed = createCoreTestBed(workbookDataFactory());
const injector = testBed.univer.__getInjector();
const worksheet = testBed.sheet.getActiveSheet()!;
const skeleton = new SheetSkeleton(
worksheet,
testBed.sheet.getStyles(),
injector.get(LocaleService),
injector.get(IContextService),
injector.get(IConfigService),
injector as Injector
).calculate()!;
disposables.push(() => testBed.univer.dispose());
skeleton.setGapConfig(createSheetGapTestConfig());
expect(skeleton.gapConfig.defaultBackgroundColor).toBe('rgba(24, 119, 242, 0.08)');
expect(skeleton.gapConfig.defaultStripeColor).toBe('rgba(24, 119, 242, 0.25)');
expect(skeleton.gapConfig.rowGaps?.[3]).toEqual({
size: 10,
color: 'rgba(245, 158, 11, 0.14)',
});
expect(skeleton.gapConfig.colGaps?.[4]).toEqual({
size: 12,
color: 'rgba(244, 63, 94, 0.12)',
stripeColor: 'rgba(225, 29, 72, 0.30)',
});
expect(skeleton.rowHeightAccumulation).toEqual([28, 58, 82, 116, 116, 140, 178, 202]);
expect(skeleton.columnWidthAccumulation).toEqual([72, 197, 277, 349, 361, 433]);
expect(skeleton.getRowGapAtPosition(30)).toBe(1);
expect(skeleton.getRowGapAtPosition(86)).toBe(3);
expect(skeleton.getRowGapAtPosition(145)).toBe(6);
expect(skeleton.getColGapAtPosition(74)).toBe(1);
expect(skeleton.getColGapAtPosition(200)).toBe(2);
expect(skeleton.getColGapAtPosition(355)).toBe(4);
expect(skeleton.getNoMergeCellWithCoordByIndex(3, 2, false)).toEqual({
startY: 92,
endY: 116,
startX: 205,
endX: 277,
});
});
it('should preserve provided one-sided gap color and fill the missing side from theme', () => {
const testBed = createCoreTestBed(workbookDataFactory());
const injector = testBed.univer.__getInjector();
+37 -12
View File
@@ -76,6 +76,39 @@ export interface ISheetGapConfig {
defaultBackgroundColor?: string;
}
/**
* Reusable gap fixture for visual and integration testing.
*/
export function createSheetGapTestConfig(overrides: Partial<ISheetGapConfig> = {}): ISheetGapConfig {
const baseConfig: ISheetGapConfig = {
defaultBackgroundColor: 'rgba(24, 119, 242, 0.08)',
defaultStripeColor: 'rgba(24, 119, 242, 0.25)',
rowGaps: {
1: { size: 6 },
3: { size: 10, color: 'rgba(245, 158, 11, 0.14)' },
6: { size: 14, color: 'rgba(16, 185, 129, 0.12)', stripeColor: 'rgba(5, 150, 105, 0.35)' },
},
colGaps: {
1: { size: 5 },
2: { size: 8, stripeColor: 'rgba(59, 130, 246, 0.35)' },
4: { size: 12, color: 'rgba(244, 63, 94, 0.12)', stripeColor: 'rgba(225, 29, 72, 0.30)' },
},
};
return {
...baseConfig,
...overrides,
rowGaps: {
...baseConfig.rowGaps,
...overrides.rowGaps,
},
colGaps: {
...baseConfig.colGaps,
...overrides.colGaps,
},
};
}
/**
* Optional gap size getter for coordinate calculation functions.
*/
@@ -1068,20 +1101,12 @@ export class SheetSkeleton extends Skeleton {
rowOffset: number;
} {
const column = searchArray(this.columnWidthAccumulation, offsetX);
let columnOffset = 0;
if (column === 0) {
columnOffset = offsetX;
} else {
columnOffset = offsetX - this._columnWidthAccumulation[column - 1];
}
const columnStart = (this._columnWidthAccumulation[column - 1] || 0) + this.getColGapSize(column);
const columnOffset = offsetX - columnStart;
const row = searchArray(this.rowHeightAccumulation, offsetY);
let rowOffset = 0;
if (row === 0) {
rowOffset = offsetY;
} else {
rowOffset = offsetY - this._rowHeightAccumulation[row - 1];
}
const rowStart = (this._rowHeightAccumulation[row - 1] || 0) + this.getRowGapSize(row);
const rowOffset = offsetY - rowStart;
return {
row,
column,
@@ -19,6 +19,7 @@ import type { IBoundRectNoAngle, IViewportInfo } from '../../../basics/vector2';
import {
BooleanNumber,
BorderStyleTypes,
createSheetGapTestConfig,
ILogService,
IUniverInstanceService,
LocaleType,
@@ -489,4 +490,54 @@ describe('spreadsheet integration', () => {
);
}).not.toThrow();
});
it('draws the reusable gap fixture with mixed default and item colors', () => {
const { spreadsheet, skeleton, mainCanvas } = fixture;
const context = mainCanvas.getContext() as any;
skeleton.setGapConfig(createSheetGapTestConfig());
const drawSingleGapRectSpy = vi.spyOn(spreadsheet as any, '_drawSingleGapRect');
(spreadsheet as any)._drawGapAreas(
context,
skeleton,
0,
7,
0,
5,
0,
500,
0,
320
);
expect(drawSingleGapRectSpy).toHaveBeenCalledTimes(6);
expect(drawSingleGapRectSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
size: 10,
color: 'rgba(245, 158, 11, 0.14)',
}),
expect.any(Number),
expect.any(Number),
expect.any(Number),
10,
'rgba(24, 119, 242, 0.08)',
'rgba(24, 119, 242, 0.25)'
);
expect(drawSingleGapRectSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
size: 8,
stripeColor: 'rgba(59, 130, 246, 0.35)',
}),
expect.any(Number),
expect.any(Number),
8,
expect.any(Number),
'rgba(24, 119, 242, 0.08)',
'rgba(24, 119, 242, 0.25)'
);
});
});
@@ -131,4 +131,44 @@ describe('SheetScrollManagerService', () => {
viewportScrollX: 337,
});
});
it('calculates viewport scroll from the actual cell start when gaps shift the origin', () => {
const injector = new Injector();
injector.add([
SheetSkeletonManagerService,
{
useValue: {
getCurrentSkeleton: () => ({
rowHeightAccumulation: [28, 57, 81],
columnWidthAccumulation: [72, 192, 271],
getCellWithCoordByIndex: (row: number, column: number, header?: boolean) => {
expect(header).toBe(false);
expect(row).toBe(1);
expect(column).toBe(2);
return {
startY: 33,
startX: 199,
};
},
}),
} as unknown as SheetSkeletonManagerService,
},
]);
const service = injector.createInstance(SheetScrollManagerService, createRenderContext('u-1'));
expect(service.calcViewportScrollFromRowColOffset({
sheetViewStartRow: 1,
sheetViewStartColumn: 2,
offsetX: -4,
offsetY: -3,
scrollX: 0,
scrollY: 0,
viewportScrollX: 0,
viewportScrollY: 0,
})).toEqual({
viewportScrollX: 195,
viewportScrollY: 30,
});
});
});
@@ -169,10 +169,10 @@ export class SheetScrollManagerService implements IRenderModule {
offsetY = offsetY || 0;
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
const rowAcc = skeleton?.rowHeightAccumulation[sheetViewStartRow - 1] || 0;
const colAcc = skeleton?.columnWidthAccumulation[sheetViewStartColumn - 1] || 0;
const viewportScrollX = colAcc + offsetX;
const viewportScrollY = rowAcc + offsetY;
const { startX: colStart = skeleton?.columnWidthAccumulation[sheetViewStartColumn - 1] || 0, startY: rowStart = skeleton?.rowHeightAccumulation[sheetViewStartRow - 1] || 0 } =
skeleton?.getCellWithCoordByIndex?.(sheetViewStartRow, sheetViewStartColumn, false) ?? {};
const viewportScrollX = colStart + offsetX;
const viewportScrollY = rowStart + offsetY;
return {
viewportScrollX,