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

Support infinite carousels #27

Merged
merged 1 commit into from
Sep 3, 2024
Merged
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
4 changes: 4 additions & 0 deletions src/use-snap-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export const useSnapCarousel = ({
const rect = getOffsetRect(item, item.parentElement);
if (
!currPage ||
// We allow items to explicitly mark themselves as snap points via the `data-should-snap`
// attribute. This allows callsites to augment and/or define their own "page" logic.
item.dataset.shouldSnap === 'true' ||
// Otherwise, we determine pages via the layout.
rect[farSidePos] - currPageStartPos > Math.ceil(scrollPort[dimension])
) {
acc.push([i]);
Expand Down
156 changes: 156 additions & 0 deletions stories/infinite-carousel.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
.root {
position: relative;
margin: 0 -1rem; /* bust out of storybook margin (to demonstrate full bleed carousel) */
}

.y.root {
margin: -1rem 0;
height: 100vh;
width: 300px;
display: flex;
flex-direction: column;
}

.scroll {
position: relative;
display: flex;
overflow: auto;
scroll-snap-type: x mandatory;
-ms-overflow-style: none;
scrollbar-width: none;
overscroll-behavior: contain;
scroll-padding: 0 16px;
padding: 0 16px;
}

.scroll::-webkit-scrollbar {
display: none;
}

.y .scroll {
display: block;
scroll-snap-type: y mandatory;
scroll-padding: 16px 0;
padding: 16px 0;
}

.item {
font-family: Futura, Trebuchet MS, Arial, sans-serif;
font-size: 125px;
line-height: 1;
width: 300px;
height: 300px;
max-width: 100%;
flex-shrink: 0;
color: white;
display: flex;
justify-content: end;
align-items: end;
padding: 16px 20px;
text-transform: uppercase;
text-shadow: 6px 6px 0px rgba(0, 0, 0, 0.2);
margin-right: 0.6rem;
overflow: hidden;
}

.scrollMargin .item:nth-child(9) {
scroll-margin-left: 200px;
background: black !important;
}

.item:last-child {
margin-right: 0;
}

.y .item {
margin-right: 0;
margin-bottom: 0.6rem;
}

.y .item:last-child {
margin-bottom: 0;
}

.pageIndicator {
font-family: Futura, Trebuchet MS, Arial, sans-serif;
font-weight: bold;
font-size: 14px;
position: absolute;
top: 10px;
right: 10px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.5);
pointer-events: none;
border-radius: 5px;
color: #374151;
}

.controls {
margin: 1rem 0;
display: flex;
justify-content: center;
align-items: center;
color: #374151;
padding: 0 1rem;
}

.prevButton,
.nextButton {
font-size: 18px;
transition: opacity 100ms ease-out;
}

.prevButton[disabled],
.nextButton[disabled] {
opacity: 0.4;
}

.pagination {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 0 10px;
}

.paginationItem {
display: flex;
justify-content: center;
}

.paginationButton {
display: block;
text-indent: -99999px;
overflow: hidden;
background: #374151;
width: 12px;
height: 12px;
border-radius: 50%;
margin: 5px;
transition: opacity 100ms ease-out;
}

.paginationItemActive .paginationButton {
opacity: 0.3;
}

@media only screen and (max-width: 480px) {
.item {
width: 280px;
height: 280px;
}

.pagination {
margin: 0 8px;
}

.prevButton,
.nextButton {
font-size: 15px;
}

.paginationButton {
width: 9px;
height: 9px;
margin: 4px;
}
}
168 changes: 168 additions & 0 deletions stories/infinite-carousel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import {
InfiniteCarousel,
InfiniteCarouselItem,
InfiniteCarouselRef
} from './infinite-carousel';
import { Button } from './lib/button';
import { Select } from './lib/select';

export default {
title: 'Infinite Carousel',
component: InfiniteCarousel
};

export const Default = () => {
const items = Array.from({ length: 18 }).map((_, index) => ({
id: index,
index
}));
return (
<InfiniteCarousel
items={items}
renderItem={({ item, index, isSnapPoint, shouldSnap }) => (
<InfiniteCarouselItem
key={index}
isSnapPoint={isSnapPoint}
shouldSnap={shouldSnap}
bgColor={getColor(item.index)}
>
{item.index + 1}
</InfiniteCarouselItem>
)}
/>
);
};

export const VariableWidth = () => {
const items = [
110, 300, 500, 120, 250, 300, 500, 400, 180, 300, 350, 700, 400, 230, 300
].map((width, index) => ({ id: index, index, width }));
return (
<InfiniteCarousel
items={items}
renderItem={({ item, index, isSnapPoint, shouldSnap }) => (
<InfiniteCarouselItem
key={index}
isSnapPoint={isSnapPoint}
shouldSnap={shouldSnap}
bgColor={getColor(item.index)}
width={item.width}
>
{item.index + 1}
</InfiniteCarouselItem>
)}
/>
);
};

export const VerticalAxis = () => {
const items = Array.from({ length: 18 }).map((_, index) => ({
id: index,
index
}));
return (
<InfiniteCarousel
axis="y"
items={items}
renderItem={({ item, index, isSnapPoint, shouldSnap }) => (
<InfiniteCarouselItem
key={index}
isSnapPoint={isSnapPoint}
shouldSnap={shouldSnap}
bgColor={getColor(item.index)}
>
{item.index + 1}
</InfiniteCarouselItem>
)}
/>
);
};

export const DynamicItems = () => {
const carouselRef = useRef<InfiniteCarouselRef>(null);
const [items, setItems] = useState(() =>
Array.from({ length: 6 }).map((_, index) => ({ id: index, index }))
);
const addItem = () => {
setItems((prev) => [...prev, { id: prev.length, index: prev.length }]);
};
const removeItem = () => {
setItems((prev) => prev.slice(0, -1));
};
useLayoutEffect(() => {
if (!carouselRef.current) {
return;
}
carouselRef.current.refresh();
}, [items]);
return (
<>
<div style={{ display: 'flex', gap: '10px', margin: '0 0 10px' }}>
<Button onClick={() => removeItem()}>Remove Item</Button>
<Button onClick={() => addItem()}>Add Item</Button>
</div>
<InfiniteCarousel
ref={carouselRef}
items={items}
renderItem={({ item, index, isSnapPoint, shouldSnap }) => (
<InfiniteCarouselItem
key={index}
isSnapPoint={isSnapPoint}
shouldSnap={shouldSnap}
bgColor={getColor(item.index)}
>
{item.index + 1}
</InfiniteCarouselItem>
)}
/>
</>
);
};

export const ScrollBehavior = () => {
const scrollBehaviors: ScrollBehavior[] = ['smooth', 'instant', 'auto'];
const [scrollBehavior, setScrollBehavior] = useState(scrollBehaviors[0]);
const items = Array.from({ length: 18 }).map((_, index) => ({
id: index,
index
}));
return (
<>
<div style={{ margin: '0 0 10px' }}>
<Select
onChange={(e) => {
setScrollBehavior(e.target.value as ScrollBehavior);
}}
value={scrollBehavior}
>
{scrollBehaviors.map((value) => (
<option key={value} value={value}>
{value.slice(0, 1).toUpperCase() + value.slice(1)}
</option>
))}
</Select>
</div>
<InfiniteCarousel
scrollBehavior={scrollBehavior}
items={items}
renderItem={({ item, index, isSnapPoint, shouldSnap }) => (
<InfiniteCarouselItem
key={index}
isSnapPoint={isSnapPoint}
shouldSnap={shouldSnap}
bgColor={getColor(item.index)}
>
{item.index + 1}
</InfiniteCarouselItem>
)}
/>
</>
);
};

/* Utils */

const getColor = (i: number) => {
return `hsl(-${i * 12} 100% 50%)`;
};
Loading
Loading