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 =>
hotel bedroom
) + +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 = () => ( + hotel bedroom + ) + + 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 ( +
+ +
+ ); + } + + return ( +
+ { + observeCycleScroll(el); + observeImageChange(el); + }} + /> + {images.map((image, index) => ( + { + // eslint-disable-next-line no-param-reassign + imagesRef.current[index] = el; + observeImageChange(el); + }} + /> + ))} + { + observeCycleScroll(el); + observeImageChange(el); + }} + /> +
+ ); +}); + +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 = () => ( + hotel bedroom +) +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'.
- {showNav && ( - - )} -
-
START_SCROLL_INDEX - ? { +
+ {showNav && ( + + )} +
+
START_SCROLL_INDEX + ? { '--scroll-index': totalIndicators > DISPLAYED_TOTAL ? Math.min( - currentIndex - START_SCROLL_INDEX, - totalIndicators - DISPLAYED_TOTAL, - ) + currentIndex - START_SCROLL_INDEX, + totalIndicators - DISPLAYED_TOTAL, + ) : 0, } - : undefined - } - > - {[...Array(totalIndicators)].map((val, index) => ( -
+ {showNav && ( + + )}
- {showNav && ( - - )}
-
-); + ) +}; BpkPageIndicator.propTypes = { - indicatorLabel: PropTypes.string.isRequired, - prevNavLabel: PropTypes.string.isRequired, - nextNavLabel: PropTypes.string.isRequired, + indicatorLabel: PropTypes.string, + prevNavLabel: PropTypes.string, + nextNavLabel: PropTypes.string, currentIndex: PropTypes.number.isRequired, totalIndicators: PropTypes.number.isRequired, variant: PropTypes.oneOf(Object.keys(VARIANT)), diff --git a/packages/bpk-component-page-indicator/src/__snapshots__/BpkPageIndicator-test.js.snap b/packages/bpk-component-page-indicator/src/__snapshots__/BpkPageIndicator-test.js.snap deleted file mode 100644 index 875e68e665..0000000000 --- a/packages/bpk-component-page-indicator/src/__snapshots__/BpkPageIndicator-test.js.snap +++ /dev/null @@ -1,292 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BpkPageIndicator should render correctly 1`] = ` - -
-
-
-
-
-
-
-
-
-`; - -exports[`BpkPageIndicator should support custom class names 1`] = ` - -
-
-
-
-
-
-
-
-
-`; - -exports[`BpkPageIndicator should support showNav attribute 1`] = ` - -
-
- -
-
-
-
- -
-
-
-`; - -exports[`BpkPageIndicator should support style attribute 1`] = ` - -
-
-
-
-
-
-
-
-
-`;