@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeleap/web",
3
- "version": "3.10.2",
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, useCallback } from '@codeleap/common'
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.5,
38
- refreshPosition: 2,
27
+ refreshThreshold: 0.1,
28
+ refreshPosition: 16,
39
29
  refresh: true,
40
- numColumns: 2,
30
+ columnItemsSpacing: 8,
31
+ rowItemsSpacing: 8,
32
+ overscan: 2,
41
33
  }
42
34
 
43
- const GridCP = React.forwardRef<'div', GridProps>((flatGridProps, ref) => {
35
+ export function Grid<T = any>(props: GridProps<T>) {
44
36
  const allProps = {
45
- ...defaultProps,
46
- ...flatGridProps,
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
- data,
55
- ListLoadingIndicatorComponent,
46
+ columnItemsSpacing,
47
+ rowItemsSpacing,
56
48
  ListSeparatorComponent,
57
- virtualizerOptions = {},
58
- numColumns,
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 separator = separators && <ListSeparatorComponent separatorStyles={variantStyles.separator} />
62
+ const { layoutProps, onLoadMore } = useInfiniteScroll(allProps)
69
63
 
70
- const {
71
- dataVirtualizer,
72
- parentRef,
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 columns = React.useMemo(() => {
81
- return generateColumns(numColumns)
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
- const columnItems = columnVirtualizer.getVirtualItems()
71
+ const gridLength = data?.length || 0
94
72
 
95
- const renderItem = useCallback((_item: VirtualItem) => {
96
- if (!RenderItem) return null
73
+ const isFirst = _item?.index === 0
74
+ const isLast = _item?.index === gridLength - 1
75
+ const isOnly = isFirst && isLast
97
76
 
98
- const showIndicator = (_item?.index === (data?.length / numColumns)) && !!ListLoadingIndicatorComponent
77
+ const _itemProps = {
78
+ ..._item,
79
+ isOnly,
80
+ isLast,
81
+ isFirst,
82
+ item: _item?.data
83
+ }
99
84
 
100
- const gridLength = data?.length || 0
85
+ if (!_itemProps?.item) return null
101
86
 
102
87
  return <>
103
- {/* Necessary for correct list render */}
104
- <div
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
- }, [RenderItem, data?.length, dataVirtualizer?.measureElement])
91
+ }, [])
154
92
 
155
93
  return (
156
94
  <ListLayout
157
95
  {...allProps}
158
96
  {...layoutProps}
159
- variantStyles={variantStyles} // @ts-ignore
160
- ref={ref}
97
+ variantStyles={variantStyles}
161
98
  >
162
- {items?.map((item) => renderItem(item))}
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
- export const Grid = GridCP as unknown as GridComponentType
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 | 'column'
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(() => ({ content: style })))
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 { AugmentedRenderItemInfo, ListProps } from '../List'
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' | 'renderItem'> &
10
+ Omit<ListProps<T, Data>, 'variants' | 'styles'> &
20
11
  ComponentVariants<typeof GridPresets> & {
21
12
  styles?: StylesOf<GridComposition>
22
- numColumns?: number
23
- renderItem: (data: GridAugmentedRenderItemInfo<T>) => React.ReactElement
13
+ columnItemsSpacing?: number
14
+ numColumns: number
24
15
  } & ComponentCommonProps
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { StylesOf, TypeGuards, useCodeleapContext } from '@codeleap/common'
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
- ListRefreshControlComponent,
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 { Theme } = useCodeleapContext()
40
-
41
- const getKeyStyle = React.useCallback((key: ListParts) => {
42
- return [
43
- variantStyles[key],
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
- // @ts-ignore
55
- <View css={[getKeyStyle('wrapper'), style]}>
56
- {!!ListHeaderComponent && <ListHeaderComponent />}
81
+ <View css={[getKeyStyle('wrapper'), style]} ref={scrollableRef}>
82
+ {!!ListHeaderComponent ? <ListHeaderComponent /> : null}
57
83
 
58
- {isEmpty ? <ListEmptyComponent debugName={debugName} {...placeholder} /> : (
59
- <View
60
- ref={parentRef}
61
- css={[getKeyStyle('innerWrapper')]}
62
- >
63
- <View
64
- //@ts-ignore
65
- ref={ref}
66
- css={[
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
- </View>
93
- )}
96
+ )}
97
+
98
+ {(isFetching || isFetchingNextPage) ? <ListLoadingIndicatorComponent /> : null}
94
99
 
95
- {!!ListFooterComponent && <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, useCallback } from '@codeleap/common'
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.5,
35
- refreshPosition: 2,
31
+ refreshThreshold: 0.1,
32
+ refreshPosition: 16,
36
33
  refresh: true,
34
+ rowItemsSpacing: 8,
35
+ overscan: 2,
37
36
  }
38
37
 
39
- export const List: ListComponentType = React.forwardRef<'div', ListProps>((flatListProps, ref) => {
40
- const allProps = { // @ts-ignore
38
+ export function List<T = any>(props: ListProps<T>) {
39
+ const allProps = {
41
40
  ...List.defaultProps,
42
- ...flatListProps,
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 = separators && <ListSeparatorComponent separatorStyles={variantStyles.separator} />
65
+ const separator = React.useMemo(() => {
66
+ return separators ? <ListSeparatorComponent separatorStyles={variantStyles.separator} /> : null
67
+ }, [])
69
68
 
70
- const renderItem = useCallback((_item: VirtualItem) => {
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: data?.[_item?.index]
83
+ item: _item?.data,
88
84
  }
89
85
 
90
- return (
91
- <div
92
- css={[variantStyles.itemWrapper]}
93
- key={_item?.key}
94
- data-index={_item?.index}
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} // @ts-ignore
109
- ref={ref}
96
+ variantStyles={variantStyles}
110
97
  >
111
- {/* Necessary for correct list render */}
112
- <div css={[variantStyles.list, { transform: `translateY(${items?.[0]?.start}px)` }]}>
113
- {items?.map((item) => renderItem(item))}
114
- </div>
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> = VirtualItem & {
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
- } & ComponentCommonProps
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 UseInfiniteScrollProps<TS extends Element = any, T extends Element = any> =
8
- ListProps &
9
- GridProps &
10
- Pick<VirtualizerOptions<TS, T>, 'overscan'>
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<TS extends Element = any, T extends Element = any> = {
13
- dataVirtualizer: Virtualizer<TS, T>
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
- parentRef: React.MutableRefObject<undefined>
22
- dataVirtualizer: Virtualizer<TS, T>
24
+ scrollableRef: React.MutableRefObject<undefined>
23
25
  }
24
26
  }
25
27
 
26
- export const useInfiniteScroll = (props: UseInfiniteScrollProps): UseInfiniteScrollReturn => {
27
- const {
28
- onRefresh,
29
- data,
30
- hasNextPage,
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
- const isRefresh = React.useMemo(() => {
55
- const _offset = dataVirtualizer?.scrollOffset
56
- const _refresh = _offset <= refreshThreshold && dataVirtualizer?.isScrolling
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
- return _refresh
59
- }, [dataVirtualizer?.scrollOffset, dataVirtualizer?.isScrolling])
46
+ const [refresh, setRefresh] = React.useState(false)
60
47
 
61
- const isEmpty = !data || !data?.length
48
+ const containerRef = React.useRef(null)
62
49
 
63
- const items = dataVirtualizer?.getVirtualItems()
50
+ const { offset } = useContainerPosition(containerRef)
51
+ const { scrollTop, isScrolling } = useScroller(offset, 2)
64
52
 
65
- onUpdate(() => {
66
- if (isRefresh) {
67
- setRefreshing(true)
53
+ const handleRefresh = React.useCallback(() => {
54
+ if (!refresh && isScrolling) {
55
+ setRefresh(true)
68
56
  onRefresh?.()
69
57
 
70
58
  setTimeout(() => {
71
- setRefreshing(false)
72
- }, refreshDebounce)
59
+ setRefresh(false)
60
+ }, debounce)
73
61
  }
74
- }, [!!isRefresh])
62
+ }, [refresh, isScrolling])
75
63
 
76
64
  onUpdate(() => {
77
- const [lastItem] = [...(items ?? [])]?.reverse()
78
-
79
- if (!lastItem) {
80
- return
65
+ if (scrollTop <= threshold) {
66
+ handleRefresh()
81
67
  }
68
+ }, [scrollTop])
82
69
 
83
- const itemsLength = (data?.length / numColumns) - 1
70
+ return {
71
+ refresh,
72
+ scrollableRef: containerRef,
73
+ }
74
+ }
84
75
 
85
- if (
86
- lastItem?.index >= itemsLength &&
87
- hasNextPage &&
88
- !isFetchingNextPage
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
- data?.length,
96
- isFetchingNextPage,
97
- items,
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
- items,
102
- dataVirtualizer,
103
- isRefresh,
104
- count,
105
- parentRef,
115
+ onLoadMore: infiniteLoader,
116
+ isRefresh: refresh,
106
117
  layoutProps: {
107
- parentRef,
108
- refreshing,
118
+ scrollableRef,
119
+ refreshing: refresh,
109
120
  isEmpty,
110
- dataVirtualizer
111
121
  }
112
122
  }
113
123
  }