Files
bsky-social-app/src/view/com/pager/PagerWithHeader.web.tsx
T
Claude 3652b30811 Add web typecheck pass (tsconfig.check.web.json) and fix web type drift
The main typecheck models native resolution (.ios/.android/.native), so
.web variants were never checked in their real resolution context. Add a
second pass that roots the program at the web entry point and resolves
with moduleSuffixes [".web", ""], wire it up as pnpm typecheck:web, and
add it to the lint workflow matrix.

Ambient shims (src/platform/*.web-check.d.ts, web pass only) pin
react-native-svg, expo-file-system, and expo-image-manipulator to their
native declarations, since their .web.d.ts files expose a different API
surface than the one the app is written against (and, for
expo-file-system/legacy, raw package sources that break under suffix
remapping).

The rest fixes the drift the new pass surfaced, notably:
- List.web: ListRef now carries ListMethods (was RefObject<View>),
  scrollToOffset accepts optional animated, ListProps defaults ItemT
- Dialog.web: ScrollableInner accepts a (never-attached) ScrollView ref,
  InnerFlatList label optional + ListMethods ref, Handle accepts props
- Menu.web exports MenuControlProps; ContextMenu.web gets a properly
  typed Trigger wrapper and accepts align
- Pager/TabBar/PagerWithHeader web variants align with the shared
  contract (testID, onTabPressed, drag shared values, setMinimumHeight)
- useNavigationTabState.web now reports isAtFeeds/isAtBookmarks, fixing
  drawer highlighting for those routes on web
- Web-only throw-stubs get real signatures; duplicated prop types move
  into .shared.ts files (CaptchaWebView, EditImageDialog, OpenCameraBtn,
  GestureActionView, ValuePropositionPager)
- findListNativeTag helper replaces findNodeHandle casts on List refs
- Assorted call-site type fixes (keyExtractor params, PostQuotes record
  narrowing, saveBytesToDisk bytes param, openCamera web signature)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCuMKXWHuyGoNhTVAHBMyk
2026-07-16 21:13:17 +03:00

196 lines
4.6 KiB
TypeScript

import {forwardRef, memo, useCallback, useState} from 'react'
import {type JSX} from 'react'
import {type ScrollView, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
import {
Pager,
type PagerRef,
type RenderTabBarFnProps,
} from '#/view/com/pager/Pager'
import {atoms as a, web} from '#/alf'
import * as Layout from '#/components/Layout'
import {type ListMethods} from '../util/List'
import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams {
headerHeight: number
isFocused: boolean
scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null>
}
export interface PagerWithHeaderProps {
testID?: string
children:
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
| ((props: PagerWithHeaderChildParams) => JSX.Element)
items: string[]
isHeaderReady: boolean
renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: (height: number) => void
}) => JSX.Element
initialPage?: number
onPageSelected?: (index: number) => void
onCurrentPageSelected?: (index: number) => void
allowHeaderOverScroll?: boolean // Ignored on web.
}
export const PagerWithHeader = forwardRef<PagerRef, PagerWithHeaderProps>(
function PageWithHeaderImpl(
{
children,
testID,
items,
isHeaderReady,
renderHeader,
initialPage,
onPageSelected,
onCurrentPageSelected,
}: PagerWithHeaderProps,
ref,
) {
const [currentPage, setCurrentPage] = useState(0)
const renderTabBar = useCallback(
(props: RenderTabBarFnProps) => {
return (
<PagerTabBar
items={items}
renderHeader={renderHeader}
isHeaderReady={isHeaderReady}
currentPage={currentPage}
onCurrentPageSelected={onCurrentPageSelected}
onSelect={props.onSelect}
tabBarAnchor={props.tabBarAnchor}
testID={testID}
/>
)
},
[
items,
isHeaderReady,
renderHeader,
currentPage,
onCurrentPageSelected,
testID,
],
)
const onPageSelectedInner = useCallback(
(index: number) => {
setCurrentPage(index)
onPageSelected?.(index)
},
[onPageSelected, setCurrentPage],
)
return (
<Pager
ref={ref}
testID={testID}
initialPage={initialPage}
onPageSelected={onPageSelectedInner}
renderTabBar={renderTabBar}>
{toArray(children)
.filter(Boolean)
.map((child, i) => {
const isReady = isHeaderReady
return (
<View
key={i}
collapsable={false}
style={{
display: isReady ? undefined : 'none',
}}>
<PagerItem isFocused={i === currentPage} renderTab={child} />
</View>
)
})}
</Pager>
)
},
)
let PagerTabBar = ({
currentPage,
items,
isHeaderReady,
testID,
renderHeader,
onCurrentPageSelected,
onSelect,
tabBarAnchor,
}: {
currentPage: number
items: string[]
testID?: string
renderHeader?: ({
setMinimumHeight,
}: {
setMinimumHeight: (height: number) => void
}) => JSX.Element
isHeaderReady: boolean
onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void
tabBarAnchor?: JSX.Element | null | undefined
}): React.ReactNode => {
return (
<>
<Layout.Center>{renderHeader?.({setMinimumHeight: noop})}</Layout.Center>
{tabBarAnchor}
<Layout.Center
style={[
a.z_10,
web([
a.sticky,
{
top: 0,
display: isHeaderReady ? undefined : 'none',
},
]),
]}>
<TabBar
testID={testID}
items={items}
selectedPage={currentPage}
onSelect={onSelect}
onPressSelected={onCurrentPageSelected}
dragProgress={undefined as any /* native-only */}
dragState={undefined as any /* native-only */}
/>
</Layout.Center>
</>
)
}
PagerTabBar = memo(PagerTabBar)
function PagerItem({
isFocused,
renderTab,
}: {
isFocused: boolean
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
}) {
const scrollElRef = useAnimatedRef()
if (renderTab == null) {
return null
}
return renderTab({
headerHeight: 0,
isFocused,
scrollElRef: scrollElRef as React.MutableRefObject<
ListMethods | ScrollView | null
>,
})
}
function toArray<T>(v: T | T[]): T[] {
if (Array.isArray(v)) {
return v
}
return [v]
}
function noop() {}