@react-navigation/elements 2.0.0-rc.18 → 2.0.0-rc.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,10 @@ import * as React from 'react';
3
3
  import {
4
4
  Animated,
5
5
  Image,
6
- type LayoutChangeEvent,
7
6
  Platform,
7
+ type StyleProp,
8
8
  StyleSheet,
9
+ type TextStyle,
9
10
  View,
10
11
  } from 'react-native';
11
12
 
@@ -21,7 +22,7 @@ export function HeaderBackButton({
21
22
  backImage,
22
23
  label,
23
24
  labelStyle,
24
- labelVisible = Platform.OS === 'ios',
25
+ displayMode = Platform.OS === 'ios' ? 'default' : 'minimal',
25
26
  onLabelLayout,
26
27
  onPress,
27
28
  pressColor,
@@ -38,29 +39,10 @@ export function HeaderBackButton({
38
39
  const { colors, fonts } = useTheme();
39
40
  const { direction } = useLocale();
40
41
 
41
- const [initialLabelWidth, setInitialLabelWidth] = React.useState<
42
- undefined | number
43
- >(undefined);
44
-
45
- const handleLabelLayout = (e: LayoutChangeEvent) => {
46
- onLabelLayout?.(e);
47
-
48
- const { layout } = e.nativeEvent;
49
-
50
- setInitialLabelWidth(
51
- (direction === 'rtl' ? layout.y : layout.x) + layout.width
52
- );
53
- };
54
-
55
- const shouldTruncateLabel = () => {
56
- return (
57
- !label ||
58
- (initialLabelWidth &&
59
- titleLayout &&
60
- screenLayout &&
61
- (screenLayout.width - titleLayout.width) / 2 < initialLabelWidth + 26)
62
- );
63
- };
42
+ const [labelWidth, setLabelWidth] = React.useState<number | null>(null);
43
+ const [truncatedLabelWidth, setTruncatedLabelWidth] = React.useState<
44
+ number | null
45
+ >(null);
64
46
 
65
47
  const renderBackImage = () => {
66
48
  if (backImage) {
@@ -71,7 +53,7 @@ export function HeaderBackButton({
71
53
  style={[
72
54
  styles.icon,
73
55
  direction === 'rtl' && styles.flip,
74
- Boolean(labelVisible) && styles.iconWithLabel,
56
+ displayMode !== 'minimal' && styles.iconWithLabel,
75
57
  Boolean(tintColor) && { tintColor },
76
58
  ]}
77
59
  resizeMode="contain"
@@ -83,32 +65,74 @@ export function HeaderBackButton({
83
65
  };
84
66
 
85
67
  const renderLabel = () => {
86
- const leftLabelText = shouldTruncateLabel() ? truncatedLabel : label;
87
-
88
- if (!labelVisible || leftLabelText === undefined) {
68
+ if (displayMode === 'minimal') {
89
69
  return null;
90
70
  }
91
71
 
72
+ const availableSpace =
73
+ titleLayout && screenLayout
74
+ ? (screenLayout.width - titleLayout.width) / 2 -
75
+ (ICON_WIDTH + ICON_MARGIN_START)
76
+ : null;
77
+
78
+ const potentialLabelText =
79
+ displayMode === 'default' ? label : truncatedLabel;
80
+ const finalLabelText =
81
+ availableSpace && labelWidth && truncatedLabelWidth
82
+ ? availableSpace > labelWidth
83
+ ? potentialLabelText
84
+ : availableSpace > truncatedLabelWidth
85
+ ? truncatedLabel
86
+ : null
87
+ : potentialLabelText;
88
+
89
+ const commonStyle: Animated.WithAnimatedValue<StyleProp<TextStyle>> = [
90
+ fonts.regular,
91
+ styles.label,
92
+ labelStyle,
93
+ ];
94
+
95
+ const hiddenStyle: Animated.WithAnimatedValue<StyleProp<TextStyle>> = [
96
+ commonStyle,
97
+ {
98
+ position: 'absolute',
99
+ top: 0,
100
+ left: 0,
101
+ opacity: 0,
102
+ },
103
+ ];
104
+
92
105
  const labelElement = (
93
106
  <View style={styles.labelWrapper}>
94
- <Animated.Text
95
- accessible={false}
96
- onLayout={
97
- // This measurement is used to determine if we should truncate the label when it doesn't fit
98
- // Only measure it when label is not truncated because we want the measurement of full label
99
- leftLabelText === label ? handleLabelLayout : undefined
100
- }
101
- style={[
102
- tintColor ? { color: tintColor } : null,
103
- fonts.regular,
104
- styles.label,
105
- labelStyle,
106
- ]}
107
- numberOfLines={1}
108
- allowFontScaling={!!allowFontScaling}
109
- >
110
- {leftLabelText}
111
- </Animated.Text>
107
+ {label && displayMode === 'default' ? (
108
+ <Animated.Text
109
+ style={hiddenStyle}
110
+ numberOfLines={1}
111
+ onLayout={(e) => setLabelWidth(e.nativeEvent.layout.width)}
112
+ >
113
+ {label}
114
+ </Animated.Text>
115
+ ) : null}
116
+ {truncatedLabel ? (
117
+ <Animated.Text
118
+ style={hiddenStyle}
119
+ numberOfLines={1}
120
+ onLayout={(e) => setTruncatedLabelWidth(e.nativeEvent.layout.width)}
121
+ >
122
+ {truncatedLabel}
123
+ </Animated.Text>
124
+ ) : null}
125
+ {finalLabelText ? (
126
+ <Animated.Text
127
+ accessible={false}
128
+ onLayout={onLabelLayout}
129
+ style={[tintColor ? { color: tintColor } : null, commonStyle]}
130
+ numberOfLines={1}
131
+ allowFontScaling={!!allowFontScaling}
132
+ >
133
+ {finalLabelText}
134
+ </Animated.Text>
135
+ ) : null}
112
136
  </View>
113
137
  );
114
138
 
@@ -167,6 +191,12 @@ export function HeaderBackButton({
167
191
  );
168
192
  }
169
193
 
194
+ const ICON_WIDTH = Platform.OS === 'ios' ? 13 : 24;
195
+ const ICON_HEIGHT = Platform.OS === 'ios' ? 21 : 24;
196
+ const ICON_MARGIN_START = Platform.OS === 'ios' ? 8 : 3;
197
+ const ICON_MARGIN_END = Platform.OS === 'ios' ? 22 : 3;
198
+ const ICON_MARGIN_VERTICAL = Platform.OS === 'ios' ? 8 : 3;
199
+
170
200
  const styles = StyleSheet.create({
171
201
  container: {
172
202
  paddingHorizontal: 0,
@@ -195,20 +225,13 @@ const styles = StyleSheet.create({
195
225
  default: { marginEnd: 3 },
196
226
  }),
197
227
  },
198
- icon: Platform.select({
199
- ios: {
200
- height: 21,
201
- width: 13,
202
- marginStart: 8,
203
- marginEnd: 22,
204
- marginVertical: 8,
205
- },
206
- default: {
207
- height: 24,
208
- width: 24,
209
- margin: 3,
210
- },
211
- }),
228
+ icon: {
229
+ height: ICON_HEIGHT,
230
+ width: ICON_WIDTH,
231
+ marginStart: ICON_MARGIN_START,
232
+ marginEnd: ICON_MARGIN_END,
233
+ marginVertical: ICON_MARGIN_VERTICAL,
234
+ },
212
235
  iconWithLabel:
213
236
  Platform.OS === 'ios'
214
237
  ? {
package/src/types.tsx CHANGED
@@ -6,6 +6,8 @@ import type {
6
6
  ViewStyle,
7
7
  } from 'react-native';
8
8
 
9
+ export type HeaderBackButtonDisplayMode = 'default' | 'generic' | 'minimal';
10
+
9
11
  export type Layout = { width: number; height: number };
10
12
 
11
13
  export type HeaderOptions = {
@@ -41,13 +43,20 @@ export type HeaderOptions = {
41
43
  tintColor?: string;
42
44
  pressColor?: string;
43
45
  pressOpacity?: number;
44
- labelVisible?: boolean;
46
+ displayMode?: HeaderBackButtonDisplayMode;
45
47
  href?: undefined;
46
48
  }) => React.ReactNode;
47
49
  /**
48
- * Whether a label is visible in the left button. Used to add extra padding.
50
+ * How the back button displays icon and title.
51
+ *
52
+ * Supported values:
53
+ * - "default" - Displays one of the following depending on the available space: previous screen's title, truncated title (e.g. 'Back') or no title (only icon).
54
+ * - "generic" – Displays one of the following depending on the available space: truncated title (e.g. 'Back') or no title (only icon).
55
+ * - "minimal" – Always displays only the icon without a title.
56
+ *
57
+ * Defaults to "default" on iOS, and "minimal" on Android.
49
58
  */
50
- headerLeftLabelVisible?: boolean;
59
+ headerBackButtonDisplayMode?: HeaderBackButtonDisplayMode;
51
60
  /**
52
61
  * Style object for the container of the `headerLeft` element`.
53
62
  */
@@ -209,7 +218,7 @@ export type HeaderBackButtonProps = Omit<HeaderButtonProps, 'children'> & {
209
218
  * Whether the label text is visible.
210
219
  * Defaults to `true` on iOS and `false` on Android.
211
220
  */
212
- labelVisible?: boolean;
221
+ displayMode?: HeaderBackButtonDisplayMode;
213
222
  /**
214
223
  * Style object for the label.
215
224
  */