Skip to content

Commit

Permalink
feat(ScrollLock): export useScroll to scroll locked layout (#8137)
Browse files Browse the repository at this point in the history
* feat(ScrollContext): add ability to manual scroll window when scroll locked

* feat(ScrollContext): add tests to new logic, and export hook `useScrollingLockedScroll`

* fix: fix test

* test: add tests

* fix: remove throw Error

* fix: rename useScrollingLockedScroll to useManualScroll and export as useScroll

* fix(ScrollContext): add manual scroll to ElementScrollController
  • Loading branch information
EldarMuhamethanov authored Feb 12, 2025
1 parent 67b998d commit aed03e0
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 58 deletions.
181 changes: 171 additions & 10 deletions packages/vkui/src/components/AppRoot/ScrollContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe(useScrollLock, () => {

clearWindowMeasuresMock();
});

test('unmount check', () => {
const h = renderHook(useScrollLock, {
wrapper: ({ children }) => (
Expand All @@ -101,32 +100,93 @@ describe(useScrollLock, () => {
expect(jestWorkaroundGetOverscrollBehaviorPropertyValue(document.documentElement)).toBe('');
});

test('context api', () => {
test.each([true, false])('context api with locked=%s', (locked) => {
const contextRef = createRef<ScrollContextInterface>();
render(
const Fixture = () => (
<GlobalScrollController elRef={createRef<HTMLElement>()}>
<ChildWithContext contextRef={contextRef} />
</GlobalScrollController>,
</GlobalScrollController>
);

const { rerender } = render(<Fixture />);

const clearWindowMeasuresMock = mockWindowMeasures(50, 50);
const clearElementScrollMock = mockElementScroll(document.body, 100, 100);
const clearMockWindowScrollToMock = mockWindowScrollTo();

if (locked) {
contextRef.current?.incrementScrollLockCounter();
rerender(<Fixture />);
}

expect(contextRef.current?.getScroll()).toEqual({ x: 0, y: 50 });
expect(contextRef.current?.getScroll({ compensateKeyboardHeight: false })).toEqual({
x: 0,
y: 0,
});
contextRef.current?.scrollTo(10, 10);
expect(contextRef.current?.getScroll()).toEqual({ x: 10, y: 60 });

if (locked) {
expect(getPositionOfBody()).toEqual([`-${10}px`, `-${10}px`]);
expect(window.pageYOffset).toBe(0);
} else {
expect(window.pageYOffset).toBe(10);
}

contextRef.current?.scrollTo();
expect(contextRef.current?.getScroll()).toEqual({ x: 0, y: 50 });

clearWindowMeasuresMock();
clearElementScrollMock();
clearMockWindowScrollToMock();
});

test('scroll when not locked and then when locked', () => {
const contextRef = createRef<ScrollContextInterface>();
const Fixture = () => (
<GlobalScrollController elRef={createRef<HTMLElement>()}>
<ChildWithContext contextRef={contextRef} />
</GlobalScrollController>
);

const { rerender } = render(<Fixture />);

const clearWindowMeasuresMock = mockWindowMeasures(50, 50);
const clearElementScrollMock = mockElementScroll(document.body, 100, 100);
const clearMockWindowScrollToMock = mockWindowScrollTo();

// Скролим не залоченный скролл
contextRef.current?.scrollTo(10, 10);
expect(window.pageYOffset).toBe(10);
expect(window.pageXOffset).toBe(10);

// Блокируем скролл
contextRef.current?.incrementScrollLockCounter();
rerender(<Fixture />);

// Блокируем скролл - отступы остаются те же
expect(window.pageYOffset).toBe(10);
expect(window.pageXOffset).toBe(10);
expect(getPositionOfBody()).toEqual([`-${10}px`, `-${10}px`]);

// Скролим залоченный скролл
contextRef.current?.scrollTo(25, 25);

expect(getPositionOfBody()).toEqual([`-${25}px`, `-${25}px`]);

// Выключаем блокировку скролла
contextRef.current?.decrementScrollLockCounter();
rerender(<Fixture />);

// Отступы window должны пересчитаться
expect(window.pageYOffset).toBe(25);
expect(window.pageXOffset).toBe(25);

clearWindowMeasuresMock();
clearElementScrollMock();
clearMockWindowScrollToMock();
});
});

describe(ElementScrollController, () => {
Expand Down Expand Up @@ -227,6 +287,97 @@ describe(useScrollLock, () => {
contextRef.current?.scrollTo();
expect(contextRef.current?.getScroll()).toEqual({ x: 0, y: 0 });
});

test.each([true, false])('context api with locked=%s', (locked) => {
const contextRef = createRef<ScrollContextInterface>();
const elRef = createRef<HTMLElement>();
setRef(document.createElement('div'), elRef);
const Fixture = () => (
<ElementScrollController elRef={elRef}>
<ChildWithContext contextRef={contextRef} />
</ElementScrollController>
);

const { rerender } = render(<Fixture />);

const clearElementMeasuresMock = mockElementMeasures(elRef.current!, 50, 50);
const clearElementScrollMock = mockElementScroll(elRef.current!, 100, 100);
const clearElementScrollToMock = mockElementScrollTo(elRef.current!);

if (locked) {
contextRef.current?.incrementScrollLockCounter();
rerender(<Fixture />);
}

expect(contextRef.current?.getScroll()).toEqual({
x: 0,
y: 0,
});
contextRef.current?.scrollTo(10, 10);
expect(contextRef.current?.getScroll()).toEqual({ x: 10, y: 10 });

if (locked) {
expect(getPositionOfElement(elRef.current)).toEqual([`-${10}px`, `-${10}px`]);
expect(elRef.current?.scrollTop).toBe(0);
} else {
expect(elRef.current?.scrollTop).toBe(10);
}

contextRef.current?.scrollTo();
expect(contextRef.current?.getScroll()).toEqual({ x: 0, y: 0 });

clearElementMeasuresMock();
clearElementScrollMock();
clearElementScrollToMock();
});

test('scroll when not locked and then when locked', () => {
const contextRef = createRef<ScrollContextInterface>();
const elRef = createRef<HTMLElement>();
setRef(document.createElement('div'), elRef);
const Fixture = () => (
<ElementScrollController elRef={elRef}>
<ChildWithContext contextRef={contextRef} />
</ElementScrollController>
);

const { rerender } = render(<Fixture />);

const clearElementMeasuresMock = mockElementMeasures(elRef.current!, 50, 50);
const clearElementScrollMock = mockElementScroll(elRef.current!, 100, 100);
const clearElementScrollToMock = mockElementScrollTo(elRef.current!);

// Скролим не залоченный скролл
contextRef.current?.scrollTo(10, 10);
expect(elRef.current?.scrollLeft).toBe(10);
expect(elRef.current?.scrollTop).toBe(10);

// Блокируем скролл
contextRef.current?.incrementScrollLockCounter();
rerender(<Fixture />);

// Блокируем скролл - отступы остаются те же
expect(elRef.current?.scrollLeft).toBe(10);
expect(elRef.current?.scrollTop).toBe(10);
expect(getPositionOfElement(elRef.current)).toEqual([`-${10}px`, `-${10}px`]);

// Скролим залоченный скролл
contextRef.current?.scrollTo(25, 25);

expect(getPositionOfElement(elRef.current)).toEqual([`-${25}px`, `-${25}px`]);

// Выключаем блокировку скролла
contextRef.current?.decrementScrollLockCounter();
rerender(<Fixture />);

// Отступы window должны пересчитаться
expect(elRef.current?.scrollLeft).toBe(25);
expect(elRef.current?.scrollTop).toBe(25);

clearElementMeasuresMock();
clearElementScrollMock();
clearElementScrollToMock();
});
});
});

Expand Down Expand Up @@ -258,6 +409,16 @@ function getStyleAttributeObject(el: HTMLElement | null) {
);
}

function getPositionOfBody() {
const styles = getStyleAttributeObject(document.body);
return styles && [styles.left, styles.top];
}

function getPositionOfElement(element: HTMLElement | null) {
const styles = element && getStyleAttributeObject(element);
return styles && [styles.left, styles.top];
}

function mockWindowMeasures(width: number, height: number) {
const originalW = window.innerWidth;
const originalH = window.innerHeight;
Expand All @@ -273,9 +434,9 @@ function mockWindowScrollTo() {
const original = window.scrollTo;
Object.defineProperty(window, 'scrollTo', {
configurable: true,
value: (x: number, y: number) => {
Object.defineProperty(window, 'pageXOffset', { configurable: true, value: x });
Object.defineProperty(window, 'pageYOffset', { configurable: true, value: y });
value: ({ left, top }: { left: number; top: number }) => {
Object.defineProperty(window, 'pageXOffset', { configurable: true, value: left });
Object.defineProperty(window, 'pageYOffset', { configurable: true, value: top });
},
});
return function clearMock() {
Expand Down Expand Up @@ -309,9 +470,9 @@ function mockElementScrollTo(el: HTMLElement) {
const original = el.scrollTo.bind(el);
Object.defineProperty(el, 'scrollTo', {
configurable: true,
value: (x: number, y: number) => {
Object.defineProperty(el, 'scrollLeft', { configurable: true, value: x });
Object.defineProperty(el, 'scrollTop', { configurable: true, value: y });
value: ({ left, top }: { left: number; top: number }) => {
Object.defineProperty(el, 'scrollLeft', { configurable: true, value: left });
Object.defineProperty(el, 'scrollTop', { configurable: true, value: top });
},
});
return function clearMock() {
Expand Down
Loading

0 comments on commit aed03e0

Please sign in to comment.