diff --git a/.changeset/short-adults-know.md b/.changeset/short-adults-know.md
new file mode 100644
index 0000000000..ae06e05778
--- /dev/null
+++ b/.changeset/short-adults-know.md
@@ -0,0 +1,5 @@
+---
+'@sumup-oss/circuit-ui': minor
+---
+
+Added a new CarouselPagination component. Use it to display larger amounts of content in limited spaces by dividing it into multiple pieces to be discoverd by the users through interaction.
diff --git a/.eslintrc.js b/.eslintrc.js
index 6cd4c31fe1..8ef89f7c34 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -47,18 +47,12 @@ module.exports = require('@sumup-oss/foundry/eslint')({
},
},
{
- files: ['**/*.stories.*'],
+ files: ['**/*.stories.*', '**/*.spec.*'],
rules: {
'import/no-relative-packages': 'off',
'react-server-components/use-client': 'off',
},
},
- {
- files: ['**/*.spec.*'],
- rules: {
- 'react-server-components/use-client': 'off',
- },
- },
{
files: ['packages/circuit-ui/components/legacy/**/*'],
rules: {
diff --git a/.storybook/fixtures.ts b/.storybook/fixtures.ts
new file mode 100644
index 0000000000..a47ae1a8b0
--- /dev/null
+++ b/.storybook/fixtures.ts
@@ -0,0 +1,22 @@
+export const images: { src: string; alt: string }[] = [
+ {
+ src: '/images/sumup-coffee-transaction.jpg',
+ alt: 'Next to a cup of coffee lays a phone showing a card transaction in the SumUp app',
+ },
+ {
+ src: '/images/sumup-product-catalog.jpg',
+ alt: 'A person holds a tablet showing a grid of products in the SumUp POS app. In the background, cakes and pastries sit on a table',
+ },
+ {
+ src: '/images/sumup-tablet-insights.jpg',
+ alt: 'A tablet shows sales insights in the SumUp POS app. On the table next to it are a SumUp Solo card reader with printer and two bowls with fruits',
+ },
+ {
+ src: '/images/sumup-solo-cradle.jpg',
+ alt: 'A SumUp Solo card reader sits in its charging cradle next to a tray of red, green and dark-brown mini tarts',
+ },
+ {
+ src: '/images/sumup-solo-printer.jpg',
+ alt: 'A person taps their phone onto a SumUp Solo card reader, perhaps to pay for the assortment of food around it on the table',
+ },
+];
diff --git a/.storybook/public/images/illustration-waves-2.jpg b/.storybook/public/images/illustration-waves-2.jpg
deleted file mode 100644
index 4c84b33fb8..0000000000
Binary files a/.storybook/public/images/illustration-waves-2.jpg and /dev/null differ
diff --git a/.storybook/public/images/illustration-waves-3.jpg b/.storybook/public/images/illustration-waves-3.jpg
deleted file mode 100644
index 6ff7bd8439..0000000000
Binary files a/.storybook/public/images/illustration-waves-3.jpg and /dev/null differ
diff --git a/.storybook/public/images/illustration-waves.jpg b/.storybook/public/images/illustration-waves.jpg
deleted file mode 100644
index 6fb7a86d16..0000000000
Binary files a/.storybook/public/images/illustration-waves.jpg and /dev/null differ
diff --git a/.storybook/public/images/sumup-coffee-transaction.jpg b/.storybook/public/images/sumup-coffee-transaction.jpg
new file mode 100644
index 0000000000..c1eb353744
Binary files /dev/null and b/.storybook/public/images/sumup-coffee-transaction.jpg differ
diff --git a/.storybook/public/images/sumup-product-catalog.jpg b/.storybook/public/images/sumup-product-catalog.jpg
new file mode 100644
index 0000000000..3e0ed1b418
Binary files /dev/null and b/.storybook/public/images/sumup-product-catalog.jpg differ
diff --git a/.storybook/public/images/sumup-solo-cradle.jpg b/.storybook/public/images/sumup-solo-cradle.jpg
new file mode 100644
index 0000000000..9db7ae64c1
Binary files /dev/null and b/.storybook/public/images/sumup-solo-cradle.jpg differ
diff --git a/.storybook/public/images/sumup-solo-printer.jpg b/.storybook/public/images/sumup-solo-printer.jpg
new file mode 100644
index 0000000000..b855cfc570
Binary files /dev/null and b/.storybook/public/images/sumup-solo-printer.jpg differ
diff --git a/.storybook/public/images/sumup-tablet-insights.jpg b/.storybook/public/images/sumup-tablet-insights.jpg
new file mode 100644
index 0000000000..a4081fa42e
Binary files /dev/null and b/.storybook/public/images/sumup-tablet-insights.jpg differ
diff --git a/packages/circuit-ui/components/Carousel/Carousel.spec.tsx b/packages/circuit-ui/components/Carousel/Carousel.spec.tsx
index 515d8a6b99..3b4ca70221 100644
--- a/packages/circuit-ui/components/Carousel/Carousel.spec.tsx
+++ b/packages/circuit-ui/components/Carousel/Carousel.spec.tsx
@@ -15,10 +15,12 @@
import { describe, expect, it, vi } from 'vitest';
+import { images } from '../../../../.storybook/fixtures.js';
import { axe, render, screen } from '../../util/test-utils.js';
import { Carousel } from './Carousel.js';
-import { SLIDES } from './__fixtures__/index.js';
+
+const slides = images.map((image) => ({ image }));
describe('Carousel', () => {
const baseProps = {
@@ -37,7 +39,7 @@ describe('Carousel', () => {
it('should render with children as a function', () => {
const children = vi.fn(() =>
Carousel heading
);
render(
-
+
{children}
,
);
@@ -47,7 +49,7 @@ describe('Carousel', () => {
it('should render with children as a node', () => {
render(
-
+
Carousel heading
,
);
@@ -56,7 +58,7 @@ describe('Carousel', () => {
});
it('should have no accessibility violations', async () => {
- const { container } = render();
+ const { container } = render();
const actual = await axe(container);
expect(actual).toHaveNoViolations();
diff --git a/packages/circuit-ui/components/Carousel/Carousel.stories.tsx b/packages/circuit-ui/components/Carousel/Carousel.stories.tsx
index 6ff670dfc2..1ccb75a0c3 100644
--- a/packages/circuit-ui/components/Carousel/Carousel.stories.tsx
+++ b/packages/circuit-ui/components/Carousel/Carousel.stories.tsx
@@ -15,6 +15,8 @@
import { useState } from 'react';
+import { images } from '../../../../.storybook/fixtures.js';
+
import { Container } from './components/Container/index.js';
import { Slides } from './components/Slides/index.js';
import { Slide } from './components/Slide/index.js';
@@ -32,7 +34,8 @@ import {
ANIMATION_DURATION,
SLIDE_DURATION,
} from './constants.js';
-import { SLIDES } from './__fixtures__/index.js';
+
+const slides = images.map((image) => ({ image }));
export default {
title: 'Components/Carousel',
@@ -47,7 +50,7 @@ export const Stateful = (args: CarouselProps) => (
);
Stateful.args = {
- slides: SLIDES,
+ slides,
slideDuration: SLIDE_DURATION,
animationDuration: ANIMATION_DURATION,
aspectRatio: ASPECT_RATIO,
@@ -61,7 +64,7 @@ Stateful.args = {
};
export const Composed = () => {
- const total = SLIDES.length;
+ const total = slides.length;
const [step, setStep] = useState(0);
const goBack = () => setStep(step === 0 ? total - 1 : step - 1);
const goForward = () => setStep(step === total - 1 ? 0 : step + 1);
@@ -70,7 +73,7 @@ export const Composed = () => {
- {SLIDES.map(({ image }, index) => (
+ {slides.map(({ image }, index) => (
{
-
+ Previous
-
+ Next
diff --git a/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.tsx b/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.tsx
index bd725fd1c3..d53ead8126 100644
--- a/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.tsx
+++ b/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.tsx
@@ -26,8 +26,8 @@ export default {
export const OnlyImage = (args: SlideProps) => (
);
@@ -35,8 +35,8 @@ export const OnlyImage = (args: SlideProps) => (
export const TextAndImage = (args: SlideProps) => (
{
diff --git a/packages/circuit-ui/components/CarouselPagination/CarouselPagination.mdx b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.mdx
new file mode 100644
index 0000000000..f0e0088a99
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.mdx
@@ -0,0 +1,25 @@
+import { Meta, Status, Props, Story } from '../../../../.storybook/components';
+import * as Stories from './CarouselPagination.stories';
+
+
+
+# CarouselPagination
+
+
+
+The carousel pagination component allows users to navigate content that is divided into multiple slides or pages.
+
+
+
+
+## Usage
+
+Consider adjusting the layout or using a different component if the number of slides/pages exceeds five.
+
+For inline use cases, create an illusion of continuity through cropped images or text, so users quickly understand that they can get more content by moving beyond the screen edge.
+
+Do not place the carousel pagination on busy, dynamic backgrounds (i.e. on images, illustrations), in order to ensure optimal contrast and discoverability.
+
+Ensure that content outside the viewport is searchable and becomes visible when focused. Consider lazy-loading content that is initially hidden to optimize performance.
+
+
diff --git a/packages/circuit-ui/components/CarouselPagination/CarouselPagination.module.css b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.module.css
new file mode 100644
index 0000000000..094fefb1c9
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.module.css
@@ -0,0 +1,55 @@
+.base {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+}
+
+.cue {
+ display: block;
+ padding: var(--cui-spacings-byte);
+ background: none;
+ border: none;
+ border-radius: var(--cui-border-radius-pill);
+}
+
+a.cue,
+button.cue {
+ cursor: pointer;
+}
+
+.shape {
+ display: block;
+ width: var(--cui-spacings-kilo);
+ height: var(--cui-spacings-kilo);
+ background-color: var(--cui-fg-placeholder);
+ border-radius: var(--cui-border-radius-pill);
+ transition:
+ background-color var(--cui-transitions-default),
+ width var(--cui-transitions-default);
+}
+
+a.cue:hover .shape,
+button.cue:hover .shape {
+ background-color: var(--cui-fg-placeholder-hovered);
+}
+
+a.cue:active .shape,
+button.cue:active .shape {
+ background-color: var(--cui-fg-placeholder-pressed);
+}
+
+.cue[aria-current] .shape {
+ width: var(--cui-spacings-peta);
+ background-color: var(--cui-bg-strong);
+}
+
+a.cue[aria-current]:hover .shape,
+button.cue[aria-current]:hover .shape {
+ background-color: var(--cui-bg-strong-hovered);
+}
+
+a.cue[aria-current]:active .shape,
+button.cue[aria-current]:active .shape {
+ background-color: var(--cui-bg-strong-pressed);
+}
diff --git a/packages/circuit-ui/components/CarouselPagination/CarouselPagination.spec.tsx b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.spec.tsx
new file mode 100644
index 0000000000..261f39818f
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.spec.tsx
@@ -0,0 +1,90 @@
+/**
+ * Copyright 2024, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it, vi } from 'vitest';
+import { createRef } from 'react';
+
+import { render, screen } from '../../util/test-utils.js';
+
+import { CarouselPagination } from './CarouselPagination.js';
+
+describe('CarouselPagination', () => {
+ const baseProps = {
+ slides: [
+ { id: 'foo', label: 'Foo' },
+ { id: 'bar', label: 'Bar' },
+ { id: 'baz', label: 'Baz' },
+ ],
+ currentId: 'foo',
+ };
+
+ it('should merge a custom class name with the default ones', () => {
+ const className = 'foo';
+ render();
+ const list = screen.getByRole('list');
+ expect(list?.className).toContain(className);
+ });
+
+ it('should forward a ref', () => {
+ const ref = createRef();
+ render();
+ const list = screen.getByRole('list');
+ expect(ref.current).toBe(list);
+ });
+
+ it('should render as buttons when the slides have onClick handlers', () => {
+ const slides = [
+ { id: 'foo', label: 'Foo', onClick: vi.fn() },
+ { id: 'bar', label: 'Bar', onClick: vi.fn() },
+ { id: 'baz', label: 'Baz', onClick: vi.fn() },
+ ];
+ render();
+ const buttons = screen.getAllByRole('button');
+ expect(buttons).toHaveLength(slides.length);
+ });
+
+ it('should render as links when the slides have href attributes', () => {
+ const slides = [
+ { id: 'foo', label: 'Foo', href: '#foo', onClick: vi.fn() },
+ { id: 'bar', label: 'Bar', href: '#bar', onClick: vi.fn() },
+ { id: 'baz', label: 'Baz', href: '#baz', onClick: vi.fn() },
+ ];
+ render();
+ const links = screen.getAllByRole('link');
+ expect(links).toHaveLength(slides.length);
+ });
+
+ it('should mark the current active item', () => {
+ const slides = [
+ { id: 'foo', label: 'Foo', href: '#foo' },
+ { id: 'bar', label: 'Bar', href: '#bar' },
+ { id: 'baz', label: 'Baz', href: '#baz' },
+ ];
+ render();
+ const currentSlide = screen.getByRole('link', { name: /foo/i });
+ expect(currentSlide).toHaveAttribute('aria-current');
+ });
+
+ it('should mark the current active item with the provided type', () => {
+ const slides = [
+ { id: 'foo', label: 'Foo', href: '#foo' },
+ { id: 'bar', label: 'Bar', href: '#bar' },
+ { id: 'baz', label: 'Baz', href: '#baz' },
+ ];
+ render();
+ const currentSlide = screen.getByRole('link', { name: /foo/i });
+ expect(currentSlide).toHaveAttribute('aria-current', 'page');
+ });
+});
diff --git a/packages/circuit-ui/components/CarouselPagination/CarouselPagination.stories.tsx b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.stories.tsx
new file mode 100644
index 0000000000..074387425f
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.stories.tsx
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2024, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState } from 'react';
+
+import {
+ CarouselPagination,
+ type CarouselPaginationProps,
+} from './CarouselPagination.js';
+import { Carousel as ExampleCarousel } from './examples/Carousel.js';
+
+export default {
+ title: 'Components/CarouselPagination',
+ component: CarouselPagination,
+};
+
+export const Base = (args: CarouselPaginationProps) => {
+ const [currentId, setCurrentId] = useState(args.currentId);
+
+ const slides = args.slides.map((slide) => ({
+ ...slide,
+ onClick: () => {
+ setCurrentId(slide.id);
+ },
+ }));
+
+ return ;
+};
+
+Base.args = {
+ slides: [
+ { id: 'foo', label: 'Foo' },
+ { id: 'bar', label: 'Bar' },
+ { id: 'baz', label: 'Baz' },
+ ],
+ currentId: 'foo',
+};
+
+export const Carousel = (args: CarouselPaginationProps) => (
+
+);
+
+Carousel.args = {
+ slides: [
+ {
+ id: 'coffee-transaction',
+ label: 'coffee-transaction',
+ href: '#coffee-transaction',
+ target: '_self',
+ image: { src: '/images/sumup-coffee-transaction.jpg', alt: '' },
+ },
+ {
+ id: 'product-catalog',
+ label: 'Product catalog',
+ href: '#product-catalog',
+ target: '_self',
+ image: { src: '/images/sumup-product-catalog.jpg', alt: '' },
+ },
+ {
+ id: 'solo-cradle',
+ label: 'Solo with cradle',
+ href: '#solo-cradle',
+ target: '_self',
+ image: { src: '/images/sumup-solo-cradle.jpg', alt: '' },
+ },
+ {
+ id: 'solo-printer',
+ label: 'Solo printer',
+ href: '#solo-printer',
+ target: '_self',
+ image: { src: '/images/sumup-solo-printer.jpg', alt: '' },
+ },
+ {
+ id: 'tablet-insights',
+ label: 'Tablet insights',
+ href: '#tablet-insights',
+ target: '_self',
+ image: { src: '/images/sumup-tablet-insights.jpg', alt: '' },
+ },
+ ],
+ currentId: 'coffee-transaction',
+};
+Carousel.parameters = {
+ chromatic: { disableSnapshot: true },
+};
diff --git a/packages/circuit-ui/components/CarouselPagination/CarouselPagination.tsx b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.tsx
new file mode 100644
index 0000000000..a7e2f5d2c1
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/CarouselPagination.tsx
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2024, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use client';
+
+import {
+ forwardRef,
+ type AnchorHTMLAttributes,
+ type ButtonHTMLAttributes,
+ type HTMLAttributes,
+} from 'react';
+
+import { useComponents } from '../ComponentsContext/useComponents.js';
+import type { AsPropType } from '../../types/prop-types.js';
+import { utilClasses } from '../../styles/utility.js';
+import { clsx } from '../../styles/clsx.js';
+
+import classes from './CarouselPagination.module.css';
+
+type LinkElProps = AnchorHTMLAttributes;
+type ButtonElProps = ButtonHTMLAttributes;
+
+export type Slide = LinkElProps &
+ ButtonElProps & {
+ /**
+ * A unique identifier
+ */
+ id: string | number;
+ /**
+ * A concise description of the slide content
+ */
+ label: string;
+ };
+
+export interface CarouselPaginationProps
+ extends HTMLAttributes {
+ /**
+ * The collection of slides that can be navigated using this component.
+ */
+ slides: Slide[];
+ /**
+ * The unique identifier of the current slide
+ */
+ currentId: string | number;
+ /**
+ * Specify the nature of the slide content for the [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) attribute.
+ *
+ * `page`: Represents the current page within a set of pages such as the link
+ * to the current document in a breadcrumb.
+ *
+ * `step`: Represents the current step within a process such as the current
+ * step in an enumerated multi step checkout flow.
+ */
+ type?: 'page' | 'step';
+}
+
+/**
+ * The carousel pagination component allows users to navigate content that is
+ * divided into multiple slides or pages.
+ */
+export const CarouselPagination = forwardRef<
+ HTMLUListElement,
+ CarouselPaginationProps
+>(({ slides, currentId, type, className, ...props }, ref) => {
+ const components = useComponents();
+ const Link = components.Link as AsPropType;
+
+ return (
+
+ {slides.map(({ id, label, ...item }) => {
+ let Element: AsPropType;
+ if (item.href) {
+ Element = Link;
+ } else if (item.onClick) {
+ Element = 'button';
+ } else {
+ Element = 'span';
+ }
+ return (
+ -
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+});
diff --git a/packages/circuit-ui/components/CarouselPagination/examples/Carousel.module.css b/packages/circuit-ui/components/CarouselPagination/examples/Carousel.module.css
new file mode 100644
index 0000000000..a690ccc52f
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/examples/Carousel.module.css
@@ -0,0 +1,39 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 100vw;
+}
+
+.slides {
+ display: flex;
+ gap: var(--cui-spacings-mega);
+ max-width: 100%;
+ padding: var(--cui-spacings-mega);
+ margin-right: calc(var(--cui-spacings-mega));
+ margin-left: calc(-1 * var(--cui-spacings-mega));
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+ scroll-padding: var(--cui-spacings-mega);
+ scrollbar-width: none;
+ scroll-behavior: smooth;
+}
+
+.slides::-webkit-scrollbar {
+ display: none;
+}
+
+.slide {
+ flex-shrink: 0;
+ width: 80vw;
+ height: min(60vw, 80vh);
+ scroll-snap-align: start;
+}
+
+.image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ background: var(--cui-bg-highlight);
+ border-radius: var(--cui-border-radius-kilo);
+}
diff --git a/packages/circuit-ui/components/CarouselPagination/examples/Carousel.tsx b/packages/circuit-ui/components/CarouselPagination/examples/Carousel.tsx
new file mode 100644
index 0000000000..054402c565
--- /dev/null
+++ b/packages/circuit-ui/components/CarouselPagination/examples/Carousel.tsx
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2025, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+
+import {
+ CarouselPagination,
+ type CarouselPaginationProps,
+} from '../CarouselPagination.js';
+
+import classes from './Carousel.module.css';
+
+type Slide = {
+ id: string;
+ label: string;
+ image?: {
+ src: string;
+ alt: string;
+ };
+};
+
+export interface CarouselProps extends CarouselPaginationProps {
+ slides: Slide[];
+}
+
+export function Carousel({
+ currentId: initialCurrentId,
+ slides,
+}: CarouselProps) {
+ const trackRef = useRef(null);
+ const [currentId, setCurrentId] = useState(initialCurrentId || slides[0].id);
+
+ useEffect(() => {
+ const trackEl = trackRef.current;
+
+ if (!trackEl) {
+ return undefined;
+ }
+
+ const handleScroll = () => {
+ const { scrollLeft, scrollWidth } = trackEl;
+ const index = Math.round((scrollLeft / scrollWidth) * slides.length);
+
+ const id = slides[index]?.id;
+
+ if (currentId !== id) {
+ setCurrentId(id);
+ }
+ };
+
+ trackEl.addEventListener('scroll', handleScroll);
+
+ return () => {
+ trackEl.removeEventListener('scroll', handleScroll);
+ };
+ }, [slides, currentId]);
+
+ return (
+
+
+ {slides.map((slide) => (
+
+ {slide.image && (
+

+ )}
+
+ ))}
+
+
+
+ );
+}
diff --git a/packages/circuit-ui/components/Carousel/__fixtures__/index.ts b/packages/circuit-ui/components/CarouselPagination/index.ts
similarity index 54%
rename from packages/circuit-ui/components/Carousel/__fixtures__/index.ts
rename to packages/circuit-ui/components/CarouselPagination/index.ts
index 90d108dd16..90fe00cfe3 100644
--- a/packages/circuit-ui/components/Carousel/__fixtures__/index.ts
+++ b/packages/circuit-ui/components/CarouselPagination/index.ts
@@ -1,5 +1,5 @@
/**
- * Copyright 2019, SumUp Ltd.
+ * Copyright 2024, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -13,23 +13,6 @@
* limitations under the License.
*/
-export const SLIDES = [
- {
- image: {
- src: '/images/illustration-waves.jpg',
- alt: 'Aerial photo of turbulent blue ocean waves',
- },
- },
- {
- image: {
- src: '/images/illustration-waves-2.jpg',
- alt: 'Aerial photo of turbulent turquoise ocean waves',
- },
- },
- {
- image: {
- src: '/images/illustration-waves-3.jpg',
- alt: 'Aerial photo of turbulent ocean water with white foam',
- },
- },
-];
+export { CarouselPagination } from './CarouselPagination.js';
+
+export type { CarouselPaginationProps } from './CarouselPagination.js';
diff --git a/packages/circuit-ui/components/Image/Image.stories.tsx b/packages/circuit-ui/components/Image/Image.stories.tsx
index 5e00679927..7a869a75de 100644
--- a/packages/circuit-ui/components/Image/Image.stories.tsx
+++ b/packages/circuit-ui/components/Image/Image.stories.tsx
@@ -28,6 +28,6 @@ export default {
export const Base = (args: ImageProps) => ;
Base.args = {
- src: '/images/illustration-waves.jpg',
- alt: 'Aerial photo of turbulent blue-turquoise ocean waves',
+ src: '/images/sumup-tablet-insights.jpg',
+ alt: 'A tablet shows sales insights in the SumUp POS app. On the table next to it are a SumUp Solo card reader with printer and two bowls with fruits',
};
diff --git a/packages/circuit-ui/components/Modal/Modal.stories.tsx b/packages/circuit-ui/components/Modal/Modal.stories.tsx
index 89ea759ad0..2dc0f90f15 100644
--- a/packages/circuit-ui/components/Modal/Modal.stories.tsx
+++ b/packages/circuit-ui/components/Modal/Modal.stories.tsx
@@ -201,8 +201,8 @@ CustomStyles.args = {
children: () => (
- {images.map((src) => (
+ {images.map((image) => (
))}
diff --git a/packages/circuit-ui/components/Step/examples/YesOrNoSlider.tsx b/packages/circuit-ui/components/Step/examples/YesOrNoSlider.tsx
index 90fa5fca5b..8ac2adaf2c 100644
--- a/packages/circuit-ui/components/Step/examples/YesOrNoSlider.tsx
+++ b/packages/circuit-ui/components/Step/examples/YesOrNoSlider.tsx
@@ -49,7 +49,7 @@ const Swipeable = ({
};
export interface YesOrNoSliderProps extends StepProps {
- images: string[];
+ images: { src: string; alt: string }[];
}
export function YesOrNoSlider({ images, ...stepProps }: YesOrNoSliderProps) {
@@ -84,8 +84,8 @@ export function YesOrNoSlider({ images, ...stepProps }: YesOrNoSliderProps) {
>
handleSwipe(eventData, actions)}>
diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts
index f654e1ab4a..d93a7938b5 100644
--- a/packages/circuit-ui/index.ts
+++ b/packages/circuit-ui/index.ts
@@ -195,6 +195,8 @@ export {
CarouselComposer,
} from './components/Carousel/index.js';
export type { CarouselProps } from './components/Carousel/index.js';
+export { CarouselPagination } from './components/CarouselPagination/index.js';
+export type { CarouselPaginationProps } from './components/CarouselPagination/index.js';
export { Avatar } from './components/Avatar/index.js';
export type { AvatarProps } from './components/Avatar/index.js';