Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@react-aria/dnd/stories/VirtualizedListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ React.forwardRef(function (props: any, ref) {
<Context.Provider value={{state, dropState}}>
<Virtualizer
{...mergeProps(collectionProps, listBoxProps)}
onScroll={undefined}
Copy link
Member

Choose a reason for hiding this comment

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

do we need these? At a glance the Virtualizer type has this as optional

Copy link
Member Author

Choose a reason for hiding this comment

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

there was a type error because collectionProps and listBoxProps have HTMLAttributes, which uses React's onScroll type, which is a UIEvent. But Virtualizer needs the native scroll event, which is just Event (idk why React is different). This was not caught before because the onScroll listener inside ScrollView was untyped (e was implicitly any).

Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC, this could be a breaking change in the way its handled here? Before, we invoked the callback twice actually, once with a native event and once in our callback. Now the user wouldn't be receiving a UIEvent instance anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

this was already true but was not correctly reflected in the types

Copy link
Contributor

@nwidynski nwidynski Mar 12, 2026

Choose a reason for hiding this comment

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

That's not what I meant. Your change to useScrollView L74 pulls this prop out from otherProps, where as before it was both passthrough'd AND invoked via props.onScroll. This means a user relying on receiving a UIEvent wont do so anymore after this change.

I specifically excluded onScroll from being passed to useScrollView in #9115 because of that. Or what am I missing? Why was this already true?

Copy link
Member Author

Choose a reason for hiding this comment

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

oh that's true but I checked that collectionProps and listBoxProps do not have an onScroll handler

Copy link
Contributor

@nwidynski nwidynski Mar 13, 2026

Choose a reason for hiding this comment

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

That's true, but someone may be using react-arias Virtualizer or ScrollView independently. In any way, I just wanted to raise the issue because I remembered being bummed about having to make that backwards compatible 😅. It might not be a big deal to break, idk.

ref={domRef}
className={classNames(dndStyles, 'droppable-collection', 'is-virtualized', {'is-drop-target': isDropTarget})}
scrollDirection="vertical"
Expand Down
6 changes: 2 additions & 4 deletions packages/@react-aria/utils/src/scrollIntoView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {getScrollParents} from './getScrollParents';
import {isChrome, isIOS} from './platform';
import {isIOS} from './platform';

interface ScrollIntoViewOpts {
/** The position to align items along the block axis in. */
Expand Down Expand Up @@ -133,9 +133,7 @@ export function scrollIntoViewport(targetElement: Element | null, opts: ScrollIn
if (targetElement && targetElement.isConnected) {
let root = document.scrollingElement || document.documentElement;
let isScrollPrevented = window.getComputedStyle(root).overflow === 'hidden';
// If scrolling is not currently prevented then we aren't in a overlay nor is a overlay open, just use element.scrollIntoView to bring the element into view
// Also ignore in chrome because of this bug: https://issues.chromium.org/issues/40074749
if (!isScrollPrevented && !isChrome()) {
if (!isScrollPrevented) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixes scroll into view when table is taller than the viewport. Underlying chrome bug is now fixed: https://issues.chromium.org/issues/40074749

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, why did the shim not work correctly there? Could maybe be addressed in #9780?

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't dive too deep into it, but I think it's the same issue Daniel was addressing.

let {left: originalLeft, top: originalTop} = targetElement.getBoundingClientRect();

// use scrollIntoView({block: 'nearest'}) instead of .focus to check if the element is fully in view or not since .focus()
Expand Down
120 changes: 90 additions & 30 deletions packages/@react-aria/virtualizer/src/ScrollView.tsx
Copy link
Member

Choose a reason for hiding this comment

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

I noticed that resizing the window at all now breaks the S2 Table rendering. Haven't pulled it down locally, but I remember there were issues in the past with when to call updateSizeEvent

Image

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, I was creating a Rect instead of a Size and typescript wasn't catching it. 🤦

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

// @ts-ignore
import {flushSync} from 'react-dom';
import {getEventTarget, useEffectEvent, useEvent, useLayoutEffect, useObjectRef, useResizeObserver} from '@react-aria/utils';
import {getEventTarget, nodeContains, useEffectEvent, useLayoutEffect, useObjectRef, useResizeObserver} from '@react-aria/utils';
import {getScrollLeft} from './utils';
import {Point, Rect, Size} from '@react-stately/virtualizer';
import React, {
CSSProperties,
ForwardedRef,
Expand All @@ -25,17 +26,17 @@ import React, {
useRef,
useState
} from 'react';
import {Rect, Size} from '@react-stately/virtualizer';
import {useLocale} from '@react-aria/i18n';

interface ScrollViewProps extends HTMLAttributes<HTMLElement> {
interface ScrollViewProps extends Omit<HTMLAttributes<HTMLElement>, 'onScroll'> {
contentSize: Size,
onVisibleRectChange: (rect: Rect) => void,
children?: ReactNode,
innerStyle?: CSSProperties,
onScrollStart?: () => void,
onScrollEnd?: () => void,
scrollDirection?: 'horizontal' | 'vertical' | 'both'
scrollDirection?: 'horizontal' | 'vertical' | 'both',
onScroll?: (e: Event) => void
}

function ScrollView(props: ScrollViewProps, ref: ForwardedRef<HTMLDivElement | null>) {
Expand Down Expand Up @@ -70,39 +71,76 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
onScrollStart,
onScrollEnd,
scrollDirection = 'both',
onScroll: onScrollProp,
...otherProps
} = props;

let state = useRef({
scrollTop: 0,
scrollLeft: 0,
// Internal scroll position of the scroll view.
scrollPosition: new Point(),
// Size of the scroll view.
size: new Size(),
// Offset of the scroll view relative to the window viewport.
viewportOffset: new Point(),
// Size of the window viewport.
viewportSize: new Size(),
scrollEndTime: 0,
scrollTimeout: null as ReturnType<typeof setTimeout> | null,
width: 0,
height: 0,
isScrolling: false
}).current;
let {direction} = useLocale();

let updateVisibleRect = useCallback(() => {
// Intersect the window viewport with the scroll view itself to find the actual visible rectangle.
// This allows virtualized components to have unbounded height but still virtualize when scrolled with the page.
Copy link
Member

Choose a reason for hiding this comment

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

oo, no more required explicit height, nice!
I assume it applies to widths as well

Copy link
Member

Choose a reason for hiding this comment

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

@LFDanLu I think this should simplify our tests as well, we can more easily mock the virtualizer size vs the item sizes

Copy link
Contributor

@nwidynski nwidynski Mar 12, 2026

Choose a reason for hiding this comment

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

Btw, the VIRT_ON flag breaks in certain scenarios. I addressed these cases with further improvements specific to virtualizer in test environments within #9115.

// While there may be other scrollable elements between the <body> and the scroll view, we do not take
// their sizes into account for performance reasons. Their scroll positions are accounted for in viewportOffset
// though (due to getBoundingClientRect). This may result in more rows than absolutely necessary being rendered,
// but no more than the entire height of the viewport which is good enough for virtualization use cases.
let visibleRect = new Rect(
state.viewportOffset.x + state.scrollPosition.x,
state.viewportOffset.y + state.scrollPosition.y,
Math.max(0, Math.min(state.size.width - state.viewportOffset.x, state.viewportSize.width)),
Copy link
Member

Choose a reason for hiding this comment

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

when is this negative?

Copy link
Member Author

Choose a reason for hiding this comment

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

viewportOffset may be greater than the size of the scrollview (when it is entirely scrolled out of view)

Math.max(0, Math.min(state.size.height - state.viewportOffset.y, state.viewportSize.height))
);
onVisibleRectChange(visibleRect);
}, [state, onVisibleRectChange]);

let [isScrolling, setScrolling] = useState(false);

let onScroll = useCallback((e) => {
if (getEventTarget(e) !== e.currentTarget) {
let onScroll = useCallback((e: Event) => {
let target = getEventTarget(e) as Element;
if (!nodeContains(target, ref.current!)) {
return;
}

if (props.onScroll) {
props.onScroll(e);
if (onScrollProp && target === ref.current) {
onScrollProp(e);
}

flushSync(() => {
let scrollTop = e.currentTarget.scrollTop;
let scrollLeft = getScrollLeft(e.currentTarget, direction);
if (target !== ref.current) {
// An ancestor element or the window was scrolled. Update the position of the scroll view relative to the viewport.
let boundingRect = ref.current!.getBoundingClientRect();
let x = boundingRect.x < 0 ? -boundingRect.x : 0;
let y = boundingRect.y < 0 ? -boundingRect.y : 0;
if (x === state.viewportOffset.x && y === state.viewportOffset.y) {
return;
}

state.viewportOffset = new Point(x, y);
} else {
// The scroll view itself was scrolled. Update the local scroll position.
// Prevent rubber band scrolling from shaking when scrolling out of bounds
state.scrollTop = Math.max(0, Math.min(scrollTop, contentSize.height - state.height));
state.scrollLeft = Math.max(0, Math.min(scrollLeft, contentSize.width - state.width));
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height));
let scrollTop = target.scrollTop;
let scrollLeft = getScrollLeft(target, direction);
state.scrollPosition = new Point(
Math.max(0, Math.min(scrollLeft, contentSize.width - state.size.width)),
Math.max(0, Math.min(scrollTop, contentSize.height - state.size.height))
);
Comment on lines +135 to +139
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI, this logic does not work in various edge cases. See #9115 for changes done to getScrollOffset.ts and getOffsetType.ts.

Copy link
Member Author

Choose a reason for hiding this comment

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

the logic has not changed in this PR, but what are the cases? I couldn't easily tell from the PR.

Copy link
Contributor

@nwidynski nwidynski Mar 13, 2026

Choose a reason for hiding this comment

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

Clamping needs to be aware of the offset type, due to overscroll. I moved clamping into getScrollLeft and also added the same for getScrollTop, because of flex: <row/col>-reverse.

For example, in RTL layouts, we can get a negative float here, due to overscroll at the lower boundary.

Lastly, I noted that we probably should be more strict about these utilities being used everywhere we access or set scroll offsets. Often times there are subtle bugs introduced because of not doing so. One of the reasons why moving scroll observation into utils makes sense imo 😉

}

flushSync(() => {
updateVisibleRect();

if (!state.isScrolling) {
state.isScrolling = true;
Expand Down Expand Up @@ -138,10 +176,13 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
}, 300);
}
});
}, [props, direction, state, contentSize, onVisibleRectChange, onScrollStart, onScrollEnd]);
}, [onScrollProp, ref, direction, state, contentSize, updateVisibleRect, onScrollStart, onScrollEnd]);

// Attach event directly to ref so RAC Virtualizer doesn't need to send props upward.
useEvent(ref, 'scroll', onScroll);
// Attach a document-level capturing scroll listener so we can account for scrollable ancestors.
useEffect(() => {
document.addEventListener('scroll', onScroll, true);
return () => document.removeEventListener('scroll', onScroll, true);
Copy link
Member Author

Choose a reason for hiding this comment

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

Using a capturing scroll listener seems better than attaching scroll listeners to all ancestor elements, but does mean we will receive events for potentially unrelated elements. The nodeContains check inside onScroll should account for this and quickly return early, but we should do some performance testing here.

}, [onScroll]);

useEffect(() => {
return () => {
Expand Down Expand Up @@ -175,11 +216,18 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
let w = isTestEnv && !isClientWidthMocked ? Infinity : clientWidth;
let h = isTestEnv && !isClientHeightMocked ? Infinity : clientHeight;

if (state.width !== w || state.height !== h) {
state.width = w;
state.height = h;
// Update the window viewport size.
let viewportWidth = window.innerWidth;
let viewportHeight = window.innerHeight;
let viewportSizeChanged = state.viewportSize.width !== viewportWidth || state.viewportSize.height !== viewportHeight;
if (viewportSizeChanged) {
state.viewportSize = new Size(viewportWidth, viewportHeight);
}

if (state.size.width !== w || state.size.height !== h || viewportSizeChanged) {
state.size = new Size(w, h);
flush(() => {
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, w, h));
updateVisibleRect();
});

// If the clientWidth or clientHeight changed, scrollbars appeared or disappeared as
Expand All @@ -188,18 +236,30 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
// again, resulting in extra padding. We stop after a maximum of two layout passes to avoid
// an infinite loop. This matches how browsers behavior with native CSS grid layout.
if (!isTestEnv && clientWidth !== dom.clientWidth || clientHeight !== dom.clientHeight) {
state.width = dom.clientWidth;
state.height = dom.clientHeight;
state.size = new Size(dom.clientWidth, dom.clientHeight);
flush(() => {
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height));
updateVisibleRect();
});
}
}

isUpdatingSize.current = false;
}, [ref, state, onVisibleRectChange]);
}, [ref, state, updateVisibleRect]);
let updateSizeEvent = useEffectEvent(updateSize);

// Track the size of the entire window viewport, which is used to bound the size of the virtualizer's visible rectangle.
useLayoutEffect(() => {
// Initialize viewportRect before updating size for the first time.
state.viewportSize = new Size(window.innerWidth, window.innerHeight);
Copy link
Contributor

@nwidynski nwidynski Mar 12, 2026

Choose a reason for hiding this comment

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

This includes scrollbars. We might want to use document.scrollingElement.clientWidth/Height? Or was there specific intent to keep it tied to the window object for test environments?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's simpler to mock, and accuracy is not really important here because this is simply an upper-bound on the number of items to display.


let onWindowResize = () => {
updateSizeEvent(flushSync);
};

window.addEventListener('resize', onWindowResize);
return () => window.removeEventListener('resize', onWindowResize);
}, [state]);

// Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
let lastContentSize = useRef<Size | null>(null);
let [update, setUpdate] = useState({});
Expand Down Expand Up @@ -250,7 +310,7 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
if (scrollDirection === 'horizontal') {
style.overflowX = 'auto';
style.overflowY = 'hidden';
} else if (scrollDirection === 'vertical' || contentSize.width === state.width) {
} else if (scrollDirection === 'vertical' || contentSize.width === state.size.width) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated, but a question I raised in #9115. Do you still happen to carry reproduction steps for this happening? I couldn't see this issue in my testing after refactoring.

Copy link
Member Author

Choose a reason for hiding this comment

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

looks like it was added in #5380

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I found the specific comment too, but could not reproduce the flickering that prompted this change anymore. @LFDanLu Do you still have notes on the specific reproduction steps?

Copy link
Member

Choose a reason for hiding this comment

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

oof, I do not unfortunately, but if memory serves it would happen when stepping through various browser zoom levels.

// Set overflow-x: hidden if content size is equal to the width of the scroll view.
// This prevents horizontal scrollbars from flickering during resizing due to resize observer
// firing slower than the frame rate, which may cause an infinite re-render loop.
Expand Down
5 changes: 3 additions & 2 deletions packages/@react-aria/virtualizer/src/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type RenderWrapper<T extends object, V> = (
renderChildren: (views: ReusableView<T, V>[]) => ReactElement[]
) => ReactElement | null;

interface VirtualizerProps<T extends object, V, O> extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
interface VirtualizerProps<T extends object, V, O> extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'onScroll'> {
children: (type: string, content: T) => V,
renderWrapper?: RenderWrapper<T, V>,
layout: Layout<T, O>,
Expand All @@ -33,7 +33,8 @@ interface VirtualizerProps<T extends object, V, O> extends Omit<HTMLAttributes<H
scrollDirection?: 'horizontal' | 'vertical' | 'both',
isLoading?: boolean,
onLoadMore?: () => void,
layoutOptions?: O
layoutOptions?: O,
onScroll?: (e: Event) => void
}

// forwardRef doesn't support generic parameters, so cast the result to the correct type
Expand Down
8 changes: 7 additions & 1 deletion packages/@react-spectrum/card/test/CardView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,13 @@ function DynamicCardView(props) {
}

describe('CardView', function () {
let user;
let user, innerHeight, innerWidth;
beforeAll(function () {
user = userEvent.setup({delay: null, pointerMap});
innerHeight = window.innerHeight;
innerWidth = window.innerWidth;
Object.defineProperty(window, 'innerHeight', {value: 1000, configurable: true, writable: true});
Object.defineProperty(window, 'innerWidth', {value: 1000, configurable: true, writable: true});
jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => mockWidth);
jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => mockHeight);
jest.useFakeTimers();
Expand All @@ -154,6 +158,8 @@ describe('CardView', function () {
});

afterAll(function () {
window.innerHeight = innerHeight;
window.innerWidth = innerWidth;
jest.restoreAllMocks();
});

Expand Down
1 change: 1 addition & 0 deletions packages/@react-spectrum/list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export const ListView = React.forwardRef(function ListView<T extends object>(pro
{...filterDOMProps(otherProps)}
{...gridProps}
{...styleProps}
onScroll={undefined}
isLoading={isLoading}
onLoadMore={onLoadMore}
ref={domRef}
Expand Down
7 changes: 7 additions & 0 deletions packages/@react-spectrum/list/test/ListView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ describe('ListView', function () {
manyItems.push({id: i, label: 'Foo ' + i});
}

let innerHeight, innerWidth;
beforeAll(function () {
user = userEvent.setup({delay: null, pointerMap});
innerHeight = window.innerHeight;
innerWidth = window.innerWidth;
Object.defineProperty(window, 'innerHeight', {value: 1000, configurable: true, writable: true});
Object.defineProperty(window, 'innerWidth', {value: 1000, configurable: true, writable: true});
offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000);
offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000);
scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => 40);
Expand All @@ -78,6 +83,8 @@ describe('ListView', function () {
});

afterAll(function () {
window.innerHeight = innerHeight;
window.innerWidth = innerWidth;
offsetWidth.mockReset();
offsetHeight.mockReset();
scrollHeight.mockReset();
Expand Down
47 changes: 43 additions & 4 deletions packages/@react-spectrum/s2/stories/TableView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -799,9 +799,9 @@ for (let i = 0; i < 1000; i++) {
manyRows.push(row);
}

export const ManyItems: StoryObj<typeof TableView> = {
render: (args) => (
<TableView aria-label="Many items table" {...args} styles={style({width: 800, height: 400})}>
function ManyItemsTable(args) {
return (
<TableView aria-label="Many items table" {...args}>
<TableHeader columns={manyColumns}>
{(column) => (
<Column width={100} minWidth={100} isRowHeader={column.name === 'Column 1'}>{column.name}</Column>
Expand All @@ -817,7 +817,11 @@ export const ManyItems: StoryObj<typeof TableView> = {
)}
</TableBody>
</TableView>
),
);
}

export const ManyItems: StoryObj<typeof TableView> = {
render: (args) => <ManyItemsTable {...args} styles={style({width: 800, height: 400})} />,
args: {
...Example.args
},
Expand All @@ -829,6 +833,41 @@ export const ManyItems: StoryObj<typeof TableView> = {
}
};

export const ViewportScrolling: StoryObj<typeof TableView> = {
render: (args) => (
<>
<ManyItemsTable {...args} styles={style({width: 800})} />
<div style={{height: 900}} />
</>
),
args: {
...Example.args
},
name: 'viewport scrolling',
parameters: {
docs: {
disable: true
}
}
};

export const ScrollableContainer: StoryObj<typeof TableView> = {
render: (args) => (
<div style={{width: 800, height: 500, overflow: 'auto'}}>
<ManyItemsTable {...args} styles={style({width: 'full'})} />
</div>
),
args: {
...Example.args
},
name: 'scrollable container',
parameters: {
docs: {
disable: true
}
}
};

export const FlexHeight: StoryObj<typeof TableView> = {
render: (args) => (
<div className={style({display: 'flex', width: 400, height: 400, alignItems: 'stretch', flexDirection: 'column'})}>
Expand Down
7 changes: 7 additions & 0 deletions packages/@react-spectrum/table/test/TableTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,21 @@ export let tableTests = () => {
let user;
let testUtilUser = new User({advanceTimer: (time) => jest.advanceTimersByTime(time)});

let innerHeight, innerWidth;
beforeAll(function () {
user = userEvent.setup({delay: null, pointerMap});
innerHeight = window.innerHeight;
innerWidth = window.innerWidth;
Object.defineProperty(window, 'innerHeight', {value: 1000, configurable: true, writable: true});
Object.defineProperty(window, 'innerWidth', {value: 1000, configurable: true, writable: true});
offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 400);
offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 200);
jest.useFakeTimers();
});

afterAll(function () {
window.innerHeight = innerHeight;
window.innerWidth = innerWidth;
offsetWidth.mockReset();
offsetHeight.mockReset();
});
Expand Down
Loading
Loading