diff --git a/examples/bpk-component-carousel/example.js b/examples/bpk-component-carousel/example.js
new file mode 100644
index 0000000000..6e1d0453b8
--- /dev/null
+++ b/examples/bpk-component-carousel/example.js
@@ -0,0 +1,34 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 BpkCarousel from '../../packages/bpk-component-carousel'
+
+const imageUrls = [
+ "https://content.skyscnr.com/m/7470cf6a4ee49c26/original/Carousel-placeholder-4.jpg",
+ "https://content.skyscnr.com/m/183e7ddaaca13b16/original/Carousel-placeholder-2.jpg",
+ "https://content.skyscnr.com/m/f8b42e98e2b79a6/original/Carousel-placeholder-3.jpg",
+ "https://content.skyscnr.com/m/51c4c9dd04c8dc95/original/Carousel-placeholder-1.jpg",
+]
+
+const imagesList = imageUrls.map(url =>
)
+
+const DefaultExample = () => (
+
+);
+
+export default DefaultExample
diff --git a/examples/bpk-component-carousel/stories.js b/examples/bpk-component-carousel/stories.js
new file mode 100644
index 0000000000..926aa2c2f0
--- /dev/null
+++ b/examples/bpk-component-carousel/stories.js
@@ -0,0 +1,35 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 BpkCarousel from '../../packages/bpk-component-carousel'
+
+import DefaultExample from './example'
+
+export default {
+ title: 'bpk-component-carousel',
+ component: BpkCarousel,
+};
+
+
+export const Default = DefaultExample;
+
+export const VisualTest = DefaultExample;
+export const VisualTestWithZoom = VisualTest.bind({});
+VisualTestWithZoom.args = {
+ zoomEnabled: true
+};
diff --git a/packages/bpk-component-carousel/README.md b/packages/bpk-component-carousel/README.md
new file mode 100644
index 0000000000..813bb9889d
--- /dev/null
+++ b/packages/bpk-component-carousel/README.md
@@ -0,0 +1,28 @@
+# bpk-component-carousel
+
+> Backpack carousel component.
+
+## Description
+This component is used to display images in the form of a carousel, users can browse through images by swipe. It only works on mobile.
+
+## Installation
+
+Check the main [Readme](https://github.com/skyscanner/backpack#usage) for a complete installation guide.
+
+## Usage
+
+```tsx
+import BpkCarousel from '@skyscanner/backpack-web/bpk-component-carousel';
+
+const imageChangeHandler = () => {
+ console.log('Image Changed')
+}
+
+export default () => (
+ ]}
+ initialImageIndex={2}
+ onImageChanged={imageChangeHandler}
+ />
+ );
+```
diff --git a/packages/bpk-component-carousel/index.ts b/packages/bpk-component-carousel/index.ts
new file mode 100644
index 0000000000..c8bb9896f9
--- /dev/null
+++ b/packages/bpk-component-carousel/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2016 Skyscanner 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 BpkCarousel from "./src/BpkCarousel";
+
+export default BpkCarousel
diff --git a/packages/bpk-component-carousel/src/BpkCarousel-test.tsx b/packages/bpk-component-carousel/src/BpkCarousel-test.tsx
new file mode 100644
index 0000000000..f8adceab6f
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarousel-test.tsx
@@ -0,0 +1,93 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 type { ReactNode } from 'react';
+
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+
+import BpkCarousel from './BpkCarousel';
+
+
+describe('BpkCarousel', () => {
+ let images: ReactNode[]
+
+ const DemoImages = () => (
+
+ )
+
+ function generateDemoImages(count: number) {
+ return Array.from({ length: count }, (_, i) => ());
+ }
+
+ type TestCase = [
+ expectedCount: 7 | 5 | 4 | 12,
+ actualCount: number,
+ props: React.ComponentProps,
+ ];
+
+ beforeAll(() => {
+
+ window.HTMLElement.prototype.scrollIntoView = jest.fn();
+
+ (window as any).IntersectionObserver = class IntersectionObserver {
+ observe = jest.fn();
+
+ disconnect = jest.fn();
+
+ unobserve = jest.fn();
+ };
+ images = [, , , , ];
+ });
+
+ it('should render correctly', () => {
+ render();
+
+ expect(screen.getAllByRole('listitem').length).toBe(7);
+ expect(document.querySelectorAll('.bpk-page-indicator__indicator').length).toBe(
+ 5,
+ );
+ expect(document.querySelector('.bpk-carousel__page-indicator-over-image')).toBeTruthy();
+ });
+
+ it.each([
+ [12, 10, { images: generateDemoImages(10) }],
+ [7, 5, { images: generateDemoImages(5) }],
+ [5, 3, { images: generateDemoImages(3) }],
+ [4, 2, { images: generateDemoImages(2) }],
+ ])(
+ 'renders %i image(s) when there are %i image(s) available',
+ (expectedCount, actualCount, props) => {
+ render();
+
+ expect(screen.getByTestId('image-gallery-scroll-container').childElementCount).toBe(expectedCount);
+ },
+ );
+
+ it('renders only one image when only one available (no fake images for the infinite scroll)', async () => {
+ render();
+
+ expect(screen.getByTestId('image-gallery-scroll-container').childElementCount).toBe(1);
+ });
+
+ it('should render costom bottom', async () => {
+ render();
+
+ expect(screen.getByTestId('carousel-page-indicator-container')).toHaveStyle({bottom: '48px'});
+ });
+});
diff --git a/packages/bpk-component-carousel/src/BpkCarousel.module.scss b/packages/bpk-component-carousel/src/BpkCarousel.module.scss
new file mode 100644
index 0000000000..a251f0ade0
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarousel.module.scss
@@ -0,0 +1,34 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 '../../unstable__bpk-mixins/tokens';
+
+.bpk-carousel {
+ position: relative;
+
+ &__page-indicator-over-image {
+ position: absolute;
+ right: 0;
+ bottom: tokens.bpk-spacing-sm();
+ left: 0;
+ display: flex;
+ margin: auto tokens.bpk-spacing-base();
+ justify-content: center;
+ overflow: clip;
+ }
+}
diff --git a/packages/bpk-component-carousel/src/BpkCarousel.tsx b/packages/bpk-component-carousel/src/BpkCarousel.tsx
new file mode 100644
index 0000000000..3fbb16df89
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarousel.tsx
@@ -0,0 +1,73 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 { useRef, useState } from 'react';
+
+// @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
+import BpkPageIndicator, { VARIANT } from '../../bpk-component-page-indicator';
+import { cssModules } from '../../bpk-react-utils';
+
+import BpkCarouselContainer from './BpkCarouselContainer';
+import { useScrollToInitialImage } from './utils';
+
+import type { Props } from './types';
+
+import STYLES from './BpkCarousel.module.scss';
+
+const getClassName = cssModules(STYLES);
+
+const BpkCarousel = ({
+ bottom,
+ images,
+ initialImageIndex = 0,
+ onImageChanged = null,
+}: Props) => {
+ const [shownImageIndex, updateShownImageIndex] = useState(initialImageIndex);
+ const imagesRef = useRef>([]);
+
+ useScrollToInitialImage(initialImageIndex!, imagesRef);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default BpkCarousel;
diff --git a/packages/bpk-component-carousel/src/BpkCarouselContainer.module.scss b/packages/bpk-component-carousel/src/BpkCarouselContainer.module.scss
new file mode 100644
index 0000000000..ad60435057
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarouselContainer.module.scss
@@ -0,0 +1,35 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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.
+ */
+
+.bpk-carousel-container {
+ display: grid;
+ width: 100%;
+ height: 100%;
+ grid: 1fr / auto-flow 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ backface-visibility: hidden;
+ overscroll-behavior: contain;
+ scroll-snap-type: x mandatory;
+ scrollbar-width: none;
+ user-select: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
diff --git a/packages/bpk-component-carousel/src/BpkCarouselContainer.tsx b/packages/bpk-component-carousel/src/BpkCarouselContainer.tsx
new file mode 100644
index 0000000000..db8f017462
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarouselContainer.tsx
@@ -0,0 +1,107 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 type { MutableRefObject, ReactNode } from 'react';
+import { memo, useState } from 'react';
+
+import { cssModules } from '../../bpk-react-utils';
+
+import BpkCarouselImage from './BpkCarouselImage';
+import { useIntersectionObserver } from './utils';
+
+import type { OnImageChangedHandler } from './types';
+
+import STYLES from './BpkCarouselContainer.module.scss';
+
+const getClassName = cssModules(STYLES);
+
+type Props = {
+ images: ReactNode[];
+ onVisible: (visibleIndex: number) => void;
+ imagesRef: MutableRefObject>;
+ onImageChanged: OnImageChangedHandler
+};
+
+const BpkScrollContainer = memo(({ images, imagesRef, onImageChanged, onVisible }: Props) => {
+ const [root, setRoot] = useState(null);
+ const observeImageChange = useIntersectionObserver(onVisible, {
+ root,
+ threshold: 0.5,
+ }, onImageChanged);
+ const observeCycleScroll = useIntersectionObserver(
+ (index) => {
+ const imageElement = imagesRef.current && imagesRef.current[index];
+ if (imageElement) {
+ imageElement.scrollIntoView({
+ block: 'nearest',
+ inline: 'start',
+ });
+ }
+ },
+ { root, threshold: 1 },
+ );
+
+ if (images.length === 1) {
+ return (
+
+ );
+});
+
+export default BpkScrollContainer;
diff --git a/packages/bpk-component-carousel/src/BpkCarouselImage.module.scss b/packages/bpk-component-carousel/src/BpkCarouselImage.module.scss
new file mode 100644
index 0000000000..491e9e9ce9
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarouselImage.module.scss
@@ -0,0 +1,38 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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.
+ */
+
+.bpk-carousel-image {
+ display: inline-flex;
+ width: 100%;
+ min-width: 0;
+ height: 100%;
+ min-height: 0;
+ justify-content: center;
+ scroll-snap-align: start;
+ scroll-snap-stop: always;
+
+ /* helps with flickering when cycle scroll */
+ /* stylelint-disable-next-line order/properties-order */
+ isolation: isolate;
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: initial;
+ }
+}
diff --git a/packages/bpk-component-carousel/src/BpkCarouselImage.tsx b/packages/bpk-component-carousel/src/BpkCarouselImage.tsx
new file mode 100644
index 0000000000..e49690fd0b
--- /dev/null
+++ b/packages/bpk-component-carousel/src/BpkCarouselImage.tsx
@@ -0,0 +1,45 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 type { ReactNode } from "react";
+import { forwardRef } from "react";
+
+import { cssModules } from "../../bpk-react-utils";
+
+import STYLES from "./BpkCarouselImage.module.scss"
+
+const getClassName = cssModules(STYLES);
+
+type ImageProps = {
+ image: ReactNode;
+ index: number;
+};
+const BpkCarouselImage = forwardRef(({ image, index }, ref) => (
+
+ {image}
+
+));
+
+export default BpkCarouselImage
+
diff --git a/packages/bpk-component-carousel/src/accessibility-test.tsx b/packages/bpk-component-carousel/src/accessibility-test.tsx
new file mode 100644
index 0000000000..21f7e34411
--- /dev/null
+++ b/packages/bpk-component-carousel/src/accessibility-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2016 Skyscanner 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 { render } from '@testing-library/react';
+import { axe } from 'jest-axe';
+
+import BpkCarousel from './BpkCarousel';
+
+const DemoImages = () => (
+
+)
+const images = [, , , , ]
+
+
+describe('BpkCarousel accessibility tests', () => {
+
+ beforeAll(() => {
+ window.HTMLElement.prototype.scrollIntoView = jest.fn();
+
+ (window as any).IntersectionObserver = class IntersectionObserver {
+ observe = jest.fn();
+
+ disconnect = jest.fn();
+
+ unobserve = jest.fn();
+ };
+ })
+
+ it('should not have programmatically-detectable accessibility issues', async () => {
+ const { container } = render();
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/bpk-component-carousel/src/types.ts b/packages/bpk-component-carousel/src/types.ts
new file mode 100644
index 0000000000..b2520fcad5
--- /dev/null
+++ b/packages/bpk-component-carousel/src/types.ts
@@ -0,0 +1,31 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 type { ReactNode } from "react";
+
+export type OnImageChangedHandler = ((shownImageIndex: number) => void) | null | undefined;
+
+export type Props = {
+ images: ReactNode[]
+ initialImageIndex?: number;
+ onImageChanged?: OnImageChangedHandler
+ /**
+ * This prop is used to let the consumer adjust the spacing between the page indicator and the bottom of the image when variant is VARIANT.overImage
+ */
+ bottom?: number;
+};
diff --git a/packages/bpk-component-carousel/src/utils.tsx b/packages/bpk-component-carousel/src/utils.tsx
new file mode 100644
index 0000000000..7d6aa3096d
--- /dev/null
+++ b/packages/bpk-component-carousel/src/utils.tsx
@@ -0,0 +1,82 @@
+/*
+ * Backpack - Skyscanner's Design System
+ *
+ * Copyright 2022 Skyscanner 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 { useEffect, useMemo, useRef } from 'react';
+import type { MutableRefObject } from 'react';
+
+import type { OnImageChangedHandler } from './types';
+
+export function useScrollToInitialImage(
+ initialImageIndex: number,
+ imagesRef: MutableRefObject>,
+) {
+ useEffect(() => {
+ const element = imagesRef.current[initialImageIndex];
+ if (element) {
+ element.scrollIntoView({
+ block: 'nearest',
+ inline: 'start',
+ });
+ }
+ }, [initialImageIndex, imagesRef]);
+}
+
+export function useIntersectionObserver(
+ onIntersecting: (index: number) => void,
+ { root, threshold }: IntersectionObserverInit,
+ onImageChanged?: OnImageChangedHandler,
+) {
+ const callbackRef = useRef(onIntersecting);
+
+
+ useEffect(() => {
+ callbackRef.current = onIntersecting;
+ });
+
+ const observe = useMemo<(element: HTMLElement | null) => void>(() => {
+ if (!root) return () => {};
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const shownEntry = entries.find((entry) => entry.isIntersecting);
+ if (!shownEntry) {
+ return;
+ }
+ const { index } = (shownEntry.target as HTMLElement).dataset;
+ if (index) {
+ const currentIndex = parseInt(index, 10);
+ callbackRef.current(currentIndex);
+ if (onImageChanged) {
+ onImageChanged(currentIndex)
+ }
+ }
+ },
+ { root, threshold },
+ );
+
+ const observeElement = (element: HTMLElement | null) => {
+
+ if (element && observer) {
+ observer.observe(element);
+ }
+ };
+
+ return observeElement;
+ }, [onImageChanged, root, threshold]);
+
+ return observe;
+}
diff --git a/packages/bpk-component-page-indicator/src/BpkPageIndicator-test.js b/packages/bpk-component-page-indicator/src/BpkPageIndicator-test.js
index 62784902d6..c5bb266a1e 100644
--- a/packages/bpk-component-page-indicator/src/BpkPageIndicator-test.js
+++ b/packages/bpk-component-page-indicator/src/BpkPageIndicator-test.js
@@ -17,9 +17,10 @@
*/
/* @flow strict */
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
-import BpkPageIndicator, { VARIANT } from './BpkPageIndicator';
+import BpkPageIndicator from './BpkPageIndicator';
let props;
@@ -31,30 +32,28 @@ describe('BpkPageIndicator', () => {
indicatorLabel: 'Go to slide',
prevNavLabel: 'Previous slide',
nextNavLabel: 'Next slide',
+ onClick: jest.fn(),
};
});
it('should render correctly', () => {
- const { asFragment } = render();
- expect(asFragment()).toMatchSnapshot();
+ render();
+
+ expect(screen.getAllByRole('button').length).toBe(7);
});
it('should support custom class names', () => {
- const { asFragment } = render(
+ render(
,
);
- expect(asFragment()).toMatchSnapshot();
+
+ expect(document.querySelector('.custom-classname')).toBeTruthy();
});
it('should support showNav attribute', () => {
- const { asFragment } = render();
- expect(asFragment()).toMatchSnapshot();
- });
+ render();
- it('should support style attribute', () => {
- const { asFragment } = render(
- ,
- );
- expect(asFragment()).toMatchSnapshot();
+ expect(screen.getByLabelText('Previous slide')).toBeTruthy();
+ expect(screen.getByLabelText('Next slide')).toBeTruthy();
});
});
diff --git a/packages/bpk-component-page-indicator/src/BpkPageIndicator.js b/packages/bpk-component-page-indicator/src/BpkPageIndicator.js
index 42cf808a95..c862299009 100644
--- a/packages/bpk-component-page-indicator/src/BpkPageIndicator.js
+++ b/packages/bpk-component-page-indicator/src/BpkPageIndicator.js
@@ -57,78 +57,102 @@ const BpkPageIndicator = ({
showNav,
totalIndicators,
variant,
-}: Props) => (
- // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.
-
+}: Props) => {
+ /**
+ * This validation is used to avoid an a11y issue when onClick isn't available.
+ * In this case, we can set aria-hidden = true to let screen reader skip reading page indicator dots.
+ * and render the dot as div rather than button to align with aria-hidden = true.
+ */
+ const isInteractive = !!onClick;
+
+ return (
+ // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.