Commit d0cb59e
authored
feat: Fixed items support for Sortable.Grid (#310)
## Description
This PR adds support for fixed items for the `Sortable.Grid` component.
Thanks @tpaksu for a proposed solution in #305
## Example recordings
- Example 1 - without data change
- Example 2 - with data change (added/removed/inserted items)
| Example 1 | Example 2 |
|-|-|
| <video
src="https://github.com/user-attachments/assets/8b0999d1-5a2d-4684-96cc-8162b4e233c3"
/> | <video
src="https://github.com/user-attachments/assets/5ac8f434-3766-4962-8279-a4e69e066f10"
/> |
<details>
<summary>Example 1 code snippet</summary>
```tsx
import { useCallback } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import type { SortableGridRenderItem } from 'react-native-sortables';
import Sortable from 'react-native-sortables';
import { ScrollScreen } from '@/components';
import { colors, radius, sizes, spacing, text } from '@/theme';
const DATA = Array.from({ length: 12 }, (_, index) => `Item ${index + 1}`);
export default function PlaygroundExample() {
const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item, index }) => {
const fixed =
index === 0 || index === 4 || index === 9 || index === DATA.length - 1;
return (
<Sortable.Handle mode={fixed ? 'fixed' : 'draggable'}>
<View
style={[
styles.card,
{
backgroundColor: fixed ? colors.secondary : colors.primary
}
]}>
<Text style={styles.text}>{item}</Text>
</View>
</Sortable.Handle>
);
},
[]
);
return (
<ScrollScreen contentContainerStyle={styles.container} includeNavBarHeight>
<Sortable.Grid
columnGap={10}
columns={3}
data={DATA}
customHandle
renderItem={renderItem}
rowGap={10}
/>
</ScrollScreen>
);
}
const styles = StyleSheet.create({
card: {
alignItems: 'center',
borderRadius: radius.md,
height: sizes.xl,
justifyContent: 'center'
},
container: {
padding: spacing.md
},
text: {
...text.label2,
color: colors.white
}
});
```
</details>
<details>
<summary>Example 2 code snippet</summary>
```tsx
import { memo, useCallback, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, { useAnimatedRef } from 'react-native-reanimated';
import Sortable, { type SortableGridRenderItem } from 'react-native-sortables';
import {
Button,
GridCard,
Group,
Screen,
Section,
Stagger
} from '@/components';
import { IS_WEB } from '@/constants';
import { colors, flex, spacing, text } from '@/theme';
import { getItems } from '@/utils';
const AVAILABLE_DATA = getItems(18);
const COLUMNS = 4;
const FIXED_KEYS = new Set(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
export default function DataChangeExample() {
const scrollableRef = useAnimatedRef<Animated.ScrollView>();
const [data, setData] = useState(AVAILABLE_DATA.slice(0, 12));
const getNewItemName = useCallback((currentData: Array<string>) => {
if (currentData.length >= AVAILABLE_DATA.length) {
return null;
}
for (const item of AVAILABLE_DATA) {
if (!currentData.includes(item)) {
return item;
}
}
return null;
}, []);
const prependItem = useCallback(() => {
setData(prevData => {
const newItem = getNewItemName(prevData);
if (newItem) {
return [newItem, ...prevData];
}
return prevData;
});
}, [getNewItemName]);
const insertItem = useCallback(() => {
setData(prevData => {
const newItem = getNewItemName(prevData);
if (newItem) {
const index = Math.floor(Math.random() * (prevData.length - 1));
return [...prevData.slice(0, index), newItem, ...prevData.slice(index)];
}
return prevData;
});
}, [getNewItemName]);
const appendItem = useCallback(() => {
setData(prevData => {
const newItem = getNewItemName(prevData);
if (newItem) {
return [...prevData, newItem];
}
return prevData;
});
}, [getNewItemName]);
const shuffleItems = useCallback(() => {
setData(prevData => {
const shuffledData = [...prevData];
for (let i = shuffledData.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledData[i], shuffledData[j]] = [
shuffledData[j]!,
shuffledData[i]!
];
}
return shuffledData;
});
}, []);
const sortItems = useCallback(() => {
setData(prevData =>
[...prevData].sort((a, b) => +a.split(' ')[1]! - +b.split(' ')[1]!)
);
}, []);
const onRemoveItem = useCallback((item: string) => {
setData(prevData => prevData.filter(i => i !== item));
}, []);
const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item }) => (
<Sortable.Handle mode={FIXED_KEYS.has(item) ? 'fixed' : 'draggable'}>
<GridItem
item={item}
onRemoveItem={onRemoveItem}
fixed={FIXED_KEYS.has(item)}
/>
</Sortable.Handle>
),
[onRemoveItem]
);
const additionDisabled = data.length >= AVAILABLE_DATA.length;
const reorderDisabled = data.length < 2;
const menuSections = [
{
buttons: [
{ disabled: additionDisabled, onPress: prependItem, title: 'Prepend' },
{ disabled: additionDisabled, onPress: insertItem, title: 'Insert' },
{ disabled: additionDisabled, onPress: appendItem, title: 'Append' }
],
description: 'Prepend/Insert/Append items to the list',
title: 'Modify number of items'
},
{
buttons: [
{ disabled: reorderDisabled, onPress: shuffleItems, title: 'Shuffle' },
{ disabled: reorderDisabled, onPress: sortItems, title: 'Sort' }
],
description: 'Reorder items in the list',
title: 'Change order of items'
}
];
return (
<Screen includeNavBarHeight>
{/* Need to set flex: 1 for the ScrollView parent component in order
// to ensure that it occupies the entire available space */}
<Stagger
wrapperStye={index =>
index === 2 ? (IS_WEB ? flex.shrink : flex.fill) : {}
}>
{menuSections.map(({ buttons, description, title }) => (
<Section description={description} key={title} title={title}>
<View style={styles.row}>
{buttons.map(btnProps => (
<Button {...btnProps} key={btnProps.title} />
))}
</View>
</Section>
))}
<Group padding='none' style={[flex.fill, styles.scrollViewGroup]}>
<Animated.ScrollView
contentContainerStyle={styles.scrollViewContent}
ref={scrollableRef}
// @ts-expect-error - overflowY is needed for proper behavior on web
style={[flex.fill, IS_WEB && { overflowY: 'scroll' }]}>
<Group withMargin={false} bordered center>
<Text style={styles.title}>Above SortableGrid</Text>
</Group>
<Sortable.Grid
columnGap={spacing.sm}
columns={COLUMNS}
data={data}
renderItem={renderItem}
rowGap={spacing.xs}
scrollableRef={scrollableRef}
animateHeight
customHandle
hapticsEnabled
onDragEnd={({ data: newData }) => setData(newData)}
/>
<Group withMargin={false} bordered center>
<Text style={styles.title}>Below SortableGrid</Text>
</Group>
</Animated.ScrollView>
</Group>
</Stagger>
</Screen>
);
}
type GridItemProps = {
item: string;
fixed: boolean;
onRemoveItem: (item: string) => void;
};
// It is recommended to use memo for items to prevent re-renders of the entire grid
// on item order changes (renderItem takes and index argument, thus it must be called
// after every order change)
const GridItem = memo(function GridItem({
item,
onRemoveItem,
fixed
}: GridItemProps) {
return (
<Sortable.Pressable onPress={onRemoveItem.bind(null, item)}>
<GridCard style={fixed && { backgroundColor: '#999' }}>{item}</GridCard>
</Sortable.Pressable>
);
});
const styles = StyleSheet.create({
row: {
columnGap: spacing.sm,
flexDirection: 'row',
flexWrap: 'wrap',
rowGap: spacing.xs
},
scrollViewContent: {
gap: spacing.sm,
padding: spacing.sm
},
scrollViewGroup: {
overflow: 'hidden',
paddingHorizontal: spacing.none,
paddingVertical: spacing.none
},
title: {
...text.subHeading2,
color: colors.foreground3
}
});
```
</details>1 parent 2355424 commit d0cb59e
File tree
20 files changed
+253
-193
lines changed- packages/react-native-sortables/src
- providers
- layout
- flex/updates/insert
- grid/updates
- types
- layout
- props
- providers
- utils
- reanimated
20 files changed
+253
-193
lines changedLines changed: 4 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
62 | | - | |
63 | | - | |
| 63 | + | |
| 64 | + | |
64 | 65 | | |
65 | 66 | | |
66 | 67 | | |
| |||
Lines changed: 30 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
23 | 26 | | |
24 | | - | |
| 27 | + | |
25 | 28 | | |
26 | 29 | | |
27 | 30 | | |
| |||
41 | 44 | | |
42 | 45 | | |
43 | 46 | | |
44 | | - | |
| 47 | + | |
45 | 48 | | |
46 | 49 | | |
47 | 50 | | |
| |||
54 | 57 | | |
55 | 58 | | |
56 | 59 | | |
57 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
58 | 67 | | |
59 | 68 | | |
60 | 69 | | |
| |||
63 | 72 | | |
64 | 73 | | |
65 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
66 | 83 | | |
67 | 84 | | |
68 | 85 | | |
| |||
85 | 102 | | |
86 | 103 | | |
87 | 104 | | |
88 | | - | |
89 | | - | |
| 105 | + | |
| 106 | + | |
90 | 107 | | |
91 | 108 | | |
92 | 109 | | |
| |||
95 | 112 | | |
96 | 113 | | |
97 | 114 | | |
98 | | - | |
99 | | - | |
| 115 | + | |
| 116 | + | |
100 | 117 | | |
101 | 118 | | |
102 | 119 | | |
103 | 120 | | |
104 | 121 | | |
105 | 122 | | |
106 | 123 | | |
107 | | - | |
108 | | - | |
| 124 | + | |
| 125 | + | |
109 | 126 | | |
110 | 127 | | |
111 | 128 | | |
112 | 129 | | |
113 | | - | |
| 130 | + | |
114 | 131 | | |
115 | 132 | | |
116 | 133 | | |
| |||
Lines changed: 12 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 42 | + | |
| 43 | + | |
50 | 44 | | |
51 | 45 | | |
52 | 46 | | |
| |||
273 | 267 | | |
274 | 268 | | |
275 | 269 | | |
276 | | - | |
| 270 | + | |
| 271 | + | |
277 | 272 | | |
278 | 273 | | |
279 | 274 | | |
| |||
463 | 458 | | |
464 | 459 | | |
465 | 460 | | |
466 | | - | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
467 | 468 | | |
468 | 469 | | |
469 | 470 | | |
| |||
Lines changed: 4 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | | - | |
| 235 | + | |
| 236 | + | |
236 | 237 | | |
237 | 238 | | |
238 | 239 | | |
| |||
249 | 250 | | |
250 | 251 | | |
251 | 252 | | |
252 | | - | |
| 253 | + | |
| 254 | + | |
253 | 255 | | |
254 | 256 | | |
255 | 257 | | |
Lines changed: 10 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
| 15 | + | |
19 | 16 | | |
20 | 17 | | |
21 | 18 | | |
22 | 19 | | |
23 | 20 | | |
| 21 | + | |
24 | 22 | | |
25 | 23 | | |
26 | 24 | | |
| |||
236 | 234 | | |
237 | 235 | | |
238 | 236 | | |
239 | | - | |
| 237 | + | |
| 238 | + | |
240 | 239 | | |
241 | 240 | | |
242 | 241 | | |
| |||
245 | 244 | | |
246 | 245 | | |
247 | 246 | | |
248 | | - | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
249 | 251 | | |
250 | 252 | | |
251 | 253 | | |
252 | 254 | | |
253 | | - | |
| 255 | + | |
254 | 256 | | |
255 | 257 | | |
Lines changed: 33 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
6 | 9 | | |
7 | 10 | | |
8 | 11 | | |
| |||
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
| 26 | + | |
23 | 27 | | |
24 | 28 | | |
25 | 29 | | |
26 | 30 | | |
27 | | - | |
| 31 | + | |
| 32 | + | |
28 | 33 | | |
29 | 34 | | |
30 | | - | |
31 | | - | |
| 35 | + | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
37 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
38 | 63 | | |
39 | 64 | | |
40 | 65 | | |
| |||
Lines changed: 27 additions & 28 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
41 | 40 | | |
42 | | - | |
43 | | - | |
44 | | - | |
| 41 | + | |
| 42 | + | |
45 | 43 | | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 44 | + | |
53 | 45 | | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
64 | 64 | | |
65 | | - | |
66 | | - | |
67 | | - | |
| 65 | + | |
| 66 | + | |
68 | 67 | | |
69 | 68 | | |
70 | 69 | | |
| |||
0 commit comments