feat(ui): more resilient gallery scrollIntoView

This commit is contained in:
psychedelicious
2025-07-05 19:39:25 +10:00
parent 61a35f1396
commit 16c8017f1a
3 changed files with 31 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import type { AppDispatch, AppGetState } from 'app/store/store';
@@ -14,7 +14,6 @@ import { createSingleImageDragPreview, setSingleImageDragPreview } from 'feature
import { firefoxDndFix } from 'features/dnd/util';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
import {
selectGetImageNamesQueryArgs,
selectSelectedBoardId,
@@ -241,13 +240,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
}, [store]);
const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]);
useImageContextMenu(imageDTO, element);
return (
<>
<Box sx={galleryImageContainerSX} data-testid={dataTestId} data-is-dragging={isDragging}>
<Box sx={galleryImageContainerSX} data-is-dragging={isDragging} data-image-name={imageDTO.image_name}>
<Flex
role="button"
className={GALLERY_IMAGE_CLASS}
@@ -279,8 +276,8 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
GalleryImage.displayName = 'GalleryImage';
export const GalleryImagePlaceholder = memo(() => (
<Flex w="full" h="full" bg="base.850" borderRadius="base" alignItems="center" justifyContent="center">
export const GalleryImagePlaceholder = memo((props: FlexProps) => (
<Flex w="full" h="full" bg="base.850" borderRadius="base" alignItems="center" justifyContent="center" {...props}>
<Icon as={PiImageBold} boxSize={16} color="base.800" />
</Flex>
));

View File

@@ -1 +0,0 @@
export const getGalleryImageDataTestId = (imageName?: string) => `gallery-image-${imageName}`;

View File

@@ -128,60 +128,69 @@ const getImagesPerRow = (rootEl: HTMLDivElement): number => {
* Scroll the item at the given index into view if it is not currently visible.
*/
const scrollIntoView = (
index: number,
targetImageName: string,
imageNames: string[],
rootEl: HTMLDivElement,
virtuosoGridHandle: VirtuosoGridHandle,
range: ListRange
) => {
if (range.endIndex === 0) {
// No range is rendered; no need to scroll to anything.
return;
}
// First get the virtuoso grid list root element
const gridList = rootEl.querySelector('.virtuoso-grid-list') as HTMLElement;
const targetIndex = imageNames.findIndex((name) => name === targetImageName);
if (!gridList) {
// No grid - cannot scroll!
if (targetIndex === -1) {
// The image isn't in the currently rendered list.
return;
}
// Then find the specific item within the grid list
const targetItem = gridList.querySelector(`.virtuoso-grid-item[data-index="${index}"]`) as HTMLElement;
const targetItem = rootEl.querySelector(
`.virtuoso-grid-item:has([data-image-name="${targetImageName}"])`
) as HTMLElement;
if (!targetItem) {
if (index > range.endIndex) {
if (targetIndex > range.endIndex) {
virtuosoGridHandle.scrollToIndex({
index,
index: targetIndex,
behavior: 'auto',
align: 'start',
});
} else if (index < range.startIndex) {
} else if (targetIndex < range.startIndex) {
virtuosoGridHandle.scrollToIndex({
index,
index: targetIndex,
behavior: 'auto',
align: 'end',
});
} else {
log.warn(`Unable to find item index ${index} but it is in range ${range.startIndex}-${range.endIndex}`);
log.debug(
`Unable to find image ${targetImageName} at index ${targetIndex} but it is in the rendered range ${range.startIndex}-${range.endIndex}`
);
}
return;
}
// We found the image in the DOM, but it might be in the overscan range - rendered but not in the visible viewport.
// Check if it is in the viewport and scroll if necessary.
const itemRect = targetItem.getBoundingClientRect();
const rootRect = rootEl.getBoundingClientRect();
if (itemRect.top < rootRect.top) {
virtuosoGridHandle.scrollToIndex({
index,
index: targetIndex,
behavior: 'auto',
align: 'start',
});
} else if (itemRect.bottom > rootRect.bottom) {
virtuosoGridHandle.scrollToIndex({
index,
index: targetIndex,
behavior: 'auto',
align: 'end',
});
} else {
// Image is already in view
}
return;
@@ -377,22 +386,18 @@ const useKeepSelectedImageInView = (
rootRef: React.RefObject<HTMLDivElement>,
rangeRef: MutableRefObject<ListRange>
) => {
const imageName = useAppSelector(selectLastSelectedImage);
const targetImageName = useAppSelector(selectLastSelectedImage);
useEffect(() => {
const virtuosoGridHandle = virtuosoRef.current;
const rootEl = rootRef.current;
const range = rangeRef.current;
if (!virtuosoGridHandle || !rootEl || !imageNames || imageNames.length === 0) {
if (!virtuosoGridHandle || !rootEl || !targetImageName || !imageNames || imageNames.length === 0) {
return;
}
const index = imageName ? imageNames.indexOf(imageName) : 0;
if (index === -1) {
return;
}
scrollIntoView(index, rootEl, virtuosoGridHandle, range);
}, [imageName, imageNames, rangeRef, rootRef, virtuosoRef]);
scrollIntoView(targetImageName, imageNames, rootEl, virtuosoGridHandle, range);
}, [targetImageName, imageNames, rangeRef, rootRef, virtuosoRef]);
};
/**