@react-navigation/elements 3.0.0-alpha.5 → 3.0.0-alpha.51

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 (213) hide show
  1. package/lib/module/ActivityView.js +90 -0
  2. package/lib/module/ActivityView.js.map +1 -0
  3. package/lib/module/ActivityView.native.js +67 -0
  4. package/lib/module/ActivityView.native.js.map +1 -0
  5. package/lib/module/Badge.js +6 -7
  6. package/lib/module/Badge.js.map +1 -1
  7. package/lib/module/BlurEffectBackground.js +3 -3
  8. package/lib/module/BlurEffectBackground.js.map +1 -1
  9. package/lib/module/Button.js +99 -14
  10. package/lib/module/Button.js.map +1 -1
  11. package/lib/module/Color.js +565 -4
  12. package/lib/module/Color.js.map +1 -1
  13. package/lib/module/Container.js +23 -1
  14. package/lib/module/Container.js.map +1 -1
  15. package/lib/module/Header/Header.js +30 -18
  16. package/lib/module/Header/Header.js.map +1 -1
  17. package/lib/module/Header/HeaderBackButton.js +27 -13
  18. package/lib/module/Header/HeaderBackButton.js.map +1 -1
  19. package/lib/module/Header/HeaderButton.js +4 -5
  20. package/lib/module/Header/HeaderButton.js.map +1 -1
  21. package/lib/module/Header/HeaderButtonBackground.js +1 -2
  22. package/lib/module/Header/HeaderButtonBackground.js.map +1 -1
  23. package/lib/module/Header/HeaderIcon.js +14 -17
  24. package/lib/module/Header/HeaderIcon.js.map +1 -1
  25. package/lib/module/Header/HeaderSearchBar.js +36 -26
  26. package/lib/module/Header/HeaderSearchBar.js.map +1 -1
  27. package/lib/module/Header/getHeaderTitle.js.map +1 -1
  28. package/lib/module/Header/useHeaderHeight.js +1 -1
  29. package/lib/module/Header/useHeaderHeight.js.map +1 -1
  30. package/lib/module/Label/getLabel.js.map +1 -1
  31. package/lib/module/LiquidGlassView.ios.js +1 -2
  32. package/lib/module/LiquidGlassView.ios.js.map +1 -1
  33. package/lib/module/LiquidGlassView.js +0 -4
  34. package/lib/module/LiquidGlassView.js.map +1 -1
  35. package/lib/module/PlatformColor.js +1 -0
  36. package/lib/module/PlatformColor.js.map +1 -1
  37. package/lib/module/PlatformColor.native.js +1 -1
  38. package/lib/module/PlatformColor.native.js.map +1 -1
  39. package/lib/module/PlatformIcon.js +46 -0
  40. package/lib/module/PlatformIcon.js.map +1 -0
  41. package/lib/module/PlatformPressable.js +64 -21
  42. package/lib/module/PlatformPressable.js.map +1 -1
  43. package/lib/module/SafeAreaProviderCompat.js +2 -4
  44. package/lib/module/SafeAreaProviderCompat.js.map +1 -1
  45. package/lib/module/Screen.js +59 -20
  46. package/lib/module/Screen.js.map +1 -1
  47. package/lib/module/index.js +2 -7
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/internal.js +1 -1
  50. package/lib/module/internal.js.map +1 -1
  51. package/lib/module/useFrameSize.js +55 -5
  52. package/lib/module/useFrameSize.js.map +1 -1
  53. package/lib/typescript/src/ActivityView.d.ts +31 -0
  54. package/lib/typescript/src/ActivityView.d.ts.map +1 -0
  55. package/lib/typescript/src/ActivityView.native.d.ts +3 -0
  56. package/lib/typescript/src/ActivityView.native.d.ts.map +1 -0
  57. package/lib/typescript/src/Badge.d.ts +7 -6
  58. package/lib/typescript/src/Badge.d.ts.map +1 -1
  59. package/lib/typescript/src/BlurEffectBackground.d.ts +2 -2
  60. package/lib/typescript/src/BlurEffectBackground.d.ts.map +1 -1
  61. package/lib/typescript/src/Button.d.ts +54 -6
  62. package/lib/typescript/src/Button.d.ts.map +1 -1
  63. package/lib/typescript/src/Color.d.ts +4 -4
  64. package/lib/typescript/src/Color.d.ts.map +1 -1
  65. package/lib/typescript/src/Container.d.ts +8 -4
  66. package/lib/typescript/src/Container.d.ts.map +1 -1
  67. package/lib/typescript/src/Header/Header.d.ts +5 -4
  68. package/lib/typescript/src/Header/Header.d.ts.map +1 -1
  69. package/lib/typescript/src/Header/HeaderBackButton.d.ts +3 -2
  70. package/lib/typescript/src/Header/HeaderBackButton.d.ts.map +1 -1
  71. package/lib/typescript/src/Header/HeaderBackground.d.ts +5 -5
  72. package/lib/typescript/src/Header/HeaderBackground.d.ts.map +1 -1
  73. package/lib/typescript/src/Header/HeaderButton.d.ts +7 -2
  74. package/lib/typescript/src/Header/HeaderButton.d.ts.map +1 -1
  75. package/lib/typescript/src/Header/HeaderButtonBackground.d.ts +1 -1
  76. package/lib/typescript/src/Header/HeaderButtonBackground.d.ts.map +1 -1
  77. package/lib/typescript/src/Header/HeaderIcon.d.ts +10 -4
  78. package/lib/typescript/src/Header/HeaderIcon.d.ts.map +1 -1
  79. package/lib/typescript/src/Header/HeaderSearchBar.d.ts +10 -7
  80. package/lib/typescript/src/Header/HeaderSearchBar.d.ts.map +1 -1
  81. package/lib/typescript/src/Header/HeaderTitle.d.ts +4 -4
  82. package/lib/typescript/src/Header/HeaderTitle.d.ts.map +1 -1
  83. package/lib/typescript/src/Header/getHeaderTitle.d.ts +2 -2
  84. package/lib/typescript/src/Header/getHeaderTitle.d.ts.map +1 -1
  85. package/lib/typescript/src/Label/Label.d.ts +1 -1
  86. package/lib/typescript/src/Label/Label.d.ts.map +1 -1
  87. package/lib/typescript/src/Label/getLabel.d.ts +2 -2
  88. package/lib/typescript/src/Label/getLabel.d.ts.map +1 -1
  89. package/lib/typescript/src/LiquidGlassView.d.ts +7 -5
  90. package/lib/typescript/src/LiquidGlassView.d.ts.map +1 -1
  91. package/lib/typescript/src/LiquidGlassView.ios.d.ts +4 -2
  92. package/lib/typescript/src/LiquidGlassView.ios.d.ts.map +1 -1
  93. package/lib/typescript/src/MissingIcon.d.ts +3 -3
  94. package/lib/typescript/src/MissingIcon.d.ts.map +1 -1
  95. package/lib/typescript/src/PlatformColor.d.ts +1 -0
  96. package/lib/typescript/src/PlatformColor.d.ts.map +1 -1
  97. package/lib/typescript/src/PlatformColor.native.d.ts +1 -1
  98. package/lib/typescript/src/PlatformColor.native.d.ts.map +1 -1
  99. package/lib/typescript/src/PlatformIcon.d.ts +11 -0
  100. package/lib/typescript/src/PlatformIcon.d.ts.map +1 -0
  101. package/lib/typescript/src/PlatformPressable.d.ts +12 -15
  102. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  103. package/lib/typescript/src/SafeAreaProviderCompat.d.ts +1 -1
  104. package/lib/typescript/src/SafeAreaProviderCompat.d.ts.map +1 -1
  105. package/lib/typescript/src/Screen.d.ts +10 -7
  106. package/lib/typescript/src/Screen.d.ts.map +1 -1
  107. package/lib/typescript/src/Text.d.ts +1 -1
  108. package/lib/typescript/src/Text.d.ts.map +1 -1
  109. package/lib/typescript/src/index.d.ts +21 -21
  110. package/lib/typescript/src/index.d.ts.map +1 -1
  111. package/lib/typescript/src/internal.d.ts +6 -6
  112. package/lib/typescript/src/internal.d.ts.map +1 -1
  113. package/lib/typescript/src/types.d.ts +120 -70
  114. package/lib/typescript/src/types.d.ts.map +1 -1
  115. package/lib/typescript/src/useFrameSize.d.ts +2 -2
  116. package/lib/typescript/src/useFrameSize.d.ts.map +1 -1
  117. package/package.json +27 -22
  118. package/src/ActivityView.native.tsx +88 -0
  119. package/src/ActivityView.tsx +127 -0
  120. package/src/Badge.tsx +11 -10
  121. package/src/BlurEffectBackground.tsx +3 -3
  122. package/src/Button.tsx +177 -28
  123. package/src/Color.tsx +755 -8
  124. package/src/Container.tsx +27 -4
  125. package/src/Header/Header.tsx +47 -27
  126. package/src/Header/HeaderBackButton.tsx +34 -20
  127. package/src/Header/HeaderBackground.tsx +3 -3
  128. package/src/Header/HeaderButton.tsx +16 -18
  129. package/src/Header/HeaderButtonBackground.tsx +1 -4
  130. package/src/Header/HeaderIcon.tsx +26 -13
  131. package/src/Header/HeaderSearchBar.tsx +58 -49
  132. package/src/Header/HeaderTitle.tsx +3 -3
  133. package/src/Header/getHeaderTitle.tsx +4 -1
  134. package/src/Header/useHeaderHeight.tsx +1 -1
  135. package/src/Label/getLabel.tsx +1 -1
  136. package/src/LiquidGlassView.ios.tsx +14 -9
  137. package/src/LiquidGlassView.tsx +15 -11
  138. package/src/MissingIcon.tsx +2 -2
  139. package/src/PlatformColor.native.tsx +1 -1
  140. package/src/PlatformColor.tsx +4 -0
  141. package/src/PlatformIcon.tsx +70 -0
  142. package/src/PlatformPressable.tsx +108 -48
  143. package/src/SafeAreaProviderCompat.tsx +2 -3
  144. package/src/Screen.tsx +87 -29
  145. package/src/index.tsx +2 -16
  146. package/src/internal.tsx +1 -1
  147. package/src/types.tsx +153 -76
  148. package/src/useFrameSize.tsx +71 -7
  149. package/lib/module/Lazy.js +0 -42
  150. package/lib/module/Lazy.js.map +0 -1
  151. package/lib/module/assets/back-icon.ios.svg +0 -4
  152. package/lib/module/assets/back-icon@1x.android.png +0 -0
  153. package/lib/module/assets/back-icon@1x.ios.png +0 -0
  154. package/lib/module/assets/back-icon@2x.android.png +0 -0
  155. package/lib/module/assets/back-icon@2x.ios.png +0 -0
  156. package/lib/module/assets/back-icon@3x.android.png +0 -0
  157. package/lib/module/assets/back-icon@3x.ios.png +0 -0
  158. package/lib/module/assets/back-icon@4x.android.png +0 -0
  159. package/lib/module/assets/back-icon@4x.ios.png +0 -0
  160. package/lib/module/assets/clear-icon@1x.png +0 -0
  161. package/lib/module/assets/clear-icon@2x.png +0 -0
  162. package/lib/module/assets/clear-icon@3x.png +0 -0
  163. package/lib/module/assets/clear-icon@4x.png +0 -0
  164. package/lib/module/assets/close-icon@1x.png +0 -0
  165. package/lib/module/assets/close-icon@2x.png +0 -0
  166. package/lib/module/assets/close-icon@3x.png +0 -0
  167. package/lib/module/assets/close-icon@4x.png +0 -0
  168. package/lib/module/assets/search-icon-legacy@1x.ios.png +0 -0
  169. package/lib/module/assets/search-icon-legacy@2x.ios.png +0 -0
  170. package/lib/module/assets/search-icon-legacy@3x.ios.png +0 -0
  171. package/lib/module/assets/search-icon-legacy@4x.ios.png +0 -0
  172. package/lib/module/assets/search-icon.ios.svg +0 -4
  173. package/lib/module/assets/search-icon@1x.android.png +0 -0
  174. package/lib/module/assets/search-icon@1x.ios.png +0 -0
  175. package/lib/module/assets/search-icon@2x.android.png +0 -0
  176. package/lib/module/assets/search-icon@2x.ios.png +0 -0
  177. package/lib/module/assets/search-icon@3x.android.png +0 -0
  178. package/lib/module/assets/search-icon@3x.ios.png +0 -0
  179. package/lib/module/assets/search-icon@4x.android.png +0 -0
  180. package/lib/module/assets/search-icon@4x.ios.png +0 -0
  181. package/lib/typescript/src/Lazy.d.ts +0 -31
  182. package/lib/typescript/src/Lazy.d.ts.map +0 -1
  183. package/src/Lazy.tsx +0 -59
  184. package/src/assets/back-icon.ios.svg +0 -4
  185. package/src/assets/back-icon@1x.android.png +0 -0
  186. package/src/assets/back-icon@1x.ios.png +0 -0
  187. package/src/assets/back-icon@2x.android.png +0 -0
  188. package/src/assets/back-icon@2x.ios.png +0 -0
  189. package/src/assets/back-icon@3x.android.png +0 -0
  190. package/src/assets/back-icon@3x.ios.png +0 -0
  191. package/src/assets/back-icon@4x.android.png +0 -0
  192. package/src/assets/back-icon@4x.ios.png +0 -0
  193. package/src/assets/clear-icon@1x.png +0 -0
  194. package/src/assets/clear-icon@2x.png +0 -0
  195. package/src/assets/clear-icon@3x.png +0 -0
  196. package/src/assets/clear-icon@4x.png +0 -0
  197. package/src/assets/close-icon@1x.png +0 -0
  198. package/src/assets/close-icon@2x.png +0 -0
  199. package/src/assets/close-icon@3x.png +0 -0
  200. package/src/assets/close-icon@4x.png +0 -0
  201. package/src/assets/search-icon-legacy@1x.ios.png +0 -0
  202. package/src/assets/search-icon-legacy@2x.ios.png +0 -0
  203. package/src/assets/search-icon-legacy@3x.ios.png +0 -0
  204. package/src/assets/search-icon-legacy@4x.ios.png +0 -0
  205. package/src/assets/search-icon.ios.svg +0 -4
  206. package/src/assets/search-icon@1x.android.png +0 -0
  207. package/src/assets/search-icon@1x.ios.png +0 -0
  208. package/src/assets/search-icon@2x.android.png +0 -0
  209. package/src/assets/search-icon@2x.ios.png +0 -0
  210. package/src/assets/search-icon@3x.android.png +0 -0
  211. package/src/assets/search-icon@3x.ios.png +0 -0
  212. package/src/assets/search-icon@4x.android.png +0 -0
  213. package/src/assets/search-icon@4x.ios.png +0 -0
@@ -0,0 +1,127 @@
1
+ import { Activity, useCallback, useEffect, useState } from 'react';
2
+ import { Platform, View, type ViewStyle } from 'react-native';
3
+
4
+ import { Container } from './Container';
5
+
6
+ export type Props = {
7
+ /**
8
+ * Mode of the activity view
9
+ * - `normal`: The view renders normally
10
+ * - `inert`: Content is not interactive
11
+ * - `paused`: Effects are unmounted and content is not interactive
12
+ */
13
+ mode: 'normal' | 'inert' | 'paused';
14
+ /**
15
+ * Whether the content is visible or not
16
+ */
17
+ visible: boolean;
18
+ /**
19
+ * Delay before pausing effects.
20
+ * So pending animations have time to finish.
21
+ *
22
+ * Defaults to 500ms.
23
+ */
24
+ delay?: number | undefined;
25
+ /**
26
+ * The style for the container view
27
+ */
28
+ style?: Omit<React.CSSProperties & ViewStyle, 'display'> | undefined;
29
+ /**
30
+ * The content of the activity view
31
+ */
32
+ children: React.ReactNode;
33
+ };
34
+
35
+ export function ActivityView({
36
+ mode,
37
+ visible,
38
+ delay = 500,
39
+ style,
40
+ children,
41
+ }: Props) {
42
+ const [delayedPaused, setDelayedPaused] = useState(mode === 'paused');
43
+
44
+ useEffect(() => {
45
+ if (!delay) {
46
+ return;
47
+ }
48
+
49
+ const timer = setTimeout(() => {
50
+ setDelayedPaused(mode === 'paused');
51
+ }, delay);
52
+
53
+ return () => clearTimeout(timer);
54
+ }, [delay, mode]);
55
+
56
+ const display = visible ? 'flex' : 'none';
57
+ const activityMode =
58
+ mode !== 'paused' || (delay && !delayedPaused) ? 'visible' : 'hidden';
59
+
60
+ /**
61
+ * Activity has 2 modes, visible and hidden - hidden unmounts effects
62
+ * But what we want is to unmount effects, without hiding content
63
+ * So we use hidden mode, but unset display: none to make content visible
64
+ */
65
+ const onRef = useCallback(
66
+ (node: HTMLDivElement | React.ComponentRef<typeof View> | null) => {
67
+ if (Platform.OS !== 'web' || !(node && node instanceof HTMLElement)) {
68
+ return;
69
+ }
70
+
71
+ const observers: MutationObserver[] = [];
72
+
73
+ const observe = () => {
74
+ // Remove previous observers
75
+ observers.forEach((o) => o.disconnect());
76
+ observers.length = 0;
77
+
78
+ const children = node.childNodes;
79
+
80
+ // When the style attribute for children is updated by React
81
+ // We observe it and update display to make content visible
82
+ children.forEach((child) => {
83
+ if (child instanceof HTMLElement) {
84
+ child.style.display = display;
85
+
86
+ const o = new MutationObserver(() => {
87
+ child.style.display = display;
88
+ });
89
+
90
+ o.observe(child, {
91
+ attributes: true,
92
+ attributeFilter: ['style'],
93
+ });
94
+
95
+ observers.push(o);
96
+ }
97
+ });
98
+ };
99
+
100
+ observe();
101
+
102
+ // React removes refs when `Activity` is hidden
103
+ // So we render outside of the `Activity` and observer child list
104
+ const observer = new MutationObserver(observe);
105
+
106
+ observer.observe(node, {
107
+ childList: true,
108
+ });
109
+
110
+ return () => {
111
+ observer.disconnect();
112
+ observers.forEach((o) => o.disconnect());
113
+ };
114
+ },
115
+ [display]
116
+ );
117
+
118
+ return (
119
+ <Container ref={onRef} style={{ display: 'contents' }}>
120
+ <Activity mode={activityMode}>
121
+ <Container inert={mode !== 'normal'} style={{ ...style, display }}>
122
+ {children}
123
+ </Container>
124
+ </Activity>
125
+ </Container>
126
+ );
127
+ }
package/src/Badge.tsx CHANGED
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  import { Color } from './Color';
13
13
 
14
- type Props = TextProps & {
14
+ type Props = Omit<TextProps, 'style'> & {
15
15
  /**
16
16
  * Whether the badge is visible
17
17
  */
@@ -19,15 +19,15 @@ type Props = TextProps & {
19
19
  /**
20
20
  * Content of the `Badge`.
21
21
  */
22
- children?: string | number;
22
+ children?: string | number | undefined;
23
23
  /**
24
24
  * Size of the `Badge`.
25
25
  */
26
- size?: number;
26
+ size?: number | undefined;
27
27
  /**
28
28
  * Style object for the tab bar container.
29
29
  */
30
- style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
30
+ style?: StyleProp<TextStyle> | undefined;
31
31
  };
32
32
 
33
33
  const useNativeDriver = Platform.OS !== 'web';
@@ -49,17 +49,19 @@ export function Badge({
49
49
  return;
50
50
  }
51
51
 
52
- Animated.timing(opacity, {
52
+ const animation = Animated.timing(opacity, {
53
53
  toValue: visible ? 1 : 0,
54
54
  duration: 150,
55
55
  useNativeDriver,
56
- }).start(({ finished }) => {
56
+ });
57
+
58
+ animation.start(({ finished }) => {
57
59
  if (finished && !visible) {
58
60
  setRendered(false);
59
61
  }
60
62
  });
61
63
 
62
- return () => opacity.stopAnimation();
64
+ return () => animation.stop();
63
65
  }, [opacity, rendered, visible]);
64
66
 
65
67
  if (!rendered) {
@@ -70,10 +72,9 @@ export function Badge({
70
72
  }
71
73
  }
72
74
 
73
- // @ts-expect-error: backgroundColor definitely exists
74
75
  const { backgroundColor = colors.notification, ...restStyle } =
75
- StyleSheet.flatten(style) || {};
76
- const textColor = Color(backgroundColor)?.isLight() ? 'black' : 'white';
76
+ StyleSheet.flatten(style) ?? {};
77
+ const textColor = Color.foreground(backgroundColor);
77
78
 
78
79
  const borderRadius = size / 2;
79
80
  const fontSize = Math.floor((size * 3) / 4);
@@ -31,7 +31,8 @@ export function BlurEffectBackground({
31
31
  children,
32
32
  ...rest
33
33
  }: Props) {
34
- let containerStyle, blurStyle, blurBackground, colorBackground;
34
+ let blurStyle: (ViewStyle & React.CSSProperties) | undefined;
35
+ let containerStyle, blurBackground, colorBackground;
35
36
 
36
37
  if (Platform.OS === 'web' && blurEffect && blurEffect !== 'none') {
37
38
  const blurBackgroundColor = getBlurBackgroundColor(blurEffect);
@@ -40,9 +41,8 @@ export function BlurEffectBackground({
40
41
  const backdropFilter = `saturate(180%) blur(30px)`;
41
42
 
42
43
  blurStyle = {
43
- // @ts-expect-error backdropFilter is web-only
44
44
  backdropFilter,
45
- webkitBackdropFilter: backdropFilter,
45
+ WebkitBackdropFilter: backdropFilter,
46
46
  };
47
47
 
48
48
  blurBackground = (
package/src/Button.tsx CHANGED
@@ -1,71 +1,156 @@
1
1
  import {
2
2
  type LinkProps,
3
+ type ParamListBase,
3
4
  type RootParamList,
4
5
  useLinkProps,
5
6
  useTheme,
6
7
  } from '@react-navigation/native';
7
8
  import * as React from 'react';
8
- import { type ColorValue, Platform, StyleSheet } from 'react-native';
9
+ import {
10
+ ActivityIndicator,
11
+ type ColorValue,
12
+ Platform,
13
+ StyleSheet,
14
+ View,
15
+ } from 'react-native';
9
16
 
10
17
  import { Color } from './Color';
18
+ import { PlatformIcon } from './PlatformIcon';
11
19
  import {
12
20
  PlatformPressable,
13
21
  type Props as PlatformPressableProps,
14
22
  } from './PlatformPressable';
15
23
  import { Text } from './Text';
24
+ import type { Icon } from './types';
16
25
 
17
26
  type ButtonBaseProps = Omit<PlatformPressableProps, 'children'> & {
18
- variant?: 'plain' | 'tinted' | 'filled';
19
- color?: ColorValue;
20
- children: string | string[];
27
+ /**
28
+ * Variant of the button.
29
+ *
30
+ * - `plain`: transparent background, colored text
31
+ * - `tinted`: lightly tinted background, colored text
32
+ * - `filled`: colored background, contrasting text color
33
+ *
34
+ * @default 'tinted'
35
+ */
36
+ variant?: 'plain' | 'tinted' | 'filled' | undefined;
37
+ /**
38
+ * Custom color for the button.
39
+ * Changes the text color for `plain` and `tinted` variants,
40
+ * and background color for `filled` variant.
41
+ */
42
+ color?: ColorValue | undefined;
43
+ /**
44
+ * Icon to display before the label.
45
+ *
46
+ * Supported types:
47
+ * - image: custom image source
48
+ * - sfSymbol: SF Symbol icon (iOS only)
49
+ * - materialSymbol: material symbol icon (Android only)
50
+ * - React Node: function that returns a React Element
51
+ */
52
+ icon?:
53
+ | Icon
54
+ | ((props: { color: ColorValue; size: number }) => Icon | React.ReactNode)
55
+ | undefined;
56
+ /**
57
+ * Whether to show a loading indicator instead of the icon.
58
+ */
59
+ loading?: boolean | undefined;
60
+ /**
61
+ * Label text to display inside the button.
62
+ */
63
+ children: string | string[] | React.ReactElement | React.ReactElement[];
21
64
  };
22
65
 
23
66
  type ButtonLinkProps<
24
67
  ParamList extends {} = RootParamList,
25
- RouteName extends keyof ParamList = keyof ParamList,
26
- > = LinkProps<ParamList, RouteName> & Omit<ButtonBaseProps, 'onPress'>;
68
+ RouteName extends Extract<keyof ParamList, string> = Extract<
69
+ keyof ParamList,
70
+ string
71
+ >,
72
+ > = LinkProps<NoInfer<ParamList>, RouteName> & ButtonBaseProps;
27
73
 
28
74
  const BUTTON_RADIUS = 40;
75
+ const ICON_SIZE = 14;
29
76
 
77
+ /**
78
+ * Component to render a button that navigates to a screen using a path.
79
+ * Uses an anchor tag on the web.
80
+ *
81
+ * @param props.in Name of the current or parent screen whose navigator contains the target screen.
82
+ * @param props.screen Name of the screen to navigate to (e.g. `'Feeds'`).
83
+ * @param props.params Params to pass to the screen to navigate to (e.g. `{ sort: 'hot' }`).
84
+ * @param props.href Optional absolute path to use for the href (e.g. `/feeds/hot`).
85
+ * @param props.action Optional action to override the in-page navigation. The `href` is still derived from `screen`, so this can be used to render a link while dispatching a different action (e.g. a `replace`).
86
+ */
30
87
  export function Button<
31
88
  ParamList extends {} = RootParamList,
32
- RouteName extends keyof ParamList = keyof ParamList,
89
+ RouteName extends Extract<keyof ParamList, string> = Extract<
90
+ keyof ParamList,
91
+ string
92
+ >,
33
93
  >(props: ButtonLinkProps<ParamList, RouteName>): React.JSX.Element;
34
94
 
95
+ /**
96
+ * Component to render a button.
97
+ */
35
98
  export function Button(props: ButtonBaseProps): React.JSX.Element;
36
99
 
37
- export function Button<
38
- ParamList extends {} = RootParamList,
39
- RouteName extends keyof ParamList = keyof ParamList,
40
- >(props: ButtonBaseProps | ButtonLinkProps<ParamList, RouteName>) {
100
+ export function Button(
101
+ props: ButtonBaseProps | ButtonLinkProps<ParamListBase>
102
+ ) {
41
103
  if ('screen' in props || 'action' in props) {
42
- // @ts-expect-error: This is already type-checked by the prop types
43
104
  return <ButtonLink {...props} />;
44
105
  } else {
45
106
  return <ButtonBase {...props} />;
46
107
  }
47
108
  }
48
109
 
49
- function ButtonLink<
50
- const ParamList extends {} = RootParamList,
51
- const RouteName extends keyof ParamList = keyof ParamList,
52
- >({
110
+ function ButtonLink({
111
+ in: parent,
53
112
  screen,
54
113
  params,
55
114
  action,
56
115
  href,
116
+ onPress,
57
117
  ...rest
58
- }: ButtonLinkProps<ParamList, RouteName>) {
59
- // @ts-expect-error: This is already type-checked by the prop types
60
- const props = useLinkProps({ screen, params, action, href });
118
+ }: ButtonLinkProps<ParamListBase>) {
119
+ // @ts-expect-error: destructuring loses the relationship between target props
120
+ const props = useLinkProps({ in: parent, screen, params, action, href });
121
+
122
+ const [isPending, startTransition] = React.useTransition();
61
123
 
62
- return <ButtonBase {...rest} {...props} />;
124
+ // Avoid flashing the loading indicator when the transition is fast
125
+ const isPendingDeferred = React.useDeferredValue(isPending);
126
+
127
+ return (
128
+ <ButtonBase
129
+ {...rest}
130
+ {...props}
131
+ loading={
132
+ typeof rest.loading === 'boolean'
133
+ ? rest.loading
134
+ : isPending && isPendingDeferred
135
+ }
136
+ onPress={(e) => {
137
+ onPress?.(e);
138
+
139
+ startTransition(() => {
140
+ props.onPress(e);
141
+ });
142
+ }}
143
+ />
144
+ );
63
145
  }
64
146
 
65
147
  function ButtonBase({
66
148
  variant = 'tinted',
67
149
  color: customColor,
150
+ icon,
151
+ loading = false,
68
152
  android_ripple,
153
+ disabled,
69
154
  style,
70
155
  children,
71
156
  ...rest
@@ -90,18 +175,43 @@ function ButtonBase({
90
175
  break;
91
176
  case 'filled':
92
177
  backgroundColor = color;
93
- textColor = Color(color)?.isDark()
94
- ? 'white'
95
- : (Color(color)?.darken(0.71).string() ?? '#fff');
178
+ textColor = Color.foreground(backgroundColor);
96
179
  break;
97
180
  }
98
181
 
182
+ if (disabled || loading) {
183
+ textColor = dark ? 'rgba(235, 235, 245, 0.3)' : 'rgba(60, 60, 67, 0.3)';
184
+
185
+ if (variant !== 'plain') {
186
+ backgroundColor = dark
187
+ ? 'rgba(118, 118, 128, 0.24)'
188
+ : 'rgba(118, 118, 128, 0.12)';
189
+ }
190
+ }
191
+
192
+ const rippleColor =
193
+ Color(textColor)?.fade(0.88).string() ??
194
+ (variant === 'filled' || dark
195
+ ? 'rgba(255, 255, 255, 0.12)'
196
+ : 'rgba(0, 0, 0, 0.12)');
197
+
198
+ const iconNode = loading ? (
199
+ <ActivityIndicator color={textColor} size={ICON_SIZE} />
200
+ ) : (
201
+ renderIcon(icon, textColor, ICON_SIZE)
202
+ );
203
+
99
204
  return (
100
205
  <PlatformPressable
101
206
  {...rest}
207
+ aria-disabled={
208
+ // Keep the button focusable while loading
209
+ disabled || loading || undefined
210
+ }
211
+ disabled={disabled}
102
212
  android_ripple={{
103
- radius: BUTTON_RADIUS,
104
- color: Color(textColor)?.fade(0.85).string() ?? 'rgba(0, 0, 0, 0.1)',
213
+ foreground: true,
214
+ color: rippleColor,
105
215
  ...android_ripple,
106
216
  }}
107
217
  pressOpacity={Platform.OS === 'ios' ? undefined : 1}
@@ -110,19 +220,58 @@ function ButtonBase({
110
220
  }
111
221
  style={[{ backgroundColor }, styles.button, style]}
112
222
  >
113
- <Text style={[{ color: textColor }, fonts.regular, styles.text]}>
114
- {children}
115
- </Text>
223
+ <View style={styles.content}>
224
+ {iconNode && <View style={styles.icon}>{iconNode}</View>}
225
+ <Text style={[{ color: textColor }, fonts.regular, styles.text]}>
226
+ {children}
227
+ </Text>
228
+ </View>
116
229
  </PlatformPressable>
117
230
  );
118
231
  }
119
232
 
233
+ function renderIcon(
234
+ icon: ButtonBaseProps['icon'],
235
+ color: ColorValue,
236
+ size: number
237
+ ) {
238
+ if (!icon) {
239
+ return null;
240
+ }
241
+
242
+ const iconValue = typeof icon === 'function' ? icon({ color, size }) : icon;
243
+
244
+ if (React.isValidElement(iconValue)) {
245
+ return iconValue;
246
+ }
247
+
248
+ if (
249
+ typeof iconValue === 'object' &&
250
+ iconValue != null &&
251
+ 'type' in iconValue
252
+ ) {
253
+ return <PlatformIcon icon={iconValue} size={size} color={color} />;
254
+ }
255
+
256
+ return null;
257
+ }
258
+
120
259
  const styles = StyleSheet.create({
121
260
  button: {
122
261
  paddingHorizontal: 24,
123
262
  paddingVertical: 10,
124
263
  borderRadius: BUTTON_RADIUS,
125
264
  borderCurve: 'continuous',
265
+ overflow: 'hidden',
266
+ },
267
+ content: {
268
+ flexDirection: 'row',
269
+ alignItems: 'center',
270
+ justifyContent: 'center',
271
+ gap: 8,
272
+ },
273
+ icon: {
274
+ marginLeft: -6,
126
275
  },
127
276
  text: {
128
277
  fontSize: 14,