mirror of
https://github.com/status-im/react-navigation.git
synced 2025-02-24 17:18:09 +00:00
Add back button label to header (#257)
This commit is contained in:
parent
4b2f94544a
commit
79f21277cb
@ -120,6 +120,7 @@ All `navigationOptions` for the `StackNavigator`:
|
||||
- `header` - a config object for the header bar:
|
||||
- `visible` - Boolean toggle of header visibility. Only works when `headerMode` is `screen`.
|
||||
- `title` - Title string used by the navigation bar, or a custom React component
|
||||
- `backTitle` - Title string used by the back button or `null` to disable label. Defaults to `title` value by default
|
||||
- `right` - Custom React Element to display on the right side of the header
|
||||
- `left` - Custom React Element to display on the left side of the header
|
||||
- `style` - Style object for the navigation bar
|
||||
|
@ -436,3 +436,14 @@ export type NavigationSceneRenderer = (
|
||||
export type NavigationStyleInterpolator = (
|
||||
props: NavigationSceneRendererProps,
|
||||
) => Style;
|
||||
|
||||
export type LayoutEvent = {
|
||||
nativeEvent: {
|
||||
layout: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
};
|
||||
};
|
@ -20,11 +20,11 @@ import addNavigationHelpers from '../addNavigationHelpers';
|
||||
import type {
|
||||
NavigationScene,
|
||||
NavigationRouter,
|
||||
NavigationState,
|
||||
NavigationAction,
|
||||
NavigationScreenProp,
|
||||
NavigationSceneRendererProps,
|
||||
NavigationStyleInterpolator,
|
||||
LayoutEvent,
|
||||
Style,
|
||||
} from '../TypeDefinition';
|
||||
|
||||
@ -34,9 +34,9 @@ type SubViewProps = NavigationSceneRendererProps & {
|
||||
onNavigateBack?: () => void,
|
||||
};
|
||||
|
||||
type Navigation = NavigationScreenProp<NavigationState, NavigationAction>;
|
||||
type Navigation = NavigationScreenProp<*, NavigationAction>;
|
||||
|
||||
type SubViewRenderer = (subViewProps: SubViewProps) => ?React.Element<*>;
|
||||
type SubViewRenderer = (subViewProps: SubViewProps) => ?React.Element<any>;
|
||||
|
||||
export type HeaderProps = NavigationSceneRendererProps & {
|
||||
mode: HeaderMode,
|
||||
@ -50,10 +50,16 @@ export type HeaderProps = NavigationSceneRendererProps & {
|
||||
|
||||
type SubViewName = 'left' | 'title' | 'right';
|
||||
|
||||
type HeaderState = {
|
||||
widths: {
|
||||
[key: number]: number,
|
||||
},
|
||||
};
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
|
||||
class Header extends React.Component<void, HeaderProps, void> {
|
||||
class Header extends React.PureComponent<void, HeaderProps, HeaderState> {
|
||||
|
||||
static HEIGHT = APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
static Title = HeaderTitle;
|
||||
@ -72,13 +78,9 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
|
||||
props: HeaderProps;
|
||||
|
||||
shouldComponentUpdate(nextProps: HeaderProps, nextState: any): boolean {
|
||||
return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call(
|
||||
this,
|
||||
nextProps,
|
||||
nextState
|
||||
);
|
||||
}
|
||||
state = {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_getHeaderTitle(navigation: Navigation): ?string {
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
@ -91,6 +93,14 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
return typeof title === 'string' ? title : undefined;
|
||||
}
|
||||
|
||||
_getBackButtonTitle(navigation: Navigation): ?string {
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header') || {};
|
||||
if (header.backTitle === null) {
|
||||
return undefined;
|
||||
}
|
||||
return header.backTitle || this._getHeaderTitle(navigation);
|
||||
}
|
||||
|
||||
_getHeaderTintColor(navigation: Navigation): ?string {
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
if (header && header.tintColor) {
|
||||
@ -107,61 +117,53 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_renderTitleComponent = (props: SubViewProps): React.Element<HeaderTitle> => {
|
||||
_renderTitleComponent = (props: SubViewProps) => {
|
||||
const titleStyle = this._getHeaderTitleStyle(props.navigation);
|
||||
const color = this._getHeaderTintColor(props.navigation);
|
||||
const title = this._getHeaderTitle(props.navigation);
|
||||
return <HeaderTitle style={[color ? { color } : null, titleStyle]}>{title}</HeaderTitle>;
|
||||
return (
|
||||
<HeaderTitle
|
||||
style={[color ? { color } : null, titleStyle]}
|
||||
>
|
||||
{title}
|
||||
</HeaderTitle>
|
||||
);
|
||||
};
|
||||
|
||||
_renderLeftComponent = (props: SubViewProps): ?React.Element<HeaderBackButton> => {
|
||||
_renderLeftComponent = (props: SubViewProps) => {
|
||||
if (props.scene.index === 0 || !props.onNavigateBack) {
|
||||
return null;
|
||||
}
|
||||
const tintColor = this._getHeaderTintColor(props.navigation);
|
||||
// @todo(grabobu):
|
||||
// We have implemented support for back button label (which works 100% fine),
|
||||
// but when title is too long, it will overlap the <HeaderTitle />.
|
||||
// We had to revert the PR implementing that because of Android issues,
|
||||
// I will land it this week and re-enable that for next release.
|
||||
//
|
||||
// const previousNavigation = addNavigationHelpers({
|
||||
// ...props.navigation,
|
||||
// state: props.scenes[props.scene.index - 1].route,
|
||||
// });
|
||||
// const backButtonTitle = this._getHeaderTitle(previousNavigation);
|
||||
const previousNavigation = addNavigationHelpers({
|
||||
...props.navigation,
|
||||
state: props.scenes[props.scene.index - 1].route,
|
||||
});
|
||||
const backButtonTitle = this._getBackButtonTitle(previousNavigation);
|
||||
return (
|
||||
<HeaderBackButton
|
||||
onPress={props.onNavigateBack}
|
||||
tintColor={tintColor}
|
||||
title={backButtonTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = () => null;
|
||||
|
||||
_renderLeft = (props: NavigationSceneRendererProps): ?React.Element<*> => this._renderSubView(
|
||||
_renderLeft(props: NavigationSceneRendererProps): ?React.Element<*> {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'left',
|
||||
this.props.renderLeftComponent,
|
||||
this._renderLeftComponent,
|
||||
HeaderStyleInterpolator.forLeft,
|
||||
);
|
||||
}
|
||||
|
||||
_renderTitle = (props: NavigationSceneRendererProps, options: *): ?React.Element<*> => {
|
||||
const style = {};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (!options.hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!options.hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_renderTitle(props: NavigationSceneRendererProps): ?React.Element<*> {
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
props,
|
||||
'title',
|
||||
this.props.renderTitleComponent,
|
||||
this._renderTitleComponent,
|
||||
@ -169,13 +171,15 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
);
|
||||
}
|
||||
|
||||
_renderRight = (props: NavigationSceneRendererProps): ?React.Element<*> => this._renderSubView(
|
||||
_renderRight(props: NavigationSceneRendererProps): ?React.Element<*> {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'right',
|
||||
this.props.renderRightComponent,
|
||||
this._renderRightComponent,
|
||||
HeaderStyleInterpolator.forRight,
|
||||
);
|
||||
}
|
||||
|
||||
_renderSubView(
|
||||
props: NavigationSceneRendererProps,
|
||||
@ -212,16 +216,34 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
subView = defaultRenderer(subViewProps);
|
||||
}
|
||||
|
||||
if (subView === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
const onLayoutIOS = name === 'title'
|
||||
? (e: LayoutEvent) => {
|
||||
this.setState({
|
||||
widths: {
|
||||
...this.state.widths,
|
||||
[index]: e.nativeEvent.layout.width,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const titleWidth = name === 'left' || name === 'right'
|
||||
? this.state.widths[index]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
onLayout={onLayoutIOS}
|
||||
key={`${name}_${key}`}
|
||||
style={[
|
||||
titleWidth && {
|
||||
width: (props.layout.initWidth - titleWidth) / 2,
|
||||
},
|
||||
styles.item,
|
||||
styles[name],
|
||||
styleInterpolator(props),
|
||||
@ -232,61 +254,55 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
_renderHeader(props: NavigationSceneRendererProps): React.Element<*> {
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[StyleSheet.absoluteFill, styles.header]}
|
||||
key={`scene_${props.scene.key}`}
|
||||
>
|
||||
{left}
|
||||
{title}
|
||||
{right}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let appBar;
|
||||
|
||||
if (this.props.mode === 'float') {
|
||||
const scenesProps: Array<NavigationSceneRendererProps> = this.props.scenes
|
||||
.map((scene: NavigationScene, index: number) => ({
|
||||
...NavigationPropTypes.extractSceneRendererProps(this.props),
|
||||
scene,
|
||||
index,
|
||||
navigation: addNavigationHelpers({
|
||||
...this.props.navigation,
|
||||
state: scene.route,
|
||||
}),
|
||||
}));
|
||||
|
||||
appBar = scenesProps.map(this._renderHeader, this);
|
||||
} else {
|
||||
appBar = this._renderHeader({
|
||||
...this.props,
|
||||
position: new Animated.Value(this.props.scene.index),
|
||||
progress: new Animated.Value(0),
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { scenes, scene, style, position, progress, ...rest } = this.props;
|
||||
|
||||
let children = null;
|
||||
|
||||
if (this.props.mode === 'float') {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const scenesProps = (scenes.map((scene: NavigationScene, index: number) => {
|
||||
const props = NavigationPropTypes.extractSceneRendererProps(this.props);
|
||||
props.scene = scene;
|
||||
props.index = index;
|
||||
props.navigation = addNavigationHelpers({
|
||||
...this.props.navigation,
|
||||
state: scene.route,
|
||||
});
|
||||
return props;
|
||||
}): Array<NavigationSceneRendererProps>);
|
||||
const leftComponents = scenesProps.map(this._renderLeft, this);
|
||||
const rightComponents = scenesProps.map(this._renderRight, this);
|
||||
const titleComponents = scenesProps.map((props: *, i: number) =>
|
||||
this._renderTitle(props, {
|
||||
hasLeftComponent: leftComponents && !!leftComponents[i],
|
||||
hasRightComponent: rightComponents && !!rightComponents[i],
|
||||
})
|
||||
);
|
||||
|
||||
children = [
|
||||
titleComponents,
|
||||
leftComponents,
|
||||
rightComponents
|
||||
];
|
||||
} else {
|
||||
const staticRendererProps = {
|
||||
...this.props,
|
||||
position: new Animated.Value(scene.index),
|
||||
progress: new Animated.Value(0),
|
||||
};
|
||||
const leftComponent = this._renderLeft(staticRendererProps);
|
||||
const rightComponent = this._renderRight(staticRendererProps);
|
||||
const titleComponent = this._renderTitle(staticRendererProps, {
|
||||
hasLeftComponent: !!leftComponent,
|
||||
hasRightComponent: !!rightComponent,
|
||||
});
|
||||
|
||||
children = [
|
||||
titleComponent,
|
||||
leftComponent,
|
||||
rightComponent
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View {...rest} style={[styles.container, style]}>
|
||||
<View style={styles.appBar}>{children}</View>
|
||||
<View style={styles.appBar}>
|
||||
{appBar}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@ -295,8 +311,8 @@ class Header extends React.Component<void, HeaderProps, void> {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingTop: STATUSBAR_HEIGHT,
|
||||
height: STATUSBAR_HEIGHT + APPBAR_HEIGHT,
|
||||
backgroundColor: Platform.OS === 'ios' ? '#EFEFF2' : '#FFF',
|
||||
height: STATUSBAR_HEIGHT + APPBAR_HEIGHT,
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
@ -306,30 +322,25 @@ const styles = StyleSheet.create({
|
||||
elevation: 4,
|
||||
},
|
||||
appBar: {
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
position: 'absolute',
|
||||
right: 40,
|
||||
top: 0,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: Platform.OS === 'android'
|
||||
? {
|
||||
flex: 1,
|
||||
alignItems: 'flex-start',
|
||||
}
|
||||
: null,
|
||||
left: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
right: {
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -10,54 +10,107 @@ import {
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import type { LayoutEvent } from '../TypeDefinition';
|
||||
|
||||
import TouchableItem from './TouchableItem';
|
||||
|
||||
type Props = {
|
||||
onPress?: () => void,
|
||||
title?: string,
|
||||
tintColor?: ?string;
|
||||
title?: ?string,
|
||||
tintColor?: ?string,
|
||||
truncatedTitle?: ?string,
|
||||
};
|
||||
|
||||
const HeaderBackButton = ({ onPress, title, tintColor }: Props) => (
|
||||
<TouchableItem
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
title && styles.iconWithTitle,
|
||||
{ tintColor },
|
||||
]}
|
||||
source={require('./assets/back-icon.png')}
|
||||
/>
|
||||
{Platform.OS === 'ios' && title && (
|
||||
<Text style={[styles.title, { color: tintColor }]}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
|
||||
HeaderBackButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired,
|
||||
tintColor: PropTypes.string,
|
||||
type DefaultProps = {
|
||||
tintColor: ?string,
|
||||
truncatedTitle: ?string,
|
||||
};
|
||||
|
||||
HeaderBackButton.defaultProps = {
|
||||
tintColor: Platform.select({
|
||||
ios: '#037aff',
|
||||
}),
|
||||
type State = {
|
||||
containerWidth?: number,
|
||||
initialTextWidth?: number,
|
||||
};
|
||||
|
||||
class HeaderBackButton extends React.PureComponent<DefaultProps, Props, State> {
|
||||
static propTypes = {
|
||||
onPress: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
tintColor: PropTypes.string,
|
||||
truncatedTitle: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
tintColor: Platform.select({
|
||||
ios: '#037aff',
|
||||
}),
|
||||
truncatedTitle: 'Back',
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
_onContainerLayout = (e: LayoutEvent) => {
|
||||
if (Platform.OS !== 'ios') {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
containerWidth: e.nativeEvent.layout.width,
|
||||
});
|
||||
};
|
||||
|
||||
_onTextLayout = (e: LayoutEvent) => {
|
||||
if (this.state.initialTextWidth) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onPress, title, tintColor, truncatedTitle } = this.props;
|
||||
|
||||
const renderTruncated = this.state.containerWidth && this.state.initialTextWidth
|
||||
? this.state.containerWidth < this.state.initialTextWidth
|
||||
: false;
|
||||
|
||||
return (
|
||||
<TouchableItem
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<View
|
||||
onLayout={this._onContainerLayout}
|
||||
style={styles.container}
|
||||
>
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
title && styles.iconWithTitle,
|
||||
{ tintColor },
|
||||
]}
|
||||
source={require('./assets/back-icon.png')}
|
||||
/>
|
||||
{Platform.OS === 'ios' && title && (
|
||||
<Text
|
||||
ellipsizeMode="middle"
|
||||
onLayout={this._onTextLayout}
|
||||
style={[styles.title, { color: tintColor }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{renderTruncated ? truncatedTitle : title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
title: {
|
||||
|
@ -23,7 +23,6 @@ const HeaderTitle = ({ style, ...rest }: Props) => (
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: Platform.OS === 'ios' ? 17 : 18,
|
||||
fontWeight: Platform.OS === 'ios' ? '600' : '500',
|
||||
color: 'rgba(0, 0, 0, .9)',
|
||||
|
Loading…
x
Reference in New Issue
Block a user