Skip to content

Commit 788afc0

Browse files
ethansharM-i-k-e-llidord-wix
authored
Create new GridList component based on old GridView (#1914)
* Initial implementation of GridList component based on old GridView * Support orientation change and keepItemSize in GridList * Update src/components/gridList/gridList.api.json Co-authored-by: Miki Leib <[email protected]> * Move grid list logic to useGridLayout hook and fix code review * Update src/components/gridList/gridList.api.json Co-authored-by: Lidor Dafna <[email protected]> * Code review fixes Co-authored-by: Miki Leib <[email protected]> Co-authored-by: Lidor Dafna <[email protected]>
1 parent 55acba9 commit 788afc0

File tree

12 files changed

+323
-0
lines changed

12 files changed

+323
-0
lines changed

demo/src/data/products.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,46 @@ const products = [
118118
quantity: 8
119119
},
120120
mediaUrl: 'https://static.wixstatic.com/media/84770f_c611ded729fd4461a1bb57134d4e9dd2.png_128'
121+
},
122+
{
123+
name: 'I\'m a Product',
124+
formattedPrice: '$19.99',
125+
inventory: {
126+
trackingMethod: 'status',
127+
status: 'In Stock',
128+
quantity: 3
129+
},
130+
mediaUrl: 'https://images.pexels.com/photos/3612182/pexels-photo-3612182.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=150'
131+
},
132+
{
133+
name: 'I\'m a Product',
134+
formattedPrice: '$19.99',
135+
inventory: {
136+
trackingMethod: 'status',
137+
status: 'In Stock',
138+
quantity: 22
139+
},
140+
mediaUrl: 'https://images.pexels.com/photos/4841529/pexels-photo-4841529.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=150'
141+
},
142+
{
143+
name: 'I\'m a Product',
144+
formattedPrice: '$19.99',
145+
inventory: {
146+
trackingMethod: 'status',
147+
status: 'In Stock',
148+
quantity: 10
149+
},
150+
mediaUrl: 'https://images.pexels.com/photos/4173450/pexels-photo-4173450.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=150'
151+
},
152+
{
153+
name: 'I\'m a Product',
154+
formattedPrice: '$19.99',
155+
inventory: {
156+
trackingMethod: 'status',
157+
status: 'In Stock',
158+
quantity: 10
159+
},
160+
mediaUrl: 'https://images.pexels.com/photos/10513273/pexels-photo-10513273.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=150'
121161
}
122162
];
123163

demo/src/screens/MenuStructure.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export const navigationData = {
112112
screen: 'unicorn.components.FaderScreen'
113113
},
114114
{title: 'Wizard', tags: 'wizard', screen: 'unicorn.components.WizardScreen'},
115+
{title: 'GridList', tags: 'grid list', screen: 'unicorn.components.GridListScreen'},
115116
{title: 'GridView', tags: 'grid view', screen: 'unicorn.components.GridViewScreen'}
116117
]
117118
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, {Component} from 'react';
2+
import {StyleSheet} from 'react-native';
3+
import {View, Text, Constants, GridList, Card, Spacings, BorderRadiuses, GridListProps} from 'react-native-ui-lib';
4+
import products from '../../data/products';
5+
6+
class GridListScreen extends Component {
7+
state = {
8+
orientation: Constants.orientation
9+
};
10+
11+
renderItem: GridListProps<typeof products[0]>['renderItem'] = ({item}) => {
12+
return (
13+
<Card flex>
14+
<Card.Section imageSource={{uri: item.mediaUrl}} imageStyle={styles.itemImage}/>
15+
<View padding-s2>
16+
<Text $textDefault>{item.name}</Text>
17+
<Text $textDefault>{item.formattedPrice}</Text>
18+
{item.inventory.status === 'Out of Stock' && (
19+
<Text text90M $textDangerLight>
20+
{item.inventory.status}
21+
</Text>
22+
)}
23+
</View>
24+
</Card>
25+
);
26+
};
27+
28+
render() {
29+
return (
30+
<GridList<typeof products[0]>
31+
ListHeaderComponent={
32+
<Text h1 marginB-s5>
33+
GridList
34+
</Text>
35+
}
36+
data={products}
37+
renderItem={this.renderItem}
38+
// numColumns={2}
39+
maxItemWidth={140}
40+
itemSpacing={Spacings.s3}
41+
listPadding={Spacings.s5}
42+
// keepItemSize
43+
contentContainerStyle={styles.list}
44+
/>
45+
);
46+
}
47+
}
48+
49+
const styles = StyleSheet.create({
50+
list: {
51+
paddingTop: Spacings.s5
52+
},
53+
itemImage: {
54+
width: '100%',
55+
height: 85,
56+
borderRadius: BorderRadiuses.br10
57+
}
58+
});
59+
60+
export default GridListScreen;

demo/src/screens/componentScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function registerScreens(registrar) {
2626
registrar('unicorn.components.HintsScreen', () => require('./HintsScreen').default);
2727
registrar('unicorn.components.IconScreen', () => require('./IconScreen').default);
2828
registrar('unicorn.components.ImageScreen', () => require('./ImageScreen').default);
29+
registrar('unicorn.components.GridListScreen', () => require('./GridListScreen').default);
2930
registrar('unicorn.components.GridViewScreen', () => require('./GridViewScreen').default);
3031
registrar('unicorn.components.KeyboardAwareScrollViewScreen', () => require('./KeyboardAwareScrollViewScreen').default);
3132
registrar('unicorn.components.MaskedInputScreen', () => require('./MaskedInputScreen').default);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="react" />
2+
import { GridListProps, GridListBaseProps } from './types';
3+
declare function GridList<T = any>(props: GridListProps<T>): JSX.Element;
4+
export { GridListBaseProps, GridListProps };
5+
export default GridList;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FlatListProps } from 'react-native';
2+
export interface GridListBaseProps {
3+
/**
4+
* Allow a responsive item width to the maximum item width
5+
*/
6+
maxItemWidth?: number;
7+
/**
8+
* Number of items to show in a row (ignored when passing maxItemWidth)
9+
*/
10+
numColumns?: number;
11+
/**
12+
* Spacing between each item
13+
*/
14+
itemSpacing?: number;
15+
/**
16+
* List padding (used for item size calculation)
17+
*/
18+
listPadding?: number;
19+
/**
20+
* whether to keep the items initial size when orientation changes,
21+
* in which case the apt number of columns will be calculated automatically.
22+
*/
23+
keepItemSize?: boolean;
24+
/**
25+
* Pass when you want to use a custom container width for calculation
26+
*/
27+
containerWidth?: number;
28+
}
29+
export declare type GridListProps<T = any> = GridListBaseProps & FlatListProps<T>;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { GridListBaseProps } from './types';
2+
export declare const DEFAULT_NUM_COLUMNS = 3;
3+
export declare const DEFAULT_ITEM_SPACINGS: number;
4+
declare const useGridLayout: (props: GridListBaseProps) => {
5+
itemContainerStyle: {
6+
width: number;
7+
marginRight: number;
8+
marginBottom: number;
9+
};
10+
numberOfColumns: number;
11+
itemSize: number;
12+
};
13+
export default useGridLayout;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "GridList",
3+
"category": "layoutsAndTemplates",
4+
"description": "An auto-generated grid list that calculate item size according to given props",
5+
"extends": ["FlatList"],
6+
"example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/GridListScreen.tsx",
7+
"props": [
8+
{"name": "numColumns", "type": "number", "description": "Number of items to show in a row (ignored when passing maxItemWidth)"},
9+
{"name": "itemSpacing", "type": "number", "description": "Spacing between each item"},
10+
{
11+
"name": "maxItemWidth",
12+
"type": "number",
13+
"description": "Allow a responsive item width to the maximum item width"
14+
},
15+
{
16+
"name": "listPadding",
17+
"type": "number",
18+
"description": "List padding (used for item size calculation)"
19+
},
20+
{
21+
"name": "containerWidth",
22+
"type": "number",
23+
"description": "Pass when you want to use a custom container width for calculation"
24+
},
25+
{
26+
"name": "keepItemSize",
27+
"type": "boolean",
28+
"description": "whether to keep the items initial size when orientation changes, in which case the apt number of columns will be calculated automatically."
29+
}
30+
],
31+
"snippet": [
32+
"<GridList>",
33+
" data={items$1}",
34+
" maxItemWidth={140$2}",
35+
" numColumns={2$3}",
36+
" itemSpacing={Spacings.s3$4}",
37+
" containerWidth={Constants.screenWidth - (Spacings.s5 * 2)}",
38+
" contentContainerStyle={{padding: Spacings.s5}}",
39+
"/>"
40+
]
41+
}

src/components/gridList/index.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, {useCallback, useMemo} from 'react';
2+
import {FlatList} from 'react-native';
3+
import useGridLayout from './useGridLayout';
4+
import View from '../view';
5+
import {GridListProps, GridListBaseProps} from './types';
6+
7+
function GridList<T = any>(props: GridListProps<T>) {
8+
const {
9+
renderItem,
10+
numColumns,
11+
itemSpacing,
12+
maxItemWidth,
13+
listPadding = 0,
14+
keepItemSize,
15+
containerWidth,
16+
contentContainerStyle,
17+
...others
18+
} = props;
19+
20+
const {itemContainerStyle, numberOfColumns} = useGridLayout({
21+
numColumns,
22+
itemSpacing,
23+
maxItemWidth,
24+
listPadding,
25+
keepItemSize,
26+
containerWidth
27+
});
28+
29+
const listContentStyle = useMemo(() => {
30+
return [{paddingHorizontal: listPadding}, contentContainerStyle];
31+
}, [listPadding, contentContainerStyle]);
32+
33+
const _renderItem = useCallback((...args) => {
34+
// @ts-expect-error
35+
return <View style={itemContainerStyle}>{renderItem?.(...args)}</View>;
36+
},
37+
[renderItem, itemContainerStyle]);
38+
39+
return (
40+
<FlatList
41+
key={numberOfColumns}
42+
{...others}
43+
contentContainerStyle={listContentStyle}
44+
renderItem={_renderItem}
45+
numColumns={numberOfColumns}
46+
/>
47+
);
48+
}
49+
50+
export {GridListBaseProps, GridListProps};
51+
export default GridList;

src/components/gridList/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {FlatListProps} from 'react-native';
2+
3+
export interface GridListBaseProps {
4+
/**
5+
* Allow a responsive item width to the maximum item width
6+
*/
7+
maxItemWidth?: number;
8+
/**
9+
* Number of items to show in a row (ignored when passing maxItemWidth)
10+
*/
11+
numColumns?: number;
12+
/**
13+
* Spacing between each item
14+
*/
15+
itemSpacing?: number;
16+
/**
17+
* List padding (used for item size calculation)
18+
*/
19+
listPadding?: number;
20+
/**
21+
* whether to keep the items initial size when orientation changes,
22+
* in which case the apt number of columns will be calculated automatically.
23+
*/
24+
keepItemSize?: boolean;
25+
/**
26+
* Pass when you want to use a custom container width for calculation
27+
*/
28+
containerWidth?: number;
29+
30+
}
31+
32+
export type GridListProps<T = any> = GridListBaseProps & FlatListProps<T>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {useMemo} from 'react';
2+
import {Spacings} from 'style';
3+
import {useOrientation} from 'hooks';
4+
import {Constants} from '../../commons/new';
5+
import {GridListBaseProps} from './types';
6+
7+
export const DEFAULT_NUM_COLUMNS = 3;
8+
export const DEFAULT_ITEM_SPACINGS = Spacings.s4;
9+
10+
const useGridLayout = (props: GridListBaseProps) => {
11+
const {
12+
numColumns = DEFAULT_NUM_COLUMNS,
13+
itemSpacing = DEFAULT_ITEM_SPACINGS,
14+
maxItemWidth,
15+
listPadding = 0,
16+
keepItemSize,
17+
containerWidth
18+
} = props;
19+
20+
const {orientation} = useOrientation();
21+
22+
const _containerWidth = useMemo(() => {
23+
return (containerWidth ?? Constants.screenWidth) - listPadding * 2;
24+
// eslint-disable-next-line react-hooks/exhaustive-deps
25+
}, [listPadding, orientation, containerWidth]);
26+
27+
const numberOfColumns = useMemo(() => {
28+
if (maxItemWidth) {
29+
return Math.ceil((_containerWidth + itemSpacing) / (maxItemWidth + itemSpacing));
30+
} else {
31+
return numColumns;
32+
}
33+
// eslint-disable-next-line react-hooks/exhaustive-deps
34+
}, [numColumns, maxItemWidth, itemSpacing, keepItemSize ? _containerWidth : undefined]);
35+
36+
const itemWidth = useMemo(() => {
37+
return (_containerWidth - itemSpacing * (numberOfColumns - 1)) / numberOfColumns;
38+
39+
// eslint-disable-next-line react-hooks/exhaustive-deps
40+
}, [numberOfColumns, itemSpacing, keepItemSize ? undefined : _containerWidth]);
41+
42+
const itemContainerStyle = useMemo(() => {
43+
return {width: itemWidth /* + itemSpacing */, marginRight: itemSpacing, marginBottom: itemSpacing};
44+
}, [itemWidth, itemSpacing]);
45+
46+
return {itemContainerStyle, numberOfColumns};
47+
};
48+
49+
export default useGridLayout;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export {default as FeatureHighlight, FeatureHighlightProps} from './components/f
6161
export {default as FloatingButton, FloatingButtonProps} from './components/floatingButton';
6262
export {default as GradientSlider, GradientSliderProps} from './components/slider/GradientSlider';
6363
export {default as GridListItem, GridListItemProps} from './components/gridListItem';
64+
export {default as GridList, GridListProps} from './components/gridList';
6465
export {default as GridView, GridViewProps} from './components/gridView';
6566
export {default as HapticService, HapticType} from './services/HapticService';
6667
export {default as Hint, HintProps} from './components/hint';

0 commit comments

Comments
 (0)