diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.spec.ts b/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.spec.ts new file mode 100644 index 000000000..9f82cbe1f --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; + +import { getNextBoardIndex } from "./_board-navigation"; + +describe("get next board index", () => { + test.each([ + [3, 0, 1, 1], + [3, 2, 1, 0], + [3, 2, -1, 1], + [3, 0, -1, 2], + ] as const)("moves through boards with wraparound", (boardCount, currentIndex, direction, expected) => { + expect(getNextBoardIndex(boardCount, currentIndex, direction)).toBe(expected); + }); + + test.each([ + [0, 0], + [1, 0], + [3, -1], + [3, 3], + ])("does not navigate with an invalid current board", (boardCount, currentIndex) => { + expect(getNextBoardIndex(boardCount, currentIndex, 1)).toBeNull(); + }); +}); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.ts b/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.ts new file mode 100644 index 000000000..3d8e723f3 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_board-navigation.ts @@ -0,0 +1,4 @@ +export const getNextBoardIndex = (boardCount: number, currentIndex: number, direction: -1 | 1) => { + if (boardCount <= 1 || currentIndex < 0 || currentIndex >= boardCount) return null; + return (currentIndex + direction + boardCount) % boardCount; +}; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index 304b251eb..322f669b8 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -40,6 +40,7 @@ import { CategoryEditModal } from "~/components/board/sections/category/category import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions"; import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; import { HeaderButton } from "~/components/layout/header/button"; +import { getNextBoardIndex } from "./_board-navigation"; export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const [isEditMode] = useEditMode(); @@ -223,9 +224,18 @@ const BoardNavigationHotkeys = () => { const hotkeyEntries = useMemo(() => { const entries: [string, () => void][] = []; - if (boards.length > 1) { - entries.push(["mod+shift+ArrowRight", () => navigateToBoard((currentIndex + 1) % boards.length)]); - entries.push(["mod+shift+ArrowLeft", () => navigateToBoard((currentIndex - 1 + boards.length) % boards.length)]); + // Only register the arrow shortcuts when the current board is known. If the + // board hasn't synced with the getAllBoards cache yet, currentIndex is -1 and + // the modulo arithmetic would otherwise jump to an unexpected board. + if (boards.length > 1 && currentIndex !== -1) { + entries.push(["mod+shift+ArrowRight", () => { + const nextIndex = getNextBoardIndex(boards.length, currentIndex, 1); + if (nextIndex !== null) navigateToBoard(nextIndex); + }]); + entries.push(["mod+shift+ArrowLeft", () => { + const nextIndex = getNextBoardIndex(boards.length, currentIndex, -1); + if (nextIndex !== null) navigateToBoard(nextIndex); + }]); } for (let i = 0; i < Math.min(boards.length, 9); i++) {