@codeleap/web 3.12.0 → 3.12.1
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 +1 -1
- package/src/components/EmptyPlaceholder/index.tsx +14 -14
- package/src/components/Grid/index.tsx +10 -5
- package/src/components/List/ListLayout.tsx +21 -21
- package/src/components/List/index.tsx +9 -4
- package/src/components/List/types.ts +1 -0
- package/src/components/List/useInfiniteScroll.ts +21 -21
- package/src/lib/ListMasonry.tsx +80 -0
- package/src/lib/index.ts +1 -0
package/package.json
CHANGED
|
@@ -75,6 +75,19 @@ export const EmptyPlaceholder = (props: EmptyPlaceholderProps) => {
|
|
|
75
75
|
return getNestedStylesByKey('loader', variantStyles)
|
|
76
76
|
}, [variantStyles])
|
|
77
77
|
|
|
78
|
+
const _Image = React.useMemo(() => {
|
|
79
|
+
if (TypeGuards.isNil(IconEmpty)) return null
|
|
80
|
+
|
|
81
|
+
if (TypeGuards.isString(IconEmpty)) {
|
|
82
|
+
return <Icon debugName={debugName} name={IconEmpty as IconPlaceholder} forceStyle={variantStyles.icon} />
|
|
83
|
+
} else if (React.isValidElement(IconEmpty)) {
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
return <IconEmpty {...props} />
|
|
86
|
+
} else {
|
|
87
|
+
return <IconEmpty {...props} />
|
|
88
|
+
}
|
|
89
|
+
}, [IconEmpty])
|
|
90
|
+
|
|
78
91
|
if (loading) {
|
|
79
92
|
return (
|
|
80
93
|
<View css={[variantStyles.wrapper, variantStyles['wrapper:loading']]}>
|
|
@@ -99,20 +112,7 @@ export const EmptyPlaceholder = (props: EmptyPlaceholderProps) => {
|
|
|
99
112
|
</View>
|
|
100
113
|
)
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
const _Image = React.useMemo(() => {
|
|
104
|
-
if (TypeGuards.isNil(IconEmpty)) return null
|
|
105
|
-
|
|
106
|
-
if (TypeGuards.isString(IconEmpty)) {
|
|
107
|
-
return <Icon debugName={debugName} name={IconEmpty as IconPlaceholder} forceStyle={variantStyles.icon} />
|
|
108
|
-
} else if (React.isValidElement(IconEmpty)) {
|
|
109
|
-
// @ts-ignore
|
|
110
|
-
return <IconEmpty {...props} />
|
|
111
|
-
} else {
|
|
112
|
-
return <IconEmpty {...props} />
|
|
113
|
-
}
|
|
114
|
-
}, [])
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
return (
|
|
117
117
|
<View {...wrapperProps} css={[variantStyles.wrapper, style]}>
|
|
118
118
|
<View {...imageWrapperProps} css={variantStyles.imageWrapper}>
|
|
@@ -5,7 +5,7 @@ import { EmptyPlaceholder } from '../EmptyPlaceholder'
|
|
|
5
5
|
import { GridPresets } from './styles'
|
|
6
6
|
import { GridProps } from './types'
|
|
7
7
|
import { ListLayout, useInfiniteScroll } from '../List'
|
|
8
|
-
import {
|
|
8
|
+
import { ItemMasonryProps, ListMasonry } from '../../lib'
|
|
9
9
|
|
|
10
10
|
export * from './styles'
|
|
11
11
|
export * from './types'
|
|
@@ -30,6 +30,7 @@ const defaultProps: Partial<GridProps> = {
|
|
|
30
30
|
columnItemsSpacing: 8,
|
|
31
31
|
rowItemsSpacing: 8,
|
|
32
32
|
overscan: 2,
|
|
33
|
+
enabledItemsRehydrateIndicator: true
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function Grid<T = any>(props: GridProps<T>) {
|
|
@@ -51,6 +52,7 @@ export function Grid<T = any>(props: GridProps<T>) {
|
|
|
51
52
|
separators,
|
|
52
53
|
masonryProps = {},
|
|
53
54
|
numColumns,
|
|
55
|
+
enabledItemsRehydrateIndicator,
|
|
54
56
|
} = allProps
|
|
55
57
|
|
|
56
58
|
const variantStyles = useDefaultComponentStyle<'u:Grid', typeof GridPresets>('u:Grid', {
|
|
@@ -59,13 +61,13 @@ export function Grid<T = any>(props: GridProps<T>) {
|
|
|
59
61
|
styles,
|
|
60
62
|
})
|
|
61
63
|
|
|
62
|
-
const { layoutProps, onLoadMore } = useInfiniteScroll(allProps)
|
|
64
|
+
const { layoutProps, onLoadMore, onRefreshItems } = useInfiniteScroll(allProps)
|
|
63
65
|
|
|
64
66
|
const separator = React.useMemo(() => {
|
|
65
67
|
return separators ? <ListSeparatorComponent separatorStyles={variantStyles.separator} /> : null
|
|
66
68
|
}, [])
|
|
67
69
|
|
|
68
|
-
const renderItem = React.useCallback((_item:
|
|
70
|
+
const renderItem = React.useCallback((_item: ItemMasonryProps<any>) => {
|
|
69
71
|
if (!RenderItem) return null
|
|
70
72
|
|
|
71
73
|
const gridLength = data?.length || 0
|
|
@@ -96,15 +98,18 @@ export function Grid<T = any>(props: GridProps<T>) {
|
|
|
96
98
|
{...layoutProps}
|
|
97
99
|
variantStyles={variantStyles}
|
|
98
100
|
>
|
|
99
|
-
<
|
|
100
|
-
items={data
|
|
101
|
+
<ListMasonry
|
|
102
|
+
items={data}
|
|
101
103
|
render={renderItem}
|
|
102
104
|
itemKey={item => item?.id}
|
|
103
105
|
columnGutter={columnItemsSpacing}
|
|
104
106
|
rowGutter={rowItemsSpacing}
|
|
105
107
|
columnCount={numColumns}
|
|
108
|
+
maxColumnCount={numColumns}
|
|
106
109
|
onRender={onLoadMore}
|
|
107
110
|
overscanBy={overscan}
|
|
111
|
+
onRefreshItems={onRefreshItems}
|
|
112
|
+
itemsRehydrateIndicator={enabledItemsRehydrateIndicator}
|
|
108
113
|
{...masonryProps}
|
|
109
114
|
/>
|
|
110
115
|
</ListLayout>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { StylesOf } from '@codeleap/common'
|
|
2
|
+
import { StylesOf, TypeGuards } from '@codeleap/common'
|
|
3
3
|
import { ListComposition, ListParts } from './styles'
|
|
4
4
|
import { ListProps } from './types'
|
|
5
5
|
import { View } from '../View'
|
|
@@ -17,13 +17,13 @@ type ListRefreshControlComponent = Partial<ListLayoutProps> & {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const DefaultRefreshIndicator = (props: ListRefreshControlComponent) => {
|
|
20
|
-
const {
|
|
21
|
-
refreshing,
|
|
22
|
-
variantStyles,
|
|
23
|
-
refreshPosition,
|
|
24
|
-
refreshControlProps,
|
|
25
|
-
debugName,
|
|
26
|
-
refreshSize,
|
|
20
|
+
const {
|
|
21
|
+
refreshing,
|
|
22
|
+
variantStyles,
|
|
23
|
+
refreshPosition,
|
|
24
|
+
refreshControlProps,
|
|
25
|
+
debugName,
|
|
26
|
+
refreshSize,
|
|
27
27
|
refreshControlIndicatorProps,
|
|
28
28
|
} = props
|
|
29
29
|
|
|
@@ -77,22 +77,22 @@ export const ListLayout = (props: ListLayoutProps) => {
|
|
|
77
77
|
<View css={[getKeyStyle('wrapper'), style]} ref={scrollableRef}>
|
|
78
78
|
{!!ListHeaderComponent ? <ListHeaderComponent /> : null}
|
|
79
79
|
|
|
80
|
-
{isEmpty
|
|
81
|
-
? <ListEmptyComponent debugName={debugName} {...placeholder} />
|
|
82
|
-
: (
|
|
83
|
-
<View css={[getKeyStyle('innerWrapper')]}>
|
|
84
|
-
{(!ListRefreshControlComponent || !refresh) ? null : (
|
|
85
|
-
<ListRefreshControlComponent
|
|
86
|
-
{...props}
|
|
87
|
-
variantStyles={variantStyles}
|
|
88
|
-
/>
|
|
89
|
-
)}
|
|
80
|
+
{isEmpty ? <ListEmptyComponent debugName={debugName} {...placeholder} /> : null}
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
<View css={[getKeyStyle('innerWrapper'), isEmpty && { display: 'none' }]}>
|
|
83
|
+
{(!ListRefreshControlComponent || !refresh) ? null : (
|
|
84
|
+
<ListRefreshControlComponent
|
|
85
|
+
{...props}
|
|
86
|
+
variantStyles={variantStyles}
|
|
87
|
+
/>
|
|
93
88
|
)}
|
|
94
89
|
|
|
95
|
-
|
|
90
|
+
{children}
|
|
91
|
+
</View>
|
|
92
|
+
|
|
93
|
+
{((isFetching || isFetchingNextPage) && !TypeGuards.isNil(ListLoadingIndicatorComponent))
|
|
94
|
+
? <ListLoadingIndicatorComponent />
|
|
95
|
+
: null}
|
|
96
96
|
|
|
97
97
|
{!!ListFooterComponent ? <ListFooterComponent /> : null}
|
|
98
98
|
</View>
|
|
@@ -6,7 +6,7 @@ import { ListPresets } from './styles'
|
|
|
6
6
|
import { useInfiniteScroll } from './useInfiniteScroll'
|
|
7
7
|
import { ListProps } from './types'
|
|
8
8
|
import { ListLayout } from './ListLayout'
|
|
9
|
-
import {
|
|
9
|
+
import { ItemMasonryProps, ListMasonry } from '../../lib'
|
|
10
10
|
|
|
11
11
|
export * from './styles'
|
|
12
12
|
export * from './PaginationIndicator'
|
|
@@ -33,6 +33,7 @@ const defaultProps: Partial<ListProps> = {
|
|
|
33
33
|
refresh: true,
|
|
34
34
|
rowItemsSpacing: 8,
|
|
35
35
|
overscan: 2,
|
|
36
|
+
enabledItemsRehydrateIndicator: true,
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function List<T = any>(props: ListProps<T>) {
|
|
@@ -52,6 +53,7 @@ export function List<T = any>(props: ListProps<T>) {
|
|
|
52
53
|
overscan,
|
|
53
54
|
separators,
|
|
54
55
|
masonryProps = {},
|
|
56
|
+
enabledItemsRehydrateIndicator,
|
|
55
57
|
} = allProps
|
|
56
58
|
|
|
57
59
|
const variantStyles = useDefaultComponentStyle<'u:List', typeof ListPresets>('u:List', {
|
|
@@ -60,13 +62,13 @@ export function List<T = any>(props: ListProps<T>) {
|
|
|
60
62
|
styles,
|
|
61
63
|
})
|
|
62
64
|
|
|
63
|
-
const { layoutProps, onLoadMore } = useInfiniteScroll(allProps)
|
|
65
|
+
const { layoutProps, onLoadMore, onRefreshItems } = useInfiniteScroll(allProps)
|
|
64
66
|
|
|
65
67
|
const separator = React.useMemo(() => {
|
|
66
68
|
return separators ? <ListSeparatorComponent separatorStyles={variantStyles.separator} /> : null
|
|
67
69
|
}, [])
|
|
68
70
|
|
|
69
|
-
const renderItem = React.useCallback((_item:
|
|
71
|
+
const renderItem = React.useCallback((_item: ItemMasonryProps<any>) => {
|
|
70
72
|
if (!RenderItem) return null
|
|
71
73
|
|
|
72
74
|
const listLength = data?.length || 0
|
|
@@ -96,12 +98,15 @@ export function List<T = any>(props: ListProps<T>) {
|
|
|
96
98
|
variantStyles={variantStyles}
|
|
97
99
|
>
|
|
98
100
|
<ListMasonry
|
|
99
|
-
items={data
|
|
101
|
+
items={data}
|
|
100
102
|
render={renderItem}
|
|
101
103
|
itemKey={item => item?.id}
|
|
102
104
|
rowGutter={rowItemsSpacing}
|
|
103
105
|
onRender={onLoadMore}
|
|
104
106
|
overscanBy={overscan}
|
|
107
|
+
columnCount={1}
|
|
108
|
+
onRefreshItems={onRefreshItems}
|
|
109
|
+
itemsRehydrateIndicator={enabledItemsRehydrateIndicator}
|
|
105
110
|
{...masonryProps}
|
|
106
111
|
/>
|
|
107
112
|
</ListLayout>
|
|
@@ -23,6 +23,7 @@ export type UseInfiniteScrollReturn<Item extends Element = any> = {
|
|
|
23
23
|
refreshing: boolean
|
|
24
24
|
scrollableRef: React.MutableRefObject<undefined>
|
|
25
25
|
}
|
|
26
|
+
onRefreshItems: AnyFunction
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
type UseRefreshOptions = {
|
|
@@ -51,26 +52,22 @@ export const useRefresh = (onRefresh = () => null, options: UseRefreshOptions) =
|
|
|
51
52
|
enabled,
|
|
52
53
|
} = options
|
|
53
54
|
|
|
54
|
-
if (!enabled) return {
|
|
55
|
-
refresh: false,
|
|
56
|
-
scrollableRef: null,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
55
|
const [refresh, setRefresh] = React.useState(false)
|
|
60
56
|
|
|
61
57
|
const pushToTopRef = React.useRef(0)
|
|
62
|
-
const containerRef = React.useRef(null)
|
|
63
58
|
|
|
64
|
-
const refresher = React.useCallback(async () => {
|
|
59
|
+
const refresher = React.useCallback(async (_onRefresh: AnyFunction) => {
|
|
65
60
|
setRefresh(true)
|
|
66
|
-
await
|
|
61
|
+
await _onRefresh?.()
|
|
67
62
|
|
|
68
63
|
setTimeout(() => {
|
|
69
64
|
setRefresh(false)
|
|
70
65
|
pushToTopRef.current = 0
|
|
71
|
-
},
|
|
66
|
+
}, 2500)
|
|
72
67
|
}, [])
|
|
73
68
|
|
|
69
|
+
const containerRef = React.useRef(null)
|
|
70
|
+
|
|
74
71
|
const onScroll = scrollDebounce(() => {
|
|
75
72
|
if (containerRef.current) {
|
|
76
73
|
const rect = containerRef.current?.getBoundingClientRect()
|
|
@@ -87,7 +84,7 @@ export const useRefresh = (onRefresh = () => null, options: UseRefreshOptions) =
|
|
|
87
84
|
|
|
88
85
|
if (percentage < threshold) {
|
|
89
86
|
if (pushToTopRef.current === 2) {
|
|
90
|
-
refresher()
|
|
87
|
+
refresher(onRefresh)
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
pushToTopRef.current = pushToTopRef.current + 1
|
|
@@ -96,16 +93,19 @@ export const useRefresh = (onRefresh = () => null, options: UseRefreshOptions) =
|
|
|
96
93
|
}, debounce)
|
|
97
94
|
|
|
98
95
|
useEffect(() => {
|
|
99
|
-
|
|
96
|
+
if (enabled) {
|
|
97
|
+
window.addEventListener('scroll', onScroll)
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener('scroll', onScroll)
|
|
101
|
+
}
|
|
103
102
|
}
|
|
104
|
-
}, [])
|
|
103
|
+
}, [enabled])
|
|
105
104
|
|
|
106
105
|
return {
|
|
107
106
|
refresh,
|
|
108
107
|
scrollableRef: containerRef,
|
|
108
|
+
refresher,
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -120,7 +120,7 @@ export function useInfiniteScroll<Item extends Element = any>(props: UseInfinite
|
|
|
120
120
|
refreshDebounce,
|
|
121
121
|
loadMoreOptions = {},
|
|
122
122
|
onLoadMore,
|
|
123
|
-
threshold =
|
|
123
|
+
threshold = 16,
|
|
124
124
|
} = props
|
|
125
125
|
|
|
126
126
|
const infiniteLoader = useInfiniteLoader(
|
|
@@ -130,13 +130,12 @@ export function useInfiniteScroll<Item extends Element = any>(props: UseInfinite
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
isItemLoaded: (index, items) => !!items?.[index],
|
|
133
|
-
minimumBatchSize: 32,
|
|
134
133
|
threshold: threshold,
|
|
135
134
|
...loadMoreOptions,
|
|
136
135
|
},
|
|
137
136
|
)
|
|
138
137
|
|
|
139
|
-
const
|
|
138
|
+
const refreshHookReturn = useRefresh(
|
|
140
139
|
onRefresh,
|
|
141
140
|
{
|
|
142
141
|
threshold: refreshThreshold,
|
|
@@ -149,11 +148,12 @@ export function useInfiniteScroll<Item extends Element = any>(props: UseInfinite
|
|
|
149
148
|
|
|
150
149
|
return {
|
|
151
150
|
onLoadMore: infiniteLoader,
|
|
152
|
-
isRefresh: refresh,
|
|
151
|
+
isRefresh: refreshHookReturn.refresh,
|
|
153
152
|
layoutProps: {
|
|
154
|
-
scrollableRef,
|
|
155
|
-
refreshing: refresh,
|
|
153
|
+
scrollableRef: refreshHookReturn.scrollableRef,
|
|
154
|
+
refreshing: refreshHookReturn.refresh,
|
|
156
155
|
isEmpty,
|
|
157
|
-
}
|
|
156
|
+
},
|
|
157
|
+
onRefreshItems: refreshHookReturn.refresher,
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useWindowSize } from '@react-hook/window-size'
|
|
3
|
+
import { AnyFunction, onUpdate, usePrevious } from '@codeleap/common'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useMasonry,
|
|
7
|
+
usePositioner,
|
|
8
|
+
useScroller,
|
|
9
|
+
useContainerPosition,
|
|
10
|
+
useResizeObserver,
|
|
11
|
+
MasonryProps,
|
|
12
|
+
RenderComponentProps,
|
|
13
|
+
} from 'masonic'
|
|
14
|
+
|
|
15
|
+
export type ItemMasonryProps<T> = RenderComponentProps<T>
|
|
16
|
+
|
|
17
|
+
export type ListMasonryProps<T> = MasonryProps<T> & {
|
|
18
|
+
onRefreshItems: AnyFunction
|
|
19
|
+
itemsRehydrateIndicator: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ListMasonry<Item>(props: ListMasonryProps<Item>) {
|
|
23
|
+
const data = props?.items || []
|
|
24
|
+
|
|
25
|
+
const dataPreviousLength = usePrevious(data?.length)
|
|
26
|
+
|
|
27
|
+
const masonryUpdater = React.useMemo(() => {
|
|
28
|
+
if (data?.length < dataPreviousLength) {
|
|
29
|
+
return data?.length
|
|
30
|
+
} else {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}, [dataPreviousLength, data?.length])
|
|
34
|
+
|
|
35
|
+
onUpdate(() => {
|
|
36
|
+
if (!!masonryUpdater && !!props?.itemsRehydrateIndicator) {
|
|
37
|
+
props?.onRefreshItems?.(() => null)
|
|
38
|
+
}
|
|
39
|
+
}, [masonryUpdater])
|
|
40
|
+
|
|
41
|
+
const containerRef = React.useRef(null)
|
|
42
|
+
|
|
43
|
+
const windowSize = useWindowSize({
|
|
44
|
+
initialWidth: props?.ssrWidth,
|
|
45
|
+
initialHeight: props?.ssrHeight,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const containerPosition = useContainerPosition(containerRef, windowSize)
|
|
49
|
+
|
|
50
|
+
const listProps = Object.assign(
|
|
51
|
+
{
|
|
52
|
+
offset: containerPosition?.offset,
|
|
53
|
+
width: containerPosition?.width || containerRef?.current?.clientWidth || windowSize?.[0],
|
|
54
|
+
height: windowSize?.[1],
|
|
55
|
+
containerRef,
|
|
56
|
+
scrollFps: props?.scrollFps || 12,
|
|
57
|
+
},
|
|
58
|
+
props
|
|
59
|
+
) as any
|
|
60
|
+
|
|
61
|
+
listProps.positioner = usePositioner({
|
|
62
|
+
width: listProps?.width,
|
|
63
|
+
columnGutter: listProps?.columnGutter,
|
|
64
|
+
columnWidth: listProps?.columnWidth,
|
|
65
|
+
columnCount: listProps?.columnCount,
|
|
66
|
+
maxColumnCount: listProps?.columnCount,
|
|
67
|
+
rowGutter: listProps?.rowGutter
|
|
68
|
+
}, [masonryUpdater])
|
|
69
|
+
|
|
70
|
+
const { scrollTop, isScrolling } = useScroller(listProps?.offset, listProps?.scrollFps)
|
|
71
|
+
|
|
72
|
+
listProps.resizeObserver = useResizeObserver(listProps.positioner)
|
|
73
|
+
|
|
74
|
+
return useMasonry({
|
|
75
|
+
...listProps,
|
|
76
|
+
scrollTop,
|
|
77
|
+
isScrolling,
|
|
78
|
+
items: data,
|
|
79
|
+
})
|
|
80
|
+
}
|
package/src/lib/index.ts
CHANGED