-
Notifications
You must be signed in to change notification settings - Fork 21
ITEP-163854 - replace react virtuoso - p4 #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// Copyright (C) 2022-2025 Intel Corporation | ||
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE | ||
|
||
import { renderHook } from '@testing-library/react'; | ||
|
||
import { useScrollToTargetItem } from './use-scroll-to-target-item.hook'; | ||
|
||
jest.useFakeTimers(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we do it in the |
||
|
||
describe('useScrollToTargetItem', () => { | ||
const mockCallback = jest.fn(); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
jest.clearAllTimers(); | ||
}); | ||
|
||
it('should not call callback when container is not provided', () => { | ||
renderHook(() => | ||
useScrollToTargetItem({ | ||
gap: 10, | ||
container: null, | ||
targetIndex: 5, | ||
callback: mockCallback, | ||
}) | ||
); | ||
|
||
jest.advanceTimersByTime(500); | ||
|
||
expect(mockCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('calls callback with correct scroll position', () => { | ||
const gap = 0; | ||
const childWidth = 100; | ||
const childHeight = 100; | ||
const targetIndex = 12; | ||
const containerWidth = 200; | ||
|
||
const container = document.createElement('div'); | ||
Object.defineProperty(container, 'clientWidth', { value: containerWidth }); | ||
|
||
const child = document.createElement('div'); | ||
Object.defineProperty(child, 'clientWidth', { value: childWidth }); | ||
Object.defineProperty(child, 'clientHeight', { value: childHeight }); | ||
|
||
container.appendChild(child); | ||
|
||
const itemsPerRow = Math.floor(containerWidth / childWidth); // 2 | ||
const targetRow = Math.floor(targetIndex / itemsPerRow); // 6 | ||
const expectedScrollPos = (childHeight + gap) * targetRow; // 600 | ||
|
||
renderHook(() => | ||
useScrollToTargetItem({ | ||
gap, | ||
container, | ||
targetIndex, | ||
callback: mockCallback, | ||
}) | ||
); | ||
|
||
jest.advanceTimersByTime(500); | ||
|
||
expect(mockCallback).toHaveBeenCalledWith(expectedScrollPos); | ||
}); | ||
|
||
it('return zero when container has no children', () => { | ||
const container = document.createElement('div'); | ||
Object.defineProperty(container, 'clientWidth', { value: 1000 }); | ||
|
||
renderHook(() => | ||
useScrollToTargetItem({ | ||
gap: 10, | ||
container, | ||
targetIndex: 5, | ||
callback: mockCallback, | ||
}) | ||
); | ||
|
||
jest.advanceTimersByTime(500); | ||
|
||
expect(mockCallback).toHaveBeenCalledWith(0); | ||
}); | ||
|
||
describe('should not call callback with invalid index', () => { | ||
it.each([undefined, null, -1, 1.5, NaN])('targetIndex: %p', (invalidIndex) => { | ||
renderHook(() => | ||
useScrollToTargetItem({ | ||
gap: 10, | ||
container: document.createElement('div'), | ||
targetIndex: invalidIndex as number, | ||
callback: mockCallback, | ||
}) | ||
); | ||
|
||
jest.advanceTimersByTime(500); | ||
|
||
expect(mockCallback).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Copyright (C) 2022-2025 Intel Corporation | ||
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE | ||
|
||
import { DependencyList, useEffect } from 'react'; | ||
|
||
import { isNil } from 'lodash-es'; | ||
|
||
interface useScrollToTargetItemProps { | ||
gap: number; | ||
dependencies?: DependencyList; | ||
container?: Element | null; | ||
targetIndex?: number; | ||
callback: (scrollTo: number) => void; | ||
} | ||
|
||
const isValidIndex = (index?: number): index is number => !isNil(index) && Number.isInteger(index) && index >= 0; | ||
|
||
export const useScrollToTargetItem = ({ | ||
gap, | ||
container, | ||
targetIndex, | ||
dependencies = [], | ||
callback, | ||
}: useScrollToTargetItemProps) => { | ||
useEffect(() => { | ||
const timeoutId = setTimeout(() => { | ||
if (!container || !isValidIndex(targetIndex)) { | ||
return; | ||
} | ||
|
||
const containerWidth = container.clientWidth; | ||
const childrenWidth = container.firstElementChild?.clientWidth ?? 1; | ||
const childrenHeight = container.firstElementChild?.clientHeight ?? 1; | ||
const childrenPreRow = Math.floor(containerWidth / childrenWidth); | ||
const targetRow = Math.floor(targetIndex / childrenPreRow); | ||
const scrollTo = (childrenHeight + gap) * targetRow; | ||
|
||
callback(scrollTo); | ||
// we don't want to scroll immediately | ||
// in case of changed view mode we have to scroll once view is rendered | ||
}, 500); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a slow network might break this behavior. Could we not handle this on the parent? Maybe pass a "isReady" or "isEnabled" prop once the parent renders, perhaps inside a useEffect? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The hook re-executes when |
||
|
||
return () => { | ||
timeoutId && clearTimeout(timeoutId); | ||
}; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, dependencies); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,8 +12,8 @@ export const INITIAL_VIEW_MODE = ViewModes.MEDIUM; | |
export const VIEW_MODE_LABEL = 'View mode'; | ||
|
||
export const VIEW_MODE_SETTINGS = { | ||
[ViewModes.SMALL]: { minItemSize: 112, gap: 4, maxColumns: 11 }, | ||
[ViewModes.LARGE]: { minItemSize: 300, gap: 8, maxColumns: 4 }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
[ViewModes.MEDIUM]: { minItemSize: 150, gap: 8, maxColumns: 8 }, | ||
[ViewModes.LARGE]: { minItemSize: 300, gap: 12, maxColumns: 4 }, | ||
[ViewModes.SMALL]: { minItemSize: 112, gap: 4, maxColumns: 11 }, | ||
[ViewModes.DETAILS]: { size: 81, gap: 0 }, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider moving the retrieval of the container element inside the useScrollToTargetItem hook or ensuring that it updates correctly, as accessing ref.current outside of the hookβs effect may lead to stale references.
Copilot uses AI. Check for mistakes.