-
Notifications
You must be signed in to change notification settings - Fork 734
Feat/sortable grid list component #1918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
c76c2e5
Initial implementation of GridList component based on old GridView
ethanshar 21fe9a3
Support orientation change and keepItemSize in GridList
ethanshar f113f6d
Initial implementation of SortableGridList component
ethanshar 8255109
Adjust SortableGridList to work with ScrollView
ethanshar 5d2c319
Update src/components/gridList/gridList.api.json
ethanshar 417ecee
Move grid list logic to useGridLayout hook and fix code review
ethanshar 4f51e2d
Merge GridList changes
ethanshar 7a15a86
Minor fixes
ethanshar 9371830
Merge branch 'master' into feat/GridListComponent
ethanshar 08e997c
Merge branch 'feat/GridListComponent' into feat/SortableGridListCompo…
ethanshar fbd2df3
Update src/components/gridList/gridList.api.json
ethanshar e6b0d17
Merge branch 'master' into feat/GridListComponent
ethanshar 1d2accd
Code review fixes
ethanshar 432b9ce
Merge branch 'feat/GridListComponent' into feat/SortableGridListCompo…
ethanshar a9a3088
Merge branch 'master' into feat/SortableGridListComponent
ethanshar b0b68c3
Fix issue with canceling drag before it starts
ethanshar 5b1ee63
Code review fixes
ethanshar 4fce6f1
Add api json file
ethanshar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
demo/src/screens/componentScreens/SortableGridListScreen.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import React, {Component} from 'react'; | ||
import {StyleSheet} from 'react-native'; | ||
import { | ||
View, | ||
Text, | ||
Constants, | ||
SortableGridList, | ||
Card, | ||
Spacings, | ||
BorderRadiuses, | ||
GridListProps, | ||
SortableGridListProps | ||
} from 'react-native-ui-lib'; | ||
|
||
import products from '../../data/products'; | ||
|
||
class SortableGridListScreen extends Component { | ||
state = { | ||
orientation: Constants.orientation | ||
}; | ||
|
||
onOrderChange: SortableGridListProps['onOrderChange'] = (_newOrderedData, newOrder) => { | ||
console.log('newOrder:', newOrder); | ||
}; | ||
|
||
renderItem: GridListProps<typeof products[0]>['renderItem'] = ({item}) => { | ||
return ( | ||
<Card flex onPress={() => console.log('item press')}> | ||
<Card.Section imageSource={{uri: item.mediaUrl}} imageStyle={styles.itemImage}/> | ||
</Card> | ||
); | ||
}; | ||
|
||
render() { | ||
return ( | ||
<View flex> | ||
<Text h1 margin-s5> | ||
SortableGridList | ||
</Text> | ||
<SortableGridList | ||
data={products} | ||
renderItem={this.renderItem} | ||
// numColumns={2} | ||
maxItemWidth={140} | ||
itemSpacing={Spacings.s3} | ||
// itemSpacing={0} | ||
listPadding={Spacings.s5} | ||
// keepItemSize | ||
contentContainerStyle={styles.list} | ||
onOrderChange={this.onOrderChange} | ||
/> | ||
</View> | ||
); | ||
} | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
list: { | ||
padding: Spacings.s5 | ||
}, | ||
itemImage: { | ||
width: '100%', | ||
// height: 85, | ||
height: 108.7, | ||
borderRadius: BorderRadiuses.br10 | ||
} | ||
}); | ||
|
||
export default SortableGridListScreen; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
generatedTypes/src/components/sortableGridList/SortableItem.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import { StyleProp, ViewStyle } from 'react-native'; | ||
import Animated from 'react-native-reanimated'; | ||
import usePresenter, { ItemsOrder } from './usePresenter'; | ||
interface SortableItemProps extends ReturnType<typeof usePresenter> { | ||
index: number; | ||
itemsOrder: Animated.SharedValue<ItemsOrder>; | ||
onChange: () => void; | ||
style: StyleProp<ViewStyle>; | ||
} | ||
declare function SortableItem(props: PropsWithChildren<SortableItemProps>): JSX.Element; | ||
export default SortableItem; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/// <reference types="react" /> | ||
import { FlatListProps, ScrollViewProps } from 'react-native'; | ||
import { GridListBaseProps } from '../gridList'; | ||
import { ItemsOrder } from './usePresenter'; | ||
export interface SortableGridListProps<T = any> extends GridListBaseProps, ScrollViewProps { | ||
data: FlatListProps<T>['data']; | ||
renderItem: FlatListProps<T>['renderItem']; | ||
onOrderChange?: (newData: T[], newOrder: ItemsOrder) => void; | ||
} | ||
declare function SortableGridList<T = any>(props: SortableGridListProps<T>): JSX.Element; | ||
export default SortableGridList; |
26 changes: 26 additions & 0 deletions
26
generatedTypes/src/components/sortableGridList/usePresenter.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export declare const WINDOW_WIDTH: number; | ||
export declare const DEFAULT_NO_OF_COLUMNS = 3; | ||
export declare const getItemSize: (numOfColumns: number, viewWidth: number) => number; | ||
export declare const animationConfig: { | ||
easing: (value: number) => number; | ||
duration: number; | ||
}; | ||
export declare type ItemsLayouts = ({ | ||
width: number; | ||
height: number; | ||
} | undefined)[]; | ||
export declare type ItemsOrder = number[]; | ||
declare const usePresenter: (itemsSize: number, numOfColumns: number, itemSpacing: number) => { | ||
updateItemLayout: (index: number, layout: { | ||
width: number; | ||
height: number; | ||
}) => void; | ||
getTranslationByOrderChange: (newOrder: number, oldOrder: number) => { | ||
x: number; | ||
y: number; | ||
}; | ||
getOrderByPosition: (positionX: number, positionY: number) => number; | ||
getItemOrderById: (itemsOrder: ItemsOrder, itemId: number) => number; | ||
getIdByItemOrder: (itemsOrder: ItemsOrder, orderIndex: number) => number; | ||
}; | ||
export default usePresenter; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import React, {PropsWithChildren, useCallback} from 'react'; | ||
import {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; | ||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'; | ||
import Animated, { | ||
runOnJS, | ||
useAnimatedReaction, | ||
useAnimatedStyle, | ||
useSharedValue, | ||
withSpring, | ||
withTiming | ||
} from 'react-native-reanimated'; | ||
import usePresenter, {ItemsOrder, animationConfig} from './usePresenter'; | ||
import View from '../view'; | ||
|
||
interface SortableItemProps extends ReturnType<typeof usePresenter> { | ||
index: number; | ||
itemsOrder: Animated.SharedValue<ItemsOrder>; | ||
onChange: () => void; | ||
style: StyleProp<ViewStyle>; | ||
} | ||
|
||
function SortableItem(props: PropsWithChildren<SortableItemProps>) { | ||
const { | ||
index, | ||
itemsOrder, | ||
onChange, | ||
style, | ||
getItemOrderById, | ||
getOrderByPosition, | ||
getIdByItemOrder, | ||
getTranslationByOrderChange, | ||
updateItemLayout | ||
} = props; | ||
const translateX = useSharedValue(0); | ||
const translateY = useSharedValue(0); | ||
|
||
const isFloating = useSharedValue(false); | ||
const isDragging = useSharedValue(false); | ||
const tempItemsOrder = useSharedValue(itemsOrder.value); | ||
const tempTranslateX = useSharedValue(0); | ||
const tempTranslateY = useSharedValue(0); | ||
|
||
const onLayout = useCallback((event: LayoutChangeEvent) => { | ||
'worklet'; | ||
const {width, height} = event.nativeEvent.layout; | ||
updateItemLayout(index, {width, height}); | ||
}, []); | ||
|
||
useAnimatedReaction(() => itemsOrder.value.indexOf(index), // Note: It doesn't work with the getItemOrderById util | ||
(newOrder, prevOrder) => { | ||
if (prevOrder !== null && newOrder !== prevOrder) { | ||
const translation = getTranslationByOrderChange(newOrder, prevOrder); | ||
translateX.value = withTiming(translateX.value + translation.x, animationConfig); | ||
translateY.value = withTiming(translateY.value + translation.y, animationConfig); | ||
} else if (newOrder === index) { | ||
translateX.value = withTiming(0, animationConfig); | ||
translateY.value = withTiming(0, animationConfig); | ||
} | ||
}); | ||
|
||
const longPressGesture = Gesture.LongPress() | ||
.onStart(() => { | ||
isFloating.value = true; | ||
}) | ||
.onTouchesCancelled(() => { | ||
if (!isDragging.value) { | ||
isFloating.value = false; | ||
} | ||
}) | ||
.minDuration(250); | ||
|
||
const dragGesture = Gesture.Pan() | ||
.manualActivation(true) | ||
.onTouchesMove((_e, state) => { | ||
if (isFloating.value) { | ||
isDragging.value = true; | ||
state.activate(); | ||
} else { | ||
isDragging.value = false; | ||
state.fail(); | ||
} | ||
}) | ||
.onStart(() => { | ||
tempTranslateX.value = translateX.value; | ||
tempTranslateY.value = translateY.value; | ||
tempItemsOrder.value = itemsOrder.value; | ||
}) | ||
.onUpdate(event => { | ||
translateX.value = tempTranslateX.value + event.translationX; | ||
translateY.value = tempTranslateY.value + event.translationY; | ||
|
||
// Swapping items | ||
const oldOrder = getItemOrderById(itemsOrder.value, index); | ||
const newOrder = getOrderByPosition(translateX.value, translateY.value) + index; | ||
|
||
if (oldOrder !== newOrder) { | ||
const itemIdToSwap = getIdByItemOrder(itemsOrder.value, newOrder); | ||
|
||
if (itemIdToSwap !== undefined) { | ||
const newItemsOrder = [...itemsOrder.value]; | ||
newItemsOrder[newOrder] = index; | ||
newItemsOrder[oldOrder] = itemIdToSwap; | ||
itemsOrder.value = newItemsOrder; | ||
} | ||
} | ||
}) | ||
.onEnd(() => { | ||
const translation = getTranslationByOrderChange(getItemOrderById(itemsOrder.value, index), | ||
getItemOrderById(tempItemsOrder.value, index)); | ||
|
||
translateX.value = withTiming(tempTranslateX.value + translation.x, animationConfig); | ||
translateY.value = withTiming(tempTranslateY.value + translation.y, animationConfig); | ||
}) | ||
.onFinalize(() => { | ||
if (isDragging.value) { | ||
isDragging.value = false; | ||
isFloating.value = false; | ||
if (tempItemsOrder.value.toString() !== itemsOrder.value.toString()) { | ||
runOnJS(onChange)(); | ||
} | ||
} | ||
}) | ||
.simultaneousWithExternalGesture(longPressGesture); | ||
|
||
const gesture = Gesture.Race(dragGesture, longPressGesture); | ||
|
||
const animatedStyle = useAnimatedStyle(() => { | ||
const scale = withSpring(isFloating.value ? 1.1 : 1); | ||
const zIndex = isFloating.value ? 100 : withTiming(0, animationConfig); | ||
|
||
return { | ||
zIndex, | ||
transform: [{translateX: translateX.value}, {translateY: translateY.value}, {scale}] | ||
}; | ||
}); | ||
return ( | ||
<View reanimated style={[style, animatedStyle]} onLayout={onLayout}> | ||
<GestureDetector gesture={gesture}> | ||
<View>{props.children}</View> | ||
</GestureDetector> | ||
</View> | ||
); | ||
} | ||
|
||
export default SortableItem; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React, {useCallback} from 'react'; | ||
import {StyleSheet, FlatListProps, ScrollView, ScrollViewProps} from 'react-native'; | ||
import {GestureHandlerRootView} from 'react-native-gesture-handler'; | ||
import {useSharedValue} from 'react-native-reanimated'; | ||
import _ from 'lodash'; | ||
import {GridListBaseProps} from '../gridList'; | ||
import SortableItem from './SortableItem'; | ||
import usePresenter, {ItemsOrder} from './usePresenter'; | ||
|
||
import useGridLayout, {DEFAULT_ITEM_SPACINGS, DEFAULT_NUM_COLUMNS} from '../gridList/useGridLayout'; | ||
|
||
export interface SortableGridListProps<T = any> extends GridListBaseProps, ScrollViewProps { | ||
data: FlatListProps<T>['data']; | ||
renderItem: FlatListProps<T>['renderItem']; | ||
onOrderChange?: (newData: T[], newOrder: ItemsOrder) => void; | ||
} | ||
|
||
function SortableGridList<T = any>(props: SortableGridListProps<T>) { | ||
const {renderItem, onOrderChange, contentContainerStyle, ...others} = props; | ||
|
||
const {itemContainerStyle, numberOfColumns} = useGridLayout(props); | ||
const {numColumns = DEFAULT_NUM_COLUMNS, itemSpacing = DEFAULT_ITEM_SPACINGS, data} = others; | ||
const itemsOrder = useSharedValue<number[]>(_.map(props.data, (_v, i) => i)); | ||
|
||
// TODO: Get the number of columns from GridList calculation somehow | ||
const presenter = usePresenter(props.data?.length ?? 0, numColumns, itemSpacing); | ||
|
||
const onChange = useCallback(() => { | ||
const newData: T[] = []; | ||
if (data?.length) { | ||
itemsOrder.value.forEach(itemIndex => { | ||
newData.push(data[itemIndex]); | ||
}); | ||
} | ||
|
||
onOrderChange?.(newData, itemsOrder.value); | ||
}, [onOrderChange, data]); | ||
|
||
const _renderItem = useCallback(({item, index}) => { | ||
const lastItemInRow = (index + 1) % numberOfColumns === 0; | ||
return ( | ||
<SortableItem | ||
key={index} | ||
{...presenter} | ||
style={[itemContainerStyle, lastItemInRow && {marginRight: 0}]} | ||
itemsOrder={itemsOrder} | ||
index={index} | ||
onChange={onChange} | ||
> | ||
{/* @ts-expect-error */} | ||
{renderItem({item, index})} | ||
</SortableItem> | ||
); | ||
}, []); | ||
|
||
return ( | ||
<GestureHandlerRootView> | ||
<ScrollView contentContainerStyle={[styles.listContent, contentContainerStyle]}> | ||
{data?.map((item, index) => _renderItem({item, index}))} | ||
</ScrollView> | ||
</GestureHandlerRootView> | ||
); | ||
} | ||
|
||
export default SortableGridList; | ||
|
||
const styles = StyleSheet.create({ | ||
listContent: { | ||
flexWrap: 'wrap', | ||
flexDirection: 'row' | ||
} | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "SortableGridList", | ||
"category": "layoutsAndTemplates", | ||
"description": "An sortable grid list (based on GridList component)", | ||
"notes": "This component supports square items only", | ||
"extends": ["GridList"], | ||
"example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/SortableGridListScreen.tsx", | ||
"props": [ | ||
{"name": "data", "type": "any[]", "description": "Data of items"}, | ||
{"name": "renderItem", "type": "FlatListProps['renderItem']", "description": "Custom render item callback"}, | ||
{ | ||
"name": "onOrderChange", | ||
"type": "(newData: T[], newOrder: ItemsOrder) => void", | ||
"description": "Order change callback" | ||
} | ||
], | ||
"snippet": [ | ||
"<SortableGridList>", | ||
" data={items$1}", | ||
" maxItemWidth={140$2}", | ||
" itemSpacing={Spacings.s3$3}", | ||
" listPadding={Spacings.s5}", | ||
" onOrderChange={$4}", | ||
"/>" | ||
] | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.