@react-navigation/elements 2.0.0-rc.20 → 2.0.0-rc.21

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.
Files changed (126) hide show
  1. package/lib/commonjs/Header/Header.js +95 -30
  2. package/lib/commonjs/Header/Header.js.map +1 -1
  3. package/lib/commonjs/Header/HeaderBackButton.js +7 -21
  4. package/lib/commonjs/Header/HeaderBackButton.js.map +1 -1
  5. package/lib/commonjs/Header/HeaderIcon.js +44 -0
  6. package/lib/commonjs/Header/HeaderIcon.js.map +1 -0
  7. package/lib/commonjs/Header/HeaderSearchBar.js +267 -0
  8. package/lib/commonjs/Header/HeaderSearchBar.js.map +1 -0
  9. package/lib/commonjs/PlatformPressable.js +4 -2
  10. package/lib/commonjs/PlatformPressable.js.map +1 -1
  11. package/lib/commonjs/Screen.js +16 -15
  12. package/lib/commonjs/Screen.js.map +1 -1
  13. package/lib/commonjs/assets/back-icon-mask.png +0 -0
  14. package/lib/commonjs/assets/back-icon@4x.ios.png +0 -0
  15. package/lib/commonjs/assets/clear-icon.png +0 -0
  16. package/lib/commonjs/assets/clear-icon@1x.png +0 -0
  17. package/lib/commonjs/assets/clear-icon@2x.png +0 -0
  18. package/lib/commonjs/assets/clear-icon@3x.png +0 -0
  19. package/lib/commonjs/assets/clear-icon@4x.png +0 -0
  20. package/lib/commonjs/assets/close-icon.png +0 -0
  21. package/lib/commonjs/assets/close-icon@1x.png +0 -0
  22. package/lib/commonjs/assets/close-icon@2x.png +0 -0
  23. package/lib/commonjs/assets/close-icon@3x.png +0 -0
  24. package/lib/commonjs/assets/close-icon@4x.png +0 -0
  25. package/lib/commonjs/assets/search-icon.png +0 -0
  26. package/lib/commonjs/assets/search-icon@1x.android.png +0 -0
  27. package/lib/commonjs/assets/search-icon@1x.ios.png +0 -0
  28. package/lib/commonjs/assets/search-icon@2x.android.png +0 -0
  29. package/lib/commonjs/assets/search-icon@2x.ios.png +0 -0
  30. package/lib/commonjs/assets/search-icon@3x.android.png +0 -0
  31. package/lib/commonjs/assets/search-icon@3x.ios.png +0 -0
  32. package/lib/commonjs/assets/search-icon@4x.android.png +0 -0
  33. package/lib/commonjs/assets/search-icon@4x.ios.png +0 -0
  34. package/lib/commonjs/index.js +4 -1
  35. package/lib/commonjs/index.js.map +1 -1
  36. package/lib/module/Header/Header.js +96 -32
  37. package/lib/module/Header/Header.js.map +1 -1
  38. package/lib/module/Header/HeaderBackButton.js +7 -21
  39. package/lib/module/Header/HeaderBackButton.js.map +1 -1
  40. package/lib/module/Header/HeaderIcon.js +39 -0
  41. package/lib/module/Header/HeaderIcon.js.map +1 -0
  42. package/lib/module/Header/HeaderSearchBar.js +260 -0
  43. package/lib/module/Header/HeaderSearchBar.js.map +1 -0
  44. package/lib/module/PlatformPressable.js +4 -2
  45. package/lib/module/PlatformPressable.js.map +1 -1
  46. package/lib/module/Screen.js +16 -15
  47. package/lib/module/Screen.js.map +1 -1
  48. package/lib/module/assets/back-icon-mask.png +0 -0
  49. package/lib/module/assets/back-icon@4x.ios.png +0 -0
  50. package/lib/module/assets/clear-icon.png +0 -0
  51. package/lib/module/assets/clear-icon@1x.png +0 -0
  52. package/lib/module/assets/clear-icon@2x.png +0 -0
  53. package/lib/module/assets/clear-icon@3x.png +0 -0
  54. package/lib/module/assets/clear-icon@4x.png +0 -0
  55. package/lib/module/assets/close-icon.png +0 -0
  56. package/lib/module/assets/close-icon@1x.png +0 -0
  57. package/lib/module/assets/close-icon@2x.png +0 -0
  58. package/lib/module/assets/close-icon@3x.png +0 -0
  59. package/lib/module/assets/close-icon@4x.png +0 -0
  60. package/lib/module/assets/search-icon.png +0 -0
  61. package/lib/module/assets/search-icon@1x.android.png +0 -0
  62. package/lib/module/assets/search-icon@1x.ios.png +0 -0
  63. package/lib/module/assets/search-icon@2x.android.png +0 -0
  64. package/lib/module/assets/search-icon@2x.ios.png +0 -0
  65. package/lib/module/assets/search-icon@3x.android.png +0 -0
  66. package/lib/module/assets/search-icon@3x.ios.png +0 -0
  67. package/lib/module/assets/search-icon@4x.android.png +0 -0
  68. package/lib/module/assets/search-icon@4x.ios.png +0 -0
  69. package/lib/module/index.js +4 -1
  70. package/lib/module/index.js.map +1 -1
  71. package/lib/typescript/commonjs/src/Header/Header.d.ts +13 -0
  72. package/lib/typescript/commonjs/src/Header/Header.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/src/Header/HeaderBackButton.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/src/Header/HeaderIcon.d.ts +5 -0
  75. package/lib/typescript/commonjs/src/Header/HeaderIcon.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/src/Header/HeaderSearchBar.d.ts +10 -0
  77. package/lib/typescript/commonjs/src/Header/HeaderSearchBar.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/src/PlatformPressable.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/src/Screen.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/src/types.d.ts +63 -8
  82. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/tsconfig.build.tsbuildinfo +1 -1
  84. package/lib/typescript/module/src/Header/Header.d.ts +13 -0
  85. package/lib/typescript/module/src/Header/Header.d.ts.map +1 -1
  86. package/lib/typescript/module/src/Header/HeaderBackButton.d.ts.map +1 -1
  87. package/lib/typescript/module/src/Header/HeaderIcon.d.ts +5 -0
  88. package/lib/typescript/module/src/Header/HeaderIcon.d.ts.map +1 -0
  89. package/lib/typescript/module/src/Header/HeaderSearchBar.d.ts +10 -0
  90. package/lib/typescript/module/src/Header/HeaderSearchBar.d.ts.map +1 -0
  91. package/lib/typescript/module/src/PlatformPressable.d.ts.map +1 -1
  92. package/lib/typescript/module/src/Screen.d.ts.map +1 -1
  93. package/lib/typescript/module/src/index.d.ts.map +1 -1
  94. package/lib/typescript/module/src/types.d.ts +63 -8
  95. package/lib/typescript/module/src/types.d.ts.map +1 -1
  96. package/lib/typescript/module/tsconfig.build.tsbuildinfo +1 -1
  97. package/package.json +2 -2
  98. package/src/Header/Header.tsx +142 -53
  99. package/src/Header/HeaderBackButton.tsx +4 -15
  100. package/src/Header/HeaderIcon.tsx +36 -0
  101. package/src/Header/HeaderSearchBar.tsx +283 -0
  102. package/src/PlatformPressable.tsx +6 -1
  103. package/src/Screen.tsx +18 -14
  104. package/src/assets/back-icon-mask.png +0 -0
  105. package/src/assets/back-icon@4x.ios.png +0 -0
  106. package/src/assets/clear-icon.png +0 -0
  107. package/src/assets/clear-icon@1x.png +0 -0
  108. package/src/assets/clear-icon@2x.png +0 -0
  109. package/src/assets/clear-icon@3x.png +0 -0
  110. package/src/assets/clear-icon@4x.png +0 -0
  111. package/src/assets/close-icon.png +0 -0
  112. package/src/assets/close-icon@1x.png +0 -0
  113. package/src/assets/close-icon@2x.png +0 -0
  114. package/src/assets/close-icon@3x.png +0 -0
  115. package/src/assets/close-icon@4x.png +0 -0
  116. package/src/assets/search-icon.png +0 -0
  117. package/src/assets/search-icon@1x.android.png +0 -0
  118. package/src/assets/search-icon@1x.ios.png +0 -0
  119. package/src/assets/search-icon@2x.android.png +0 -0
  120. package/src/assets/search-icon@2x.ios.png +0 -0
  121. package/src/assets/search-icon@3x.android.png +0 -0
  122. package/src/assets/search-icon@3x.ios.png +0 -0
  123. package/src/assets/search-icon@4x.android.png +0 -0
  124. package/src/assets/search-icon@4x.ios.png +0 -0
  125. package/src/index.tsx +10 -1
  126. package/src/types.tsx +66 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-navigation/elements",
3
3
  "description": "UI Components for React Navigation",
4
- "version": "2.0.0-rc.20",
4
+ "version": "2.0.0-rc.21",
5
5
  "keywords": [
6
6
  "react-native",
7
7
  "react-navigation",
@@ -99,5 +99,5 @@
99
99
  ]
100
100
  ]
101
101
  },
102
- "gitHead": "b32550a43d0e2b532cfe100d9cf17837184a636e"
102
+ "gitHead": "f1b77a90eee4444e990aa86d7feecffff85d020b"
103
103
  }
@@ -1,7 +1,8 @@
1
- import { useTheme } from '@react-navigation/native';
1
+ import { useNavigation, useTheme } from '@react-navigation/native';
2
2
  import * as React from 'react';
3
3
  import {
4
4
  Animated,
5
+ type LayoutChangeEvent,
5
6
  Platform,
6
7
  StyleSheet,
7
8
  View,
@@ -12,9 +13,14 @@ import {
12
13
  useSafeAreaInsets,
13
14
  } from 'react-native-safe-area-context';
14
15
 
16
+ import searchIcon from '../assets/search-icon.png';
15
17
  import type { HeaderOptions, Layout } from '../types';
16
18
  import { getDefaultHeaderHeight } from './getDefaultHeaderHeight';
19
+ import { HeaderBackButton } from './HeaderBackButton';
17
20
  import { HeaderBackground } from './HeaderBackground';
21
+ import { HeaderButton } from './HeaderButton';
22
+ import { HeaderIcon } from './HeaderIcon';
23
+ import { HeaderSearchBar } from './HeaderSearchBar';
18
24
  import { HeaderShownContext } from './HeaderShownContext';
19
25
  import { HeaderTitle } from './HeaderTitle';
20
26
 
@@ -22,6 +28,19 @@ import { HeaderTitle } from './HeaderTitle';
22
28
  const IPAD_MINI_MEDIUM_WIDTH = 414;
23
29
 
24
30
  type Props = HeaderOptions & {
31
+ /**
32
+ * Options for the back button.
33
+ */
34
+ back?: {
35
+ /**
36
+ * Title of the previous screen.
37
+ */
38
+ title: string | undefined;
39
+ /**
40
+ * The `href` to use for the anchor tag on web
41
+ */
42
+ href: string | undefined;
43
+ };
25
44
  /**
26
45
  * Whether the header is in a modal
27
46
  */
@@ -57,16 +76,40 @@ export function Header(props: Props) {
57
76
  const frame = useSafeAreaFrame();
58
77
  const { colors } = useTheme();
59
78
 
79
+ const navigation = useNavigation();
60
80
  const isParentHeaderShown = React.useContext(HeaderShownContext);
61
81
 
82
+ const [searchBarVisible, setSearchBarVisible] = React.useState(false);
83
+ const [titleLayout, setTitleLayout] = React.useState<Layout | undefined>(
84
+ undefined
85
+ );
86
+
87
+ const onTitleLayout = (e: LayoutChangeEvent) => {
88
+ const { height, width } = e.nativeEvent.layout;
89
+
90
+ setTitleLayout((titleLayout) => {
91
+ if (
92
+ titleLayout &&
93
+ height === titleLayout.height &&
94
+ width === titleLayout.width
95
+ ) {
96
+ return titleLayout;
97
+ }
98
+
99
+ return { height, width };
100
+ });
101
+ };
102
+
62
103
  const {
63
104
  // eslint-disable-next-line @eslint-react/no-unstable-default-props
64
105
  layout = frame,
65
106
  modal = false,
107
+ back,
66
108
  title,
67
109
  headerTitle: customTitle,
68
110
  headerTitleAlign = Platform.OS === 'ios' ? 'center' : 'left',
69
- headerLeft,
111
+ headerLeft = back ? (props) => <HeaderBackButton {...props} /> : undefined,
112
+ headerSearchBarOptions,
70
113
  headerTransparent,
71
114
  headerTintColor,
72
115
  headerBackground,
@@ -77,6 +120,7 @@ export function Header(props: Props) {
77
120
  headerRightContainerStyle: rightContainerStyle,
78
121
  headerTitleContainerStyle: titleContainerStyle,
79
122
  headerBackButtonDisplayMode = Platform.OS === 'ios' ? 'default' : 'minimal',
123
+ headerBackTitleStyle,
80
124
  headerBackgroundContainerStyle: backgroundContainerStyle,
81
125
  headerStyle: customHeaderStyle,
82
126
  headerShadowVisible,
@@ -208,6 +252,13 @@ export function Header(props: Props) {
208
252
  pressColor: headerPressColor,
209
253
  pressOpacity: headerPressOpacity,
210
254
  displayMode: headerBackButtonDisplayMode,
255
+ titleLayout,
256
+ screenLayout: layout,
257
+ canGoBack: Boolean(back),
258
+ onPress: back ? navigation.goBack : undefined,
259
+ label: back?.title,
260
+ labelStyle: headerBackTitleStyle,
261
+ href: back?.href,
211
262
  })
212
263
  : null;
213
264
 
@@ -216,6 +267,7 @@ export function Header(props: Props) {
216
267
  tintColor: iconTintColor,
217
268
  pressColor: headerPressColor,
218
269
  pressOpacity: headerPressOpacity,
270
+ canGoBack: Boolean(back),
219
271
  })
220
272
  : null;
221
273
 
@@ -262,53 +314,88 @@ export function Header(props: Props) {
262
314
  >
263
315
  {leftButton}
264
316
  </Animated.View>
265
- <Animated.View
266
- pointerEvents="box-none"
267
- style={[
268
- styles.title,
269
- {
270
- // Avoid the title from going offscreen or overlapping buttons
271
- maxWidth:
272
- headerTitleAlign === 'center'
273
- ? layout.width -
274
- ((leftButton
275
- ? headerBackButtonDisplayMode !== 'minimal'
276
- ? 80
277
- : 32
278
- : 16) +
279
- (rightButton ? 16 : 0) +
280
- Math.max(insets.left, insets.right)) *
281
- 2
282
- : layout.width -
283
- ((leftButton ? 52 : 16) +
284
- (rightButton ? 52 : 16) +
285
- insets.left -
286
- insets.right),
287
- },
288
- headerTitleAlign === 'left' && leftButton
289
- ? { marginStart: 4 }
290
- : { marginStart: 16 },
291
- titleContainerStyle,
292
- ]}
293
- >
294
- {headerTitle({
295
- children: title,
296
- allowFontScaling: titleAllowFontScaling,
297
- tintColor: headerTintColor,
298
- style: titleStyle,
299
- })}
300
- </Animated.View>
301
- <Animated.View
302
- pointerEvents="box-none"
303
- style={[
304
- styles.end,
305
- styles.expand,
306
- { marginEnd: insets.right },
307
- rightContainerStyle,
308
- ]}
309
- >
310
- {rightButton}
311
- </Animated.View>
317
+ {Platform.OS === 'ios' || !searchBarVisible ? (
318
+ <>
319
+ <Animated.View
320
+ pointerEvents="box-none"
321
+ style={[
322
+ styles.title,
323
+ {
324
+ // Avoid the title from going offscreen or overlapping buttons
325
+ maxWidth:
326
+ headerTitleAlign === 'center'
327
+ ? layout.width -
328
+ ((leftButton
329
+ ? headerBackButtonDisplayMode !== 'minimal'
330
+ ? 80
331
+ : 32
332
+ : 16) +
333
+ (rightButton ? 16 : 0) +
334
+ Math.max(insets.left, insets.right)) *
335
+ 2
336
+ : layout.width -
337
+ ((leftButton ? 52 : 16) +
338
+ (rightButton ? 52 : 16) +
339
+ insets.left -
340
+ insets.right),
341
+ },
342
+ headerTitleAlign === 'left' && leftButton
343
+ ? { marginStart: 4 }
344
+ : { marginStart: 16 },
345
+ titleContainerStyle,
346
+ ]}
347
+ >
348
+ {headerTitle({
349
+ children: title,
350
+ allowFontScaling: titleAllowFontScaling,
351
+ tintColor: headerTintColor,
352
+ onLayout: onTitleLayout,
353
+ style: titleStyle,
354
+ })}
355
+ </Animated.View>
356
+ <Animated.View
357
+ pointerEvents="box-none"
358
+ style={[
359
+ styles.end,
360
+ styles.expand,
361
+ { marginEnd: insets.right },
362
+ rightContainerStyle,
363
+ ]}
364
+ >
365
+ {rightButton}
366
+ {headerSearchBarOptions ? (
367
+ <HeaderButton
368
+ tintColor={iconTintColor}
369
+ pressColor={headerPressColor}
370
+ pressOpacity={headerPressOpacity}
371
+ onPress={() => setSearchBarVisible(true)}
372
+ >
373
+ <HeaderIcon
374
+ style={
375
+ Boolean(iconTintColor) && { tintColor: iconTintColor }
376
+ }
377
+ source={searchIcon}
378
+ />
379
+ </HeaderButton>
380
+ ) : null}
381
+ </Animated.View>
382
+ </>
383
+ ) : null}
384
+ {Platform.OS === 'ios' || searchBarVisible ? (
385
+ <HeaderSearchBar
386
+ {...headerSearchBarOptions}
387
+ visible={searchBarVisible}
388
+ onClose={() => {
389
+ setSearchBarVisible(false);
390
+ headerSearchBarOptions?.onClose?.();
391
+ }}
392
+ style={[
393
+ Platform.OS === 'ios'
394
+ ? [StyleSheet.absoluteFill, { backgroundColor: colors.card }]
395
+ : !leftButton && { marginStart: 8 },
396
+ ]}
397
+ />
398
+ ) : null}
312
399
  </View>
313
400
  </Animated.View>
314
401
  );
@@ -327,12 +414,14 @@ const styles = StyleSheet.create({
327
414
  justifyContent: 'center',
328
415
  },
329
416
  start: {
330
- justifyContent: 'center',
331
- alignItems: 'flex-start',
417
+ flexDirection: 'row',
418
+ alignItems: 'center',
419
+ justifyContent: 'flex-start',
332
420
  },
333
421
  end: {
334
- justifyContent: 'center',
335
- alignItems: 'flex-end',
422
+ flexDirection: 'row',
423
+ alignItems: 'center',
424
+ justifyContent: 'flex-end',
336
425
  },
337
426
  expand: {
338
427
  flexGrow: 1,
@@ -15,6 +15,7 @@ import backIconMask from '../assets/back-icon-mask.png';
15
15
  import { MaskedView } from '../MaskedView';
16
16
  import type { HeaderBackButtonProps } from '../types';
17
17
  import { HeaderButton } from './HeaderButton';
18
+ import { HeaderIcon, ICON_MARGIN } from './HeaderIcon';
18
19
 
19
20
  export function HeaderBackButton({
20
21
  disabled,
@@ -49,16 +50,13 @@ export function HeaderBackButton({
49
50
  return backImage({ tintColor: tintColor ?? colors.text });
50
51
  } else {
51
52
  return (
52
- <Image
53
+ <HeaderIcon
53
54
  style={[
54
55
  styles.icon,
55
- direction === 'rtl' && styles.flip,
56
56
  displayMode !== 'minimal' && styles.iconWithLabel,
57
57
  Boolean(tintColor) && { tintColor },
58
58
  ]}
59
- resizeMode="contain"
60
59
  source={backIcon}
61
- fadeDuration={0}
62
60
  />
63
61
  );
64
62
  }
@@ -72,7 +70,7 @@ export function HeaderBackButton({
72
70
  const availableSpace =
73
71
  titleLayout && screenLayout
74
72
  ? (screenLayout.width - titleLayout.width) / 2 -
75
- (ICON_WIDTH + ICON_MARGIN_START)
73
+ (ICON_WIDTH + ICON_MARGIN)
76
74
  : null;
77
75
 
78
76
  const potentialLabelText =
@@ -192,10 +190,7 @@ export function HeaderBackButton({
192
190
  }
193
191
 
194
192
  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
193
  const ICON_MARGIN_END = Platform.OS === 'ios' ? 22 : 3;
198
- const ICON_MARGIN_VERTICAL = Platform.OS === 'ios' ? 8 : 3;
199
194
 
200
195
  const styles = StyleSheet.create({
201
196
  container: {
@@ -220,17 +215,11 @@ const styles = StyleSheet.create({
220
215
  // Otherwise it messes with the measurement of the label
221
216
  flexDirection: 'row',
222
217
  alignItems: 'flex-start',
223
- ...Platform.select({
224
- ios: { marginEnd: 8 },
225
- default: { marginEnd: 3 },
226
- }),
218
+ marginEnd: ICON_MARGIN,
227
219
  },
228
220
  icon: {
229
- height: ICON_HEIGHT,
230
221
  width: ICON_WIDTH,
231
- marginStart: ICON_MARGIN_START,
232
222
  marginEnd: ICON_MARGIN_END,
233
- marginVertical: ICON_MARGIN_VERTICAL,
234
223
  },
235
224
  iconWithLabel:
236
225
  Platform.OS === 'ios'
@@ -0,0 +1,36 @@
1
+ import { useLocale, useTheme } from '@react-navigation/native';
2
+ import { Image, type ImageProps, Platform, StyleSheet } from 'react-native';
3
+
4
+ export function HeaderIcon({ source, style, ...rest }: ImageProps) {
5
+ const { colors } = useTheme();
6
+ const { direction } = useLocale();
7
+
8
+ return (
9
+ <Image
10
+ source={source}
11
+ resizeMode="contain"
12
+ fadeDuration={0}
13
+ style={[
14
+ styles.icon,
15
+ direction === 'rtl' && styles.flip,
16
+ { tintColor: colors.text },
17
+ style,
18
+ ]}
19
+ {...rest}
20
+ />
21
+ );
22
+ }
23
+
24
+ export const ICON_SIZE = Platform.OS === 'ios' ? 21 : 24;
25
+ export const ICON_MARGIN = Platform.OS === 'ios' ? 8 : 3;
26
+
27
+ const styles = StyleSheet.create({
28
+ icon: {
29
+ width: ICON_SIZE,
30
+ height: ICON_SIZE,
31
+ margin: ICON_MARGIN,
32
+ },
33
+ flip: {
34
+ transform: 'scaleX(-1)',
35
+ },
36
+ });
@@ -0,0 +1,283 @@
1
+ import { useNavigation, useTheme } from '@react-navigation/native';
2
+ import Color from 'color';
3
+ import * as React from 'react';
4
+ import {
5
+ Animated,
6
+ Image,
7
+ Platform,
8
+ type StyleProp,
9
+ StyleSheet,
10
+ TextInput,
11
+ View,
12
+ type ViewStyle,
13
+ } from 'react-native';
14
+
15
+ import clearIcon from '../assets/clear-icon.png';
16
+ import closeIcon from '../assets/close-icon.png';
17
+ import searchIcon from '../assets/search-icon.png';
18
+ import { PlatformPressable } from '../PlatformPressable';
19
+ import { Text } from '../Text';
20
+ import type { HeaderOptions } from '../types';
21
+ import { HeaderButton } from './HeaderButton';
22
+ import { HeaderIcon } from './HeaderIcon';
23
+
24
+ type Props = HeaderOptions['headerSearchBarOptions'] & {
25
+ visible: boolean;
26
+ onClose: () => void;
27
+ style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
28
+ };
29
+
30
+ const INPUT_TYPE_TO_MODE = {
31
+ text: 'text',
32
+ number: 'numeric',
33
+ phone: 'tel',
34
+ email: 'email',
35
+ } as const;
36
+
37
+ export function HeaderSearchBar({
38
+ visible,
39
+ inputType,
40
+ autoFocus = true,
41
+ placeholder = 'Search',
42
+ cancelButtonText = 'Cancel',
43
+ onChangeText,
44
+ onClose,
45
+ style,
46
+ ...rest
47
+ }: Props) {
48
+ const navigation = useNavigation();
49
+ const { dark, colors, fonts } = useTheme();
50
+ const [value, setValue] = React.useState('');
51
+ const [rendered, setRendered] = React.useState(visible);
52
+ const [visibleAnim] = React.useState(
53
+ () => new Animated.Value(visible ? 1 : 0)
54
+ );
55
+ const [clearVisibleAnim] = React.useState(() => new Animated.Value(0));
56
+
57
+ const visibleValueRef = React.useRef(visible);
58
+ const clearVisibleValueRef = React.useRef(false);
59
+ const inputRef = React.useRef<TextInput>(null);
60
+
61
+ React.useEffect(() => {
62
+ // Avoid act warning in tests just by rendering header
63
+ if (visible === visibleValueRef.current) {
64
+ return;
65
+ }
66
+
67
+ Animated.timing(visibleAnim, {
68
+ toValue: visible ? 1 : 0,
69
+ duration: 100,
70
+ useNativeDriver: true,
71
+ }).start(({ finished }) => {
72
+ if (finished) {
73
+ setRendered(visible);
74
+ visibleValueRef.current = visible;
75
+ }
76
+ });
77
+
78
+ return () => {
79
+ visibleAnim.stopAnimation();
80
+ };
81
+ }, [visible, visibleAnim]);
82
+
83
+ const hasText = value !== '';
84
+
85
+ React.useEffect(() => {
86
+ if (clearVisibleValueRef.current === hasText) {
87
+ return;
88
+ }
89
+
90
+ Animated.timing(clearVisibleAnim, {
91
+ toValue: hasText ? 1 : 0,
92
+ duration: 100,
93
+ useNativeDriver: true,
94
+ }).start(({ finished }) => {
95
+ if (finished) {
96
+ clearVisibleValueRef.current = hasText;
97
+ }
98
+ });
99
+ }, [clearVisibleAnim, hasText]);
100
+
101
+ const onClear = React.useCallback(() => {
102
+ inputRef.current?.clear();
103
+ inputRef.current?.focus();
104
+ setValue('');
105
+ // FIXME: figure out how to create a SyntheticEvent
106
+ // @ts-expect-error: we don't have the native event here
107
+ onChangeText?.({ nativeEvent: { text: '' } });
108
+ }, [onChangeText]);
109
+
110
+ React.useEffect(
111
+ () =>
112
+ navigation?.addListener('blur', () => {
113
+ onClear();
114
+ onClose();
115
+ }),
116
+ [navigation, onClear, onClose]
117
+ );
118
+
119
+ if (!visible && !rendered) {
120
+ return null;
121
+ }
122
+
123
+ return (
124
+ <Animated.View
125
+ pointerEvents={visible ? 'auto' : 'none'}
126
+ accessibilityLiveRegion="polite"
127
+ accessibilityElementsHidden={!visible}
128
+ importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}
129
+ style={[styles.container, { opacity: visibleAnim }, style]}
130
+ >
131
+ <View style={styles.searchbarContainer}>
132
+ <HeaderIcon source={searchIcon} style={styles.inputSearchIcon} />
133
+ <TextInput
134
+ {...rest}
135
+ ref={inputRef}
136
+ onChange={onChangeText}
137
+ onChangeText={setValue}
138
+ autoFocus={autoFocus}
139
+ inputMode={INPUT_TYPE_TO_MODE[inputType ?? 'text']}
140
+ placeholder={placeholder}
141
+ placeholderTextColor={Color(colors.text).alpha(0.5).string()}
142
+ cursorColor={colors.primary}
143
+ selectionHandleColor={colors.primary}
144
+ selectionColor={Color(colors.primary).alpha(0.3).string()}
145
+ style={[
146
+ fonts.regular,
147
+ styles.searchbar,
148
+ {
149
+ backgroundColor: Platform.select({
150
+ ios: dark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
151
+ default: 'transparent',
152
+ }),
153
+ color: colors.text,
154
+ borderBottomColor: colors.border,
155
+ },
156
+ ]}
157
+ />
158
+ {Platform.OS === 'ios' ? (
159
+ <PlatformPressable
160
+ onPress={onClear}
161
+ style={[
162
+ {
163
+ opacity: clearVisibleAnim,
164
+ transform: [{ scale: clearVisibleAnim }],
165
+ },
166
+ styles.clearButton,
167
+ ]}
168
+ >
169
+ <Image
170
+ source={clearIcon}
171
+ resizeMode="contain"
172
+ style={[styles.clearIcon, { tintColor: colors.text }]}
173
+ />
174
+ </PlatformPressable>
175
+ ) : null}
176
+ </View>
177
+ {Platform.OS !== 'ios' ? (
178
+ <HeaderButton
179
+ onPress={() => {
180
+ if (value) {
181
+ onClear();
182
+ } else {
183
+ onClose();
184
+ }
185
+ }}
186
+ style={styles.closeButton}
187
+ >
188
+ <HeaderIcon source={closeIcon} />
189
+ </HeaderButton>
190
+ ) : null}
191
+ {Platform.OS === 'ios' ? (
192
+ <PlatformPressable
193
+ onPress={() => {
194
+ onClear();
195
+ onClose();
196
+ }}
197
+ style={styles.cancelButton}
198
+ >
199
+ <Text
200
+ style={[
201
+ fonts.regular,
202
+ { color: colors.primary },
203
+ styles.cancelText,
204
+ ]}
205
+ >
206
+ {cancelButtonText}
207
+ </Text>
208
+ </PlatformPressable>
209
+ ) : null}
210
+ </Animated.View>
211
+ );
212
+ }
213
+
214
+ const styles = StyleSheet.create({
215
+ container: {
216
+ flex: 1,
217
+ flexDirection: 'row',
218
+ alignItems: 'stretch',
219
+ },
220
+ inputSearchIcon: {
221
+ position: 'absolute',
222
+ opacity: 0.5,
223
+ left: Platform.select({ ios: 16, default: 4 }),
224
+ top: Platform.select({ ios: -1, default: 17 }),
225
+ ...Platform.select({
226
+ ios: {
227
+ height: 18,
228
+ width: 18,
229
+ },
230
+ default: {},
231
+ }),
232
+ },
233
+ closeButton: {
234
+ position: 'absolute',
235
+ opacity: 0.5,
236
+ right: Platform.select({ ios: 0, default: 8 }),
237
+ top: Platform.select({ ios: -2, default: 17 }),
238
+ },
239
+ clearButton: {
240
+ position: 'absolute',
241
+ right: 0,
242
+ top: -7,
243
+ bottom: 0,
244
+ justifyContent: 'center',
245
+ padding: 8,
246
+ },
247
+ clearIcon: {
248
+ height: 16,
249
+ width: 16,
250
+ opacity: 0.5,
251
+ },
252
+ cancelButton: {
253
+ alignSelf: 'center',
254
+ top: -4,
255
+ },
256
+ cancelText: {
257
+ fontSize: 17,
258
+ marginHorizontal: 12,
259
+ },
260
+ searchbarContainer: {
261
+ flex: 1,
262
+ },
263
+ searchbar: Platform.select({
264
+ ios: {
265
+ flex: 1,
266
+ fontSize: 17,
267
+ paddingHorizontal: 32,
268
+ marginLeft: 16,
269
+ marginTop: -2,
270
+ marginBottom: 5,
271
+ borderRadius: 8,
272
+ },
273
+ default: {
274
+ flex: 1,
275
+ fontSize: 18,
276
+ paddingHorizontal: 36,
277
+ marginRight: 8,
278
+ marginTop: 8,
279
+ marginBottom: 8,
280
+ borderBottomWidth: 1,
281
+ },
282
+ }),
283
+ });
@@ -118,7 +118,12 @@ export function PlatformPressable({
118
118
  }
119
119
  style={[
120
120
  {
121
- cursor: 'pointer', // Add hover effect on iPad and VisionOS
121
+ cursor:
122
+ Platform.OS === 'web' || Platform.OS === 'ios'
123
+ ? // Pointer cursor on web
124
+ // Hover effect on iPad and visionOS
125
+ 'pointer'
126
+ : 'auto',
122
127
  opacity: !ANDROID_SUPPORTS_RIPPLE ? opacity : 1,
123
128
  },
124
129
  style,