Move the fork to List.web
This commit is contained in:
@@ -0,0 +1,224 @@
|
|||||||
|
import React, {memo, startTransition} from 'react'
|
||||||
|
import {FlatListProps, ScrollView, StyleSheet, View} from 'react-native'
|
||||||
|
import {addStyle} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
|
||||||
|
export type ListMethods = FlatList_INTERNAL
|
||||||
|
export type ListProps<ItemT> = Omit<
|
||||||
|
FlatListProps<ItemT>,
|
||||||
|
'onScroll' // Use ScrollContext instead.
|
||||||
|
> & {
|
||||||
|
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||||
|
}
|
||||||
|
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||||
|
|
||||||
|
function ListImpl<ItemT>(
|
||||||
|
{
|
||||||
|
ListHeaderComponent,
|
||||||
|
ListFooterComponent,
|
||||||
|
contentContainerStyle,
|
||||||
|
contentOffset,
|
||||||
|
data,
|
||||||
|
desktopFixedHeight,
|
||||||
|
keyExtractor,
|
||||||
|
onEndReached,
|
||||||
|
onEndReachedThreshold,
|
||||||
|
onScrolledDownChange,
|
||||||
|
renderItem,
|
||||||
|
extraData,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ListProps<ItemT>,
|
||||||
|
ref: React.Ref<ListMethods>,
|
||||||
|
) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
if (!isMobile) {
|
||||||
|
contentContainerStyle = addStyle(
|
||||||
|
contentContainerStyle,
|
||||||
|
styles.containerScroll,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentOffset && contentOffset?.y !== 0) {
|
||||||
|
// NOTE
|
||||||
|
// we use paddingTop & contentOffset to space around the floating header
|
||||||
|
// but reactnative web puts the paddingTop on the wrong element (style instead of the contentContainer)
|
||||||
|
// so we manually correct it here
|
||||||
|
// -prf
|
||||||
|
style = addStyle(style, {
|
||||||
|
paddingTop: 0,
|
||||||
|
})
|
||||||
|
contentContainerStyle = addStyle(contentContainerStyle, {
|
||||||
|
paddingTop: Math.abs(contentOffset.y),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = null
|
||||||
|
if (ListHeaderComponent != null) {
|
||||||
|
if (typeof ListHeaderComponent === 'object') {
|
||||||
|
header = ListHeaderComponent
|
||||||
|
} else if (typeof ListHeaderComponent === 'function') {
|
||||||
|
// @ts-ignore We aren't using classes so it's a render function.
|
||||||
|
header = ListHeaderComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let footer = null
|
||||||
|
if (ListFooterComponent != null) {
|
||||||
|
if (typeof ListFooterComponent === 'object') {
|
||||||
|
footer = ListFooterComponent
|
||||||
|
} else if (typeof ListFooterComponent === 'function') {
|
||||||
|
// @ts-ignore We aren't using classes so it's a render function.
|
||||||
|
footer = ListFooterComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeRef = React.useRef(null)
|
||||||
|
React.useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
scrollToTop() {
|
||||||
|
window.scrollTo({top: 0})
|
||||||
|
},
|
||||||
|
scrollToOffset({
|
||||||
|
animated,
|
||||||
|
offset,
|
||||||
|
}: {
|
||||||
|
animated: boolean
|
||||||
|
offset: number
|
||||||
|
}) {
|
||||||
|
window.scrollTo({
|
||||||
|
left: 0,
|
||||||
|
top: offset,
|
||||||
|
behavior: animated ? 'smooth' : 'instant',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} as any), // TODO: Types.
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onVisible = React.useCallback(() => {
|
||||||
|
onEndReached?.({
|
||||||
|
distanceFromEnd: onEndReachedThreshold || 0,
|
||||||
|
})
|
||||||
|
}, [onEndReachedThreshold, onEndReached])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView {...props} style={style} ref={nativeRef}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.contentContainer,
|
||||||
|
contentContainerStyle,
|
||||||
|
desktopFixedHeight ? styles.minHeightViewport : null,
|
||||||
|
pal.border,
|
||||||
|
]}>
|
||||||
|
{header}
|
||||||
|
{(data as Array<ItemT>).map((item, index) => (
|
||||||
|
<Row<ItemT>
|
||||||
|
key={keyExtractor!(item, index)}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
renderItem={renderItem}
|
||||||
|
extraData={extraData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{onEndReached && (
|
||||||
|
<Tail threshold={onEndReachedThreshold} onVisible={onVisible} />
|
||||||
|
)}
|
||||||
|
{footer}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let Row = function RowImpl<ItemT>({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
renderItem,
|
||||||
|
extraData: _unused,
|
||||||
|
}: {
|
||||||
|
item: ItemT
|
||||||
|
index: number
|
||||||
|
renderItem:
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||||
|
extraData: any
|
||||||
|
}): React.ReactNode {
|
||||||
|
if (!renderItem) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.row}>
|
||||||
|
{renderItem({item, index, separators: null as any})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row = React.memo(Row)
|
||||||
|
|
||||||
|
let Tail = ({
|
||||||
|
threshold = 0,
|
||||||
|
onVisible,
|
||||||
|
}: {
|
||||||
|
threshold?: number | null | undefined
|
||||||
|
onVisible: () => void
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const tailRef = React.useRef(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
onVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: (threshold || 0) * 100 + '%',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const tail: Element | null = tailRef.current!
|
||||||
|
observer.observe(tail)
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(tail)
|
||||||
|
}
|
||||||
|
}, [onVisible, threshold])
|
||||||
|
|
||||||
|
return <View ref={tailRef} />
|
||||||
|
}
|
||||||
|
Tail = React.memo(Tail)
|
||||||
|
|
||||||
|
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
|
||||||
|
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
|
||||||
|
) => React.ReactElement
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
contentContainer: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 600,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
containerScroll: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 600,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
// @ts-ignore web
|
||||||
|
contentVisibility: 'auto',
|
||||||
|
},
|
||||||
|
minHeightViewport: {
|
||||||
|
// @ts-ignore web only
|
||||||
|
minHeight: '100vh',
|
||||||
|
},
|
||||||
|
})
|
||||||
+35
-143
@@ -51,18 +51,9 @@ export function CenteredView({
|
|||||||
|
|
||||||
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||||
{
|
{
|
||||||
data,
|
|
||||||
extraData,
|
|
||||||
contentOffset,
|
|
||||||
keyExtractor,
|
|
||||||
renderItem,
|
|
||||||
style,
|
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
onEndReached,
|
style,
|
||||||
onEndReachedThreshold,
|
contentOffset,
|
||||||
onScroll: _unused, // Not supported on the web.
|
|
||||||
ListHeaderComponent,
|
|
||||||
ListFooterComponent,
|
|
||||||
desktopFixedHeight,
|
desktopFixedHeight,
|
||||||
...props
|
...props
|
||||||
}: React.PropsWithChildren<FlatListProps<ItemT> & AddedProps>,
|
}: React.PropsWithChildren<FlatListProps<ItemT> & AddedProps>,
|
||||||
@@ -89,144 +80,45 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
|||||||
paddingTop: Math.abs(contentOffset.y),
|
paddingTop: Math.abs(contentOffset.y),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (desktopFixedHeight) {
|
||||||
const nativeRef = React.useRef(null)
|
if (typeof desktopFixedHeight === 'number') {
|
||||||
React.useImperativeHandle(
|
// @ts-ignore Web only -prf
|
||||||
ref,
|
style = addStyle(style, {
|
||||||
() =>
|
height: `calc(100vh - ${desktopFixedHeight}px)`,
|
||||||
({
|
})
|
||||||
scrollToTop() {
|
} else {
|
||||||
window.scrollTo({top: 0})
|
style = addStyle(style, styles.fixedHeight)
|
||||||
},
|
}
|
||||||
scrollToOffset({
|
if (!isMobile) {
|
||||||
animated,
|
// NOTE
|
||||||
offset,
|
// react native web produces *three* wrapping divs
|
||||||
}: {
|
// the first two use the `style` prop and the innermost uses the
|
||||||
animated: boolean
|
// `contentContainerStyle`. Unfortunately the stable-gutter style
|
||||||
offset: number
|
// needs to be applied to only the "middle" of these. To hack
|
||||||
}) {
|
// around this, we set data-stable-gutters which can then be
|
||||||
window.scrollTo({
|
// styled in our external CSS.
|
||||||
left: 0,
|
// -prf
|
||||||
top: offset,
|
// @ts-ignore web only -prf
|
||||||
behavior: animated ? 'smooth' : 'instant',
|
props.dataSet = props.dataSet || {}
|
||||||
})
|
// @ts-ignore web only -prf
|
||||||
},
|
props.dataSet.stableGutters = '1'
|
||||||
} as any), // TODO: Types.
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onVisible = React.useCallback(() => {
|
|
||||||
onEndReached?.({
|
|
||||||
distanceFromEnd: onEndReachedThreshold || 0,
|
|
||||||
})
|
|
||||||
}, [onEndReachedThreshold, onEndReached])
|
|
||||||
|
|
||||||
let header = null
|
|
||||||
if (ListHeaderComponent != null) {
|
|
||||||
if (typeof ListHeaderComponent === 'object') {
|
|
||||||
header = ListHeaderComponent
|
|
||||||
} else if (typeof ListHeaderComponent === 'function') {
|
|
||||||
// @ts-ignore We aren't using classes so it's a render function.
|
|
||||||
header = ListHeaderComponent()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let footer = null
|
|
||||||
if (ListFooterComponent != null) {
|
|
||||||
if (typeof ListFooterComponent === 'object') {
|
|
||||||
footer = ListFooterComponent
|
|
||||||
} else if (typeof ListFooterComponent === 'function') {
|
|
||||||
// @ts-ignore We aren't using classes so it's a render function.
|
|
||||||
footer = ListFooterComponent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.ScrollView {...props} style={style} ref={nativeRef}>
|
<Animated.FlatList
|
||||||
<View
|
ref={ref}
|
||||||
style={[
|
contentContainerStyle={[
|
||||||
styles.contentContainer,
|
styles.contentContainer,
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
desktopFixedHeight ? styles.minHeightViewport : null,
|
pal.border,
|
||||||
pal.border,
|
]}
|
||||||
]}>
|
style={style}
|
||||||
{header}
|
contentOffset={contentOffset}
|
||||||
{(data as Array<ItemT>).map((item, index) => (
|
{...props}
|
||||||
<Row<ItemT>
|
/>
|
||||||
key={keyExtractor!(item, index)}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
renderItem={renderItem}
|
|
||||||
extraData={extraData}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{onEndReached && (
|
|
||||||
<Tail threshold={onEndReachedThreshold} onVisible={onVisible} />
|
|
||||||
)}
|
|
||||||
{footer}
|
|
||||||
</View>
|
|
||||||
</Animated.ScrollView>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
let Row = function RowImpl<ItemT>({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
renderItem,
|
|
||||||
extraData: _unused,
|
|
||||||
}: {
|
|
||||||
item: ItemT
|
|
||||||
index: number
|
|
||||||
renderItem:
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
|
||||||
extraData: any
|
|
||||||
}): React.ReactNode {
|
|
||||||
if (!renderItem) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={styles.row}>
|
|
||||||
{renderItem({item, index, separators: null as any})}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row = React.memo(Row)
|
|
||||||
|
|
||||||
let Tail = ({
|
|
||||||
threshold = 0,
|
|
||||||
onVisible,
|
|
||||||
}: {
|
|
||||||
threshold?: number | null | undefined
|
|
||||||
onVisible: () => void
|
|
||||||
}): React.ReactNode => {
|
|
||||||
const tailRef = React.useRef(null)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
entries => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
onVisible()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rootMargin: (threshold || 0) * 100 + '%',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const tail: Element | null = tailRef.current!
|
|
||||||
observer.observe(tail)
|
|
||||||
return () => {
|
|
||||||
observer.unobserve(tail)
|
|
||||||
}
|
|
||||||
}, [onVisible, threshold])
|
|
||||||
|
|
||||||
return <View ref={tailRef} />
|
|
||||||
}
|
|
||||||
Tail = React.memo(Tail)
|
|
||||||
|
|
||||||
export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||||
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
|
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
|
||||||
ref: React.Ref<Animated.ScrollView>,
|
ref: React.Ref<Animated.ScrollView>,
|
||||||
|
|||||||
Reference in New Issue
Block a user