-
Notifications
You must be signed in to change notification settings - Fork 37
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
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 |
---|---|---|
|
@@ -16,6 +16,7 @@ import { | |
} from 'react-aria-components'; | ||
|
||
import { VIEW_MODE_SETTINGS, ViewModes } from '../media-view-modes/utils'; | ||
import { useGetTargetPosition } from './use-get-target-position.hook'; | ||
|
||
import classes from './media-items-list.module.scss'; | ||
|
||
|
@@ -26,6 +27,7 @@ interface MediaItemsListProps<T> { | |
viewMode: ViewModes; | ||
mediaItems: T[]; | ||
height?: Responsive<DimensionValue>; | ||
scrollToIndex?: number; | ||
viewModeSettings?: ViewModeSettings; | ||
endReached?: () => void; | ||
itemContent: (item: T) => ReactNode; | ||
|
@@ -38,6 +40,7 @@ export const MediaItemsList = <T extends object>({ | |
height, | ||
viewMode, | ||
mediaItems, | ||
scrollToIndex, | ||
ariaLabel = 'media items list', | ||
viewModeSettings = VIEW_MODE_SETTINGS, | ||
itemContent, | ||
|
@@ -49,9 +52,6 @@ export const MediaItemsList = <T extends object>({ | |
const isDetails = viewMode === ViewModes.DETAILS; | ||
const layout = isDetails ? 'stack' : 'grid'; | ||
|
||
const ref = useRef<HTMLDivElement | null>(null); | ||
useLoadMore({ onLoadMore: endReached }, ref); | ||
|
||
const layoutOptions = isDetails | ||
? { | ||
gap: config.gap, | ||
|
@@ -64,6 +64,19 @@ export const MediaItemsList = <T extends object>({ | |
preserveAspectRatio: true, | ||
}; | ||
|
||
const ref = useRef<HTMLDivElement | null>(null); | ||
useLoadMore({ onLoadMore: endReached }, ref); | ||
|
||
const container = ref?.current?.firstElementChild; | ||
camiloHimura marked this conversation as resolved.
Show resolved
Hide resolved
camiloHimura marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
useGetTargetPosition({ | ||
gap: config.gap, | ||
container, | ||
targetIndex: scrollToIndex, | ||
dependencies: [scrollToIndex, viewMode, container], | ||
callback: (top) => ref.current?.scrollTo({ top, behavior: 'smooth' }), | ||
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. I'd expect react aria to have a specific function to focus on a virtualized element, which then triggers the scrolling. Did you check if there's not a more explicit way to do this? 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. I couldn't find a specific helper or handler in either the react-aria utilities or the component itself |
||
}); | ||
|
||
return ( | ||
<View id={id} UNSAFE_className={classes.mainContainer} height={height}> | ||
<Virtualizer layout={isDetails ? ListLayout : GridLayout} layoutOptions={layoutOptions}> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright (C) 2022-2025 Intel Corporation | ||
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE | ||
|
||
import { renderHook } from '@testing-library/react'; | ||
|
||
import { useGetTargetPosition } from './use-get-target-position.hook'; | ||
|
||
describe('useGetTargetPosition', () => { | ||
const mockCallback = jest.fn(); | ||
|
||
beforeEach(() => { | ||
jest.useFakeTimers(); | ||
jest.clearAllMocks(); | ||
jest.clearAllTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('should not call callback when container is not provided', () => { | ||
renderHook(() => | ||
useGetTargetPosition({ | ||
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(() => | ||
useGetTargetPosition({ | ||
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(() => | ||
useGetTargetPosition({ | ||
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(() => | ||
useGetTargetPosition({ | ||
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,50 @@ | ||
// Copyright (C) 2022-2025 Intel Corporation | ||
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE | ||
|
||
import { DependencyList, useEffect } from 'react'; | ||
|
||
import { isNil } from 'lodash-es'; | ||
|
||
interface useGetTargetPositionProps { | ||
gap: number; | ||
delay?: number; | ||
container?: Element | null; | ||
targetIndex?: number; | ||
dependencies?: DependencyList; | ||
callback: (scrollTo: number) => void; | ||
} | ||
|
||
const isValidIndex = (index?: number): index is number => !isNil(index) && Number.isInteger(index) && index >= 0; | ||
|
||
export const useGetTargetPosition = ({ | ||
gap, | ||
delay = 500, | ||
container, | ||
targetIndex, | ||
dependencies = [], | ||
callback, | ||
}: useGetTargetPositionProps) => { | ||
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 childrenPerRow = Math.floor(containerWidth / childrenWidth); | ||
const targetRow = Math.floor(targetIndex / childrenPerRow); | ||
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 | ||
}, delay); | ||
|
||
return () => { | ||
timeoutId && clearTimeout(timeoutId); | ||
}; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, dependencies); | ||
}; |
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.
The
INITIAL_VIEW_MODE
import is unused and can be removed to clean up dead code.Copilot uses AI. Check for mistakes.