Add @microbit/ui-carousel package - #118
Merged
Merged
Conversation
New source-shipped workspace for the carousel currently duplicated in CreateAI and the Python Editor homepage (on branch), modelled on ui-patterns. Peers: @microbit/ui, swiper (^12–^14; the apps sit at 12 and 14), react, react-intl. No components yet.
Ported from CreateAI's Panda implementation with the Python Editor's two
fixes folded in (touchStartPreventDefault for overlay dismissal; the
slidesPerGroup guard follows with Carousel). Deliberate API deltas: a
pre-translated containerLabel replaces the app catalog id, RTL follows
useLocale() instead of a hardcoded false, and the native-platform check
is gone — apps pass navigation={false} instead.
This is unused and has never been used in apps.
The release workflow routes a tag prefix to both the package directory and the npm name, so directory = npm suffix is a repo invariant. Adds the ui-carousel-vX.Y.Z tag case it enables.
Swiper refuses to start drags on its focusableElements list; with cards fully covered by an overlay button, a card-face drag set isMoved without capturing the translate baseline, and released as slideTo(0) (swiper#5524). Exclude button/label from the list, and ignore pointer-caused focus in the slide-focus handler so mouse-down can't slide mid-gesture.
Exclude button/label from Swiper's focusableElements: it refuses drags
from them, and with cards fully covered by an overlay button a card-face
drag poisoned the gesture and snapped to slide 0 on release (swiper#5524,
live in CreateAI today). Drop touchStartPreventDefault={false}, a
Chakra-era workaround: react-aria overlays dismiss via pointer events,
and Swiper's default preventDefault suppresses native link/image drags
and mouse-focus side effects. Reset scrollLeft in the slide focus
handler: the browser scrolls the overflow-hidden container to reveal
focused elements, desyncing the transform-positioned view when tabbing
forward to a clipped card's trailing controls.
microbit-robert
commented
Sep 2, 2026
Comment on lines
+10
to
+15
| export { default as Carousel, type CarouselProps } from "./Carousel"; | ||
| export { default as CarouselRow, type CarouselRowProps } from "./CarouselRow"; | ||
| export { | ||
| default as SwiperCarousel, | ||
| type SwiperCarouselProps, | ||
| } from "./SwiperCarousel"; |
Contributor
Author
There was a problem hiding this comment.
Or just CarouselRow?
Contributor
Author
|
Moving to draft as this will need some accessibility fixes. |
microbit-robert
marked this pull request as draft
September 2, 2026 15:51
tag="ul" produced invalid list structure (li under a div wrapper, announced as an empty list), and list semantics are wrong here anyway: focus only ever lands on the cards' overlay controls, and screen readers don't reliably announce ancestor list position on Tab. Swiper's default group-per-slide markup with our translated "8 of 12" labels announces on focus entering each slide; the container becomes a labelled region. slideRole="presentation" conflicted with those labels.
The deleted "& ul" margin reset was also suppressing Swiper's own margin-inline auto on .swiper; auto margins disable grid-item stretch, so the carousel sized to its slides, measured itself as fitting, and locked — no drag, no buttons. Reapply the reset by class.
aria-hidden buttons excluded voice-control users, and Swiper's a11y handling kept re-adding tabindex="0" to the wired navigation elements — a focusable aria-hidden element whenever an edge state changed. The buttons now manage their own edge visibility from Swiper's events, carry translated labels, and stay out of the tab order (the cards are the keyboard path: every card contains a tab stop).
Forced through Swiper's speed param rather than CSS: a zeroed transition-duration never fires the transitionend Swiper waits on.
Storybook indexes every named export of a stories file as a story, so exampleCards rendered as a broken phantom entry.
SwiperCarousel no longer forces slide width; Carousel sizes the slide directly, so items no longer need a single div root to be sized (a div root still opts into equal-height stretching).
Swiper's own resize handler runs before ours (registered with priority), so the grid was built with the previous group size — one discrete resize (maximise, devtools dock) left paging stale until the next event.
handleSlideFocus owns focus-follows-slide (it also slides partially visible cards into view); with both active they race from opposite sides of a requestAnimationFrame.
Drop the caller-less padding and swiperWrapperClassName props, the Pagination module and the navigation/pagination stylesheets (nothing renders their elements since the buttons detached from Swiper), unused --swiper-* vars and an inert alignItems style. ReactElement[] replaces JSX.Element[] ahead of React 19 types; carouselMessage is re-exported for parity with ui-patterns.
CarouselRow's heading now labels the carousel via aria-labelledby; titleSuffix replaces titleElement for adornments, which stay outside the accessible name. A union type requires exactly one naming (title, containerLabel or ariaLabelledBy) at compile time, so an unnamed or double-named carousel no longer typechecks.
Deploying ui with
|
| Latest commit: |
c5195c3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://63dd45b3.ui-2wg.pages.dev |
| Branch Preview URL: | https://carousel.ui-2wg.pages.dev |
The icons now pass contrast checks when the buttons are not focused and have a semi-transparent background.
microbit-robert
marked this pull request as ready for review
September 3, 2026 15:28
microbit-matt-hillsdon
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extract common carousel component which will be used across apps that support multiple projects.
Currently exports three components, although only
CarouselRowwould be used as things stand.Exports
CarouselRow- heading with optional actions and carousel belowCarousel- the common carousel row of 260px wide cards with responsive slides-per-page based on the window widthSwiperCarousel- a thin Swiper wrapper with list semantics, translated ARIA annotations and edge-pinnedprev/next buttons. Swiper props pass through.
Deliberate changes from the app copies
containerLabel(pre-translated string) replacescontainerMessageIdtitle/actionsare ReactNodesThe three internal aria strings ship in this package's catalogs under
ui-carousel.ids (seeded from the apps' existing translations; ar/cy/it are English backfill until Crowdin).RTL follows
useLocale()instead of being hardcoded to false and the carousel direction updates dynamicallyisNativePlatform()prop removed, apps can passnavigation={false}insteadThe
_shortHeightpadding override moves to the CreateAI call siteEnsure that
slidesPerGroupis never calculated as 0Disable Swiper's scrollOnFocus and use our own focus handling which ensures that cards aren't hanging off the page
Fix stale slidesPerGroup by running swiper.updateSlides() when this is changed in recalculateBreakpoints
CarouselRow's heading names the carousel viaaria-labelledbyandtitleSuffixrenders additional elements (e.g. tooltips) outside the accessible name. Naming is enforced by typecheck.Accessibility fixes
Fix invalid HTML and remove presentation roles. We no longer use
ulandliand have gone back to divs / groups. This is because we focus the card buttons or links, never the listitems themselves, so we don't get the list semantics for free - therefore, keep the slide of total slides labels.Prev/next buttons are named and exposed to assistive tech (voice control can activate them) but stay out of the tab order. The keyboard path is tabbing through the cards themselves. The prev/next buttons now managed by the component rather than Swiper's navigation module, which was fighting our tab-index attribute.
prefers-reduced-motionmakes all carousel movement instantAutoplay module removed. It was unused and there was no pause control
Decorative chevrons are aria-hidden and darkened (gray.500 → gray.800) to improve contrast
Post-merge actions
packages/ui-carousel/lang/ui.en.jsonto Crowdin asnew/packages/ui-carousel/before the next translation sync