Context
packages/loopover-ui-kit/src/components/carousel.tsx's Carousel component accepts an orientation?: "horizontal" | "vertical" prop, and every other piece of the component branches on it: CarouselContent switches between -ml-4 (horizontal) and -mt-4 flex-col (vertical); CarouselItem switches between pl-4 and pt-4; CarouselPrevious/CarouselNext reposition themselves (left/right vs. top/bottom) and rotate 90 degrees for the vertical layout.
The component's keyboard handler does not follow this pattern. handleKeyDown (lines 86-97) is registered unconditionally regardless of orientation and only ever responds to ArrowLeft/ArrowRight:
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
For a orientation="vertical" carousel — whose CarouselPrevious/CarouselNext buttons are already visually rotated to point up/down — ArrowUp/ArrowDown do nothing, while ArrowLeft/ArrowRight (which have no on-screen meaning in the vertical layout) still scroll the carousel. This is a real keyboard-interaction inconsistency, not just a visual one: a keyboard user operating a vertical carousel gets no working arrow-key navigation that matches what they see.
Requirements
- Make
handleKeyDown orientation-aware: when orientation === "vertical", respond to ArrowUp/ArrowDown (mapping ArrowUp → scrollPrev(), ArrowDown → scrollNext()); when orientation === "horizontal" (the default), keep the existing ArrowLeft/ArrowRight → scrollPrev()/scrollNext() mapping.
- Use the same
orientation resolution the component already computes for CarouselContext (orientation || (opts?.axis === "y" ? "vertical" : "horizontal")), not a second, independent orientation check.
- Do not change
scrollPrev/scrollNext, the Embla wiring, or any non-keyboard behavior.
Deliverables
Test Coverage Requirements
packages/loopover-ui-kit is not in the root vitest.config.ts's coverage.include and is not Codecov-gated — still add the tests above per this package's own "suite must run and pass" acceptance bar (packages/loopover-ui-kit/vitest.config.ts). carousel.tsx currently has zero existing test coverage; this issue's regression test is also this component's first.
Expected Outcome
A Carousel rendered with orientation="vertical" responds to ArrowUp/ArrowDown for keyboard navigation, matching the orientation-aware layout its CarouselContent/CarouselItem/CarouselPrevious/CarouselNext siblings already provide, instead of silently doing nothing for vertical keyboard users.
Links & Resources
packages/loopover-ui-kit/src/components/carousel.tsx:86-97 (the ungated handler)
packages/loopover-ui-kit/src/components/carousel.tsx:121-128 (the existing orientation resolution logic to reuse)
packages/loopover-ui-kit/src/components/carousel.tsx:195-220 (CarouselPrevious, showing the orientation-aware positioning/rotation the keyboard handling should match)
Context
packages/loopover-ui-kit/src/components/carousel.tsx'sCarouselcomponent accepts anorientation?: "horizontal" | "vertical"prop, and every other piece of the component branches on it:CarouselContentswitches between-ml-4(horizontal) and-mt-4 flex-col(vertical);CarouselItemswitches betweenpl-4andpt-4;CarouselPrevious/CarouselNextreposition themselves (left/right vs. top/bottom) and rotate 90 degrees for the vertical layout.The component's keyboard handler does not follow this pattern.
handleKeyDown(lines 86-97) is registered unconditionally regardless oforientationand only ever responds toArrowLeft/ArrowRight:For a
orientation="vertical"carousel — whoseCarouselPrevious/CarouselNextbuttons are already visually rotated to point up/down —ArrowUp/ArrowDowndo nothing, whileArrowLeft/ArrowRight(which have no on-screen meaning in the vertical layout) still scroll the carousel. This is a real keyboard-interaction inconsistency, not just a visual one: a keyboard user operating a vertical carousel gets no working arrow-key navigation that matches what they see.Requirements
handleKeyDownorientation-aware: whenorientation === "vertical", respond toArrowUp/ArrowDown(mappingArrowUp→scrollPrev(),ArrowDown→scrollNext()); whenorientation === "horizontal"(the default), keep the existingArrowLeft/ArrowRight→scrollPrev()/scrollNext()mapping.orientationresolution the component already computes forCarouselContext(orientation || (opts?.axis === "y" ? "vertical" : "horizontal")), not a second, independent orientation check.scrollPrev/scrollNext, the Embla wiring, or any non-keyboard behavior.Deliverables
handleKeyDowninpackages/loopover-ui-kit/src/components/carousel.tsxbranches on the resolvedorientationand bindsArrowUp/ArrowDownfor vertical carousels,ArrowLeft/ArrowRightfor horizontal (the current, unchanged default)Carouselwithorientation="vertical"and assertingArrowDown/ArrowUpkey events call the underlying EmblascrollNext/scrollPrev, and thatArrowLeft/ArrowRightdo not trigger scrolling in that modeArrowLeft/ArrowRight) is unchangedTest Coverage Requirements
packages/loopover-ui-kitis not in the rootvitest.config.ts'scoverage.includeand is not Codecov-gated — still add the tests above per this package's own "suite must run and pass" acceptance bar (packages/loopover-ui-kit/vitest.config.ts).carousel.tsxcurrently has zero existing test coverage; this issue's regression test is also this component's first.Expected Outcome
A
Carouselrendered withorientation="vertical"responds toArrowUp/ArrowDownfor keyboard navigation, matching the orientation-aware layout itsCarouselContent/CarouselItem/CarouselPrevious/CarouselNextsiblings already provide, instead of silently doing nothing for vertical keyboard users.Links & Resources
packages/loopover-ui-kit/src/components/carousel.tsx:86-97(the ungated handler)packages/loopover-ui-kit/src/components/carousel.tsx:121-128(the existingorientationresolution logic to reuse)packages/loopover-ui-kit/src/components/carousel.tsx:195-220(CarouselPrevious, showing the orientation-aware positioning/rotation the keyboard handling should match)