@planningcenter/chat-react-native 1.5.0 → 1.6.0

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 (206) hide show
  1. package/build/components/conversations.d.ts.map +1 -1
  2. package/build/components/conversations.js +29 -8
  3. package/build/components/conversations.js.map +1 -1
  4. package/build/components/display/button.d.ts +71 -0
  5. package/build/components/display/button.d.ts.map +1 -0
  6. package/build/components/display/button.js +136 -0
  7. package/build/components/display/button.js.map +1 -0
  8. package/build/components/display/button_color_utils.d.ts +24 -0
  9. package/build/components/display/button_color_utils.d.ts.map +1 -0
  10. package/build/components/display/button_color_utils.js +43 -0
  11. package/build/components/display/button_color_utils.js.map +1 -0
  12. package/build/components/display/heading.d.ts +4 -0
  13. package/build/components/display/heading.d.ts.map +1 -1
  14. package/build/components/display/heading.js +3 -0
  15. package/build/components/display/heading.js.map +1 -1
  16. package/build/components/display/icon.d.ts +8 -4
  17. package/build/components/display/icon.d.ts.map +1 -1
  18. package/build/components/display/icon.js +21 -13
  19. package/build/components/display/icon.js.map +1 -1
  20. package/build/components/display/image.d.ts +7 -2
  21. package/build/components/display/image.d.ts.map +1 -1
  22. package/build/components/display/image.js +5 -5
  23. package/build/components/display/image.js.map +1 -1
  24. package/build/components/display/index.d.ts +10 -7
  25. package/build/components/display/index.d.ts.map +1 -1
  26. package/build/components/display/index.js +10 -7
  27. package/build/components/display/index.js.map +1 -1
  28. package/build/components/display/spinner.d.ts +5 -1
  29. package/build/components/display/spinner.d.ts.map +1 -1
  30. package/build/components/display/spinner.js +19 -13
  31. package/build/components/display/spinner.js.map +1 -1
  32. package/build/components/display/text.d.ts +13 -3
  33. package/build/components/display/text.d.ts.map +1 -1
  34. package/build/components/display/text.js +17 -5
  35. package/build/components/display/text.js.map +1 -1
  36. package/build/components/display/text_button.d.ts +37 -0
  37. package/build/components/display/text_button.d.ts.map +1 -0
  38. package/build/components/display/text_button.js +36 -0
  39. package/build/components/display/text_button.js.map +1 -0
  40. package/build/components/display/text_inline_button.d.ts +12 -0
  41. package/build/components/display/text_inline_button.d.ts.map +1 -0
  42. package/build/components/display/text_inline_button.js +53 -0
  43. package/build/components/display/text_inline_button.js.map +1 -0
  44. package/build/components/index.d.ts +1 -0
  45. package/build/components/index.d.ts.map +1 -1
  46. package/build/components/index.js +1 -0
  47. package/build/components/index.js.map +1 -1
  48. package/build/components/primitive/avatar_primitive.d.ts +1 -1
  49. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  50. package/build/components/primitive/avatar_primitive.js +6 -9
  51. package/build/components/primitive/avatar_primitive.js.map +1 -1
  52. package/build/contexts/api_provider.d.ts +4 -6
  53. package/build/contexts/api_provider.d.ts.map +1 -1
  54. package/build/contexts/api_provider.js +13 -20
  55. package/build/contexts/api_provider.js.map +1 -1
  56. package/build/contexts/chat_context.d.ts +7 -5
  57. package/build/contexts/chat_context.d.ts.map +1 -1
  58. package/build/contexts/chat_context.js +40 -4
  59. package/build/contexts/chat_context.js.map +1 -1
  60. package/build/hooks/index.d.ts +4 -0
  61. package/build/hooks/index.d.ts.map +1 -1
  62. package/build/hooks/index.js +4 -0
  63. package/build/hooks/index.js.map +1 -1
  64. package/build/hooks/use_create_android_ripple_color.d.ts +4 -0
  65. package/build/hooks/use_create_android_ripple_color.d.ts.map +1 -0
  66. package/build/hooks/use_create_android_ripple_color.js +15 -0
  67. package/build/hooks/use_create_android_ripple_color.js.map +1 -0
  68. package/build/hooks/use_current_person.d.ts +3 -0
  69. package/build/hooks/use_current_person.d.ts.map +1 -0
  70. package/build/hooks/use_current_person.js +13 -0
  71. package/build/hooks/use_current_person.js.map +1 -0
  72. package/build/hooks/use_font_scale.d.ts +4 -0
  73. package/build/hooks/use_font_scale.d.ts.map +1 -0
  74. package/build/hooks/use_font_scale.js +8 -0
  75. package/build/hooks/use_font_scale.js.map +1 -0
  76. package/build/hooks/use_suspense_api.d.ts +61 -0
  77. package/build/hooks/use_suspense_api.d.ts.map +1 -0
  78. package/build/hooks/use_suspense_api.js +39 -0
  79. package/build/hooks/use_suspense_api.js.map +1 -0
  80. package/build/navigation/index.d.ts +1 -0
  81. package/build/navigation/index.d.ts.map +1 -1
  82. package/build/navigation/index.js +7 -4
  83. package/build/navigation/index.js.map +1 -1
  84. package/build/screens/conversation_screen.d.ts.map +1 -1
  85. package/build/screens/conversation_screen.js +59 -6
  86. package/build/screens/conversation_screen.js.map +1 -1
  87. package/build/screens/display.d.ts.map +1 -1
  88. package/build/screens/display.js +277 -51
  89. package/build/screens/display.js.map +1 -1
  90. package/build/screens/not_found.js +1 -1
  91. package/build/screens/not_found.js.map +1 -1
  92. package/build/types/api_primitives.d.ts +23 -0
  93. package/build/types/api_primitives.d.ts.map +1 -0
  94. package/build/types/api_primitives.js +2 -0
  95. package/build/types/api_primitives.js.map +1 -0
  96. package/build/types/index.d.ts +4 -0
  97. package/build/types/index.d.ts.map +1 -0
  98. package/build/types/index.js +4 -0
  99. package/build/types/index.js.map +1 -0
  100. package/build/types/resources/conversation.d.ts +15 -0
  101. package/build/types/resources/conversation.d.ts.map +1 -0
  102. package/build/types/resources/conversation.js +2 -0
  103. package/build/types/resources/conversation.js.map +1 -0
  104. package/build/types/resources/index.d.ts +5 -0
  105. package/build/types/resources/index.d.ts.map +1 -0
  106. package/build/types/resources/index.js +5 -0
  107. package/build/types/resources/index.js.map +1 -0
  108. package/build/types/resources/message.d.ts +16 -0
  109. package/build/types/resources/message.d.ts.map +1 -0
  110. package/build/types/resources/message.js +2 -0
  111. package/build/types/resources/message.js.map +1 -0
  112. package/build/types/resources/oauth_token.d.ts +9 -0
  113. package/build/types/resources/oauth_token.d.ts.map +1 -0
  114. package/build/types/resources/oauth_token.js +2 -0
  115. package/build/types/resources/oauth_token.js.map +1 -0
  116. package/build/types/resources/person.d.ts +9 -0
  117. package/build/types/resources/person.d.ts.map +1 -0
  118. package/build/types/resources/person.js +2 -0
  119. package/build/types/resources/person.js.map +1 -0
  120. package/build/types/resources/reaction.d.ts +10 -0
  121. package/build/types/resources/reaction.d.ts.map +1 -0
  122. package/build/types/resources/reaction.js +2 -0
  123. package/build/types/resources/reaction.js.map +1 -0
  124. package/build/types/utils/index.d.ts +4 -0
  125. package/build/types/utils/index.d.ts.map +1 -0
  126. package/build/types/utils/index.js +4 -0
  127. package/build/types/utils/index.js.map +1 -0
  128. package/build/utils/client/client.d.ts +21 -12
  129. package/build/utils/client/client.d.ts.map +1 -1
  130. package/build/utils/client/client.js +24 -22
  131. package/build/utils/client/client.js.map +1 -1
  132. package/build/utils/index.d.ts +1 -0
  133. package/build/utils/index.d.ts.map +1 -1
  134. package/build/utils/index.js +1 -0
  135. package/build/utils/index.js.map +1 -1
  136. package/build/utils/session.d.ts +0 -5
  137. package/build/utils/session.d.ts.map +1 -1
  138. package/build/utils/session.js +0 -10
  139. package/build/utils/session.js.map +1 -1
  140. package/build/utils/styles.d.ts +5 -0
  141. package/build/utils/styles.d.ts.map +1 -1
  142. package/build/utils/styles.js +9 -0
  143. package/build/utils/styles.js.map +1 -1
  144. package/build/utils/theme.d.ts +3 -1
  145. package/build/utils/theme.d.ts.map +1 -1
  146. package/build/utils/theme.js +6 -2
  147. package/build/utils/theme.js.map +1 -1
  148. package/build/vendor/tapestry/alias_tokens_color_map.d.ts +8 -0
  149. package/build/vendor/tapestry/alias_tokens_color_map.d.ts.map +1 -1
  150. package/build/vendor/tapestry/alias_tokens_color_map.js +8 -0
  151. package/build/vendor/tapestry/alias_tokens_color_map.js.map +1 -1
  152. package/build/vendor/tapestry/tokens.d.ts +21 -0
  153. package/build/vendor/tapestry/tokens.d.ts.map +1 -1
  154. package/build/vendor/tapestry/tokens.js +21 -0
  155. package/build/vendor/tapestry/tokens.js.map +1 -1
  156. package/package.json +4 -3
  157. package/src/__tests__/client.ts +72 -19
  158. package/src/__tests__/session.ts +0 -11
  159. package/src/__utils__/handlers.ts +1 -1
  160. package/src/components/conversations.tsx +33 -11
  161. package/src/components/display/button.tsx +293 -0
  162. package/src/components/display/button_color_utils.ts +72 -0
  163. package/src/components/display/heading.tsx +12 -0
  164. package/src/components/display/icon.tsx +35 -16
  165. package/src/components/display/image.tsx +29 -7
  166. package/src/components/display/index.ts +10 -7
  167. package/src/components/display/spinner.tsx +42 -13
  168. package/src/components/display/text.tsx +34 -13
  169. package/src/components/display/text_button.tsx +102 -0
  170. package/src/components/display/text_inline_button.tsx +91 -0
  171. package/src/components/index.tsx +1 -0
  172. package/src/components/primitive/avatar_primitive.tsx +12 -6
  173. package/src/contexts/api_provider.tsx +18 -26
  174. package/src/contexts/chat_context.tsx +61 -7
  175. package/src/hooks/index.ts +4 -0
  176. package/src/hooks/use_create_android_ripple_color.ts +18 -0
  177. package/src/hooks/use_current_person.ts +15 -0
  178. package/src/hooks/use_font_scale.ts +9 -0
  179. package/src/hooks/use_suspense_api.ts +60 -0
  180. package/src/navigation/index.tsx +14 -4
  181. package/src/screens/conversation_screen.tsx +83 -7
  182. package/src/screens/display.tsx +447 -51
  183. package/src/screens/not_found.tsx +1 -1
  184. package/src/types/api_primitives.ts +24 -0
  185. package/src/types/index.ts +3 -0
  186. package/src/types/resources/conversation.ts +15 -0
  187. package/src/types/resources/index.ts +4 -0
  188. package/src/types/resources/message.ts +18 -0
  189. package/src/types/resources/oauth_token.ts +8 -0
  190. package/src/types/resources/person.ts +9 -0
  191. package/src/types/resources/reaction.ts +9 -0
  192. package/src/types/utils/index.ts +6 -0
  193. package/src/utils/client/client.ts +41 -34
  194. package/src/utils/client/types.d.ts +2 -0
  195. package/src/utils/index.ts +1 -0
  196. package/src/utils/session.ts +0 -13
  197. package/src/utils/styles.ts +12 -0
  198. package/src/utils/theme.ts +9 -3
  199. package/src/vendor/tapestry/alias_tokens_color_map.ts +12 -0
  200. package/src/vendor/tapestry/tokens.ts +21 -0
  201. package/build/utils/api.d.ts +0 -9
  202. package/build/utils/api.d.ts.map +0 -1
  203. package/build/utils/api.js +0 -36
  204. package/build/utils/api.js.map +0 -1
  205. package/src/types.d.ts +0 -35
  206. package/src/utils/api.ts +0 -47
@@ -0,0 +1,293 @@
1
+ import React from 'react'
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
3
+ import type { PressableProps, ViewStyle } from 'react-native'
4
+ import LinearGradient from 'react-native-linear-gradient'
5
+ import { Icon } from './icon'
6
+ import { useTheme, useFontScale, useCreateAndroidRippleColor } from '../../hooks'
7
+ import { tokens } from '../../vendor/tapestry/tokens'
8
+ import { platformFontWeightBold, platformPressedOpacityStyle } from '../../utils'
9
+ import { Spinner } from './spinner'
10
+ import { getColorKey, useColorOptionMap, useGradientColorMap } from './button_color_utils'
11
+ import type { AppearanceUnion } from './button_color_utils'
12
+
13
+ // =================================
14
+ // ====== Constants ================
15
+ // =================================
16
+
17
+ const SIZES = {
18
+ sm: 'sm',
19
+ md: 'md',
20
+ lg: 'lg',
21
+ } as const
22
+
23
+ type SizeUnion = (typeof SIZES)[keyof typeof SIZES]
24
+ type SizeStyle = Record<
25
+ SizeUnion,
26
+ {
27
+ fontSize: number
28
+ paddingHorizontal: number
29
+ height: number
30
+ borderRadius: number
31
+ gap: number
32
+ }
33
+ >
34
+
35
+ const VARIANTS = {
36
+ fill: 'fill',
37
+ outline: 'outline',
38
+ } as const
39
+
40
+ type VariantUnion = (typeof VARIANTS)[keyof typeof VARIANTS]
41
+ type VariantColors = Record<
42
+ VariantUnion,
43
+ {
44
+ backgroundColor: string
45
+ color: string
46
+ }
47
+ >
48
+
49
+ // =================================
50
+ // ====== Component ================
51
+ // =================================
52
+
53
+ interface ButtonProps extends PressableProps {
54
+ /**
55
+ * Specifies whether fonts should be scaled down automatically to fit given style constraints.
56
+ */
57
+ adjustsFontSizeToFit?: boolean
58
+ /**
59
+ * Specifies whether fonts should scale to respect the device's text size accessibility settings. The default is true.
60
+ */
61
+ allowFontScaling?: boolean
62
+ /**
63
+ * Updates the button's colors
64
+ */
65
+ appearance?: AppearanceUnion
66
+ /**
67
+ * Styles the inner View that wraps the button's content
68
+ */
69
+ buttonInnerStyles?: ViewStyle
70
+ /**
71
+ * Styles the outer LinearGradient that gives the button it's backgrounnd and outline color
72
+ */
73
+ buttonOuterStyles?: ViewStyle
74
+ /**
75
+ * Generates an icon to the left of the button text
76
+ */
77
+ iconNameLeft?: string
78
+ /**
79
+ * Generates an icon to the right of the button text
80
+ */
81
+ iconNameRight?: string
82
+ /**
83
+ * Disables the button and replaces its content with a spinner
84
+ */
85
+ loading?: boolean
86
+ /**
87
+ * Specifies the maximum size a font can reach when allowFontScaling is enabled.
88
+ */
89
+ maxFontSizeMultiplier?: number
90
+ /**
91
+ * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
92
+ */
93
+ minimumFontScale?: number
94
+ /**
95
+ * Changes the overall size of the button and its contents
96
+ */
97
+ size?: SizeUnion
98
+ /**
99
+ * Renders as text within the button
100
+ */
101
+ title: string
102
+ /**
103
+ * Fill buttons are visually more propmonitent then outline buttons
104
+ */
105
+ variant?: VariantUnion
106
+ }
107
+
108
+ export function Button({
109
+ adjustsFontSizeToFit = false,
110
+ allowFontScaling = true,
111
+ appearance = 'primary',
112
+ buttonInnerStyles,
113
+ buttonOuterStyles,
114
+ disabled = false,
115
+ iconNameLeft,
116
+ iconNameRight,
117
+ loading,
118
+ maxFontSizeMultiplier,
119
+ minimumFontScale,
120
+ size = 'md',
121
+ title,
122
+ variant = 'fill',
123
+ ...props
124
+ }: ButtonProps) {
125
+ const styles = useStyles({ appearance, disabled, loading, maxFontSizeMultiplier, size, variant })
126
+ const gradientOptionsMap = useGradientColorMap()
127
+ const colorKey = getColorKey({ disabled, loading, appearance })
128
+
129
+ const textStyles = [styles.text, disabled && styles.textDisabled, loading && styles.iconLoading]
130
+ const iconStyles = [styles.icon, disabled && styles.iconDisabled, loading && styles.textLoading]
131
+
132
+ const androidRippleColor = useCreateAndroidRippleColor({ color: gradientOptionsMap[colorKey][0] })
133
+
134
+ return (
135
+ <Pressable
136
+ style={({ pressed }) => [styles.pressable, pressed && platformPressedOpacityStyle]}
137
+ accessibilityRole="button"
138
+ disabled={disabled || loading}
139
+ accessibilityState={{ busy: loading }}
140
+ android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }}
141
+ {...props}
142
+ >
143
+ <LinearGradient
144
+ start={{ x: 0.1, y: 0.1 }}
145
+ end={{ x: 0.9, y: 0.9 }}
146
+ colors={gradientOptionsMap[colorKey]}
147
+ style={[styles.colorWrapper, buttonOuterStyles]}
148
+ >
149
+ {loading && (
150
+ <Spinner
151
+ size={styles.spinner.fontSize}
152
+ maxFontSizeMultiplier={maxFontSizeMultiplier || 0}
153
+ />
154
+ )}
155
+ <View style={[styles.innerWrapper, buttonInnerStyles]}>
156
+ {iconNameLeft && (
157
+ <Icon
158
+ name={iconNameLeft}
159
+ style={iconStyles}
160
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
161
+ />
162
+ )}
163
+ <Text
164
+ allowFontScaling={allowFontScaling}
165
+ minimumFontScale={minimumFontScale}
166
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
167
+ adjustsFontSizeToFit={adjustsFontSizeToFit}
168
+ numberOfLines={1}
169
+ style={textStyles}
170
+ >
171
+ {title}
172
+ </Text>
173
+ {iconNameRight && (
174
+ <Icon
175
+ name={iconNameRight}
176
+ style={iconStyles}
177
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
178
+ />
179
+ )}
180
+ </View>
181
+ </LinearGradient>
182
+ </Pressable>
183
+ )
184
+ }
185
+
186
+ // =================================
187
+ // ====== Styles ===================
188
+ // =================================
189
+
190
+ const useStyles = ({
191
+ appearance = 'primary',
192
+ disabled = false,
193
+ loading = false,
194
+ maxFontSizeMultiplier,
195
+ size = 'md',
196
+ variant = 'fill',
197
+ }: Partial<ButtonProps>) => {
198
+ const { colors } = useTheme()
199
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
200
+ const colorOptionMap = useColorOptionMap()
201
+
202
+ const outlineOffsetSm = 2
203
+ const outlineOffset = 4
204
+
205
+ const sizeStyleMap: SizeStyle = {
206
+ [SIZES.sm]: {
207
+ fontSize: 12,
208
+ paddingHorizontal: 12 * fontScale,
209
+ height: 24 * fontScale - outlineOffsetSm,
210
+ borderRadius: 24 * fontScale,
211
+ gap: 4 * fontScale,
212
+ },
213
+ [SIZES.md]: {
214
+ fontSize: 14,
215
+ paddingHorizontal: 16 * fontScale,
216
+ height: 32 * fontScale - outlineOffset,
217
+ borderRadius: 32 * fontScale,
218
+ gap: 6 * fontScale,
219
+ },
220
+ [SIZES.lg]: {
221
+ fontSize: 16,
222
+ paddingHorizontal: 24 * fontScale,
223
+ height: 40 * fontScale - outlineOffset,
224
+ borderRadius: 40 * fontScale,
225
+ gap: 8 * fontScale,
226
+ },
227
+ }
228
+
229
+ const variantOutlineColor =
230
+ disabled || loading ? colors.fillColorNeutral090 : colors.fillColorNeutral100Inverted
231
+
232
+ const variantStyleMap: VariantColors = {
233
+ fill: {
234
+ backgroundColor: 'transparent',
235
+ color: tokens.colorNeutral100White,
236
+ },
237
+ outline: {
238
+ backgroundColor: variantOutlineColor,
239
+ color: colorOptionMap[appearance],
240
+ },
241
+ }
242
+
243
+ return StyleSheet.create({
244
+ pressable: {
245
+ borderRadius: sizeStyleMap[size].borderRadius,
246
+ overflow: 'hidden',
247
+ },
248
+ colorWrapper: {
249
+ flexDirection: 'row',
250
+ justifyContent: 'center',
251
+ alignItems: 'center',
252
+ borderRadius: sizeStyleMap[size].borderRadius,
253
+ },
254
+ innerWrapper: {
255
+ flexDirection: 'row',
256
+ justifyContent: 'center',
257
+ alignItems: 'center',
258
+ margin: size === 'sm' ? 1 : 2,
259
+ gap: sizeStyleMap[size].gap,
260
+ borderRadius: sizeStyleMap[size].borderRadius,
261
+ height: sizeStyleMap[size].height,
262
+ backgroundColor: variantStyleMap[variant].backgroundColor,
263
+ paddingHorizontal: sizeStyleMap[size].paddingHorizontal,
264
+ },
265
+ text: {
266
+ textAlign: 'center',
267
+ textAlignVertical: 'center',
268
+ includeFontPadding: false,
269
+ fontWeight: platformFontWeightBold,
270
+ fontSize: sizeStyleMap[size].fontSize,
271
+ color: variantStyleMap[variant].color,
272
+ },
273
+ textDisabled: {
274
+ color: colors.textColorDefaultDisabled,
275
+ },
276
+ textLoading: {
277
+ opacity: 0,
278
+ },
279
+ icon: {
280
+ fontSize: sizeStyleMap[size].fontSize,
281
+ color: variantStyleMap[variant].color,
282
+ },
283
+ iconDisabled: {
284
+ color: colors.iconColorDefaultDisabled,
285
+ },
286
+ iconLoading: {
287
+ opacity: 0,
288
+ },
289
+ spinner: {
290
+ fontSize: sizeStyleMap[size].fontSize,
291
+ },
292
+ })
293
+ }
@@ -0,0 +1,72 @@
1
+ import { useTheme } from '../../hooks'
2
+
3
+ // =================================
4
+ // ====== Exports ==================
5
+ // =================================
6
+
7
+ export { getColorKey, useColorOptionMap, useGradientColorMap }
8
+ export type { AppearanceUnion }
9
+
10
+ // =================================
11
+ // ====== Constants ================
12
+ // =================================
13
+
14
+ const APPEARANCES = {
15
+ primary: 'primary',
16
+ danger: 'danger',
17
+ } as const
18
+
19
+ type AppearanceUnion = (typeof APPEARANCES)[keyof typeof APPEARANCES]
20
+
21
+ const COLOR_OPTIONS = {
22
+ ...APPEARANCES,
23
+ disabled: 'disabled',
24
+ } as const
25
+
26
+ type ColorOptionUnion = (typeof COLOR_OPTIONS)[keyof typeof COLOR_OPTIONS]
27
+
28
+ // =================================
29
+ // ====== Hooks ====================
30
+ // =================================
31
+
32
+ type ColorOptionMap = Record<ColorOptionUnion, string>
33
+ function useColorOptionMap(): ColorOptionMap {
34
+ const { colors } = useTheme()
35
+
36
+ return {
37
+ primary: colors.interaction,
38
+ danger: colors.fillColorStatusErrorMedium,
39
+ disabled: colors.textColorDefaultDisabled,
40
+ }
41
+ }
42
+
43
+ type GradientColorMap = Record<ColorOptionUnion, [string, string]>
44
+ function useGradientColorMap(): GradientColorMap {
45
+ const { colors } = useTheme()
46
+
47
+ const defaultColorStart = colors.buttonStart || colors.interaction
48
+ const defaultColorEnd = colors.buttonEnd || colors.interaction
49
+
50
+ return {
51
+ primary: [defaultColorStart, defaultColorEnd],
52
+ danger: [colors.fillColorStatusErrorMedium, colors.fillColorStatusErrorMedium],
53
+ disabled: [
54
+ colors.fillColorButtonNeutralSolidDisabled,
55
+ colors.fillColorButtonNeutralSolidDisabled,
56
+ ],
57
+ }
58
+ }
59
+
60
+ // =================================
61
+ // ====== Functions ================
62
+ // =================================
63
+
64
+ interface GetColorKeyArgs {
65
+ disabled: boolean | null
66
+ loading?: boolean
67
+ appearance: AppearanceUnion
68
+ }
69
+ function getColorKey({ disabled, loading, appearance }: GetColorKeyArgs) {
70
+ if (disabled || loading) return 'disabled'
71
+ return appearance
72
+ }
@@ -9,7 +9,15 @@ import {
9
9
  import { tokens } from '../../vendor/tapestry/tokens'
10
10
  import { platformFontWeightBold } from '../../utils/styles'
11
11
 
12
+ // =================================
13
+ // ====== Component ================
14
+ // =================================
15
+
12
16
  interface TextProps extends ReactNativeTextProps {
17
+ /**
18
+ * Changes the styles and size of the text.
19
+ * Semantically all React Native headings have the same 'hierarchical' level.
20
+ */
13
21
  variant?: 'h1' | 'h2' | 'h3' | 'h4'
14
22
  }
15
23
 
@@ -33,6 +41,10 @@ export function Heading({ style, variant = 'h1', children, ...props }: TextProps
33
41
  )
34
42
  }
35
43
 
44
+ // =================================
45
+ // ====== Styles ===================
46
+ // =================================
47
+
36
48
  const useStyles = () => {
37
49
  const { colors } = useTheme()
38
50
 
@@ -1,9 +1,9 @@
1
1
  import React from 'react'
2
- import { PixelRatio, StyleSheet, View } from 'react-native'
3
- import type { ViewStyle } from 'react-native'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import type { StyleProp, ViewStyle } from 'react-native'
4
4
  import { SvgXml } from 'react-native-svg'
5
5
  import type { XmlProps } from 'react-native-svg'
6
- import { useTheme } from '../../hooks'
6
+ import { useFontScale, useTheme } from '../../hooks'
7
7
  import * as general from '@planningcenter/icons/paths/general'
8
8
  import * as groups from '@planningcenter/icons/paths/groups'
9
9
  import * as calendar from '@planningcenter/icons/paths/calendar'
@@ -12,6 +12,10 @@ import * as churchCenter from '@planningcenter/icons/paths/church-center'
12
12
  import * as logomark from '@planningcenter/icons/paths/logomark'
13
13
  import * as brand from '@planningcenter/icons/paths/brand'
14
14
 
15
+ // =================================
16
+ // ====== Constants ================
17
+ // =================================
18
+
15
19
  const FALLBACK_SIZE = 12
16
20
 
17
21
  const ICONS = {
@@ -29,7 +33,11 @@ type IconStyle = ViewStyle & {
29
33
  color?: string
30
34
  }
31
35
 
32
- interface IconProps extends Omit<XmlProps, 'xml'> {
36
+ // =================================
37
+ // ====== Component ================
38
+ // =================================
39
+
40
+ interface IconProps extends Omit<XmlProps, 'xml' | 'fontSize'> {
33
41
  /**
34
42
  * Made up of the set.iconName.
35
43
  * Example: "general.textMessage"
@@ -40,10 +48,14 @@ interface IconProps extends Omit<XmlProps, 'xml'> {
40
48
  * Providing a fontSize style will allow the icon to scale with the device's text a11y size.
41
49
  */
42
50
  size?: number
51
+ /**
52
+ * Specifies the maximum size a font can reach when allowFontScaling is enabled.
53
+ */
54
+ maxFontSizeMultiplier?: number
43
55
  /**
44
56
  * Icon can handle ViewStyle, color, and fontSize.
45
57
  */
46
- style?: IconStyle
58
+ style?: StyleProp<IconStyle>
47
59
  }
48
60
 
49
61
  export function Icon({
@@ -52,11 +64,13 @@ export function Icon({
52
64
  style,
53
65
  accessibilityElementsHidden,
54
66
  accessibilityLabel,
67
+ maxFontSizeMultiplier,
55
68
  ...props
56
69
  }: IconProps) {
70
+ const flattenStyles = StyleSheet.flatten(style)
71
+ const iconSize = useGetIconSize(size, flattenStyles, maxFontSizeMultiplier)
57
72
  const path = getIconPath(name)
58
- const iconSize = getIconSize(size, style)
59
- const styles = useStyles(iconSize)
73
+ const styles = useStyles({ iconSize })
60
74
 
61
75
  if (!path) {
62
76
  console.warn(`No icon available named ${name}. Remember to use the format "set.iconName"`)
@@ -74,35 +88,40 @@ export function Icon({
74
88
  `}
75
89
  height={iconSize}
76
90
  width={iconSize}
77
- style={{ ...styles.icon, ...style }}
91
+ style={{ ...styles.icon, ...flattenStyles }}
78
92
  {...props}
79
93
  />
80
94
  )
81
95
  }
82
96
 
83
- const getIconSize = (size?: number, style?: IconStyle) => {
84
- const fontSize = StyleSheet.flatten(style)?.fontSize
97
+ const useGetIconSize = (size?: number, style?: IconStyle, maxFontSizeMultiplier?: number) => {
98
+ const fontSize = style?.fontSize
99
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
85
100
 
86
- if (fontSize) return fontSize * PixelRatio.getFontScale()
101
+ if (fontSize) return fontSize * fontScale
87
102
 
88
103
  return size || FALLBACK_SIZE
89
104
  }
90
105
 
91
- const getIconPath = (name: string) => {
106
+ const getIconPath = (name: string): string => {
92
107
  const [setName, iconName] = name.split('.')
93
108
 
94
109
  return ICONS[setName]?.[iconName]
95
110
  }
96
111
 
97
- const useStyles = (size: number) => {
112
+ // =================================
113
+ // ====== Styles ===================
114
+ // =================================
115
+
116
+ const useStyles = ({ iconSize }: { iconSize: number }) => {
98
117
  const { colors } = useTheme()
99
118
 
100
119
  return StyleSheet.create({
101
120
  noIcon: {
102
121
  backgroundColor: colors.iconColorDefaultDisabled,
103
- width: size,
104
- height: size,
105
- borderRadius: size / 2,
122
+ width: iconSize,
123
+ height: iconSize,
124
+ borderRadius: iconSize / 2,
106
125
  },
107
126
  icon: {
108
127
  color: colors.iconColorDefaultPrimary,
@@ -12,9 +12,18 @@ import {
12
12
  import { useTheme } from '../../hooks'
13
13
  import { Spinner } from './spinner'
14
14
 
15
+ // =================================
16
+ // ====== Component ================
17
+ // =================================
18
+
15
19
  export interface ImageProps extends ReactNativeImageProps {
16
20
  /**
17
- * Should the image show the loading indicator by default.
21
+ * Describes the image to screen-readers and marks the image as `accessible`.
22
+ * Passing an empty string will hide the image from screen-readers.
23
+ */
24
+ alt: string
25
+ /**
26
+ * Shows the image's loading spinner right away. Enabled by default.
18
27
  */
19
28
  defaultLoading?: boolean
20
29
  /**
@@ -44,13 +53,14 @@ export function Image({
44
53
  loadingBackgroundStyles,
45
54
  style = {},
46
55
  wrapperStyle = {},
56
+ alt,
47
57
  ...props
48
58
  }: ImageProps) {
49
59
  const [loading, setLoading] = useState(defaultLoading)
50
60
 
51
61
  const imageStyles = StyleSheet.flatten(style)
52
62
  const { width = '100%', height = '100%', borderRadius = 0 } = imageStyles || {}
53
- const styles = useStyles(width, height, borderRadius)
63
+ const styles = useStyles({ width, height, borderRadius })
54
64
 
55
65
  const handleOnLoad = (event: any) => {
56
66
  setLoading(false)
@@ -58,11 +68,17 @@ export function Image({
58
68
  }
59
69
 
60
70
  return (
61
- <View style={wrapperStyle}>
71
+ <View
72
+ style={wrapperStyle}
73
+ accessible={Boolean(alt)}
74
+ accessibilityRole="image"
75
+ accessibilityState={{ busy: loading }}
76
+ >
62
77
  <ReactNativeImage
63
78
  style={[styles.image, imageStyles]}
64
79
  onLoad={handleOnLoad}
65
80
  source={source}
81
+ alt={loading ? '' : alt}
66
82
  {...props}
67
83
  />
68
84
  {loading && loadingEnabled && (
@@ -74,11 +90,17 @@ export function Image({
74
90
  )
75
91
  }
76
92
 
77
- const useStyles = (
78
- width: DimensionValue,
79
- height: DimensionValue,
93
+ // =================================
94
+ // ====== Styles ===================
95
+ // =================================
96
+
97
+ interface Styles {
98
+ width: DimensionValue
99
+ height: DimensionValue
80
100
  borderRadius: AnimatableNumericValue | string
81
- ) => {
101
+ }
102
+
103
+ const useStyles = ({ width, height, borderRadius }: Styles) => {
82
104
  const { colors } = useTheme()
83
105
 
84
106
  return StyleSheet.create({
@@ -1,7 +1,10 @@
1
- export { Avatar } from './avatar'
2
- export { AvatarGroup } from './avatar_group'
3
- export { Heading } from './heading'
4
- export { Icon } from './icon'
5
- export { Image, ImageProps } from './image'
6
- export { Spinner } from './spinner'
7
- export { Text } from './text'
1
+ export * from './avatar'
2
+ export * from './avatar_group'
3
+ export * from './button'
4
+ export * from './heading'
5
+ export * from './icon'
6
+ export * from './image'
7
+ export * from './spinner'
8
+ export * from './text'
9
+ export * from './text_button'
10
+ export * from './text_inline_button'
@@ -1,15 +1,32 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
2
  import { Animated, Easing, StyleSheet, View } from 'react-native'
3
- import { useTheme } from '../../hooks'
3
+ import { useFontScale, useTheme } from '../../hooks'
4
+
5
+ // =================================
6
+ // ====== Constants ================
7
+ // =================================
8
+
9
+ const PREVENT_SCALING_DEFAULT = 1
10
+
11
+ // =================================
12
+ // ====== Component ================
13
+ // =================================
4
14
 
5
15
  interface SpinnerProps {
6
16
  /**
7
17
  * Size of the spinner in px
8
18
  * */
9
19
  size?: number
20
+ /**
21
+ * Specifies the maximum size spinner can scale to if the device's font-size is increased.
22
+ */
23
+ maxFontSizeMultiplier?: number
10
24
  }
11
25
 
12
- export function Spinner({ size = 20 }: SpinnerProps) {
26
+ export function Spinner({
27
+ size = 20,
28
+ maxFontSizeMultiplier = PREVENT_SCALING_DEFAULT,
29
+ }: SpinnerProps) {
13
30
  const rotation = useRef(new Animated.Value(0)).current
14
31
 
15
32
  const animation = Animated.loop(
@@ -26,7 +43,7 @@ export function Spinner({ size = 20 }: SpinnerProps) {
26
43
  outputRange: ['0deg', '360deg'],
27
44
  })
28
45
 
29
- const styles = useStyles(rotateValue, size)
46
+ const styles = useStyles({ maxFontSizeMultiplier, rotateValue, size })
30
47
 
31
48
  useEffect(() => {
32
49
  animation.start()
@@ -47,8 +64,20 @@ export function Spinner({ size = 20 }: SpinnerProps) {
47
64
  )
48
65
  }
49
66
 
50
- const useStyles = (rotate: Animated.AnimatedInterpolation<string | number>, size: number) => {
67
+ // =================================
68
+ // ====== Styles ===================
69
+ // =================================
70
+
71
+ interface Styles {
72
+ maxFontSizeMultiplier: number | undefined
73
+ rotateValue: Animated.AnimatedInterpolation<string | number>
74
+ size: number
75
+ }
76
+
77
+ const useStyles = ({ maxFontSizeMultiplier, rotateValue, size }: Styles) => {
51
78
  const { colors } = useTheme()
79
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
80
+ const scalableSize = size * fontScale
52
81
 
53
82
  return StyleSheet.create({
54
83
  container: {
@@ -63,15 +92,15 @@ const useStyles = (rotate: Animated.AnimatedInterpolation<string | number>, size
63
92
  opacity: 0.7,
64
93
  },
65
94
  animatedContainer: {
66
- width: size,
67
- height: size,
68
- borderRadius: size / 2,
69
- transform: [{ rotate }],
95
+ width: scalableSize,
96
+ height: scalableSize,
97
+ borderRadius: scalableSize / 2,
98
+ transform: [{ rotate: rotateValue }],
70
99
  },
71
100
  circle: {
72
- width: size,
73
- height: size,
74
- borderRadius: size / 2,
101
+ width: scalableSize,
102
+ height: scalableSize,
103
+ borderRadius: scalableSize / 2,
75
104
  borderStyle: 'solid',
76
105
  borderWidth: 3,
77
106
  },
@@ -88,8 +117,8 @@ const useStyles = (rotate: Animated.AnimatedInterpolation<string | number>, size
88
117
  position: 'absolute',
89
118
  top: 0,
90
119
  left: 0,
91
- width: size / 2,
92
- height: size / 2,
120
+ width: scalableSize / 2,
121
+ height: scalableSize / 2,
93
122
  overflow: 'hidden',
94
123
  zIndex: 200,
95
124
  },