Skip to content

Commit 757f3f1

Browse files
authored
Feat/sortable list locked items (#2296)
* Improve types * Add inert support * inert -> locked and improve UI * Improve readability * Use types (and fix typescript)
1 parent 0ce2c6e commit 757f3f1

File tree

6 files changed

+92
-45
lines changed

6 files changed

+92
-45
lines changed

demo/src/screens/componentScreens/SortableListScreen.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
import _ from 'lodash';
22
import React, {useCallback, useState, useRef} from 'react';
33
import {StyleSheet} from 'react-native';
4-
import {SortableList, View, TouchableOpacity, Text, Icon, Assets, Colors, Button} from 'react-native-ui-lib';
4+
import {
5+
SortableList,
6+
SortableListItemProps,
7+
View,
8+
TouchableOpacity,
9+
Text,
10+
Icon,
11+
Assets,
12+
Colors,
13+
Button
14+
} from 'react-native-ui-lib';
515
import {renderHeader} from '../ExampleScreenPresenter';
616

7-
interface Item {
8-
originalIndex: number;
9-
id: string;
17+
interface Item extends SortableListItemProps {
18+
text: string;
1019
}
1120

12-
const data = _.times(30, index => {
21+
const data: Item[] = _.times(30, index => {
22+
let text = `${index}`;
23+
if (index === 3) {
24+
text = 'Locked item';
25+
}
26+
1327
return {
14-
originalIndex: index,
15-
id: `${index}`
28+
text,
29+
id: `${index}`,
30+
locked: index === 3
1631
};
1732
});
1833

@@ -57,23 +72,26 @@ const SortableListScreen = () => {
5772

5873
const renderItem = useCallback(({item, index: _index}: {item: Item; index: number}) => {
5974
const isSelected = selectedItems.includes(item);
75+
const {locked} = item;
76+
const Container = locked ? View : TouchableOpacity;
6077
return (
61-
<TouchableOpacity
78+
<Container
6279
style={[styles.itemContainer, isSelected && styles.selectedItemContainer]}
6380
onPress={() => toggleItemSelection(item)}
6481
// overriding the BG color to anything other than white will cause Android's elevation to fail
6582
// backgroundColor={Colors.red30}
6683
centerV
84+
centerH={locked}
6785
paddingH-page
6886
>
6987
<View flex row spread centerV>
70-
<Icon source={Assets.icons.demo.drag} tintColor={Colors.$iconDisabled}/>
71-
<Text center $textDefault>
72-
{item.originalIndex}
88+
{!locked && <Icon source={Assets.icons.demo.drag} tintColor={Colors.$iconDisabled}/>}
89+
<Text center $textDefault={!locked} $textNeutralLight={locked}>
90+
{item.text}
7391
</Text>
74-
<Icon source={Assets.icons.demo.chevronRight} tintColor={Colors.$iconDefault}/>
92+
{!locked && <Icon source={Assets.icons.demo.chevronRight} tintColor={Colors.$iconDefault}/>}
7593
</View>
76-
</TouchableOpacity>
94+
</Container>
7795
);
7896
},
7997
[selectedItems, toggleItemSelection]);

src/components/sortableList/SortableListContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {createContext} from 'react';
22
import {ViewProps} from 'react-native';
33
import {SharedValue} from 'react-native-reanimated';
4+
import {Data, SortableListItemProps} from './types';
45

5-
export interface SortableListContextType {
6-
data: any
6+
export interface SortableListContextType<ItemT extends SortableListItemProps> {
7+
data: Data<ItemT>;
78
itemsOrder: SharedValue<string[]>;
9+
lockedIds: SharedValue<Dictionary<boolean>>;
810
onChange: () => void;
911
itemHeight: SharedValue<number>;
1012
onItemLayout: ViewProps['onLayout'];

src/components/sortableList/SortableListItem.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import {useDidUpdate} from 'hooks';
1717
import SortableListContext from './SortableListContext';
1818
import usePresenter from './usePresenter';
1919
import {HapticService, HapticType} from '../../services';
20-
export interface SortableListItemProps {
20+
export interface InternalSortableListItemProps {
2121
index: number;
2222
}
2323

24-
type Props = PropsWithChildren<SortableListItemProps>;
24+
type Props = PropsWithChildren<InternalSortableListItemProps>;
2525

2626
const animationConfig = {
2727
easing: Easing.inOut(Easing.ease),
@@ -36,12 +36,14 @@ const SortableListItem = (props: Props) => {
3636
itemHeight,
3737
onItemLayout,
3838
itemsOrder,
39+
lockedIds,
3940
onChange,
4041
enableHaptic,
4142
scale: propsScale = 1
4243
} = useContext(SortableListContext);
4344
const {getTranslationByIndexChange, getItemIndexById, getIndexByPosition, getIdByItemIndex} = usePresenter();
4445
const id: string = data[index].id;
46+
const locked: boolean = data[index].locked;
4547
const initialIndex = useSharedValue<number>(map(data, 'id').indexOf(id));
4648
const currIndex = useSharedValue(initialIndex.value);
4749
const translateY = useSharedValue<number>(0);
@@ -76,6 +78,7 @@ const SortableListItem = (props: Props) => {
7678

7779
const dragOnLongPressGesture = Gesture.Pan()
7880
.activateAfterLongPress(250)
81+
.enabled(!locked)
7982
.onStart(() => {
8083
isDragging.value = true;
8184
translateY.value = getTranslationByIndexChange(currIndex.value, initialIndex.value, itemHeight.value);
@@ -100,8 +103,16 @@ const SortableListItem = (props: Props) => {
100103
newIndex = Math.sign(newIndex - oldIndex) + oldIndex;
101104
}
102105

103-
const itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
106+
let itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
104107

108+
// Skip locked item(s)
109+
while (lockedIds.value[itemIdToSwap]) {
110+
const skipDirection = Math.sign(newIndex - oldIndex);
111+
newIndex = skipDirection + newIndex;
112+
itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
113+
}
114+
115+
// Swap items
105116
if (itemIdToSwap !== undefined) {
106117
const newItemsOrder = [...itemsOrder.value];
107118
newItemsOrder[newIndex] = id;

src/components/sortableList/index.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,31 @@
11
/* eslint-disable react-hooks/exhaustive-deps */
2-
import {map, mapKeys} from 'lodash';
2+
import {map, mapKeys, filter, reduce} from 'lodash';
33
import React, {useMemo, useCallback} from 'react';
4-
import {FlatList, FlatListProps, LayoutChangeEvent} from 'react-native';
4+
import {FlatList, LayoutChangeEvent} from 'react-native';
55
import {useSharedValue} from 'react-native-reanimated';
66
import {GestureHandlerRootView} from 'react-native-gesture-handler';
7-
import SortableListContext, {SortableListContextType} from './SortableListContext';
7+
import SortableListContext from './SortableListContext';
88
import SortableListItem from './SortableListItem';
99
import {useDidUpdate, useThemeProps} from 'hooks';
10+
import {SortableListProps, SortableListItemProps} from './types';
11+
export {SortableListProps, SortableListItemProps};
1012

11-
interface ItemWithId {
12-
id: string;
13-
}
14-
15-
export interface SortableListProps<ItemT extends ItemWithId>
16-
extends Omit<FlatListProps<ItemT>, 'extraData' | 'data'>,
17-
Pick<SortableListContextType, 'scale'> {
18-
/**
19-
* The data of the list, do not update the data.
20-
*/
21-
data: FlatListProps<ItemT>['data'];
22-
/**
23-
* A callback to get the new order (or swapped items).
24-
*/
25-
onOrderChange: (data: ItemT[] /* TODO: add more data? */) => void;
26-
/**
27-
* Whether to enable the haptic feedback
28-
* (please note that react-native-haptic-feedback does not support the specific haptic type on Android starting on an unknown version, you can use 1.8.2 for it to work properly)
29-
*/
30-
enableHaptic?: boolean;
13+
function generateItemsOrder<ItemT extends SortableListItemProps>(data: SortableListProps<ItemT>['data']) {
14+
return map(data, item => item.id);
3115
}
3216

33-
function generateItemsOrder<ItemT extends ItemWithId>(data: SortableListProps<ItemT>['data']) {
34-
return map(data, item => item.id);
17+
function generateLockedIds<ItemT extends SortableListItemProps>(data: SortableListProps<ItemT>['data']) {
18+
return reduce(filter(data, item => item.locked),
19+
(item, cur) => ({...item, [(cur as ItemT).id]: true}),
20+
{});
3521
}
3622

37-
const SortableList = <ItemT extends ItemWithId>(props: SortableListProps<ItemT>) => {
23+
const SortableList = <ItemT extends SortableListItemProps>(props: SortableListProps<ItemT>) => {
3824
const themeProps = useThemeProps(props, 'SortableList');
3925
const {data, onOrderChange, enableHaptic, scale, ...others} = themeProps;
4026

4127
const itemsOrder = useSharedValue<string[]>(generateItemsOrder(data));
28+
const lockedIds = useSharedValue<Dictionary<boolean>>(generateLockedIds(data));
4229
const itemHeight = useSharedValue<number>(52);
4330

4431
useDidUpdate(() => {
@@ -67,6 +54,7 @@ const SortableList = <ItemT extends ItemWithId>(props: SortableListProps<ItemT>)
6754
return {
6855
data,
6956
itemsOrder,
57+
lockedIds,
7058
onChange,
7159
itemHeight,
7260
onItemLayout,

src/components/sortableList/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {FlatListProps} from 'react-native';
2+
import {SortableListContextType} from './SortableListContext';
3+
4+
export interface SortableListItemProps {
5+
id: string;
6+
locked?: boolean;
7+
}
8+
9+
// Internal
10+
export type Data<ItemT extends SortableListItemProps> = FlatListProps<ItemT>['data'];
11+
12+
export interface SortableListProps<ItemT extends SortableListItemProps>
13+
extends Omit<FlatListProps<ItemT>, 'extraData' | 'data'>,
14+
Pick<SortableListContextType<ItemT>, 'scale'> {
15+
/**
16+
* The data of the list, do not update the data.
17+
*/
18+
data: Data<ItemT>;
19+
/**
20+
* A callback to get the new order (or swapped items).
21+
*/
22+
onOrderChange: (data: ItemT[] /* TODO: add more data? */) => void;
23+
/**
24+
* Whether to enable the haptic feedback
25+
* (please note that react-native-haptic-feedback does not support the specific haptic type on Android starting on an unknown version, you can use 1.8.2 for it to work properly)
26+
*/
27+
enableHaptic?: boolean;
28+
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export {default as SharedTransition} from './components/sharedTransition';
132132
export {default as SkeletonView, SkeletonViewProps} from './components/skeletonView';
133133
export {default as Slider, SliderProps} from './components/slider';
134134
export {default as SortableGridList, SortableGridListProps} from './components/sortableGridList';
135-
export {default as SortableList, SortableListProps} from './components/sortableList';
135+
export {default as SortableList, SortableListProps, SortableListItemProps} from './components/sortableList';
136136
export {default as StackAggregator, StackAggregatorProps} from './components/stackAggregator';
137137
export {default as StateScreen, StateScreenProps} from './components/stateScreen';
138138
export {default as Stepper, StepperProps} from './components/stepper';

0 commit comments

Comments
 (0)