@codeleap/web 3.10.2 → 3.10.3
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 +52 -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 +35 -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.3",
|
|
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,56 @@ 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
|
+
columnGutter={columnItemsSpacing}
|
|
102
|
+
rowGutter={rowItemsSpacing}
|
|
103
|
+
overscanBy={overscan}
|
|
104
|
+
render={renderItem}
|
|
105
|
+
onRender={onLoadMore}
|
|
106
|
+
itemKey={item => item?.id}
|
|
107
|
+
columnCount={numColumns}
|
|
108
|
+
{...masonryProps}
|
|
109
|
+
/>
|
|
163
110
|
</ListLayout>
|
|
164
111
|
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type GridComponentType = <T extends any[] = any[]>(props: GridProps<T>) => React.ReactElement
|
|
112
|
+
}
|
|
168
113
|
|
|
169
|
-
|
|
114
|
+
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,32 @@ 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
|
+
rowGutter={rowItemsSpacing}
|
|
101
|
+
overscanBy={overscan}
|
|
102
|
+
render={renderItem}
|
|
103
|
+
onRender={onLoadMore}
|
|
104
|
+
itemKey={item => item?.id}
|
|
105
|
+
{...masonryProps}
|
|
106
|
+
/>
|
|
115
107
|
</ListLayout>
|
|
116
108
|
)
|
|
117
|
-
}
|
|
109
|
+
}
|
|
118
110
|
|
|
119
|
-
// @ts-ignore
|
|
120
111
|
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
|
}
|