Skip to content

Commit

Permalink
Add ability to direct page logic via data attr in order to better sup…
Browse files Browse the repository at this point in the history
…port infinite carousels (#27)
  • Loading branch information
richardscarrott authored Sep 3, 2024
1 parent a7108cb commit d02829a
Show file tree
Hide file tree
Showing 4 changed files with 511 additions and 0 deletions.
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

0 comments on commit d02829a

Please # to comment.