Skip to content

Commit db2d297

Browse files
authored
feat: adds carousel layout options (#701)
1 parent 5cde96f commit db2d297

File tree

4 files changed

+138
-91
lines changed

4 files changed

+138
-91
lines changed

.changeset/rare-radios-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@utilitywarehouse/native-ui': patch
3+
---
4+
5+
Adds layouts to Carousel component and fixes accessibility issues

packages/native-ui/src/lab/Carousel/Carousel.props.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface CarouselItemsProps
4040
| 'snapToAlignment'
4141
| 'viewabilityConfig'
4242
> {
43+
centered?: boolean;
4344
children?: ReactElement<CarouselItemProps> | Array<ReactElement<CarouselItemProps>>;
4445
enabled?: boolean;
4546
inactiveItemOpacity?: number;

packages/native-ui/src/lab/Carousel/Carousel.stories.tsx

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { Meta, StoryObj } from '@storybook/react-vite';
22
import { colors } from '@utilitywarehouse/colour-system';
3-
import { FC } from 'react';
3+
import { useState } from 'react';
4+
import { LayoutChangeEvent } from 'react-native';
45
import { StyleSheet } from 'react-native-unistyles';
5-
import { Carousel, CarouselItem, CarouselItems, CarouselPagination } from '.';
6-
import { Box, Text } from '../../components';
6+
import {
7+
Carousel,
8+
CarouselItem,
9+
CarouselItemsProps,
10+
CarouselItems,
11+
CarouselPagination,
12+
} from '.';
13+
import { Box, Heading, Text } from '../../components';
714

815
const meta = {
916
title: 'Stories / Carousel',
@@ -43,22 +50,27 @@ const meta = {
4350
// @ts-expect-error - Meta type mismatch
4451
enabled: true,
4552
inactiveItemOpacity: 1,
46-
itemWidth: 300,
4753
showOverflow: false,
48-
style: {},
49-
width: 300,
5054
},
5155
} satisfies Meta<typeof CarouselItems>;
5256

5357
export default meta;
5458
type Story = StoryObj<typeof meta>;
5559

56-
interface CarouselItemProps {
60+
interface CarouselItemCardProps {
5761
backgroundColor: string;
5862
title: string;
5963
}
6064

65+
interface CarouselExampleProps extends CarouselItemsProps {
66+
items: Array<any>;
67+
title: string;
68+
}
69+
6170
const styles = StyleSheet.create(theme => ({
71+
carousel: {
72+
marginBottom: theme.space[4],
73+
},
6274
carouselItem: {
6375
aspectRatio: 1.6,
6476
borderRadius: theme.radii.lg,
@@ -69,9 +81,12 @@ const styles = StyleSheet.create(theme => ({
6981
carouselItemTitle: {
7082
color: theme.colors.white,
7183
},
84+
title: {
85+
marginBottom: theme.space[2],
86+
},
7287
}));
7388

74-
const CarouselItemCard: FC<CarouselItemProps> = ({ backgroundColor, title }) => {
89+
const CarouselItemCard = ({ backgroundColor, title }: CarouselItemCardProps ) => {
7590
return (
7691
<Box style={[styles.carouselItem, { backgroundColor }]}>
7792
<Text style={styles.carouselItemTitle}>{title}</Text>
@@ -95,26 +110,74 @@ const items = [
95110
key: 3,
96111
title: '3333',
97112
},
113+
{
114+
color: colors.cyan800,
115+
key: 4,
116+
title: '4444',
117+
},
118+
{
119+
color: colors.pink700,
120+
key: 5,
121+
title: '5555',
122+
},
98123
];
99124

125+
const CarouselExample = ({ items, title, ...props }: CarouselExampleProps) => (
126+
<Box>
127+
<Heading style={styles.title} size="h4">{title}</Heading>
128+
<Carousel style={styles.carousel}>
129+
<CarouselItems {...props}>
130+
{items.map(({ color, key, title }) => (
131+
<CarouselItem key={key}>
132+
<CarouselItemCard
133+
backgroundColor={color}
134+
key={key}
135+
title={`•••• •••• •••• ${title}`}
136+
/>
137+
</CarouselItem>
138+
))}
139+
</CarouselItems>
140+
<CarouselPagination style={{ marginVertical: 16 }} />
141+
</Carousel>
142+
</Box>
143+
);
144+
100145
export const Playground: Story = {
101-
render: args => (
102-
<Box>
103-
<Carousel>
104-
{/* @ts-expect-error - Meta type mismatch */}
105-
<CarouselItems {...args}>
106-
{items.map(({ color, key, title }) => (
107-
<CarouselItem key={key}>
108-
<CarouselItemCard
109-
backgroundColor={color}
110-
key={key}
111-
title={`•••• •••• •••• ${title}`}
112-
/>
113-
</CarouselItem>
114-
))}
115-
</CarouselItems>
116-
<CarouselPagination style={{ marginVertical: 16 }} />
117-
</Carousel>
118-
</Box>
119-
),
146+
render: args => {
147+
const [width, setWidth] = useState(0);
148+
149+
const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {
150+
setWidth(nativeEvent.layout.width);
151+
};
152+
153+
const itemWidth = width * 0.8 + 16;
154+
155+
return (
156+
<Box onLayout={handleLayout}>
157+
<CarouselExample
158+
{...args}
159+
items={items}
160+
title="Full-width"
161+
width={width}
162+
/>
163+
<CarouselExample
164+
{...args}
165+
centered
166+
items={items}
167+
itemWidth={itemWidth}
168+
showOverflow
169+
title="Fixed-width, centered"
170+
width={width}
171+
/>
172+
<CarouselExample
173+
{...args}
174+
items={items}
175+
itemWidth={itemWidth}
176+
showOverflow
177+
title="Fixed-width, flex-start"
178+
width={width}
179+
/>
180+
</Box>
181+
);
182+
}
120183
};

packages/native-ui/src/lab/Carousel/CarouselItems.tsx

Lines changed: 42 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,14 @@ import {
99
useEffect,
1010
useMemo,
1111
} from 'react';
12-
import { AccessibilityActionEvent, FlatList, ViewStyle, ViewToken } from 'react-native';
12+
import { FlatList, ViewStyle, ViewToken } from 'react-native';
1313

14-
import { Box } from '../../';
1514
import CarouselContext from './Carousel.context';
1615
import { CarouselItemProps, CarouselItemsProps, CarouselRef } from './Carousel.props';
1716

18-
const clampToRange = (number: number, min: number, max: number) =>
19-
Math.min(Math.max(number, min), max);
20-
2117
export const CarouselItems = forwardRef(function CarouselItems(
2218
{
19+
centered = false,
2320
children,
2421
enabled = true,
2522
inactiveItemOpacity = 1,
@@ -32,20 +29,21 @@ export const CarouselItems = forwardRef(function CarouselItems(
3229
}: CarouselItemsProps,
3330
ref?: ForwardedRef<CarouselRef> | null
3431
) {
35-
const { activeIndex = 0, setActiveIndex, setNumItems } = useContext(CarouselContext);
32+
const {
33+
activeIndex = 0,
34+
setActiveIndex,
35+
setNumItems,
36+
} = useContext(CarouselContext);
3637

3738
// Ensure children is always converted to an array
3839
const items: Array<ReactElement<CarouselItemProps>> = useMemo(
3940
() => Children.map(children, child => child) || [],
4041
[children]
4142
);
42-
43+
4344
const innerMargin: number = width - (itemWidth || width);
44-
const containerStyles: ViewStyle = {
45-
width,
46-
};
47-
const carouselStyles: ViewStyle = {
48-
marginHorizontal: innerMargin / 2,
45+
const styles: ViewStyle = {
46+
marginHorizontal: centered ? innerMargin / 2 : 0,
4947
overflow: showOverflow ? 'visible' : 'hidden',
5048
};
5149

@@ -67,59 +65,39 @@ export const CarouselItems = forwardRef(function CarouselItems(
6765
[onSnapToItem, setActiveIndex]
6866
);
6967

70-
const handleAccessibilityAction = ({ nativeEvent }: AccessibilityActionEvent) => {
71-
const value = nativeEvent.actionName === 'increment' ? 1 : -1;
72-
const index = clampToRange(
73-
activeIndex + value,
74-
0,
75-
items && items.length ? items.length - 1 : 0
76-
);
77-
78-
if (ref && typeof ref !== 'function' && ref?.current) {
79-
ref.current.scrollToIndex({ index });
80-
}
81-
};
82-
8368
return (
84-
<Box style={[containerStyles, style]}>
85-
<FlatList<ReactElement<CarouselItemProps>>
86-
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
87-
accessibilityLabel="Carousel"
88-
accessibilityRole="adjustable"
89-
accessible={true}
90-
bounces={false} // Prevents bouncing at the start and end of carousel scrolling (iOS only)
91-
data={items}
92-
decelerationRate="fast"
93-
getItemLayout={(_, index) => ({
94-
length: itemWidth || width,
95-
offset: (itemWidth || width) * index,
96-
index,
97-
})}
98-
horizontal
99-
initialScrollIndex={activeIndex}
100-
pagingEnabled
101-
onAccessibilityAction={handleAccessibilityAction}
102-
onViewableItemsChanged={handleViewableItemsChanged}
103-
overScrollMode="never" // Prevents stretching of first and last items when reaching each end of the carousel (Android only)
104-
ref={ref}
105-
removeClippedSubviews={!showOverflow}
106-
renderItem={({ index, item }) =>
107-
cloneElement(item, {
108-
active: index === activeIndex,
109-
inactiveOpacity: inactiveItemOpacity,
110-
key: item?.key || item.props?.id || index,
111-
width: itemWidth || width,
112-
})
113-
}
114-
scrollEnabled={enabled}
115-
showsHorizontalScrollIndicator={false}
116-
snapToInterval={itemWidth || width}
117-
snapToAlignment="center"
118-
style={carouselStyles}
119-
viewabilityConfig={{ itemVisiblePercentThreshold: 51 }}
120-
{...props}
121-
/>
122-
</Box>
69+
<FlatList<ReactElement<CarouselItemProps>>
70+
bounces={false} // Prevents bouncing at the start and end of carousel scrolling (iOS only)
71+
data={items}
72+
decelerationRate="fast"
73+
getItemLayout={(_, index) => ({
74+
length: itemWidth || width,
75+
offset: (itemWidth || width) * index,
76+
index,
77+
})}
78+
horizontal
79+
initialScrollIndex={activeIndex}
80+
pagingEnabled
81+
onViewableItemsChanged={handleViewableItemsChanged}
82+
overScrollMode="never" // Prevents stretching of first and last items when reaching each end of the carousel (Android only)
83+
ref={ref}
84+
removeClippedSubviews={!showOverflow}
85+
renderItem={({ index, item }) =>
86+
cloneElement(item, {
87+
active: index === activeIndex,
88+
inactiveOpacity: inactiveItemOpacity,
89+
key: item?.key || item.props?.id || index,
90+
width: itemWidth || width,
91+
})
92+
}
93+
scrollEnabled={enabled}
94+
showsHorizontalScrollIndicator={false}
95+
snapToInterval={itemWidth || width}
96+
snapToAlignment="center"
97+
style={[styles, style]}
98+
viewabilityConfig={{ itemVisiblePercentThreshold: 51 }}
99+
{...props}
100+
/>
123101
);
124102
});
125103

0 commit comments

Comments
 (0)