@codeleap/web 3.10.2 → 3.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/components/Grid/index.tsx +51 -107
- package/src/components/Grid/styles.ts +2 -2
- package/src/components/Grid/types.ts +4 -13
- package/src/components/List/ListLayout.tsx +66 -61
- package/src/components/List/index.tsx +34 -44
- package/src/components/List/styles.ts +1 -4
- package/src/components/List/types.ts +7 -4
- package/src/components/List/useInfiniteScroll.ts +88 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/web",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.4",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@codeleap/config": "*",
|
|
13
|
+
"@emotion/react": "link:../../apps/web/node_modules/@emotion/react",
|
|
13
14
|
"@types/react-slick": "^0.23.10",
|
|
14
15
|
"@types/react-window": "1.8.5",
|
|
15
|
-
"@emotion/react": "link:../../apps/web/node_modules/@emotion/react",
|
|
16
16
|
"react": "link:../../apps/web/node_modules/react"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@radix-ui/react-slider": "1.1.1",
|
|
24
24
|
"@radix-ui/react-tooltip": "^1.0.6",
|
|
25
|
-
"@tanstack/react-virtual": "^3.0.0-beta.54",
|
|
26
25
|
"framer-motion": "^10.10.0",
|
|
27
26
|
"js-cookie": "^3.0.1",
|
|
27
|
+
"masonic": "^3.7.0",
|
|
28
28
|
"rc-slider": "^9.7.5",
|
|
29
29
|
"react-autosize-textarea": "^7.1.0",
|
|
30
30
|
"react-input-mask": "^2.0.4",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@codeleap/common": "*",
|
|
43
|
+
"@emotion/react": "11.10.6",
|
|
43
44
|
"@reach/router": "^1.3.4",
|
|
44
45
|
"axios": "1.3.5",
|
|
45
46
|
"gatsby-plugin-intl": "^0.3.3",
|
|
@@ -47,7 +48,6 @@
|
|
|
47
48
|
"react-dom": "18.1.0",
|
|
48
49
|
"react-helmet": "^6.1.0",
|
|
49
50
|
"react-select": "*",
|
|
50
|
-
"typescript": "4.5.4"
|
|
51
|
-
"@emotion/react": "11.10.6"
|
|
51
|
+
"typescript": "4.5.4"
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { useDefaultComponentStyle
|
|
2
|
+
import { useDefaultComponentStyle } from '@codeleap/common'
|
|
3
3
|
import { View, ViewProps } from '../View'
|
|
4
4
|
import { EmptyPlaceholder } from '../EmptyPlaceholder'
|
|
5
5
|
import { GridPresets } from './styles'
|
|
6
|
-
import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'
|
|
7
6
|
import { GridProps } from './types'
|
|
8
7
|
import { ListLayout, useInfiniteScroll } from '../List'
|
|
8
|
+
import { RenderComponentProps as MasonryItemProps, Masonry as GridMasonry } from 'masonic'
|
|
9
9
|
|
|
10
10
|
export * from './styles'
|
|
11
11
|
export * from './types'
|
|
12
12
|
|
|
13
|
-
const generateColumns = (count: number) => {
|
|
14
|
-
return new Array(count).fill(0).map((_, i) => {
|
|
15
|
-
const key: string = i.toString()
|
|
16
|
-
return {
|
|
17
|
-
key,
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
13
|
const RenderSeparator = (props: { separatorStyles: ViewProps<'div'>['css'] }) => {
|
|
23
14
|
return (
|
|
24
15
|
<View css={[props?.separatorStyles]}></View>
|
|
@@ -29,34 +20,37 @@ const defaultProps: Partial<GridProps> = {
|
|
|
29
20
|
ListFooterComponent: null,
|
|
30
21
|
ListHeaderComponent: null,
|
|
31
22
|
ListLoadingIndicatorComponent: null,
|
|
32
|
-
ListRefreshControlComponent: null,
|
|
33
23
|
ListEmptyComponent: EmptyPlaceholder,
|
|
34
24
|
ListSeparatorComponent: RenderSeparator,
|
|
35
25
|
refreshDebounce: 3000,
|
|
36
26
|
refreshSize: 40,
|
|
37
|
-
refreshThreshold: 0.
|
|
38
|
-
refreshPosition:
|
|
27
|
+
refreshThreshold: 0.1,
|
|
28
|
+
refreshPosition: 16,
|
|
39
29
|
refresh: true,
|
|
40
|
-
|
|
30
|
+
columnItemsSpacing: 8,
|
|
31
|
+
rowItemsSpacing: 8,
|
|
32
|
+
overscan: 2,
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
|
|
35
|
+
export function Grid<T = any>(props: GridProps<T>) {
|
|
44
36
|
const allProps = {
|
|
45
|
-
...defaultProps,
|
|
46
|
-
...
|
|
47
|
-
}
|
|
37
|
+
...Grid.defaultProps,
|
|
38
|
+
...props,
|
|
39
|
+
} as GridProps
|
|
48
40
|
|
|
49
41
|
const {
|
|
50
42
|
variants = [],
|
|
51
43
|
responsiveVariants = {},
|
|
52
44
|
styles = {},
|
|
53
45
|
renderItem: RenderItem,
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
columnItemsSpacing,
|
|
47
|
+
rowItemsSpacing,
|
|
56
48
|
ListSeparatorComponent,
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
data,
|
|
50
|
+
overscan,
|
|
59
51
|
separators,
|
|
52
|
+
masonryProps = {},
|
|
53
|
+
numColumns,
|
|
60
54
|
} = allProps
|
|
61
55
|
|
|
62
56
|
const variantStyles = useDefaultComponentStyle<'u:Grid', typeof GridPresets>('u:Grid', {
|
|
@@ -65,105 +59,55 @@ const GridCP = React.forwardRef<'div', GridProps>((flatGridProps, ref) => {
|
|
|
65
59
|
styles,
|
|
66
60
|
})
|
|
67
61
|
|
|
68
|
-
const
|
|
62
|
+
const { layoutProps, onLoadMore } = useInfiniteScroll(allProps)
|
|
69
63
|
|
|
70
|
-
const {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
items,
|
|
74
|
-
layoutProps,
|
|
75
|
-
} = useInfiniteScroll({
|
|
76
|
-
...allProps,
|
|
77
|
-
overscan: 5,
|
|
78
|
-
})
|
|
64
|
+
const separator = React.useMemo(() => {
|
|
65
|
+
return separators ? <ListSeparatorComponent separatorStyles={variantStyles.separator} /> : null
|
|
66
|
+
}, [])
|
|
79
67
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
}, [numColumns])
|
|
83
|
-
|
|
84
|
-
const columnVirtualizer = useVirtualizer({
|
|
85
|
-
horizontal: true,
|
|
86
|
-
count: columns?.length,
|
|
87
|
-
getScrollElement: () => parentRef.current,
|
|
88
|
-
estimateSize: () => null,
|
|
89
|
-
overscan: 5,
|
|
90
|
-
...virtualizerOptions,
|
|
91
|
-
})
|
|
68
|
+
const renderItem = React.useCallback((_item: MasonryItemProps<any>) => {
|
|
69
|
+
if (!RenderItem) return null
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
const gridLength = data?.length || 0
|
|
94
72
|
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
const isFirst = _item?.index === 0
|
|
74
|
+
const isLast = _item?.index === gridLength - 1
|
|
75
|
+
const isOnly = isFirst && isLast
|
|
97
76
|
|
|
98
|
-
const
|
|
77
|
+
const _itemProps = {
|
|
78
|
+
..._item,
|
|
79
|
+
isOnly,
|
|
80
|
+
isLast,
|
|
81
|
+
isFirst,
|
|
82
|
+
item: _item?.data
|
|
83
|
+
}
|
|
99
84
|
|
|
100
|
-
|
|
85
|
+
if (!_itemProps?.item) return null
|
|
101
86
|
|
|
102
87
|
return <>
|
|
103
|
-
{
|
|
104
|
-
<
|
|
105
|
-
css={[
|
|
106
|
-
variantStyles.itemWrapper,
|
|
107
|
-
{ transform: `translateY(${_item?.start - dataVirtualizer?.options?.scrollMargin}px)` }
|
|
108
|
-
]}
|
|
109
|
-
key={_item?.key}
|
|
110
|
-
data-index={_item?.index}
|
|
111
|
-
ref={dataVirtualizer?.measureElement}
|
|
112
|
-
>
|
|
113
|
-
{_item?.index !== 0 ? separator : null}
|
|
114
|
-
|
|
115
|
-
<View css={[variantStyles.column]}>
|
|
116
|
-
{columnItems.map(column => {
|
|
117
|
-
const rowIndex = _item?.index
|
|
118
|
-
const columnIndex = column?.index
|
|
119
|
-
const itemIndex = (rowIndex * numColumns) + columnIndex
|
|
120
|
-
|
|
121
|
-
const isFirst = itemIndex === 0
|
|
122
|
-
const isLast = itemIndex === gridLength - 1
|
|
123
|
-
const isOnly = isFirst && isLast
|
|
124
|
-
|
|
125
|
-
const isLastInRow = columnIndex === numColumns
|
|
126
|
-
const isFirstInRow = columnIndex === 0
|
|
127
|
-
const isOnlyInRow = isFirstInRow && isLastInRow
|
|
128
|
-
|
|
129
|
-
const _itemProps = {
|
|
130
|
-
..._item,
|
|
131
|
-
key: itemIndex,
|
|
132
|
-
index: itemIndex,
|
|
133
|
-
isOnly,
|
|
134
|
-
isLast,
|
|
135
|
-
isFirst,
|
|
136
|
-
column,
|
|
137
|
-
isFirstInRow,
|
|
138
|
-
isLastInRow,
|
|
139
|
-
isOnlyInRow,
|
|
140
|
-
rowIndex,
|
|
141
|
-
item: data?.[itemIndex]
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!_itemProps?.item) return null
|
|
145
|
-
|
|
146
|
-
return <RenderItem {..._itemProps} />
|
|
147
|
-
})}
|
|
148
|
-
</View>
|
|
149
|
-
|
|
150
|
-
{showIndicator && <ListLoadingIndicatorComponent />}
|
|
151
|
-
</div>
|
|
88
|
+
{_item?.index <= numColumns ? null : separator}
|
|
89
|
+
<RenderItem {..._itemProps} />
|
|
152
90
|
</>
|
|
153
|
-
}, [
|
|
91
|
+
}, [])
|
|
154
92
|
|
|
155
93
|
return (
|
|
156
94
|
<ListLayout
|
|
157
95
|
{...allProps}
|
|
158
96
|
{...layoutProps}
|
|
159
|
-
variantStyles={variantStyles}
|
|
160
|
-
ref={ref}
|
|
97
|
+
variantStyles={variantStyles}
|
|
161
98
|
>
|
|
162
|
-
|
|
99
|
+
<GridMasonry
|
|
100
|
+
items={data || []}
|
|
101
|
+
render={renderItem}
|
|
102
|
+
itemKey={item => item?.id}
|
|
103
|
+
columnGutter={columnItemsSpacing}
|
|
104
|
+
rowGutter={rowItemsSpacing}
|
|
105
|
+
columnCount={numColumns}
|
|
106
|
+
onRender={onLoadMore}
|
|
107
|
+
{...masonryProps}
|
|
108
|
+
/>
|
|
163
109
|
</ListLayout>
|
|
164
110
|
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type GridComponentType = <T extends any[] = any[]>(props: GridProps<T>) => React.ReactElement
|
|
111
|
+
}
|
|
168
112
|
|
|
169
|
-
|
|
113
|
+
Grid.defaultProps = defaultProps
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createDefaultVariantFactory, includePresets } from '@codeleap/common'
|
|
2
2
|
import { ListComposition, ListParts } from '../List'
|
|
3
3
|
|
|
4
|
-
export type GridParts = ListParts
|
|
4
|
+
export type GridParts = ListParts
|
|
5
5
|
|
|
6
6
|
export type GridComposition = ListComposition | GridParts
|
|
7
7
|
|
|
8
8
|
const createGridStyle = createDefaultVariantFactory<GridComposition>()
|
|
9
9
|
|
|
10
|
-
export const GridPresets = includePresets(style => createGridStyle(() => ({
|
|
10
|
+
export const GridPresets = includePresets(style => createGridStyle(() => ({ wrapper: style })))
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { ComponentVariants, StylesOf } from '@codeleap/common'
|
|
2
|
-
import { VirtualItem } from '@tanstack/react-virtual'
|
|
3
2
|
import { ComponentCommonProps } from '../../types'
|
|
4
|
-
import {
|
|
3
|
+
import { ListProps } from '../List'
|
|
5
4
|
import { GridComposition, GridPresets } from './styles'
|
|
6
5
|
|
|
7
|
-
type GridAugmentedRenderItemInfo<T> = AugmentedRenderItemInfo<T> & {
|
|
8
|
-
column: VirtualItem
|
|
9
|
-
isLastInRow: boolean
|
|
10
|
-
isOnlyInRow: boolean
|
|
11
|
-
isFirstInRow: boolean
|
|
12
|
-
rowIndex: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
export type GridProps<
|
|
16
7
|
T = any[],
|
|
17
8
|
Data = T extends Array<infer D> ? D : never
|
|
18
9
|
> =
|
|
19
|
-
Omit<ListProps<T, Data>, 'variants' | 'styles'
|
|
10
|
+
Omit<ListProps<T, Data>, 'variants' | 'styles'> &
|
|
20
11
|
ComponentVariants<typeof GridPresets> & {
|
|
21
12
|
styles?: StylesOf<GridComposition>
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
columnItemsSpacing?: number
|
|
14
|
+
numColumns: number
|
|
24
15
|
} & ComponentCommonProps
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { StylesOf
|
|
2
|
+
import { StylesOf } from '@codeleap/common'
|
|
3
3
|
import { ListComposition, ListParts } from './styles'
|
|
4
4
|
import { ListProps } from './types'
|
|
5
5
|
import { View } from '../View'
|
|
@@ -12,87 +12,92 @@ type ListLayoutProps = Omit<ListProps, 'renderItem'> & UseInfiniteScrollReturn['
|
|
|
12
12
|
children?: React.ReactNode
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
type ListRefreshControlComponent = Partial<ListLayoutProps> & {
|
|
16
|
+
enabled: boolean
|
|
17
|
+
variantStyles: StylesOf<ListComposition>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DefaultRefreshIndicator = (props: ListRefreshControlComponent) => {
|
|
21
|
+
const {
|
|
22
|
+
refreshing,
|
|
23
|
+
variantStyles,
|
|
24
|
+
refreshPosition,
|
|
25
|
+
refreshControlProps,
|
|
26
|
+
debugName,
|
|
27
|
+
refreshSize,
|
|
28
|
+
refreshControlIndicatorProps,
|
|
29
|
+
enabled,
|
|
30
|
+
} = props
|
|
31
|
+
|
|
32
|
+
if (!enabled) return null
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<motion.div
|
|
36
|
+
css={[variantStyles?.refreshControl]}
|
|
37
|
+
initial={false}
|
|
38
|
+
animate={{
|
|
39
|
+
opacity: refreshing ? 1 : 0,
|
|
40
|
+
top: refreshing ? refreshPosition : 0
|
|
41
|
+
}}
|
|
42
|
+
{...refreshControlProps}
|
|
43
|
+
>
|
|
44
|
+
<ActivityIndicator
|
|
45
|
+
debugName={debugName + 'refresh-indicator'}
|
|
46
|
+
size={refreshSize}
|
|
47
|
+
style={variantStyles.refreshControlIndicator}
|
|
48
|
+
{...refreshControlIndicatorProps}
|
|
49
|
+
/>
|
|
50
|
+
</motion.div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
export const ListLayout = (props: ListLayoutProps) => {
|
|
16
55
|
const {
|
|
17
56
|
ListEmptyComponent,
|
|
18
57
|
ListFooterComponent,
|
|
19
58
|
ListHeaderComponent,
|
|
20
|
-
|
|
59
|
+
refresh,
|
|
60
|
+
ListRefreshControlComponent = DefaultRefreshIndicator,
|
|
21
61
|
variantStyles,
|
|
22
62
|
isEmpty,
|
|
23
63
|
isLoading,
|
|
24
|
-
refreshPosition,
|
|
25
64
|
placeholder = {},
|
|
26
|
-
parentRef,
|
|
27
65
|
style,
|
|
28
|
-
dataVirtualizer,
|
|
29
|
-
refresh,
|
|
30
|
-
refreshing,
|
|
31
|
-
refreshControlProps,
|
|
32
|
-
refreshControlIndicatorProps = {},
|
|
33
|
-
refreshSize,
|
|
34
66
|
children,
|
|
35
|
-
ref,
|
|
36
67
|
debugName,
|
|
68
|
+
isFetching,
|
|
69
|
+
isFetchingNextPage,
|
|
70
|
+
ListLoadingIndicatorComponent,
|
|
71
|
+
scrollableRef,
|
|
37
72
|
} = props
|
|
38
73
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
isLoading && variantStyles[`${key}:loading`],
|
|
45
|
-
isEmpty && variantStyles[`${key}:empty`],
|
|
46
|
-
]
|
|
47
|
-
}, [isLoading, isEmpty])
|
|
48
|
-
|
|
49
|
-
const _refreshPosition = React.useMemo(() => {
|
|
50
|
-
return Theme.spacing.value(refreshPosition)
|
|
51
|
-
}, [refreshPosition])
|
|
74
|
+
const getKeyStyle = React.useCallback((key: ListParts) => ([
|
|
75
|
+
variantStyles[key],
|
|
76
|
+
isLoading && variantStyles[`${key}:loading`],
|
|
77
|
+
isEmpty && variantStyles[`${key}:empty`],
|
|
78
|
+
]), [isLoading, isEmpty])
|
|
52
79
|
|
|
53
80
|
return (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{!!ListHeaderComponent && <ListHeaderComponent />}
|
|
81
|
+
<View css={[getKeyStyle('wrapper'), style]} ref={scrollableRef}>
|
|
82
|
+
{!!ListHeaderComponent ? <ListHeaderComponent /> : null}
|
|
57
83
|
|
|
58
|
-
{isEmpty
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
css={[getKeyStyle('innerWrapper')]}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
getKeyStyle('content'),
|
|
68
|
-
{ height: dataVirtualizer.getTotalSize() }
|
|
69
|
-
]}
|
|
70
|
-
>
|
|
71
|
-
{TypeGuards.isNil(ListRefreshControlComponent) && refresh ? (
|
|
72
|
-
<motion.div
|
|
73
|
-
css={[variantStyles?.refreshControl]}
|
|
74
|
-
initial={false}
|
|
75
|
-
animate={{
|
|
76
|
-
opacity: refreshing ? 1 : 0,
|
|
77
|
-
top: refreshing ? _refreshPosition : 0
|
|
78
|
-
}}
|
|
79
|
-
{...refreshControlProps}
|
|
80
|
-
>
|
|
81
|
-
<ActivityIndicator
|
|
82
|
-
debugName={debugName}
|
|
83
|
-
size={refreshSize}
|
|
84
|
-
style={variantStyles.refreshControlIndicator}
|
|
85
|
-
{...refreshControlIndicatorProps}
|
|
86
|
-
/>
|
|
87
|
-
</motion.div>
|
|
88
|
-
) : (<ListRefreshControlComponent /> ?? null)}
|
|
84
|
+
{isEmpty
|
|
85
|
+
? <ListEmptyComponent debugName={debugName} {...placeholder} />
|
|
86
|
+
: (
|
|
87
|
+
<View css={[getKeyStyle('innerWrapper')]}>
|
|
88
|
+
<ListRefreshControlComponent
|
|
89
|
+
{...props}
|
|
90
|
+
enabled={refresh}
|
|
91
|
+
variantStyles={variantStyles}
|
|
92
|
+
/>
|
|
89
93
|
|
|
90
94
|
{children}
|
|
91
95
|
</View>
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{(isFetching || isFetchingNextPage) ? <ListLoadingIndicatorComponent /> : null}
|
|
94
99
|
|
|
95
|
-
{!!ListFooterComponent
|
|
100
|
+
{!!ListFooterComponent ? <ListFooterComponent /> : null}
|
|
96
101
|
</View>
|
|
97
102
|
)
|
|
98
103
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { useDefaultComponentStyle
|
|
2
|
+
import { useDefaultComponentStyle } from '@codeleap/common'
|
|
3
3
|
import { View, ViewProps } from '../View'
|
|
4
4
|
import { EmptyPlaceholder } from '../EmptyPlaceholder'
|
|
5
5
|
import { ListPresets } from './styles'
|
|
6
|
-
import { VirtualItem } from '@tanstack/react-virtual'
|
|
7
6
|
import { useInfiniteScroll } from './useInfiniteScroll'
|
|
8
7
|
import { ListProps } from './types'
|
|
9
8
|
import { ListLayout } from './ListLayout'
|
|
9
|
+
import { RenderComponentProps as MasonryItemProps, List as ListMasonry } from 'masonic'
|
|
10
10
|
|
|
11
11
|
export * from './styles'
|
|
12
12
|
export * from './PaginationIndicator'
|
|
@@ -14,8 +14,6 @@ export * from './useInfiniteScroll'
|
|
|
14
14
|
export * from './types'
|
|
15
15
|
export * from './ListLayout'
|
|
16
16
|
|
|
17
|
-
export type ListComponentType = <T extends any[] = any[]>(props: ListProps<T>) => React.ReactElement
|
|
18
|
-
|
|
19
17
|
const RenderSeparator = (props: { separatorStyles: ViewProps<'div'>['css'] }) => {
|
|
20
18
|
return (
|
|
21
19
|
<View css={[props?.separatorStyles]}></View>
|
|
@@ -26,31 +24,34 @@ const defaultProps: Partial<ListProps> = {
|
|
|
26
24
|
ListFooterComponent: null,
|
|
27
25
|
ListHeaderComponent: null,
|
|
28
26
|
ListLoadingIndicatorComponent: null,
|
|
29
|
-
ListRefreshControlComponent: null,
|
|
30
27
|
ListEmptyComponent: EmptyPlaceholder,
|
|
31
28
|
ListSeparatorComponent: RenderSeparator,
|
|
32
29
|
refreshDebounce: 3000,
|
|
33
30
|
refreshSize: 40,
|
|
34
|
-
refreshThreshold: 0.
|
|
35
|
-
refreshPosition:
|
|
31
|
+
refreshThreshold: 0.1,
|
|
32
|
+
refreshPosition: 16,
|
|
36
33
|
refresh: true,
|
|
34
|
+
rowItemsSpacing: 8,
|
|
35
|
+
overscan: 2,
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
export
|
|
40
|
-
const allProps = {
|
|
38
|
+
export function List<T = any>(props: ListProps<T>) {
|
|
39
|
+
const allProps = {
|
|
41
40
|
...List.defaultProps,
|
|
42
|
-
...
|
|
43
|
-
}
|
|
41
|
+
...props,
|
|
42
|
+
} as ListProps
|
|
44
43
|
|
|
45
44
|
const {
|
|
46
45
|
variants = [],
|
|
47
46
|
responsiveVariants = {},
|
|
48
47
|
styles = {},
|
|
49
|
-
ListLoadingIndicatorComponent,
|
|
50
48
|
renderItem: RenderItem,
|
|
49
|
+
rowItemsSpacing,
|
|
51
50
|
ListSeparatorComponent,
|
|
52
51
|
data,
|
|
52
|
+
overscan,
|
|
53
53
|
separators,
|
|
54
|
+
masonryProps = {},
|
|
54
55
|
} = allProps
|
|
55
56
|
|
|
56
57
|
const variantStyles = useDefaultComponentStyle<'u:List', typeof ListPresets>('u:List', {
|
|
@@ -59,24 +60,19 @@ export const List: ListComponentType = React.forwardRef<'div', ListProps>((flatL
|
|
|
59
60
|
styles,
|
|
60
61
|
})
|
|
61
62
|
|
|
62
|
-
const {
|
|
63
|
-
items,
|
|
64
|
-
dataVirtualizer,
|
|
65
|
-
layoutProps,
|
|
66
|
-
} = useInfiniteScroll(allProps)
|
|
63
|
+
const { layoutProps, onLoadMore } = useInfiniteScroll(allProps)
|
|
67
64
|
|
|
68
|
-
const separator =
|
|
65
|
+
const separator = React.useMemo(() => {
|
|
66
|
+
return separators ? <ListSeparatorComponent separatorStyles={variantStyles.separator} /> : null
|
|
67
|
+
}, [])
|
|
69
68
|
|
|
70
|
-
const renderItem = useCallback((_item:
|
|
69
|
+
const renderItem = React.useCallback((_item: MasonryItemProps<any>) => {
|
|
71
70
|
if (!RenderItem) return null
|
|
72
71
|
|
|
73
|
-
const showIndicator = (_item?.index > data?.length - 1) && !!ListLoadingIndicatorComponent
|
|
74
|
-
|
|
75
72
|
const listLength = data?.length || 0
|
|
76
73
|
|
|
77
74
|
const isFirst = _item?.index === 0
|
|
78
75
|
const isLast = _item?.index === listLength - 1
|
|
79
|
-
|
|
80
76
|
const isOnly = isFirst && isLast
|
|
81
77
|
|
|
82
78
|
const _itemProps = {
|
|
@@ -84,37 +80,31 @@ export const List: ListComponentType = React.forwardRef<'div', ListProps>((flatL
|
|
|
84
80
|
isOnly,
|
|
85
81
|
isLast,
|
|
86
82
|
isFirst,
|
|
87
|
-
item:
|
|
83
|
+
item: _item?.data,
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ref={dataVirtualizer?.measureElement}
|
|
96
|
-
>
|
|
97
|
-
{!isFirst && separator}
|
|
98
|
-
{showIndicator && <ListLoadingIndicatorComponent />}
|
|
99
|
-
{!!_itemProps?.item && <RenderItem {..._itemProps} />}
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}, [RenderItem, data?.length, dataVirtualizer?.measureElement])
|
|
86
|
+
return <>
|
|
87
|
+
{isFirst ? null : separator}
|
|
88
|
+
<RenderItem {..._itemProps} />
|
|
89
|
+
</>
|
|
90
|
+
}, [])
|
|
103
91
|
|
|
104
92
|
return (
|
|
105
93
|
<ListLayout
|
|
106
94
|
{...allProps}
|
|
107
95
|
{...layoutProps}
|
|
108
|
-
variantStyles={variantStyles}
|
|
109
|
-
ref={ref}
|
|
96
|
+
variantStyles={variantStyles}
|
|
110
97
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
|
|
98
|
+
<ListMasonry
|
|
99
|
+
items={data || []}
|
|
100
|
+
render={renderItem}
|
|
101
|
+
itemKey={item => item?.id}
|
|
102
|
+
rowGutter={rowItemsSpacing}
|
|
103
|
+
onRender={onLoadMore}
|
|
104
|
+
{...masonryProps}
|
|
105
|
+
/>
|
|
115
106
|
</ListLayout>
|
|
116
107
|
)
|
|
117
|
-
}
|
|
108
|
+
}
|
|
118
109
|
|
|
119
|
-
// @ts-ignore
|
|
120
110
|
List.defaultProps = defaultProps
|
|
@@ -6,12 +6,9 @@ type ListStates = 'empty' | 'loading'
|
|
|
6
6
|
export type ListParts =
|
|
7
7
|
ViewComposition |
|
|
8
8
|
'innerWrapper' |
|
|
9
|
-
'content' |
|
|
10
9
|
'separator' |
|
|
11
|
-
'itemWrapper' |
|
|
12
10
|
'refreshControl' |
|
|
13
|
-
'refreshControlIndicator'
|
|
14
|
-
'list'
|
|
11
|
+
'refreshControlIndicator'
|
|
15
12
|
|
|
16
13
|
export type ListComposition = `${ListParts}:${ListStates}` | ListParts
|
|
17
14
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { ComponentVariants, PropsOf, StylesOf } from '@codeleap/common'
|
|
2
|
-
import { VirtualItem, VirtualizerOptions } from '@tanstack/react-virtual'
|
|
3
2
|
import { EmptyPlaceholderProps } from '../EmptyPlaceholder'
|
|
4
3
|
import { View, ViewProps } from '../View'
|
|
5
4
|
import { ListComposition, ListPresets } from './styles'
|
|
6
5
|
import { motion } from 'framer-motion'
|
|
7
6
|
import { ActivityIndicatorProps } from '../ActivityIndicator'
|
|
8
7
|
import { ComponentCommonProps } from '../../types'
|
|
8
|
+
import { RenderComponentProps, ListProps as ListMasonryProps } from 'masonic'
|
|
9
|
+
import { UseInfiniteScrollArgs } from './useInfiniteScroll'
|
|
9
10
|
|
|
10
|
-
export type AugmentedRenderItemInfo<T> =
|
|
11
|
+
export type AugmentedRenderItemInfo<T> = RenderComponentProps<T> & {
|
|
11
12
|
item: T
|
|
12
13
|
isFirst: boolean
|
|
13
14
|
isLast: boolean
|
|
@@ -38,7 +39,6 @@ Data = T extends Array<infer D> ? D : never
|
|
|
38
39
|
isFetchingNextPage?: boolean
|
|
39
40
|
fetchNextPage?: () => void
|
|
40
41
|
ListHeaderComponent?: () => React.ReactElement
|
|
41
|
-
virtualizerOptions?: Partial<VirtualizerOptions<any, any>>
|
|
42
42
|
refreshDebounce?: number
|
|
43
43
|
refreshSize?: number
|
|
44
44
|
refreshThreshold?: number
|
|
@@ -48,4 +48,7 @@ Data = T extends Array<infer D> ? D : never
|
|
|
48
48
|
refreshControlIndicatorProps?: Partial<ActivityIndicatorProps>
|
|
49
49
|
style?: React.CSSProperties
|
|
50
50
|
ref?: React.MutableRefObject<undefined>
|
|
51
|
-
|
|
51
|
+
rowItemsSpacing?: number
|
|
52
|
+
overscan?: number
|
|
53
|
+
masonryProps?: Partial<ListMasonryProps<T>>
|
|
54
|
+
} & ComponentCommonProps & UseInfiniteScrollArgs
|
|
@@ -1,113 +1,123 @@
|
|
|
1
|
-
import { onUpdate } from '@codeleap/common'
|
|
2
|
-
import { useVirtualizer, VirtualItem, Virtualizer, VirtualizerOptions } from '@tanstack/react-virtual'
|
|
3
1
|
import React from 'react'
|
|
2
|
+
import { AnyFunction, onUpdate, TypeGuards } from '@codeleap/common'
|
|
4
3
|
import { ListProps } from '.'
|
|
5
4
|
import { GridProps } from '../Grid'
|
|
5
|
+
import { useInfiniteLoader, LoadMoreItemsCallback, UseInfiniteLoaderOptions, useContainerPosition, useScroller } from 'masonic'
|
|
6
6
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export type UseInfiniteScrollArgs<Item extends Element = any> = {
|
|
8
|
+
threshold?: number
|
|
9
|
+
onLoadMore?: AnyFunction
|
|
10
|
+
loadMoreOptions?: Partial<UseInfiniteLoaderOptions<Item>>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type UseInfiniteScrollProps<Item extends Element = any> =
|
|
14
|
+
Partial<ListProps> &
|
|
15
|
+
Partial<GridProps> &
|
|
16
|
+
UseInfiniteScrollArgs<Item>
|
|
11
17
|
|
|
12
|
-
export type UseInfiniteScrollReturn<
|
|
13
|
-
|
|
14
|
-
count: number
|
|
15
|
-
items: VirtualItem[]
|
|
18
|
+
export type UseInfiniteScrollReturn<Item extends Element = any> = {
|
|
19
|
+
onLoadMore: LoadMoreItemsCallback<Item>
|
|
16
20
|
isRefresh: boolean
|
|
17
|
-
parentRef: React.MutableRefObject<undefined>
|
|
18
21
|
layoutProps: {
|
|
19
22
|
isEmpty: boolean
|
|
20
23
|
refreshing: boolean
|
|
21
|
-
|
|
22
|
-
dataVirtualizer: Virtualizer<TS, T>
|
|
24
|
+
scrollableRef: React.MutableRefObject<undefined>
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
isFetchingNextPage,
|
|
32
|
-
fetchNextPage,
|
|
33
|
-
virtualizerOptions = {},
|
|
34
|
-
refreshDebounce,
|
|
35
|
-
refreshThreshold,
|
|
36
|
-
overscan = 10,
|
|
37
|
-
numColumns = 1,
|
|
38
|
-
} = props
|
|
39
|
-
|
|
40
|
-
const parentRef = React.useRef()
|
|
41
|
-
|
|
42
|
-
const [refreshing, setRefreshing] = React.useState(false)
|
|
43
|
-
|
|
44
|
-
const count = hasNextPage ? data?.length + 1 : data?.length
|
|
45
|
-
|
|
46
|
-
const dataVirtualizer = useVirtualizer({
|
|
47
|
-
count,
|
|
48
|
-
getScrollElement: () => parentRef.current,
|
|
49
|
-
estimateSize: () => null,
|
|
50
|
-
overscan,
|
|
51
|
-
...virtualizerOptions,
|
|
52
|
-
})
|
|
28
|
+
type UseRefreshOptions = {
|
|
29
|
+
threshold: number
|
|
30
|
+
debounce: number
|
|
31
|
+
enabled: boolean
|
|
32
|
+
}
|
|
53
33
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
export const useRefresh = (onRefresh = () => null, options: UseRefreshOptions) => {
|
|
35
|
+
const {
|
|
36
|
+
threshold,
|
|
37
|
+
debounce,
|
|
38
|
+
enabled,
|
|
39
|
+
} = options
|
|
40
|
+
|
|
41
|
+
if (!enabled) return {
|
|
42
|
+
refresh: false,
|
|
43
|
+
scrollableRef: null,
|
|
44
|
+
}
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
}, [dataVirtualizer?.scrollOffset, dataVirtualizer?.isScrolling])
|
|
46
|
+
const [refresh, setRefresh] = React.useState(false)
|
|
60
47
|
|
|
61
|
-
const
|
|
48
|
+
const containerRef = React.useRef(null)
|
|
62
49
|
|
|
63
|
-
const
|
|
50
|
+
const { offset } = useContainerPosition(containerRef)
|
|
51
|
+
const { scrollTop, isScrolling } = useScroller(offset, 2)
|
|
64
52
|
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
53
|
+
const handleRefresh = React.useCallback(() => {
|
|
54
|
+
if (!refresh && isScrolling) {
|
|
55
|
+
setRefresh(true)
|
|
68
56
|
onRefresh?.()
|
|
69
57
|
|
|
70
58
|
setTimeout(() => {
|
|
71
|
-
|
|
72
|
-
},
|
|
59
|
+
setRefresh(false)
|
|
60
|
+
}, debounce)
|
|
73
61
|
}
|
|
74
|
-
}, [
|
|
62
|
+
}, [refresh, isScrolling])
|
|
75
63
|
|
|
76
64
|
onUpdate(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!lastItem) {
|
|
80
|
-
return
|
|
65
|
+
if (scrollTop <= threshold) {
|
|
66
|
+
handleRefresh()
|
|
81
67
|
}
|
|
68
|
+
}, [scrollTop])
|
|
82
69
|
|
|
83
|
-
|
|
70
|
+
return {
|
|
71
|
+
refresh,
|
|
72
|
+
scrollableRef: containerRef,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
) {
|
|
90
|
-
fetchNextPage?.()
|
|
91
|
-
}
|
|
92
|
-
}, [
|
|
76
|
+
export function useInfiniteScroll<Item extends Element = any>(props: UseInfiniteScrollProps<Item>): UseInfiniteScrollReturn<Item> {
|
|
77
|
+
const {
|
|
78
|
+
onRefresh,
|
|
79
|
+
data,
|
|
93
80
|
hasNextPage,
|
|
81
|
+
refresh: refreshEnabled,
|
|
94
82
|
fetchNextPage,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
83
|
+
refreshThreshold,
|
|
84
|
+
refreshDebounce,
|
|
85
|
+
loadMoreOptions = {},
|
|
86
|
+
onLoadMore,
|
|
87
|
+
threshold = 3,
|
|
88
|
+
} = props
|
|
89
|
+
|
|
90
|
+
const infiniteLoader = useInfiniteLoader(
|
|
91
|
+
async (args) => {
|
|
92
|
+
if (hasNextPage) await fetchNextPage?.()
|
|
93
|
+
if (TypeGuards.isFunction(onLoadMore)) await onLoadMore?.(args)
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
isItemLoaded: (index, items) => !!items?.[index],
|
|
97
|
+
minimumBatchSize: 32,
|
|
98
|
+
threshold: threshold,
|
|
99
|
+
...loadMoreOptions,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const { refresh, scrollableRef } = useRefresh(
|
|
104
|
+
onRefresh,
|
|
105
|
+
{
|
|
106
|
+
threshold: refreshThreshold,
|
|
107
|
+
debounce: refreshDebounce,
|
|
108
|
+
enabled: refreshEnabled,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const isEmpty = React.useMemo(() => (!data || !data?.length), [data?.length])
|
|
99
113
|
|
|
100
114
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
isRefresh,
|
|
104
|
-
count,
|
|
105
|
-
parentRef,
|
|
115
|
+
onLoadMore: infiniteLoader,
|
|
116
|
+
isRefresh: refresh,
|
|
106
117
|
layoutProps: {
|
|
107
|
-
|
|
108
|
-
refreshing,
|
|
118
|
+
scrollableRef,
|
|
119
|
+
refreshing: refresh,
|
|
109
120
|
isEmpty,
|
|
110
|
-
dataVirtualizer
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
}
|