Skip to content

Commit

Permalink
fix(scroll): 修复滚动条显示越界 & 优化滚动性能 (#1671)
Browse files Browse the repository at this point in the history
* fix(scroll): 修复滚动条显示越界 & 优化滚动性能

* chore: 增加测试和文档

* fix: 移除 log
  • Loading branch information
lijinke666 authored Aug 12, 2022
1 parent ba88dec commit cfbccb9
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 79 deletions.
93 changes: 88 additions & 5 deletions packages/s2-core/__tests__/spreadsheet/scroll-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ describe('Scroll Tests', () => {
// 模拟在行头滚动
jest
.spyOn(s2.facet, 'isScrollOverTheCornerArea')
.mockImplementation(() => true);
.mockImplementationOnce(() => true);
jest
.spyOn(s2.facet, 'isScrollOverTheViewport')
.mockImplementation(() => true);
.mockImplementationOnce(() => true);

const wheelEvent = new WheelEvent('wheel', {
deltaX: position.scrollX,
Expand Down Expand Up @@ -224,19 +224,19 @@ describe('Scroll Tests', () => {

const showHorizontalScrollBarSpy = jest
.spyOn(s2.facet, 'showHorizontalScrollBar')
.mockImplementation(() => {});
.mockImplementationOnce(() => {});

const showVerticalScrollBarSpy = jest
.spyOn(s2.facet, 'showVerticalScrollBar')
.mockImplementation(() => {});
.mockImplementationOnce(() => {});

// mock over the panel viewport
s2.facet.cornerBBox.maxY = -9999;
s2.facet.panelBBox.minX = -9999;
s2.facet.panelBBox.minY = -9999;
jest
.spyOn(s2.facet, 'isScrollOverTheViewport')
.mockImplementation(() => true);
.mockImplementationOnce(() => true);

const wheelEvent = new WheelEvent('wheel', {
deltaX: offset.scrollX,
Expand Down Expand Up @@ -423,6 +423,89 @@ describe('Scroll Tests', () => {
expect(s2.facet.vScrollBar.getCanvasBBox().x).toBe(994);
});

// https://github.com/antvis/S2/issues/1659
test.each([
{
name: 'hRowScrollBar',
key: 'width',
},
{
name: 'hScrollBar',
key: 'width',
},
{
name: 'vScrollBar',
key: 'height',
},
])('should render scroll bar real size by canvas BBox', ({ name, key }) => {
s2.changeSheetSize(200, 200); // 显示横/竖滚动条
s2.render(false);

const scrollBar = s2.facet[name];
expect(scrollBar.thumbShape.getBBox()[key]).toStrictEqual(
scrollBar.thumbLen,
);
});

test('should render scroll bar does not appear outside the canvas', () => {
s2.changeSheetSize(200, 200); // 显示横/竖滚动条
s2.render(false);

s2.updateScrollOffset({
offsetX: {
value: 999,
},
offsetY: {
value: 999,
},
});

const { hScrollBar, vScrollBar, cornerBBox, panelBBox } = s2.facet;
expect(
hScrollBar.thumbLen + hScrollBar.thumbOffset + cornerBBox.maxX,
).toStrictEqual(panelBBox.maxX);
expect(
vScrollBar.thumbLen + vScrollBar.thumbOffset + panelBBox.minY,
).toStrictEqual(panelBBox.maxY);
});

test('should trigger scroll if only contain row header scrollbar', async () => {
s2.setOptions({
frozenRowHeader: true,
style: {
layoutWidthType: 'compact',
rowCfg: {
width: 200,
},
},
});

const onRowCellScroll = jest.fn();

s2.changeSheetSize(400, 300);
s2.render(false);

jest
.spyOn(s2.facet, 'isScrollOverTheCornerArea')
.mockImplementationOnce(() => true);
jest
.spyOn(s2.facet, 'isScrollOverTheViewport')
.mockImplementationOnce(() => true);

s2.on(S2Event.ROW_CELL_SCROLL, onRowCellScroll);

const wheelEvent = new WheelEvent('wheel', {
deltaX: 20,
deltaY: 0,
});

canvas.dispatchEvent(wheelEvent);

await sleep(200);

expect(onRowCellScroll).toHaveBeenCalled();
});

describe('Scroll Overscroll Behavior Tests', () => {
const defaultOffset = {
scrollX: 10,
Expand Down
3 changes: 3 additions & 0 deletions packages/s2-core/src/common/interface/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export interface CellScrollPosition {
}

export interface CellScrollOffset {
deltaX?: number;
deltaY?: number;
offset?: number;
offsetX: number;
offsetY: number;
}
70 changes: 46 additions & 24 deletions packages/s2-core/src/facet/base-facet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,19 +537,25 @@ export abstract class BaseFacet {
) {
const maxOffset = this.cornerBBox.originalWidth - this.cornerBBox.width;
const { maxY } = this.getScrollbarPosition();
const thumbLen =
const { verticalBorderWidth } = this.spreadsheet.theme.splitLine;

const thumbSize =
(this.cornerBBox.width * this.cornerBBox.width) /
this.cornerBBox.originalWidth;

// 行头有分割线, 滚动条应该预留分割线的宽度
const displayThumbSize = thumbSize - verticalBorderWidth;

this.hRowScrollBar = new ScrollBar({
isHorizontal: true,
trackLen: this.cornerBBox.width,
thumbLen,
thumbLen: displayThumbSize,
position: {
x: this.cornerBBox.minX + this.scrollBarSize / 2,
y: maxY,
},
thumbOffset:
(rowScrollX * (this.cornerBBox.width - thumbLen)) / maxOffset,
(rowScrollX * (this.cornerBBox.width - thumbSize)) / maxOffset,
theme: this.scrollBarTheme,
scrollTargetMaxOffset: maxOffset,
});
Expand Down Expand Up @@ -594,7 +600,6 @@ export abstract class BaseFacet {
renderHScrollBar = (width: number, realWidth: number, scrollX: number) => {
if (Math.floor(width) < Math.floor(realWidth)) {
const halfScrollSize = this.scrollBarSize / 2;

const { maxY } = this.getScrollbarPosition();
const finalWidth =
width +
Expand Down Expand Up @@ -622,7 +627,6 @@ export abstract class BaseFacet {
isHorizontal: true,
trackLen: finalWidth,
thumbLen,
// position: this.viewport.bl,
position: finalPosition,
thumbOffset: (scrollX * (finalWidth - thumbLen)) / maxOffset,
theme: this.scrollBarTheme,
Expand Down Expand Up @@ -730,14 +734,22 @@ export abstract class BaseFacet {
);
};

updateHorizontalRowScrollOffset = ({ offset, offsetX, offsetY }) => {
updateHorizontalRowScrollOffset = ({
offset,
offsetX,
offsetY,
}: CellScrollOffset) => {
// 在行头区域滚动时 才更新行头水平滚动条
if (this.isScrollOverTheCornerArea({ offsetX, offsetY })) {
this.hRowScrollBar?.emitScrollChange(offset);
}
};

updateHorizontalScrollOffset = ({ offset, offsetX, offsetY }) => {
updateHorizontalScrollOffset = ({
offset,
offsetX,
offsetY,
}: CellScrollOffset) => {
// 1.行头没有滚动条 2.在数值区域滚动时 才更新数值区域水平滚动条
if (
!this.hRowScrollBar ||
Expand All @@ -747,21 +759,24 @@ export abstract class BaseFacet {
}
};

isScrollToLeft = (deltaX: number) => {
if (!this.hScrollBar) {
isScrollToLeft = ({ deltaX, offsetX, offsetY }: CellScrollOffset) => {
if (!this.hScrollBar && !this.hRowScrollBar) {
return true;
}

const isScrollRowHeaderToLeft =
!this.hRowScrollBar || this.hRowScrollBar.thumbOffset <= 0;
!this.hRowScrollBar ||
this.isScrollOverThePanelArea({ offsetY, offsetX }) ||
this.hRowScrollBar.thumbOffset <= 0;

const isScrollPanelToLeft = deltaX <= 0 && this.hScrollBar.thumbOffset <= 0;
const isScrollPanelToLeft =
!this.hScrollBar || this.hScrollBar.thumbOffset <= 0;

return isScrollPanelToLeft && isScrollRowHeaderToLeft;
return deltaX <= 0 && isScrollPanelToLeft && isScrollRowHeaderToLeft;
};

isScrollToRight = (deltaX: number) => {
if (!this.hScrollBar) {
isScrollToRight = ({ deltaX, offsetX, offsetY }: CellScrollOffset) => {
if (!this.hScrollBar && !this.hRowScrollBar) {
return true;
}

Expand All @@ -771,10 +786,13 @@ export abstract class BaseFacet {

const isScrollRowHeaderToRight =
!this.hRowScrollBar ||
this.isScrollOverThePanelArea({ offsetY, offsetX }) ||
this.hRowScrollBar.thumbOffset + this.hRowScrollBar.thumbLen >=
this.cornerBBox.width;

const isScrollPanelToRight =
(this.hRowScrollBar &&
this.isScrollOverTheCornerArea({ offsetX, offsetY })) ||
this.hScrollBar.thumbOffset + this.hScrollBar.thumbLen >= viewportWidth;

return deltaX >= 0 && isScrollPanelToRight && isScrollRowHeaderToRight;
Expand Down Expand Up @@ -802,8 +820,10 @@ export abstract class BaseFacet {
return !this.isScrollToTop(deltaY) && !this.isScrollToBottom(deltaY);
};

isHorizontalScrollOverTheViewport = (deltaX: number) => {
return !this.isScrollToLeft(deltaX) && !this.isScrollToRight(deltaX);
isHorizontalScrollOverTheViewport = (scrollOffset: CellScrollOffset) => {
return (
!this.isScrollToLeft(scrollOffset) && !this.isScrollToRight(scrollOffset)
);
};

/**
Expand All @@ -813,11 +833,8 @@ export abstract class BaseFacet {
- 未滚动到顶部或底部: 当前表格滚动, 阻止外部容器滚动
- 滚动到顶部或底部: 恢复外部容器滚动
*/
isScrollOverTheViewport = (
deltaX: number,
deltaY: number,
offsetY: number,
) => {
isScrollOverTheViewport = (scrollOffset: CellScrollOffset) => {
const { deltaY, deltaX, offsetY } = scrollOffset;
const isScrollOverTheHeader = offsetY <= this.cornerBBox.maxY;
// 光标在角头或列头时, 不触发表格自身滚动
if (isScrollOverTheHeader) {
Expand All @@ -827,7 +844,7 @@ export abstract class BaseFacet {
return this.isVerticalScrollOverTheViewport(deltaY);
}
if (deltaX !== 0) {
return this.isHorizontalScrollOverTheViewport(deltaX);
return this.isHorizontalScrollOverTheViewport(scrollOffset);
}
return false;
};
Expand Down Expand Up @@ -881,7 +898,12 @@ export abstract class BaseFacet {
this.spreadsheet.interaction.clearHoverTimer();

if (
!this.isScrollOverTheViewport(optimizedDeltaX, optimizedDeltaY, offsetY)
!this.isScrollOverTheViewport({
deltaX: optimizedDeltaX,
deltaY: optimizedDeltaY,
offsetX,
offsetY,
})
) {
this.stopScrollChainingIfNeeded(event);
return;
Expand Down Expand Up @@ -927,7 +949,7 @@ export abstract class BaseFacet {
};

protected clip(scrollX: number, scrollY: number) {
const isFrozenRowHeader = this.cfg.spreadsheet.isFrozenRowHeader();
const isFrozenRowHeader = this.spreadsheet.isFrozenRowHeader();
this.spreadsheet.panelScrollGroup?.setClip({
type: 'rect',
attrs: {
Expand Down
Loading

1 comment on commit cfbccb9

@vercel
Copy link

@vercel vercel bot commented on cfbccb9 Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

antvis-s2 – ./

antvis-s2-git-master-antv-s2.vercel.app
antvis-s2.vercel.app
antvis-s2-antv-s2.vercel.app

Please sign in to comment.