Fix inadvertant scrolling of the timeline when using pageUp/pageDown

This commit is contained in:
David Langley 2025-07-29 17:11:35 +01:00
parent a09197dd76
commit 4fecf1413c
3 changed files with 13 additions and 37 deletions

View File

@ -30,7 +30,7 @@ test.describe("Lazy Loading", () => {
});
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
// The charlies were running off the bottom of the screen.
// The charlies were running off the bottom of the screen.
// We no longer overscan the member list so the result is they are not in the dom.
// Increase the viewport size to ensure they are.
await page.setViewportSize({ width: 1000, height: 1000 });

View File

@ -89,6 +89,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
// Extract our custom props to avoid conflicts with Virtuoso props
const { items, onSelectItem, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
const isScrollingToItem = useRef<boolean>(false);
// Update the key-to-index mapping whenever items change
React.useEffect(() => {
const newKeyToIndexMap = new Map<string, number>();
@ -125,15 +127,21 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
(index: number, align?: "center" | "end" | "start"): void => {
// Ensure index is within bounds
const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
if (isScrollingToItem.current) {
// If already scrolling to an item drop this request. Adding further requests
// causes the event to bubble up and be handled by other components(unintentional timeline scrolling was observed).
return;
}
if (items[clampedIndex]) {
const key = getItemKey(items[clampedIndex]);
setfocusKey(key);
isScrollingToItem.current = true;
virtuosoHandleRef?.current?.scrollIntoView({
index: clampedIndex,
align: align,
behavior: "auto",
done: () => {
setfocusKey(key);
isScrollingToItem.current = false;
},
});
}
@ -195,11 +203,11 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
handled = true;
} else if (e.code === "PageDown" && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(currentIndex + numberDisplayed, true, `start`);
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
handled = true;
} else if (e.code === "PageUp" && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(currentIndex - numberDisplayed, false, `start`);
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
}

View File

@ -227,38 +227,6 @@ describe("ListView", () => {
expect(items[lastIndex]).toHaveAttribute("tabindex", "-1");
});
it("should prevent default and stop propagation for handled keys", () => {
render(<ListView {...defaultProps} />);
const container = screen.getByRole("grid");
// Focus the container first to establish initial focus
fireEvent.focus(container);
// Create a spy to monitor the event
const keyDownSpy = jest.fn();
container.addEventListener("keydown", keyDownSpy);
fireEvent.keyDown(container, { code: "ArrowDown" });
// Check that the event was prevented and stopped
expect(keyDownSpy).toHaveBeenCalled();
const event = keyDownSpy.mock.calls[0][0];
expect(event.defaultPrevented).toBe(true);
});
it("should not prevent default for unhandled keys", () => {
render(<ListView {...defaultProps} />);
const container = screen.getByRole("grid");
const event = new KeyboardEvent("keydown", { code: "KeyA", bubbles: true, cancelable: true });
const preventDefault = jest.spyOn(event, "preventDefault");
const stopPropagation = jest.spyOn(event, "stopPropagation");
fireEvent(container, event);
expect(preventDefault).not.toHaveBeenCalled();
expect(stopPropagation).not.toHaveBeenCalled();
});
it("should skip non-focusable items when navigating down", async () => {
// Create items where every other item is not focusable
const mixedItems = [