@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeleap/web",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "main": "src/index.ts",
5
5
  "repository": {
6
6
  "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
@@ -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 { RenderComponentProps as MasonryItemProps, Masonry as GridMasonry } from 'masonic'
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: MasonryItemProps<any>) => {
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
- <GridMasonry
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
- {children}
92
- </View>
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
- {(isFetching || isFetchingNextPage) ? <ListLoadingIndicatorComponent /> : null}
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 { RenderComponentProps as MasonryItemProps, List as ListMasonry } from 'masonic'
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: MasonryItemProps<any>) => {
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>
@@ -51,4 +51,5 @@ Data = T extends Array<infer D> ? D : never
51
51
  rowItemsSpacing?: number
52
52
  overscan?: number
53
53
  masonryProps?: Partial<ListMasonryProps<T>>
54
+ enabledItemsRehydrateIndicator?: boolean
54
55
  } & ComponentCommonProps & UseInfiniteScrollArgs
@@ -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 onRefresh?.()
61
+ await _onRefresh?.()
67
62
 
68
63
  setTimeout(() => {
69
64
  setRefresh(false)
70
65
  pushToTopRef.current = 0
71
- }, 2000)
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
- window.addEventListener('scroll', onScroll)
96
+ if (enabled) {
97
+ window.addEventListener('scroll', onScroll)
100
98
 
101
- return () => {
102
- window.removeEventListener('scroll', onScroll)
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 = 3,
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 { refresh, scrollableRef } = useRefresh(
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
@@ -4,3 +4,4 @@ export * from './useSearchParams'
4
4
  export * from './usePopState'
5
5
  export * from './useBreakpointMatch'
6
6
  export * from './useClick'
7
+ export * from './ListMasonry'