Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backpack carousel #3058

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/bpk-component-carousel/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import BpkCarousel from '../../packages/bpk-component-carousel'

const DemoImages = () => (
<img src="https://d2xf5gjipzd8cd.cloudfront.net/available/949043373/949043373_343x132.jpg" alt='' />
)

const imagesList = [<DemoImages />,<img src='https://d2xf5gjipzd8cd.cloudfront.net/available/758391025/758391025_343x132.jpg' alt='' />, <DemoImages />, <DemoImages />, <DemoImages />, <DemoImages />]


const DefaultExample = () => (
<BpkCarousel images={imagesList} initialImageIndex={1}/>
);

export default DefaultExample
11 changes: 11 additions & 0 deletions examples/bpk-component-carousel/stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import BpkCarousel from '../../packages/bpk-component-carousel'

import DefaultExample from './example'

export default {
title: 'bpk-component-carousel',
component: BpkCarousel,
};


export const Default = DefaultExample;
25 changes: 25 additions & 0 deletions packages/bpk-component-carousel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# bpk-component-boilerplate

> Backpack carousel component.

## Installation

Check the main [Readme](https://github.com/skyscanner/backpack#usage) for a complete installation guide.

## Usage

```tsx
import BpkBoilerplate from '@skyscanner/backpack-web/bpk-component-carousel';

const imageChangeHandler = () => {
console.log('Image Changed')
}

export default () => (
<BpkCarousel
images={[ <img src="https://url/1_WxH.jpg" alt='image' />]}
initialImageIndex={2}
onImageChanged={imageChangeHandler}
/>
);
```
21 changes: 21 additions & 0 deletions packages/bpk-component-carousel/index.ts
Original file line number Diff line number Diff line change
@@ -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";

export default BpkCarousel
65 changes: 65 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarousel-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { render, screen, getAllByRole } from '@testing-library/react';
import { axe } from 'jest-axe';
import userEvent from '@testing-library/user-event';
import type { ReactNode } from 'react';

import BpkCarousel from './BpkCarousel';


describe('BpkCarousel', () => {
let images: ReactNode[]

const DemoImages = () => (
<img src="https://d2xf5gjipzd8cd.cloudfront.net/available/949043373/949043373_343x132.jpg" alt='' />
)

function generateDemoImages(count: number) {
return Array.from({ length: count }, (_, i) => (<DemoImages />));
}

type TestCase = [
expectedCount: 7 | 5 | 4 | 12,
actualCount: number,
props: React.ComponentProps<typeof BpkCarousel>,
];

beforeAll(() => {

window.HTMLElement.prototype.scrollIntoView = jest.fn();

(window as any).IntersectionObserver = class IntersectionObserver {
observe = jest.fn();

disconnect = jest.fn();

unobserve = jest.fn();
};
images = [<DemoImages />, <DemoImages />, <DemoImages />, <DemoImages />, <DemoImages />];
});

it('should render correctly', () => {
const { asFragment } = render(<BpkCarousel images={images} />);

expect(asFragment()).toMatchSnapshot();
});

it.each<TestCase>([
[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(<BpkCarousel {...props} />);

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(<BpkCarousel images={[images[0]]} />);

expect(screen.getByTestId('image-gallery-scroll-container').childElementCount).toBe(1);
});
});
35 changes: 35 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarousel.d.ts
Original file line number Diff line number Diff line change
@@ -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 { OnImageChangedHandler } from "./types";
import type { ReactNode } from 'react';

export type Props = {
images: ReactNode[]
initialImageIndex?: number;
onImageChanged?: OnImageChangedHandler
};

declare const BpkCarousel: {
({ initialImageIndex, onImageChanged, ...rest }: Props): JSX.Element;
defaultProps: {
initialImageIndex: 0;
onImageChanged: null
};
};
export default BpkCarousel;
35 changes: 35 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useRef, useState } from 'react';


import { useScrollToInitialImage } from './utils';
import BpkCarouselContainer from './BpkCarouselContainer';
import BpkSlidesIndicator from './BpkCarouselSlidesIndicator';
import type { Props } from './types';

const BpkCarousel = ({
images,
initialImageIndex = 0,
onImageChanged = null
}: Props) => {
const [shownImageIndex, updateShownImageIndex] = useState(initialImageIndex);
const imagesRef = useRef<Array<HTMLElement | null>>([]);

useScrollToInitialImage(initialImageIndex, imagesRef);

return (
<>
<BpkCarouselContainer
images={images}
onVisible={updateShownImageIndex}
imagesRef={imagesRef}
onImageChanged={onImageChanged}
/>
<BpkSlidesIndicator
length={images.length}
activeIndex={shownImageIndex}
/>
</>
);
};

export default BpkCarousel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/
@keyframes bpk-keyframe-spin{100%{transform:rotate(1turn)}}.bpk-carousel-container{display:grid;width:100%;height:100%;grid:1fr / auto-flow 100%;border-radius:.75rem;overflow-x:auto;overflow-y:hidden;backface-visibility:hidden;overscroll-behavior:contain;scroll-snap-type:x mandatory;scrollbar-width:none;user-select:none}@media (max-width: 32.25rem){.bpk-carousel-container{border-radius:0}}.bpk-carousel-container::-webkit-scrollbar{display:none}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import '~bpk-mixins/index.scss';

.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;
}
}
87 changes: 87 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarouselContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { MutableRefObject, ReactNode } from 'react';
import { memo, useState } from 'react';

import { cssModules } from '../../bpk-react-utils';

import { useIntersectionObserver } from './utils';
import STYLES from './BpkCarouselContainer.module.scss';
import BpkCarouselImage from './BpkCarouselImage';
import type { OnImageChangedHandler } from './types';

const getClassName = cssModules(STYLES);

type Props = {
images: ReactNode[];
onVisible: (visibleIndex: number) => void;
imagesRef: MutableRefObject<Array<HTMLElement | null>>;
onImageChanged: OnImageChangedHandler
};

const BpkScrollContainer = memo(({ images, imagesRef, onImageChanged, onVisible }: Props) => {
const [root, setRoot] = useState<HTMLElement | null>(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 (
<div className={getClassName('bpk-carousel-container')} role="list" data-testid="image-gallery-scroll-container">
<BpkCarouselImage image={images[0]} index={0} />
</div>
);
}

return (
<div
className={getClassName('bpk-carousel-container')}
ref={setRoot}
data-testid="image-gallery-scroll-container"
role="list"
>
<BpkCarouselImage
image={images[images.length - 1]}
index={images.length - 1}
ref={(el) => {
observeCycleScroll(el);
observeImageChange(el);
}}
/>
{images.map((image, index) => (
<BpkCarouselImage
// eslint-disable-next-line react/no-array-index-key
key={index}
image={image}
index={index}
ref={(el) => {
// eslint-disable-next-line no-param-reassign
imagesRef.current[index] = el;
observeImageChange(el);
}}
/>
))}
<BpkCarouselImage
image={images[0]}
index={0}
ref={(el) => {
observeCycleScroll(el);
observeImageChange(el);
}}
/>
</div>
);
});

export default BpkScrollContainer;
18 changes: 18 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarouselImage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/
@keyframes bpk-keyframe-spin{100%{transform:rotate(1turn)}}.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;isolation:isolate}.bpk-carousel-image img{max-width:100%;max-height:100%;border-radius:.75rem;object-fit:cover}@media (max-width: 32.25rem){.bpk-carousel-image img{border-radius:0;object-fit:contain}}
28 changes: 28 additions & 0 deletions packages/bpk-component-carousel/src/BpkCarouselImage.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import '~bpk-mixins/index.scss';

.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%;
border-radius: $bpk-border-radius-md;
object-fit: cover;

@include bpk-breakpoint-mobile {
border-radius: 0;
object-fit: contain;
}
}
}
Loading
Loading