Skip to content

Commit 0b84a39

Browse files
Add prev/next callbacks (#1342)
* Add prev/next callbacks * Replace onNext/onPrev with onChange * Update types * Add changeset * Update examples * Update .changeset/happy-hornets-yell.md * Update .changeset/happy-hornets-yell.md * Update .changeset/happy-hornets-yell.md * Update .changeset/happy-hornets-yell.md
1 parent df85b29 commit 0b84a39

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

.changeset/happy-hornets-yell.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@obosbbl/grunnmuren-react": patch
3+
---
4+
5+
Adding an `onChange` prop to the `<Carousel>` component. This prop can be used to track navigation within the `<Carousel>`.
6+
The `onChange` callback is triggered when a user navigates to a new item in the Carousel. The argument to the callback is an object containing `index` of the new item scrolled into view and the `id` of that item (if set on the `<CarouselItem>`). It also provides `prevIndex` which is the index of the previous item that was in view. And `prevId`, which is the id of the previous item that was in view (if set on the `<CarouselItem>`)
7+
8+
Usage:
9+
10+
``` tsx
11+
<Carousel
12+
onChange={({ id, index, prevId, prevIndex }) => {
13+
console.log(`
14+
Carousel changed to item with id: "${id}" and index: ${index}.
15+
The previous item id was: "${prevId}" and index: ${prevIndex}.
16+
This indicates that the user navigated to the ${prevIndex < index ? 'next' : 'previous'} item.
17+
`);
18+
}}
19+
>
20+
<CarouselItems>
21+
<CarouselItem id="first">
22+
<Media>
23+
<img
24+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/f_auto,c_limit,w_2048,q_auto/v1582122753/Boligprosjekter/Oslo/Ulven/Ulven-N%C3%A6romr%C3%A5de-Oslo-OBOS-Construction-city.jpg"
25+
alt=""
26+
/>
27+
</Media>
28+
</CarouselItem>
29+
<CarouselItem id="second">
30+
<Media>
31+
<img
32+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/v1587988823/Boligprosjekter/Oslo/Frysjaparken/Frysjalia/Frysjaparken_interi%C3%B8r_30.jpg"
33+
alt=""
34+
/>
35+
</Media>
36+
</CarouselItem>
37+
<CarouselItem id="third">
38+
<Media fit="contain">
39+
<img
40+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/f_auto,c_limit,w_1080,q_auto:best/t_2_3/v1747985572/Temasider/Folk/Hans%20Petter%20%20-%20Trang%20f%C3%B8dsel/Obos-Hans-Petter-Aaserud-Photo-Einar-Aslaksen-03093_web.jpg"
41+
alt=""
42+
/>
43+
</Media>
44+
</CarouselItem>
45+
</CarouselItems>
46+
</Carousel>
47+
```

packages/react/src/carousel/carousel.stories.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,52 @@ type Story = StoryObj<typeof Carousel>;
6262
export const StandardWithLeadAndImage: Story = {
6363
args: {},
6464
};
65+
66+
export const WithNavigationCallbacks = () => (
67+
<main className="container grid gap-y-8">
68+
<Carousel
69+
onChange={({ id, index, prevId, prevIndex }) => {
70+
console.log(`
71+
Carousel changed to item with id: "${id}" and index: ${index}.
72+
The previous item id was: "${prevId}" and index: ${prevIndex}.
73+
This indicates that the user navigated to the ${prevIndex < index ? 'next' : 'previous'} item.
74+
`);
75+
}}
76+
>
77+
<CarouselItems>
78+
<CarouselItem id="first">
79+
<Media>
80+
<img
81+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/f_auto,c_limit,w_2048,q_auto/v1582122753/Boligprosjekter/Oslo/Ulven/Ulven-N%C3%A6romr%C3%A5de-Oslo-OBOS-Construction-city.jpg"
82+
alt=""
83+
/>
84+
</Media>
85+
</CarouselItem>
86+
<CarouselItem id="second">
87+
<Media>
88+
<img
89+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/v1587988823/Boligprosjekter/Oslo/Frysjaparken/Frysjalia/Frysjaparken_interi%C3%B8r_30.jpg"
90+
alt=""
91+
/>
92+
</Media>
93+
</CarouselItem>
94+
<CarouselItem id="third">
95+
<Media fit="contain">
96+
<img
97+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/f_auto,c_limit,w_1080,q_auto:best/t_2_3/v1747985572/Temasider/Folk/Hans%20Petter%20%20-%20Trang%20f%C3%B8dsel/Obos-Hans-Petter-Aaserud-Photo-Einar-Aslaksen-03093_web.jpg"
98+
alt=""
99+
/>
100+
</Media>
101+
</CarouselItem>
102+
<CarouselItem id="fourth">
103+
<Media>
104+
<img
105+
src="https://res.cloudinary.com/obosit-prd-ch-clry/image/upload/v1699879884/Boligprosjekter/Oslo/Frysjaparken/Ager/Originale%20bilder/OBOS_Frysja-Ager-Illustrasjon_av_Frysja_torg_i_Ager_borettslag.jpg"
106+
alt=""
107+
/>
108+
</Media>
109+
</CarouselItem>
110+
</CarouselItems>
111+
</Carousel>
112+
</main>
113+
);

packages/react/src/carousel/carousel.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,31 @@ import { MediaContext } from '../content';
99
import { translations } from '../translations';
1010
import { useLocale } from '../use-locale';
1111

12+
type CarouselItem = Pick<CarouselItemProps, 'id'> & {
13+
/** The index of the item that is currently in view */
14+
index: number;
15+
/** The index of the previous item that was in view */
16+
prevIndex: number;
17+
/** The id of the previous item that was in view */
18+
prevId?: CarouselItemProps['id'];
19+
};
20+
1221
type CarouselProps = {
1322
/** The <CarouselItem/> components to be displayed within the carousel. */
1423
children: React.ReactNode;
1524
/** Additional CSS className for the element. */
1625
className?: string;
26+
/**
27+
* Callback that is triggered when a user navigates to new item in the Carousel.
28+
* The argument to the callback is an object containing `index` of the new item scrolled into view and the `id` of that item (if set on the `<CarouselItem>`)
29+
* It also provides `prevIndex` which is the index of the previous item that was in view
30+
* And `prevId`, which is the id of the previous item that was in view (if set on the `<CarouselItem>`)
31+
* @param item { index: number; id?: string; prevIndex: number; prevId?: string }
32+
*/
33+
onChange?: (item: CarouselItem) => void;
1734
};
1835

19-
const Carousel = ({ className, children }: CarouselProps) => {
36+
const Carousel = ({ className, children, onChange }: CarouselProps) => {
2037
const ref = useRef<HTMLDivElement>(null);
2138
const locale = useLocale();
2239
const { previous, next } = translations;
@@ -38,6 +55,10 @@ const Carousel = ({ className, children }: CarouselProps) => {
3855
);
3956
}, [scrollTargetIndex]);
4057

58+
// Keep track of the previous index to determine if the user is scrolling forward or backward
59+
// This is used to determine which callback to call (onPrev or onNext)
60+
const prevIndex = useRef(0);
61+
4162
// Handle scrolling when user clicks the arrow icons
4263
useUpdateEffect(() => {
4364
if (!ref.current) return;
@@ -47,6 +68,17 @@ const Carousel = ({ className, children }: CarouselProps) => {
4768
inline: 'start',
4869
block: 'nearest',
4970
});
71+
72+
if (prevIndex.current !== scrollTargetIndex && onChange) {
73+
onChange({
74+
index: scrollTargetIndex,
75+
id: ref.current.children[scrollTargetIndex]?.id,
76+
prevIndex: prevIndex.current,
77+
prevId: ref.current.children[prevIndex.current]?.id,
78+
});
79+
}
80+
81+
prevIndex.current = scrollTargetIndex;
5082
}, [scrollTargetIndex]);
5183

5284
const onScroll = useDebouncedCallback(
@@ -227,13 +259,15 @@ type CarouselItemProps = {
227259
children: React.ReactNode;
228260
/** Additional CSS className for the element. */
229261
className?: string;
262+
id?: string;
230263
};
231264

232-
const CarouselItem = ({ className, children }: CarouselItemProps) => {
265+
const CarouselItem = ({ className, children, id }: CarouselItemProps) => {
233266
return (
234267
<div
235268
className={cx(className, 'shrink-0 basis-full snap-start')}
236269
data-slot="carousel-item"
270+
id={id}
237271
>
238272
<Provider
239273
values={[

0 commit comments

Comments
 (0)