New Web Layout (#2126)
* Rip out virtualization on the web
* Screw around with layout
* onEndReached
* scrollToOffset
* Fix background
* onScroll
* Shell bars
* More scroll
* Fixes
* position: sticky
* Clean up 1
* Clean up 2
* Undo PagerWithHeader changes and fork it
* Trim down both versions
* Cleanup 3
* Memoize, lint
* Don't scroll away modal or lightbox
* Add content-visibility for rows
* Fix composer
* Fix types
* Fix borked scroll animation
* Fixes to layout
* More FlatList parity
* Layout fixes
* Fix more layout
* More layout
* More layouts
* Fix profile layout
* Remove onScroll
* Display: none inactive pages
* Add an intermediate List component
* Fix type
* Add onScrolledDownChange
* Port pager to use onScrolledDownChange
* Fix on mobile
* Don't pass down onScroll (replacement TBD)
* Remove resetMainScroll
* Replace onMainScroll with MainScrollProvider
* Hook ScrollProvider to pager
* Fix the remaining special case
* Optimize a bit
* Enforce that onScroll cannot be passed
* Keep value updated even if no handler
* Also memo it
* Move the fork to List.web
* Add scroll handler
* Consolidate List props a bit
* More stuff
* Rm unused
* Simplify
* Make isScrolledDown work
* Oops
* Fixes
* Hook up context scroll handlers
* Scroll restore for tabs
* Route scroll restoration POC
* Fix some issues with restoration
* Remove bad idea
* Fix pager scroll restoration
* Undo accidental locale changes
* onContentSizeChange
* Scroll to post
* Better positioning
* Layout fixes
* Factor out navigation stuff
* Cleanup
* Oops
* Cleanup
* Fixes and types
* Naming etc
* Fix crash
* Match FL semantics
* Snap the header scroll on the web
* Add body scroll lock
* Scroll to top on search
* Fix types
* Typos
* Fix Safari overflow
* Fix search positioning
* Add border
* Patch react navigation
* Revert "Patch react navigation"
This reverts commit 62516ed9c2.
* fixes
* scroll
* scrollbar
* cleanup unrelated
* undo unrel
* flatter
* Fix css
* twk
This commit is contained in:
@@ -117,7 +117,7 @@ function FeedsTabBarTablet(
|
||||
return (
|
||||
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
|
||||
<Animated.View
|
||||
style={[pal.view, styles.tabBar, headerMinimalShellTransform]}
|
||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
onLayout={e => {
|
||||
headerHeight.value = e.nativeEvent.layout.height
|
||||
}}>
|
||||
@@ -134,13 +134,16 @@ function FeedsTabBarTablet(
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
position: 'absolute',
|
||||
// @ts-ignore Web only
|
||||
position: 'sticky',
|
||||
zIndex: 1,
|
||||
// @ts-ignore Web only -prf
|
||||
left: 'calc(50% - 299px)',
|
||||
width: 598,
|
||||
left: 'calc(50% - 300px)',
|
||||
width: 600,
|
||||
top: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -142,7 +142,8 @@ export function FeedsTabBar(
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
position: 'absolute',
|
||||
// @ts-ignore web-only
|
||||
position: isWeb ? 'fixed' : 'absolute',
|
||||
zIndex: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface PagerRef {
|
||||
export interface RenderTabBarFnProps {
|
||||
selectedPage: number
|
||||
onSelect?: (index: number) => void
|
||||
tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
|
||||
}
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react'
|
||||
import {flushSync} from 'react-dom'
|
||||
import {View} from 'react-native'
|
||||
import {s} from 'lib/styles'
|
||||
|
||||
export interface RenderTabBarFnProps {
|
||||
selectedPage: number
|
||||
onSelect?: (index: number) => void
|
||||
tabBarAnchor?: JSX.Element
|
||||
}
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
@@ -27,6 +29,8 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||
const scrollYs = React.useRef<Array<number | null>>([])
|
||||
const anchorRef = React.useRef(null)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => setSelectedPage(index),
|
||||
@@ -34,11 +38,36 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
setSelectedPage(index)
|
||||
onPageSelected?.(index)
|
||||
onPageSelecting?.(index)
|
||||
const scrollY = window.scrollY
|
||||
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||
// case we should preserve and restore scroll), or if it is somewhere below in the
|
||||
// viewport (in which case a scroll jump would be jarring). We determine this by
|
||||
// measuring where the "anchor" element is (which we place just above the tabbar).
|
||||
let anchorTop = anchorRef.current
|
||||
? (anchorRef.current as Element).getBoundingClientRect().top
|
||||
: -scrollY // If there's no anchor, treat the top of the page as one.
|
||||
const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable.
|
||||
|
||||
if (isSticking) {
|
||||
scrollYs.current[selectedPage] = window.scrollY
|
||||
} else {
|
||||
scrollYs.current[selectedPage] = null
|
||||
}
|
||||
flushSync(() => {
|
||||
setSelectedPage(index)
|
||||
onPageSelected?.(index)
|
||||
onPageSelecting?.(index)
|
||||
})
|
||||
if (isSticking) {
|
||||
const restoredScrollY = scrollYs.current[index]
|
||||
if (restoredScrollY != null) {
|
||||
window.scrollTo(0, restoredScrollY)
|
||||
} else {
|
||||
window.scrollTo(0, scrollY + anchorTop)
|
||||
}
|
||||
}
|
||||
},
|
||||
[setSelectedPage, onPageSelected, onPageSelecting],
|
||||
[selectedPage, setSelectedPage, onPageSelected, onPageSelecting],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -46,21 +75,11 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||
{tabBarPosition === 'top' &&
|
||||
renderTabBar({
|
||||
selectedPage,
|
||||
tabBarAnchor: <View ref={anchorRef} />,
|
||||
onSelect: onTabBarSelect,
|
||||
})}
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<View
|
||||
style={
|
||||
selectedPage === i
|
||||
? s.flex1
|
||||
: {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
// @ts-ignore web-only
|
||||
visibility: 'hidden',
|
||||
}
|
||||
}
|
||||
key={`page-${i}`}>
|
||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
||||
{child}
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -18,7 +18,6 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {TabBar} from './TabBar'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
@@ -235,7 +234,6 @@ let PagerTabBar = ({
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
onSelect?: (index: number) => void
|
||||
}): React.ReactNode => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const headerTransform = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
@@ -246,10 +244,7 @@ let PagerTabBar = ({
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
||||
headerTransform,
|
||||
]}>
|
||||
style={[styles.tabBarMobile, headerTransform]}>
|
||||
<View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
|
||||
{renderHeader?.()}
|
||||
</View>
|
||||
@@ -325,14 +320,6 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
width: '100%',
|
||||
},
|
||||
tabBarDesktop: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
// @ts-ignore Web only -prf
|
||||
left: 'calc(50% - 299px)',
|
||||
width: 598,
|
||||
},
|
||||
})
|
||||
|
||||
function noop() {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import * as React from 'react'
|
||||
import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {TabBar} from './TabBar'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {ListMethods} from '../util/List'
|
||||
|
||||
export interface PagerWithHeaderChildParams {
|
||||
headerHeight: number
|
||||
isFocused: boolean
|
||||
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
|
||||
}
|
||||
|
||||
export interface PagerWithHeaderProps {
|
||||
testID?: string
|
||||
children:
|
||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||
| ((props: PagerWithHeaderChildParams) => JSX.Element)
|
||||
items: string[]
|
||||
isHeaderReady: boolean
|
||||
renderHeader?: () => JSX.Element
|
||||
initialPage?: number
|
||||
onPageSelected?: (index: number) => void
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
}
|
||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
function PageWithHeaderImpl(
|
||||
{
|
||||
children,
|
||||
testID,
|
||||
items,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
onCurrentPageSelected,
|
||||
}: PagerWithHeaderProps,
|
||||
ref,
|
||||
) {
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
|
||||
const renderTabBar = React.useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<PagerTabBar
|
||||
items={items}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onSelect={props.onSelect}
|
||||
tabBarAnchor={props.tabBarAnchor}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[items, renderHeader, currentPage, onCurrentPageSelected, testID],
|
||||
)
|
||||
|
||||
const onPageSelectedInner = React.useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
|
||||
const onPageSelecting = React.useCallback((index: number) => {
|
||||
setCurrentPage(index)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
onPageSelecting={onPageSelecting}
|
||||
renderTabBar={renderTabBar}
|
||||
tabBarPosition="top">
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child, i) => {
|
||||
return (
|
||||
<View key={i} collapsable={false}>
|
||||
<PagerItem isFocused={i === currentPage} renderTab={child} />
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
let PagerTabBar = ({
|
||||
currentPage,
|
||||
items,
|
||||
testID,
|
||||
renderHeader,
|
||||
onCurrentPageSelected,
|
||||
onSelect,
|
||||
tabBarAnchor,
|
||||
}: {
|
||||
currentPage: number
|
||||
items: string[]
|
||||
testID?: string
|
||||
renderHeader?: () => JSX.Element
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
onSelect?: (index: number) => void
|
||||
tabBarAnchor?: JSX.Element | null | undefined
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
return (
|
||||
<>
|
||||
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
|
||||
{renderHeader?.()}
|
||||
</View>
|
||||
{tabBarAnchor}
|
||||
<View
|
||||
style={[
|
||||
styles.tabBarContainer,
|
||||
isMobile
|
||||
? styles.tabBarContainerMobile
|
||||
: styles.tabBarContainerDesktop,
|
||||
pal.border,
|
||||
]}>
|
||||
<TabBar
|
||||
testID={testID}
|
||||
items={items}
|
||||
selectedPage={currentPage}
|
||||
onSelect={onSelect}
|
||||
onPressSelected={onCurrentPageSelected}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
PagerTabBar = React.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
|
||||
>,
|
||||
})
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerContainerDesktop: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
tabBarContainer: {
|
||||
// @ts-ignore web-only
|
||||
position: 'sticky',
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
tabBarContainerDesktop: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
tabBarContainerMobile: {
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
},
|
||||
})
|
||||
|
||||
function toArray<T>(v: T | T[]): T[] {
|
||||
if (Array.isArray(v)) {
|
||||
return v
|
||||
}
|
||||
return [v]
|
||||
}
|
||||
Reference in New Issue
Block a user