Skip to content

Commit 1d23fcc

Browse files
MatiPl01Copilot
andauthored
feat: Fixed items support in sortable flex (#416)
## Description This PR adds support for fixed-order items in the sortable flex component that was requested in the #374 discussion. It also changes name from `fixed` to `fixed-order` as items in flex cannot be really fixed and their position still may change when other items are reordered because the sortable flex must follow the flex rendering behavior. ## Example recording https://github.com/user-attachments/assets/709be4ae-c510-457d-b29c-8e9d387fb5d2 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bd1778b commit 1d23fcc

10 files changed

Lines changed: 336 additions & 218 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { memo, useCallback, useState } from 'react';
2+
import { StyleSheet, Text, View } from 'react-native';
3+
import Sortable from 'react-native-sortables';
4+
5+
import { ScrollScreen } from '@/components';
6+
import { colors, radius, spacing, text } from '@/theme';
7+
import { getCategories } from '@/utils';
8+
9+
const DATA = getCategories(12);
10+
11+
const INITIAL_FIXED_ITEMS = new Set([DATA[1]!, DATA[5]!, DATA[6]!, DATA[11]!]);
12+
13+
export default function FixedOrderItemsExample() {
14+
const [fixedItems, setFixedItems] =
15+
useState<Set<string>>(INITIAL_FIXED_ITEMS);
16+
17+
const handleItemPress = useCallback((item: string) => {
18+
setFixedItems(oldItems => {
19+
const newItems = new Set(oldItems);
20+
if (newItems.has(item)) {
21+
newItems.delete(item);
22+
} else {
23+
newItems.add(item);
24+
}
25+
return newItems;
26+
});
27+
}, []);
28+
29+
return (
30+
<ScrollScreen contentContainerStyle={styles.container} includeNavBarHeight>
31+
<View style={styles.usageSection}>
32+
<Text style={text.heading2}>How to use this example?</Text>
33+
<Text style={text.body1}>
34+
Press on items to make them fixed or draggable.
35+
</Text>
36+
<Text style={text.body1}>
37+
Drag items that aren&apos;t grayed out to see that fixed items stay in
38+
the same ordinal position (index).
39+
</Text>
40+
</View>
41+
<Sortable.Flex columnGap={10} rowGap={10} customHandle>
42+
{DATA.map(item => (
43+
<FlexItem
44+
fixed={fixedItems.has(item)}
45+
item={item}
46+
key={item}
47+
onTap={handleItemPress}
48+
/>
49+
))}
50+
</Sortable.Flex>
51+
</ScrollScreen>
52+
);
53+
}
54+
55+
type FlexItemProps = {
56+
item: string;
57+
fixed: boolean;
58+
onTap: (item: string) => void;
59+
};
60+
61+
const FlexItem = memo(function FlexItem({ fixed, item, onTap }: FlexItemProps) {
62+
return (
63+
<Sortable.Touchable key={item} onTap={() => onTap(item)}>
64+
<Sortable.Handle mode={fixed ? 'fixed-order' : 'draggable'}>
65+
<View
66+
style={[
67+
styles.card,
68+
{ backgroundColor: fixed ? '#555' : colors.primary }
69+
]}>
70+
<Text style={styles.text}>{item}</Text>
71+
</View>
72+
</Sortable.Handle>
73+
</Sortable.Touchable>
74+
);
75+
});
76+
77+
const styles = StyleSheet.create({
78+
card: {
79+
alignItems: 'center',
80+
backgroundColor: '#36877F',
81+
borderRadius: radius.full,
82+
justifyContent: 'center',
83+
paddingHorizontal: spacing.md,
84+
paddingVertical: spacing.sm
85+
},
86+
container: {
87+
padding: spacing.md
88+
},
89+
text: {
90+
...text.label2,
91+
color: colors.white
92+
},
93+
usageSection: {
94+
gap: spacing.xs,
95+
marginBottom: spacing.md
96+
}
97+
});

example/app/src/examples/SortableFlex/features/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { default as DataChangeExample } from './DataChangeExample';
44
export { default as DebugExample } from './DebugExample';
55
export { default as DragHandleExample } from './DragHandleExample';
66
export { default as DropIndicatorExample } from './DropIndicatorExample';
7+
export { default as FixedOrderItemsExample } from './FixedOrderItemsExample';
78
export { default as FlexLayoutExample } from './FlexLayoutExample';
89
export { default as HorizontalAutoScrollExample } from './HorizontalAutoScrollExample';
910
export { default as TouchableExample } from './TouchableExample';

example/app/src/examples/SortableGrid/features/FixedItemsExample.tsx renamed to example/app/src/examples/SortableGrid/features/FixedOrderItemsExample.tsx

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,30 @@ const DATA = Array.from({ length: 12 }, (_, index) => `Item ${index + 1}`);
1010

1111
const INITIAL_FIXED_ITEMS = new Set(['Item 1', 'Item 5', 'Item 12']);
1212

13-
export default function FixedItemsExample() {
13+
export default function FixedOrderItemsExample() {
1414
const [fixedItems, setFixedItems] =
1515
useState<Set<string>>(INITIAL_FIXED_ITEMS);
1616

17-
const handleItemPress = useCallback(
18-
(item: string) => {
19-
if (fixedItems.has(item)) {
20-
fixedItems.delete(item);
17+
const handleItemPress = useCallback((item: string) => {
18+
setFixedItems(oldItems => {
19+
const newItems = new Set(oldItems);
20+
if (newItems.has(item)) {
21+
newItems.delete(item);
2122
} else {
22-
fixedItems.add(item);
23+
newItems.add(item);
2324
}
24-
setFixedItems(new Set(fixedItems));
25-
},
26-
[fixedItems]
27-
);
25+
return newItems;
26+
});
27+
}, []);
2828

2929
const renderItem = useCallback<SortableGridRenderItem<string>>(
30-
({ item }) => {
31-
const fixed = fixedItems.has(item);
32-
33-
return (
34-
<Sortable.Touchable onTap={() => handleItemPress(item)}>
35-
<Sortable.Handle mode={fixed ? 'fixed' : 'draggable'}>
36-
<GridItem fixed={fixed} item={item} />
37-
</Sortable.Handle>
38-
</Sortable.Touchable>
39-
);
40-
},
30+
({ item }) => (
31+
<GridItem
32+
fixed={fixedItems.has(item)}
33+
item={item}
34+
onTap={handleItemPress}
35+
/>
36+
),
4137
[fixedItems, handleItemPress]
4238
);
4339

@@ -68,17 +64,22 @@ export default function FixedItemsExample() {
6864
type GridItemProps = {
6965
item: string;
7066
fixed: boolean;
67+
onTap: (item: string) => void;
7168
};
7269

73-
const GridItem = memo(function GridItem({ fixed, item }: GridItemProps) {
70+
const GridItem = memo(function GridItem({ fixed, item, onTap }: GridItemProps) {
7471
return (
75-
<View
76-
style={[
77-
styles.card,
78-
{ backgroundColor: fixed ? '#555' : colors.primary }
79-
]}>
80-
<Text style={styles.text}>{item}</Text>
81-
</View>
72+
<Sortable.Touchable onTap={() => onTap(item)}>
73+
<Sortable.Handle mode={fixed ? 'fixed-order' : 'draggable'}>
74+
<View
75+
style={[
76+
styles.card,
77+
{ backgroundColor: fixed ? '#555' : colors.primary }
78+
]}>
79+
<Text style={styles.text}>{item}</Text>
80+
</View>
81+
</Sortable.Handle>
82+
</Sortable.Touchable>
8283
);
8384
});
8485

example/app/src/examples/SortableGrid/features/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export { default as DebugExample } from './DebugExample';
55
export { default as DifferentSizeItems } from './DifferentSizeItems';
66
export { default as DragHandleExample } from './DragHandleExample';
77
export { default as DropIndicatorExample } from './DropIndicatorExample';
8-
export { default as FixedItemsExample } from './FixedItemsExample';
8+
export { default as FixedOrderItemsExample } from './FixedOrderItemsExample';
99
export { default as HorizontalAutoScrollExample } from './HorizontalAutoScrollExample';
1010
export { default as MultiZoneExample } from './MultiZoneExample';
1111
export { default as OrderingStrategyExample } from './OrderingStrategyExample';

example/app/src/examples/navigation/routes.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ const routes: Routes = {
4848
name: 'Ordering Strategy'
4949
},
5050
FixedItems: {
51-
Component: SortableGrid.features.FixedItemsExample,
52-
name: 'Fixed Items'
51+
Component: SortableGrid.features.FixedOrderItemsExample,
52+
name: 'Fixed Order Items'
5353
},
5454
MultiZone: {
5555
Component: SortableGrid.features.MultiZoneExample,
@@ -127,6 +127,10 @@ const routes: Routes = {
127127
Component: SortableFlex.features.HorizontalAutoScrollExample,
128128
name: 'Horizontal Auto Scroll'
129129
},
130+
FixedItems: {
131+
Component: SortableFlex.features.FixedOrderItemsExample,
132+
name: 'Fixed Order Items'
133+
},
130134
Callbacks: {
131135
Component: SortableFlex.features.CallbacksExample,
132136
name: 'Callbacks'

packages/react-native-sortables/src/components/shared/CustomHandle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type CustomHandleProps = PropsWithChildren<{
1818
* - 'fixed': Item stays in place and cannot be dragged
1919
* @default 'draggable'
2020
*/
21-
mode?: 'draggable' | 'fixed' | 'non-draggable';
21+
mode?: 'draggable' | 'fixed-order' | 'non-draggable';
2222
}>;
2323

2424
export default function CustomHandle(props: CustomHandleProps) {
@@ -54,7 +54,7 @@ function CustomHandleComponent({
5454
const dragEnabled = mode === 'draggable';
5555

5656
useEffect(() => {
57-
return registerHandle(itemKey, handleRef, mode === 'fixed');
57+
return registerHandle(itemKey, handleRef, mode === 'fixed-order');
5858
}, [handleRef, itemKey, registerHandle, mode]);
5959

6060
const onLayout = useCallback(() => {

0 commit comments

Comments
 (0)