Move the fork to List.web

This commit is contained in:
Dan Abramov
2023-12-14 19:02:57 +00:00
parent 90ea1288f2
commit a132040a76
2 changed files with 259 additions and 143 deletions
+224
View File
@@ -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
View File
@@ -51,18 +51,9 @@ export function CenteredView({
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
{
data,
extraData,
contentOffset,
keyExtractor,
renderItem,
style,
contentContainerStyle,
onEndReached,
onEndReachedThreshold,
onScroll: _unused, // Not supported on the web.
ListHeaderComponent,
ListFooterComponent,
style,
contentOffset,
desktopFixedHeight,
...props
}: React.PropsWithChildren<FlatListProps<ItemT> & AddedProps>,
@@ -89,144 +80,45 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
paddingTop: Math.abs(contentOffset.y),
})
}
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])
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()
if (desktopFixedHeight) {
if (typeof desktopFixedHeight === 'number') {
// @ts-ignore Web only -prf
style = addStyle(style, {
height: `calc(100vh - ${desktopFixedHeight}px)`,
})
} else {
style = addStyle(style, styles.fixedHeight)
}
if (!isMobile) {
// NOTE
// react native web produces *three* wrapping divs
// the first two use the `style` prop and the innermost uses the
// `contentContainerStyle`. Unfortunately the stable-gutter style
// needs to be applied to only the "middle" of these. To hack
// around this, we set data-stable-gutters which can then be
// styled in our external CSS.
// -prf
// @ts-ignore web only -prf
props.dataSet = props.dataSet || {}
// @ts-ignore web only -prf
props.dataSet.stableGutters = '1'
}
}
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 (
<Animated.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>
</Animated.ScrollView>
<Animated.FlatList
ref={ref}
contentContainerStyle={[
styles.contentContainer,
contentContainerStyle,
pal.border,
]}
style={style}
contentOffset={contentOffset}
{...props}
/>
)
})
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(
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
ref: React.Ref<Animated.ScrollView>,