replace react-nav header hack with scroll edge interaction module
- new expo-scroll-edge-interaction native module (iOS-only) that sets up UIScrollEdgeElementContainerInteraction on iOS 26+ - new ScrollEdgeInteractionProvider context and hooks - Layout.Screen gains `transparentHeader` prop - Layout.Header.Outer auto-detects transparent mode from context - List auto-registers scroll view and applies content inset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["ios"],
|
||||
"ios": {
|
||||
"modules": ["ExpoScrollEdgeInteractionModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type {ScrollEdgeInteractionEdge} from './src/ExpoScrollEdgeInteraction.types'
|
||||
export {ExpoScrollEdgeInteractionView} from './src/ExpoScrollEdgeInteractionView'
|
||||
@@ -0,0 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoScrollEdgeInteraction'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||
s.description = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||
s.author = 'bluesky-social'
|
||||
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoScrollEdgeInteractionModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoScrollEdgeInteraction")
|
||||
|
||||
View(ExpoScrollEdgeInteractionView.self) {
|
||||
Prop("nodeHandle") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||
view.nodeHandle = prop
|
||||
}
|
||||
Prop("scrollViewTag") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||
view.scrollViewTag = prop
|
||||
}
|
||||
Prop("edge") { (view: ExpoScrollEdgeInteractionView, prop: String?) in
|
||||
view.edge = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import ExpoModulesCore
|
||||
import React
|
||||
|
||||
class ExpoScrollEdgeInteractionView: ExpoView {
|
||||
var nodeHandle: Int? {
|
||||
didSet {
|
||||
setupInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
var scrollViewTag: Int? {
|
||||
didSet {
|
||||
setupInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
var edge: String? {
|
||||
didSet {
|
||||
setupInteraction()
|
||||
}
|
||||
}
|
||||
|
||||
private var currentInteraction: NSObject?
|
||||
|
||||
private func setupInteraction() {
|
||||
guard #available(iOS 26.0, *) else { return }
|
||||
|
||||
// Clean up old interaction
|
||||
if let interaction = currentInteraction as? UIScrollEdgeElementContainerInteraction,
|
||||
let headerView = findHeaderView() {
|
||||
headerView.removeInteraction(interaction)
|
||||
currentInteraction = nil
|
||||
}
|
||||
|
||||
guard let nodeHandle = nodeHandle,
|
||||
let scrollViewTag = scrollViewTag
|
||||
else { return }
|
||||
|
||||
guard let headerView = appContext?.findView(withTag: nodeHandle, ofType: UIView.self) else {
|
||||
return
|
||||
}
|
||||
|
||||
// RCTScrollView wraps a UIScrollView — get the inner one
|
||||
let scrollView: UIScrollView?
|
||||
if let rctScrollView = appContext?.findView(withTag: scrollViewTag, ofType: RCTScrollView.self) {
|
||||
scrollView = rctScrollView.scrollView
|
||||
} else {
|
||||
scrollView = appContext?.findView(withTag: scrollViewTag, ofType: UIScrollView.self)
|
||||
}
|
||||
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
let interaction = UIScrollEdgeElementContainerInteraction()
|
||||
interaction.edge = resolveEdge()
|
||||
interaction.scrollView = scrollView
|
||||
headerView.addInteraction(interaction)
|
||||
currentInteraction = interaction
|
||||
}
|
||||
|
||||
private func resolveEdge() -> NSDirectionalRectEdge {
|
||||
switch edge {
|
||||
case "bottom": return .bottom
|
||||
case "left": return .leading
|
||||
case "right": return .trailing
|
||||
default: return .top
|
||||
}
|
||||
}
|
||||
|
||||
private func findHeaderView() -> UIView? {
|
||||
guard let nodeHandle = nodeHandle else { return nil }
|
||||
return appContext?.findView(withTag: nodeHandle, ofType: UIView.self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {type ViewStyle} from 'react-native'
|
||||
|
||||
export type ScrollEdgeInteractionEdge = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
export interface ExpoScrollEdgeInteractionViewProps {
|
||||
nodeHandle: number | null
|
||||
scrollViewTag: number | null
|
||||
edge?: ScrollEdgeInteractionEdge
|
||||
children: React.ReactNode
|
||||
style?: ViewStyle
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||
|
||||
const NativeView: React.ComponentType<ExpoScrollEdgeInteractionViewProps> =
|
||||
requireNativeViewManager('ExpoScrollEdgeInteraction')
|
||||
|
||||
export function ExpoScrollEdgeInteractionView({
|
||||
children,
|
||||
...rest
|
||||
}: ExpoScrollEdgeInteractionViewProps) {
|
||||
return <NativeView {...rest}>{children}</NativeView>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||
|
||||
export function ExpoScrollEdgeInteractionView({
|
||||
children,
|
||||
}: React.PropsWithChildren<ExpoScrollEdgeInteractionViewProps>) {
|
||||
return children
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import {useLayoutEffect, useMemo, useState} from 'react'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {IS_LIQUID_GLASS} from '#/env'
|
||||
import {atoms as a, useGutters} from '#/alf'
|
||||
import {useTransparentHeaderProps} from '#/components/Layout/ScrollEdgeInteraction'
|
||||
import {Outer as DefaultOuter, type OuterProps} from './index.shared'
|
||||
|
||||
export {
|
||||
@@ -18,68 +16,46 @@ export {
|
||||
} from './index.shared'
|
||||
|
||||
export function Outer(props: OuterProps) {
|
||||
if (IS_LIQUID_GLASS && props.transparent) {
|
||||
return <TransparentOuter {...props} />
|
||||
const transparentHeaderProps = useTransparentHeaderProps()
|
||||
if (transparentHeaderProps) {
|
||||
return (
|
||||
<TransparentOuter
|
||||
transparentHeaderProps={transparentHeaderProps}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <DefaultOuter {...props} />
|
||||
}
|
||||
|
||||
function TransparentOuter({children, headerRef}: OuterProps) {
|
||||
const [headerWidth, setHeaderWidth] = useState(0)
|
||||
const {width: screenWidth} = useWindowDimensions()
|
||||
function TransparentOuter({
|
||||
children,
|
||||
transparentHeaderProps,
|
||||
}: OuterProps & {
|
||||
transparentHeaderProps: {ref: (node: View | null) => void; onLayout: any}
|
||||
}) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
|
||||
// bit of a hack - react-native-screens initially renders the header too wide
|
||||
// so let's delay showing it until the padding has been applied -sfn
|
||||
const isInitialRender = headerWidth === 0 || headerWidth === screenWidth
|
||||
const headerElement = useMemo(() => {
|
||||
return (
|
||||
<View
|
||||
ref={headerRef}
|
||||
onLayout={evt => setHeaderWidth(evt.nativeEvent.layout.width)}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
// built-in padding, but slightly more on right than left,
|
||||
// so compensate for that. this is because we're using
|
||||
// headerRightItems
|
||||
a.pl_xs,
|
||||
isInitialRender && {opacity: 0},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}, [children, headerRef, isInitialRender])
|
||||
|
||||
// this is how expo-router handles it
|
||||
// https://github.com/expo/expo/blob/main/packages/expo-router/src/views/Screen.tsx#L34
|
||||
// note: I'm skipping handling preloading for now -sfn
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const isFocused = useIsFocused()
|
||||
useLayoutEffect(() => {
|
||||
if (isFocused) {
|
||||
navigation.setOptions({
|
||||
headerShown: true,
|
||||
// abuse the headerItems API so we get
|
||||
// the sweet sweet progressive blur header.
|
||||
// unclear why just `header: () => elem` doesn't work -sfn
|
||||
unstable_headerRightItems: () => [
|
||||
{
|
||||
type: 'custom',
|
||||
element: headerElement,
|
||||
hidesSharedBackground: true,
|
||||
},
|
||||
],
|
||||
headerTransparent: true,
|
||||
headerBackVisible: false,
|
||||
scrollEdgeEffects: {
|
||||
top: 'soft',
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [isFocused, navigation, headerElement])
|
||||
|
||||
return null
|
||||
return (
|
||||
<View
|
||||
collapsable={false}
|
||||
ref={transparentHeaderProps.ref}
|
||||
onLayout={transparentHeaderProps.onLayout}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.top_0,
|
||||
a.left_0,
|
||||
a.right_0,
|
||||
a.z_10,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
gutters,
|
||||
{paddingTop: top, minHeight: 48 + top},
|
||||
a.pb_xs,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,22 +35,6 @@ export type OuterProps = {
|
||||
noBottomBorder?: boolean
|
||||
headerRef?: React.RefObject<View | null>
|
||||
sticky?: boolean
|
||||
/**
|
||||
* Use native transparent blurred header on iOS 26 (looks very nice)
|
||||
*
|
||||
* When using this, make sure to enable these props on the scrollview for the screen:
|
||||
* ```tsx
|
||||
* <Layout.Content
|
||||
* contentInsetAdjustmentBehavior="automatic"
|
||||
* automaticallyAdjustsScrollIndicatorInsets
|
||||
* // everything else
|
||||
* />
|
||||
* ```
|
||||
* and also set `noInsetTop={IS_LIQUID_GLASS}` on `Layout.Screen`
|
||||
*
|
||||
* @platform ios
|
||||
* */
|
||||
transparent?: boolean
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import {createContext, useContext, useRef, useState} from 'react'
|
||||
import {findNodeHandle, type LayoutChangeEvent, type View} from 'react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {IS_LIQUID_GLASS} from '#/env'
|
||||
import {
|
||||
ExpoScrollEdgeInteractionView,
|
||||
type ScrollEdgeInteractionEdge,
|
||||
} from '../../../modules/expo-scroll-edge-interaction'
|
||||
|
||||
interface ScrollEdgeContextValue {
|
||||
setNodeHandle: (update: React.SetStateAction<number | null>) => void
|
||||
setScrollViewTag: (update: React.SetStateAction<number | null>) => void
|
||||
setHeaderHeight: (height: number) => void
|
||||
}
|
||||
|
||||
const ScrollEdgeContext = createContext<ScrollEdgeContextValue | null>(null)
|
||||
const ScrollEdgeHeightContext = createContext<number | null>(null)
|
||||
|
||||
export function ScrollEdgeInteractionProvider({
|
||||
children,
|
||||
edge = 'top',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
edge?: ScrollEdgeInteractionEdge
|
||||
}) {
|
||||
const [nodeHandle, setNodeHandle] = useState<number | null>(null)
|
||||
const [scrollViewTag, setScrollViewTag] = useState<number | null>(null)
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
|
||||
if (!IS_LIQUID_GLASS) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollEdgeContext.Provider
|
||||
value={{setNodeHandle, setScrollViewTag, setHeaderHeight}}>
|
||||
<ScrollEdgeHeightContext.Provider value={headerHeight}>
|
||||
<ExpoScrollEdgeInteractionView
|
||||
nodeHandle={nodeHandle}
|
||||
scrollViewTag={scrollViewTag}
|
||||
edge={edge}
|
||||
style={a.flex_1}>
|
||||
{children}
|
||||
</ExpoScrollEdgeInteractionView>
|
||||
</ScrollEdgeHeightContext.Provider>
|
||||
</ScrollEdgeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ref callback to attach to the header view. The native module
|
||||
* will add a UIScrollEdgeElementContainerInteraction to this view.
|
||||
* Returns undefined when there's no provider (non-Liquid Glass).
|
||||
*/
|
||||
export function useTransparentHeaderProps() {
|
||||
const ctx = useContext(ScrollEdgeContext)
|
||||
// Track which handle this specific hook instance set, so that
|
||||
// unmounting one header doesn't clobber another's handle.
|
||||
const ourHandle = useRef<number | null>(null)
|
||||
|
||||
if (!ctx) return undefined
|
||||
|
||||
const {setNodeHandle, setHeaderHeight} = ctx
|
||||
|
||||
const refCallback = (node: View | null) => {
|
||||
const handle = node ? findNodeHandle(node) : null
|
||||
if (handle !== null) {
|
||||
ourHandle.current = handle
|
||||
setNodeHandle(handle)
|
||||
} else {
|
||||
// Only clear if we were the last one to set it
|
||||
const prev = ourHandle.current
|
||||
ourHandle.current = null
|
||||
setNodeHandle(cur => (cur === prev ? null : cur))
|
||||
}
|
||||
}
|
||||
|
||||
const onLayout = (e: LayoutChangeEvent) => {
|
||||
setHeaderHeight(e.nativeEvent.layout.height)
|
||||
}
|
||||
|
||||
return {ref: refCallback, onLayout}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a scroll view with the scroll edge interaction provider.
|
||||
* Called automatically by List.
|
||||
* Returns a ref callback, or undefined if no provider.
|
||||
*/
|
||||
export function useScrollEdgeScrollView() {
|
||||
const ctx = useContext(ScrollEdgeContext)
|
||||
const ourTag = useRef<number | null>(null)
|
||||
|
||||
if (!ctx) return undefined
|
||||
|
||||
const {setScrollViewTag} = ctx
|
||||
|
||||
return (node: any) => {
|
||||
const tag = node ? findNodeHandle(node) : null
|
||||
if (tag !== null) {
|
||||
ourTag.current = tag
|
||||
setScrollViewTag(tag)
|
||||
} else {
|
||||
const prev = ourTag.current
|
||||
ourTag.current = null
|
||||
setScrollViewTag(cur => (cur === prev ? null : cur))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the measured header height, or null if not inside a
|
||||
* transparent header provider.
|
||||
*/
|
||||
export function useTransparentHeaderHeight(): number | null {
|
||||
return useContext(ScrollEdgeHeightContext)
|
||||
}
|
||||
@@ -22,32 +22,63 @@ import {
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||
import {ScrollEdgeInteractionProvider} from '#/components/Layout/ScrollEdgeInteraction'
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
export * from '#/components/Layout/const'
|
||||
export * as Header from '#/components/Layout/Header'
|
||||
export {useTransparentHeaderHeight} from '#/components/Layout/ScrollEdgeInteraction'
|
||||
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
noInsetTop?: boolean
|
||||
/**
|
||||
* Enables a transparent, absolutely-positioned header with the iOS 26
|
||||
* Liquid Glass scroll-edge blur effect. When set, wraps screen children
|
||||
* in a `ScrollEdgeInteractionProvider`, implies `noInsetTop`, and causes
|
||||
* `Layout.Header.Outer` to render as a transparent overlay.
|
||||
*
|
||||
* The blur effect only works with `List` (native FlatList), not with
|
||||
* `Layout.Content` (Animated.ScrollView). `List` automatically picks up
|
||||
* the header height and applies `contentInset`.
|
||||
*
|
||||
* On non-Liquid-Glass devices this is a no-op.
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
transparentHeader?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Outermost component of every screen
|
||||
*/
|
||||
export const Screen = memo(function Screen({
|
||||
children,
|
||||
style,
|
||||
noInsetTop,
|
||||
transparentHeader,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const skipInsetTop = noInsetTop || transparentHeader
|
||||
return (
|
||||
<>
|
||||
{IS_WEB && <WebCenterBorders />}
|
||||
<View
|
||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||
{...props}
|
||||
/>
|
||||
style={[
|
||||
a.util_screen_outer,
|
||||
{paddingTop: skipInsetTop ? 0 : top},
|
||||
style,
|
||||
]}
|
||||
{...props}>
|
||||
{transparentHeader ? (
|
||||
<ScrollEdgeInteractionProvider>
|
||||
{children}
|
||||
</ScrollEdgeInteractionProvider>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ export function MessagesListHeader({
|
||||
}, [moderation])
|
||||
|
||||
return (
|
||||
<Layout.Header.Outer transparent>
|
||||
<Layout.Header.Outer>
|
||||
<View style={[a.flex_1, a.flex_row, a.gap_xs, a.align_start]}>
|
||||
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
||||
<Layout.Header.BackButton />
|
||||
|
||||
@@ -89,7 +89,7 @@ export function MessagesConversationScreenInner({route}: Props) {
|
||||
<Layout.Screen
|
||||
testID="convoScreen"
|
||||
style={web([{minHeight: 0}, a.flex_1])}
|
||||
noInsetTop={IS_LIQUID_GLASS}>
|
||||
transparentHeader={IS_LIQUID_GLASS}>
|
||||
<ConvoProvider key={convoId} convoId={convoId}>
|
||||
<Inner />
|
||||
</ConvoProvider>
|
||||
|
||||
@@ -6,14 +6,20 @@ import {
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {addStyle} from '#/lib/styles'
|
||||
import {useLightbox} from '#/state/lightbox'
|
||||
import {useTheme} from '#/alf'
|
||||
import {
|
||||
useScrollEdgeScrollView,
|
||||
useTransparentHeaderHeight,
|
||||
} from '#/components/Layout/ScrollEdgeInteraction'
|
||||
import {IS_IOS} from '#/env'
|
||||
import {FlatList_INTERNAL} from './Views'
|
||||
|
||||
@@ -54,6 +60,7 @@ let List = forwardRef<ListMethods, ListProps>(
|
||||
headerOffset,
|
||||
style,
|
||||
progressViewOffset,
|
||||
contentInset,
|
||||
automaticallyAdjustsScrollIndicatorInsets = false,
|
||||
...props
|
||||
},
|
||||
@@ -63,6 +70,13 @@ let List = forwardRef<ListMethods, ListProps>(
|
||||
const t = useTheme()
|
||||
const dedupe = useDedupe(400)
|
||||
const scrollsToTop = useAllowScrollToTop()
|
||||
const transparentHeaderRef = useScrollEdgeScrollView()
|
||||
const insets = useSafeAreaInsets()
|
||||
const transparentHeaderHeight = useTransparentHeaderHeight()
|
||||
const isHeaderTransparent = transparentHeaderHeight != null
|
||||
const headerHeight = isHeaderTransparent
|
||||
? transparentHeaderHeight - insets.top
|
||||
: undefined
|
||||
|
||||
const handleScrolledDownChange = useNonReactiveCallback(
|
||||
(didScrollDown: boolean) => {
|
||||
@@ -163,10 +177,18 @@ let List = forwardRef<ListMethods, ListProps>(
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
}
|
||||
scrollIndicatorInsets={{
|
||||
top: headerOffset,
|
||||
top: headerHeight || headerOffset,
|
||||
right: 1,
|
||||
...props.scrollIndicatorInsets,
|
||||
}}
|
||||
contentInset={
|
||||
headerHeight
|
||||
? {
|
||||
...contentInset,
|
||||
top: headerHeight,
|
||||
}
|
||||
: contentInset
|
||||
}
|
||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||
contentOffset={contentOffset}
|
||||
refreshControl={refreshControl}
|
||||
@@ -175,7 +197,9 @@ let List = forwardRef<ListMethods, ListProps>(
|
||||
scrollEventThrottle={1}
|
||||
style={style}
|
||||
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
|
||||
ref={ref}
|
||||
ref={
|
||||
transparentHeaderRef ? mergeRefs([ref, transparentHeaderRef]) : ref
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user