Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions packages/loopover-ui-kit/src/components/carousel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

// Mock Embla so we can assert the keyboard handler calls scrollPrev/scrollNext (#8308) without a real
// carousel engine. useEmblaCarousel returns [refCallback, api]; the api only needs the members Carousel
// touches (scroll + canScroll + the reInit/select event wiring in its effects).
const scrollPrev = vi.fn();
const scrollNext = vi.fn();
const mockApi = {
scrollPrev,
scrollNext,
canScrollPrev: () => true,
canScrollNext: () => true,
on: vi.fn(),
off: vi.fn(),
};
vi.mock("embla-carousel-react", () => ({
default: () => [() => {}, mockApi],
}));

import { Carousel, CarouselContent, CarouselItem } from "./carousel";

function renderCarousel(orientation: "horizontal" | "vertical") {
return render(
<Carousel orientation={orientation}>
<CarouselContent>
<CarouselItem>one</CarouselItem>
<CarouselItem>two</CarouselItem>
</CarouselContent>
</Carousel>,
);
}

afterEach(() => {
scrollPrev.mockClear();
scrollNext.mockClear();
});

describe("Carousel keyboard navigation is orientation-aware (#8308)", () => {
it("horizontal (default): ArrowLeft -> scrollPrev, ArrowRight -> scrollNext", () => {
renderCarousel("horizontal");
const region = screen.getByRole("region");
fireEvent.keyDown(region, { key: "ArrowLeft" });
expect(scrollPrev).toHaveBeenCalledTimes(1);
fireEvent.keyDown(region, { key: "ArrowRight" });
expect(scrollNext).toHaveBeenCalledTimes(1);
});

it("horizontal: ArrowUp/ArrowDown do NOT scroll", () => {
renderCarousel("horizontal");
const region = screen.getByRole("region");
fireEvent.keyDown(region, { key: "ArrowUp" });
fireEvent.keyDown(region, { key: "ArrowDown" });
expect(scrollPrev).not.toHaveBeenCalled();
expect(scrollNext).not.toHaveBeenCalled();
});

it("vertical: ArrowUp -> scrollPrev, ArrowDown -> scrollNext", () => {
renderCarousel("vertical");
const region = screen.getByRole("region");
fireEvent.keyDown(region, { key: "ArrowUp" });
expect(scrollPrev).toHaveBeenCalledTimes(1);
fireEvent.keyDown(region, { key: "ArrowDown" });
expect(scrollNext).toHaveBeenCalledTimes(1);
});

it("vertical: ArrowLeft/ArrowRight (no on-screen meaning in this layout) do NOT scroll", () => {
renderCarousel("vertical");
const region = screen.getByRole("region");
fireEvent.keyDown(region, { key: "ArrowLeft" });
fireEvent.keyDown(region, { key: "ArrowRight" });
expect(scrollPrev).not.toHaveBeenCalled();
expect(scrollNext).not.toHaveBeenCalled();
});
});
21 changes: 16 additions & 5 deletions packages/loopover-ui-kit/src/components/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,29 @@ const Carousel = React.forwardRef<
api?.scrollNext();
}, [api]);

// The single source of truth for orientation the CarouselContext already exposes -- reused here so the
// keyboard handler stays orientation-aware like every other part of the component (#8308), rather than a
// second independent check.
const resolvedOrientation =
orientation || (opts?.axis === "y" ? "vertical" : "horizontal");

const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
// A vertical carousel's prev/next buttons already point up/down, so bind ArrowUp/ArrowDown to match;
// horizontal (the default) keeps ArrowLeft/ArrowRight. ArrowUp/ArrowLeft -> scrollPrev, the other -> scrollNext.
const prevKey =
resolvedOrientation === "vertical" ? "ArrowUp" : "ArrowLeft";
const nextKey =
resolvedOrientation === "vertical" ? "ArrowDown" : "ArrowRight";
if (event.key === prevKey) {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
} else if (event.key === nextKey) {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
[resolvedOrientation, scrollPrev, scrollNext],
);

React.useEffect(() => {
Expand Down Expand Up @@ -124,8 +136,7 @@ const Carousel = React.forwardRef<
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
orientation: resolvedOrientation,
scrollPrev,
scrollNext,
canScrollPrev,
Expand Down
Loading