refactor: move more header stuff to elements package

This commit is contained in:
Satyajit Sahoo
2021-02-04 15:07:22 +01:00
parent 07ba7a9687
commit 509ca49b64
22 changed files with 324 additions and 200 deletions

View File

@@ -14,10 +14,10 @@ import {
createStackNavigator,
StackScreenProps,
HeaderBackground,
useHeaderHeight,
Header,
StackHeaderProps,
} from '@react-navigation/stack';
import { useHeaderHeight } from '@react-navigation/elements';
import BlurView from '../Shared/BlurView';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';

View File

@@ -11,6 +11,7 @@ import type {
DrawerActionHelpers,
RouteProp,
} from '@react-navigation/native';
import type { HeaderOptions } from '@react-navigation/elements';
export type Scene = {
route: Route<string>;
@@ -34,79 +35,7 @@ export type DrawerNavigationConfig = {
detachInactiveScreens?: boolean;
};
export type DrawerHeaderOptions = {
/**
* String or a function that returns a React Element to be used by the header.
* Defaults to scene `title`.
* It receives `allowFontScaling`, `tintColor`, `style` and `children` in the options object as an argument.
* The title string is passed in `children`.
*/
headerTitle?:
| string
| ((props: {
/**
* Whether title font should scale to respect Text Size accessibility settings.
*/
allowFontScaling?: boolean;
/**
* Tint color for the header.
*/
tintColor?: string;
/**
* Content of the title element. Usually the title string.
*/
children?: string;
/**
* Style object for the title element.
*/
style?: StyleProp<TextStyle>;
}) => React.ReactNode);
/**
* How to align the the header title.
* Defaults to `center` on iOS and `left` on Android.
*/
headerTitleAlign?: 'left' | 'center';
/**
* Style object for the title component.
*/
headerTitleStyle?: StyleProp<TextStyle>;
/**
* Whether header title font should scale to respect Text Size accessibility settings. Defaults to `false`.
*/
headerTitleAllowFontScaling?: boolean;
/**
* Function which returns a React Element to display on the left side of the header.
*/
headerLeft?: (props: { tintColor?: string }) => React.ReactNode;
/**
* Accessibility label for the header left button.
*/
headerLeftAccessibilityLabel?: string;
/**
* Function which returns a React Element to display on the right side of the header.
*/
headerRight?: (props: { tintColor?: string }) => React.ReactNode;
/**
* Color for material ripple (Android >= 5.0 only).
*/
headerPressColorAndroid?: string;
/**
* Tint color for the header.
*/
headerTintColor?: string;
/**
* Style object for the header. You can specify a custom background color here, for example.
*/
headerStyle?: StyleProp<ViewStyle>;
/**
* Extra padding to add at the top of header to account for translucent status bar.
* By default, it uses the top value from the safe area insets of the device.
* Pass 0 or a custom value to disable the default behaviour, and customize the height.
*/
headerStatusBarHeight?: number;
};
export type DrawerNavigationOptions = DrawerHeaderOptions & {
export type DrawerNavigationOptions = HeaderOptions & {
/**
* Title text for the screen.
*/

View File

@@ -0,0 +1,54 @@
import * as React from 'react';
import { Image, Platform, StyleSheet } from 'react-native';
import { PlatformPressable } from '@react-navigation/elements';
import {
useNavigation,
DrawerActions,
ParamListBase,
} from '@react-navigation/native';
import type { DrawerNavigationProp } from '../types';
type Props = {
accessibilityLabel?: string;
pressColor?: string;
pressOpacity?: number;
tintColor?: string;
};
export default function DrawerToggleButton({ tintColor, ...rest }: Props) {
const navigation = useNavigation<DrawerNavigationProp<ParamListBase>>();
return (
<PlatformPressable
{...rest}
accessible
accessibilityRole="button"
delayPressIn={0}
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
style={styles.touchable}
hitSlop={Platform.select({
ios: undefined,
default: { top: 16, right: 16, bottom: 16, left: 16 },
})}
borderless
>
<Image
style={[styles.icon, tintColor ? { tintColor } : null]}
source={require('./assets/toggle-drawer-icon.png')}
fadeDuration={0}
/>
</PlatformPressable>
);
}
const styles = StyleSheet.create({
icon: {
height: 24,
width: 24,
margin: 3,
resizeMode: 'contain',
},
touchable: {
marginHorizontal: 11,
},
});

View File

@@ -8,7 +8,10 @@ import {
NativeEventSubscription,
} from 'react-native';
import { ScreenContainer } from 'react-native-screens';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import {
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import {
NavigationHelpersContext,
NavigationContext,
@@ -18,11 +21,11 @@ import {
useTheme,
ParamListBase,
} from '@react-navigation/native';
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import { Header, SafeAreaProviderCompat } from '@react-navigation/elements';
import { GestureHandlerRootView } from './GestureHandler';
import ScreenFallback from './ScreenFallback';
import Header from './Header';
import DrawerToggleButton from './DrawerToggleButton';
import DrawerContent from './DrawerContent';
import Drawer from './Drawer';
import DrawerStatusContext from '../utils/DrawerStatusContext';
@@ -93,7 +96,9 @@ function DrawerViewBase({
} = descriptors[activeKey].options;
const [loaded, setLoaded] = React.useState([activeKey]);
const dimensions = useSafeAreaFrame();
const insets = useSafeAreaInsets();
const { colors } = useTheme();
@@ -149,6 +154,12 @@ function DrawerViewBase({
);
};
const [headerHeights, setHeaderHeights] = React.useState<
Record<string, number>
>({});
const isParentHeaderShown = React.useContext(Header.ShownContext);
const renderSceneContent = () => {
return (
// @ts-ignore
@@ -168,10 +179,32 @@ function DrawerViewBase({
}
const {
header = (props: DrawerHeaderProps) => <Header {...props} />,
header = ({ layout, options }: DrawerHeaderProps) => (
<Header
{...options}
layout={layout}
headerTitle={
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: route.name
}
headerLeft={
options.headerLeft ??
((props) => <DrawerToggleButton {...props} />)
}
/>
),
headerShown = true,
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
} = descriptor.options;
const headerHeight = headerShown
? headerHeights[route.key] ??
Header.getDefaultHeight(dimensions, headerStatusBarHeight)
: 0;
return (
<ScreenFallback
key={route.key}
@@ -182,16 +215,36 @@ function DrawerViewBase({
{headerShown ? (
<NavigationContext.Provider value={descriptor.navigation}>
<NavigationRouteContext.Provider value={route}>
{header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as DrawerNavigationProp<ParamListBase>,
options: descriptor.options,
})}
<View
onLayout={(e) => {
const { height } = e.nativeEvent.layout;
setHeaderHeights((heights) => {
if (heights[route.key] === height) {
return heights;
}
return { ...heights, [route.key]: height };
});
}}
>
{header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as DrawerNavigationProp<ParamListBase>,
options: descriptor.options,
})}
</View>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
) : null}
{descriptor.render()}
<Header.ShownContext.Provider
value={isParentHeaderShown || headerShown !== false}
>
<Header.HeightContext.Provider value={headerHeight}>
{descriptor.render()}
</Header.HeightContext.Provider>
</Header.ShownContext.Provider>
</ScreenFallback>
);
})}

View File

@@ -48,6 +48,7 @@
"typescript": "^4.1.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0"

View File

@@ -1,14 +1,21 @@
import * as React from 'react';
import { Text, View, Image, StyleSheet, Platform } from 'react-native';
import { Text, View, StyleSheet, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { PlatformPressable } from '@react-navigation/elements';
import { DrawerActions, useTheme } from '@react-navigation/native';
import type { Layout, DrawerHeaderProps } from '../types';
import { useTheme } from '@react-navigation/native';
import HeaderShownContext from './HeaderShownContext';
import HeaderHeightContext from './HeaderHeightContext';
import type { HeaderOptions } from './types';
export const getDefaultHeaderHeight = (
layout: Layout,
statusBarHeight: number
): number => {
type Layout = { width: number; height: number };
type Props = HeaderOptions & {
/**
* Layout of the screen.
*/
layout: Layout;
};
const getDefaultHeight = (layout: Layout, statusBarHeight: number): number => {
const isLandscape = layout.width > layout.height;
let headerHeight;
@@ -28,17 +35,14 @@ export const getDefaultHeaderHeight = (
return headerHeight + statusBarHeight;
};
export default function HeaderSegment({
route,
navigation,
options,
layout,
}: DrawerHeaderProps) {
export default function Header(props: Props) {
const insets = useSafeAreaInsets();
const { colors } = useTheme();
const isParentHeaderShown = React.useContext(HeaderShownContext);
const {
title,
layout,
headerTitle,
headerTitleAlign = Platform.select({
ios: 'center',
@@ -47,52 +51,34 @@ export default function HeaderSegment({
headerLeft,
headerLeftAccessibilityLabel,
headerRight,
headerRightAccessibilityLabel,
headerPressColor,
headerPressOpacity,
headerTitleAllowFontScaling,
headerTitleStyle,
headerTintColor,
headerPressColorAndroid,
headerStyle,
headerStatusBarHeight = insets.top,
} = options;
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
} = props;
const currentTitle =
typeof headerTitle !== 'function' && headerTitle !== undefined
? headerTitle
: title !== undefined
? title
: route.name;
const defaultHeight = getDefaultHeight(layout, headerStatusBarHeight);
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const leftButton = headerLeft
? headerLeft({
tintColor: headerTintColor,
pressColor: headerPressColor,
pressOpacity: headerPressOpacity,
accessibilityLabel: headerLeftAccessibilityLabel,
})
: null;
const leftButton = headerLeft ? (
headerLeft({ tintColor: headerTintColor })
) : (
<PlatformPressable
accessible
accessibilityRole="button"
accessibilityLabel={headerLeftAccessibilityLabel}
delayPressIn={0}
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
style={styles.touchable}
pressColor={headerPressColorAndroid}
hitSlop={Platform.select({
ios: undefined,
default: { top: 16, right: 16, bottom: 16, left: 16 },
})}
borderless
>
<Image
style={[
styles.icon,
headerTintColor ? { tintColor: headerTintColor } : null,
]}
source={require('./assets/toggle-drawer-icon.png')}
fadeDuration={0}
/>
</PlatformPressable>
);
const rightButton = headerRight
? headerRight({ tintColor: headerTintColor })
? headerRight({
tintColor: headerTintColor,
pressColor: headerPressColor,
pressOpacity: headerPressOpacity,
accessibilityLabel: headerRightAccessibilityLabel,
})
: null;
return (
@@ -134,7 +120,6 @@ export default function HeaderSegment({
>
{typeof headerTitle === 'function' ? (
headerTitle({
children: currentTitle,
allowFontScaling: headerTitleAllowFontScaling,
tintColor: headerTintColor,
style: headerTitleStyle,
@@ -152,7 +137,7 @@ export default function HeaderSegment({
headerTitleStyle,
]}
>
{currentTitle}
{headerTitle}
</Text>
)}
</View>
@@ -167,6 +152,10 @@ export default function HeaderSegment({
);
}
Header.getDefaultHeight = getDefaultHeight;
Header.ShownContext = HeaderShownContext;
Header.HeightContext = HeaderHeightContext;
const styles = StyleSheet.create({
container: {
...Platform.select({
@@ -208,15 +197,6 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
}),
icon: {
height: 24,
width: 24,
margin: 3,
resizeMode: 'contain',
},
touchable: {
marginHorizontal: 11,
},
left: {
flexGrow: 1,
flexBasis: 0,

View File

@@ -0,0 +1,8 @@
import { getNamedContext } from '@react-navigation/native';
const HeaderHeightContext = getNamedContext<number | undefined>(
'HeaderHeightContext',
undefined
);
export default HeaderHeightContext;

View File

@@ -0,0 +1,5 @@
import { getNamedContext } from '@react-navigation/native';
const HeaderShownContext = getNamedContext('HeaderShownContext', false);
export default HeaderShownContext;

View File

@@ -1,3 +1,8 @@
export { default as Header } from './Header';
export { default as PlatformPressable } from './PlatformPressable';
export { default as ResourceSavingScene } from './ResourceSavingScene';
export { default as SafeAreaProviderCompat } from './SafeAreaProviderCompat';
export { default as useHeaderHeight } from './useHeaderHeight';
export * from './types';

View File

@@ -0,0 +1,87 @@
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
export type HeaderOptions = {
/**
* String or a function that returns a React Element to be used by the header.
* Defaults to scene `title`.
* It receives `allowFontScaling`, `tintColor`, `style` and `children` in the options object as an argument.
* The title string is passed in `children`.
*/
headerTitle?:
| string
| ((props: {
/**
* Whether title font should scale to respect Text Size accessibility settings.
*/
allowFontScaling?: boolean;
/**
* Tint color for the header.
*/
tintColor?: string;
/**
* Style object for the title element.
*/
style?: StyleProp<TextStyle>;
}) => React.ReactNode);
/**
* How to align the the header title.
* Defaults to `center` on iOS and `left` on Android.
*/
headerTitleAlign?: 'left' | 'center';
/**
* Style object for the title component.
*/
headerTitleStyle?: StyleProp<TextStyle>;
/**
* Whether header title font should scale to respect Text Size accessibility settings. Defaults to `false`.
*/
headerTitleAllowFontScaling?: boolean;
/**
* Function which returns a React Element to display on the left side of the header.
*/
headerLeft?: (props: {
tintColor?: string;
pressColor?: string;
pressOpacity?: number;
accessibilityLabel?: string;
}) => React.ReactNode;
/**
* Accessibility label for the header left button.
*/
headerLeftAccessibilityLabel?: string;
/**
* Function which returns a React Element to display on the right side of the header.
*/
headerRight?: (props: {
tintColor?: string;
pressColor?: string;
pressOpacity?: number;
accessibilityLabel?: string;
}) => React.ReactNode;
/**
* Accessibility label for the header right button.
*/
headerRightAccessibilityLabel?: string;
/**
* Color for material ripple (Android >= 5.0 only).
*/
headerPressColor?: string;
/**
* Color for material ripple (Android >= 5.0 only).
*/
headerPressOpacity?: number;
/**
* Tint color for the header.
*/
headerTintColor?: string;
/**
* Style object for the header. You can specify a custom background color here, for example.
*/
headerStyle?: StyleProp<ViewStyle>;
/**
* Extra padding to add at the top of header to account for translucent status bar.
* By default, it uses the top value from the safe area insets of the device.
* Pass 0 or a custom value to disable the default behaviour, and customize the height.
*/
headerStatusBarHeight?: number;
};

View File

@@ -1,12 +1,12 @@
import * as React from 'react';
import HeaderHeightContext from './HeaderHeightContext';
export default function useFloatingHeaderHeight() {
export default function useHeaderHeight() {
const height = React.useContext(HeaderHeightContext);
if (height === undefined) {
throw new Error(
"Couldn't find the header height. Are you inside a screen in Stack?"
"Couldn't find the header height. Are you inside a screen in a navigator with a header?"
);
}

View File

@@ -1,5 +1,10 @@
{
"extends": "../../tsconfig",
"references": [
{ "path": "../core" },
{ "path": "../routers" },
{ "path": "../native" }
],
"compilerOptions": {
"outDir": "./lib/typescript"
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
const contexts = new Map<string, React.Context<any>>();
export default function getNamedContext<T>(
name: string,
initialValue: T
): React.Context<T> {
let context = contexts.get(name);
if (context) {
return context;
}
context = React.createContext<T>(initialValue);
context.displayName = name;
contexts.set(name, context);
return context;
}

View File

@@ -17,4 +17,6 @@ export { default as useLinkBuilder } from './useLinkBuilder';
export { default as ServerContainer } from './ServerContainer';
export { default as getNamedContext } from './getNamedContext';
export * from './types';

View File

@@ -38,11 +38,9 @@ export {
* Utilities
*/
export { default as CardAnimationContext } from './utils/CardAnimationContext';
export { default as HeaderHeightContext } from './utils/HeaderHeightContext';
export { default as GestureHandlerRefContext } from './utils/GestureHandlerRefContext';
export { default as useCardAnimation } from './utils/useCardAnimation';
export { default as useHeaderHeight } from './utils/useHeaderHeight';
export { default as useGestureHandlerRef } from './utils/useGestureHandlerRef';
/**

View File

@@ -1,3 +0,0 @@
import * as React from 'react';
export default React.createContext<number | undefined>(undefined);

View File

@@ -1,5 +0,0 @@
import * as React from 'react';
const HeaderShownContext = React.createContext(false);
export default HeaderShownContext;

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { StackActions, useNavigationState } from '@react-navigation/native';
import { Header as BaseHeader } from '@react-navigation/elements';
import HeaderSegment from './HeaderSegment';
import HeaderTitle from './HeaderTitle';
import HeaderShownContext from '../../utils/HeaderShownContext';
import ModalPresentationContext from '../../utils/ModalPresentationContext';
import debounce from '../../utils/debounce';
import type { StackHeaderProps, StackHeaderTitleProps } from '../../types';
@@ -57,7 +57,7 @@ export default React.memo(function Header({
);
const isModal = React.useContext(ModalPresentationContext);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const isParentHeaderShown = React.useContext(BaseHeader.ShownContext);
const isFirstRouteInParent = useNavigationState(
(state) => state.routes[0].key === route.key
);

View File

@@ -8,6 +8,7 @@ import {
ViewStyle,
} from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import { Header as BaseHeader } from '@react-navigation/elements';
import HeaderBackButton from './HeaderBackButton';
import HeaderBackground from './HeaderBackground';
import memoize from '../../utils/memoize';
@@ -51,29 +52,6 @@ const warnIfHeaderStylesDefined = (styles: Record<string, any>) => {
});
};
export const getDefaultHeaderHeight = (
layout: Layout,
statusBarHeight: number
): number => {
const isLandscape = layout.width > layout.height;
let headerHeight;
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
headerHeight = 32;
} else {
headerHeight = 44;
}
} else if (Platform.OS === 'android') {
headerHeight = 56;
} else {
headerHeight = 64;
}
return headerHeight + statusBarHeight;
};
export default function HeaderSegment(props: Props) {
const [leftLabelLayout, setLeftLabelLayout] = React.useState<
Layout | undefined
@@ -176,7 +154,10 @@ export default function HeaderSegment(props: Props) {
styleInterpolator,
} = props;
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const defaultHeight = BaseHeader.getDefaultHeight(
layout,
headerStatusBarHeight
);
const {
height = defaultHeight,

View File

@@ -1,11 +1,10 @@
import * as React from 'react';
import { Animated, View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { Route, useTheme } from '@react-navigation/native';
import { Header as BaseHeader } from '@react-navigation/elements';
import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import Card from './Card';
import { forModalPresentationIOS } from '../../TransitionConfigs/CardStyleInterpolators';
import HeaderHeightContext from '../../utils/HeaderHeightContext';
import HeaderShownContext from '../../utils/HeaderShownContext';
import PreviousSceneContext from '../../utils/PreviousSceneContext';
import ModalPresentationContext from '../../utils/ModalPresentationContext';
import type {
@@ -244,13 +243,13 @@ function CardContainer({
<View style={styles.container}>
<View style={styles.scene}>
<PreviousSceneContext.Provider value={previousScene}>
<HeaderShownContext.Provider
<BaseHeader.ShownContext.Provider
value={isParentHeaderShown || headerShown !== false}
>
<HeaderHeightContext.Provider value={headerHeight}>
<BaseHeader.HeightContext.Provider value={headerHeight}>
{renderScene({ route: scene.descriptor.route })}
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
</BaseHeader.HeightContext.Provider>
</BaseHeader.ShownContext.Provider>
</PreviousSceneContext.Provider>
</View>
{headerMode !== 'float' ? (

View File

@@ -11,14 +11,16 @@ import type {
Route,
StackNavigationState,
} from '@react-navigation/native';
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import {
Header as BaseHeader,
SafeAreaProviderCompat,
} from '@react-navigation/elements';
import {
MaybeScreenContainer,
MaybeScreen,
shouldUseActivityState,
} from '../Screens';
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import CardContainer from './CardContainer';
import {
@@ -118,7 +120,7 @@ const getHeaderHeights = (
acc[curr.key] =
typeof height === 'number'
? height
: getDefaultHeaderHeight(layout, headerStatusBarHeight);
: BaseHeader.getDefaultHeight(layout, headerStatusBarHeight);
return acc;
}, {});

View File

@@ -11,7 +11,10 @@ import {
Route,
ParamListBase,
} from '@react-navigation/native';
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import {
Header as BaseHeader,
SafeAreaProviderCompat,
} from '@react-navigation/elements';
import { GestureHandlerRootView } from '../GestureHandler';
import CardStack from './CardStack';
@@ -19,7 +22,6 @@ import KeyboardManager from '../KeyboardManager';
import HeaderContainer, {
Props as HeaderContainerProps,
} from '../Header/HeaderContainer';
import HeaderShownContext from '../../utils/HeaderShownContext';
import type {
StackNavigationHelpers,
StackNavigationConfig,
@@ -460,7 +462,7 @@ export default class StackView extends React.Component<Props, State> {
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<HeaderShownContext.Consumer>
<BaseHeader.ShownContext.Consumer>
{(isParentHeaderShown) => (
<CardStack
mode={mode}
@@ -487,7 +489,7 @@ export default class StackView extends React.Component<Props, State> {
{...props}
/>
)}
</HeaderShownContext.Consumer>
</BaseHeader.ShownContext.Consumer>
)}
</KeyboardManager>
)}