@jobber/components-native 0.1.4 → 0.2.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.
@@ -0,0 +1,368 @@
1
+ import React from "react";
2
+ import {
3
+ AccessibilityProps,
4
+ I18nManager,
5
+ StyleProp,
6
+ StyleSheet,
7
+ // eslint-disable-next-line no-restricted-imports
8
+ Text,
9
+ TextProps,
10
+ ViewStyle,
11
+ } from "react-native";
12
+ import { TypographyGestureDetector } from "./TypographyGestureDetector";
13
+ import { typographyStyles as styles } from "./Typography.style";
14
+ import { tokens } from "../utils/design";
15
+ import { capitalize } from "../utils/intl";
16
+
17
+ export interface TypographyProps<T extends FontFamily>
18
+ extends Pick<TextProps, "selectable"> {
19
+ /**
20
+ * Text capitalization
21
+ */
22
+ readonly transform?: TextTransform;
23
+
24
+ /**
25
+ * Color of text
26
+ */
27
+ readonly color?: TextColor;
28
+
29
+ /**
30
+ * Alignment of text
31
+ */
32
+ readonly align?: TextAlign;
33
+
34
+ /**
35
+ * Font size
36
+ */
37
+ readonly size?: TextSize;
38
+
39
+ /**
40
+ * Text to display
41
+ */
42
+ readonly children?: string;
43
+
44
+ /**
45
+ * The maximum amount of lines the text can occupy before being truncated with "...".
46
+ * Uses predefined string values that correspond to a doubling scale for the amount of lines.
47
+ */
48
+ readonly maxLines?: TruncateLength;
49
+
50
+ /**
51
+ * Allow text to be resized based on user's device display scale
52
+ */
53
+ readonly allowFontScaling?: boolean;
54
+
55
+ /**
56
+ * Set the maximum font the text can go to size when the user scales their
57
+ * device font size
58
+ */
59
+ readonly maxFontScaleSize?: number;
60
+
61
+ /**
62
+ * Determines whether text should be scaled down to fit based on maxLines prop
63
+ */
64
+ readonly adjustsFontSizeToFit?: boolean;
65
+
66
+ /**
67
+ * Line Height
68
+ */
69
+ readonly lineHeight?: LineHeight;
70
+
71
+ /**
72
+ * Spacing between letters
73
+ */
74
+ readonly letterSpacing?: LetterSpacing;
75
+
76
+ /**
77
+ * Font Family
78
+ */
79
+ readonly fontFamily?: T;
80
+
81
+ /**
82
+ * Font style
83
+ */
84
+ readonly fontStyle?: T extends "base" ? BaseStyle : DisplayStyle;
85
+
86
+ /**
87
+ * Font weight
88
+ */
89
+ readonly fontWeight?: T extends "base" ? BaseWeight : DisplayWeight;
90
+
91
+ /**
92
+ * Reverse theme for better display on dark background
93
+ */
94
+ readonly reverseTheme?: boolean;
95
+
96
+ /**
97
+ * Accessibility role describing the context of the text
98
+ */
99
+ readonly accessibilityRole?: TextAccessibilityRole;
100
+
101
+ /**
102
+ * This will make the text inaccessible to the screen reader.
103
+ * This should be avoided unless there is a good reason.
104
+ * For example this is used in InputText to make it so the label isn't
105
+ * selectable because it is already read from the accessibilityLabel
106
+ * of the TextInput
107
+ */
108
+ readonly hideFromScreenReader?: boolean;
109
+ /**
110
+ * Have text styled with strike through
111
+ */
112
+ readonly strikeThrough?: boolean;
113
+ }
114
+
115
+ const maxNumberOfLines = {
116
+ single: 1,
117
+ small: 2,
118
+ base: 4,
119
+ large: 8,
120
+ extraLarge: 16,
121
+ unlimited: undefined,
122
+ };
123
+
124
+ export const Typography = React.memo(InternalTypography);
125
+
126
+ function InternalTypography<T extends FontFamily = "base">({
127
+ fontFamily,
128
+ fontStyle,
129
+ fontWeight,
130
+ transform,
131
+ color,
132
+ align,
133
+ size = "default",
134
+ children,
135
+ maxLines = "unlimited",
136
+ allowFontScaling = true,
137
+ maxFontScaleSize,
138
+ adjustsFontSizeToFit = false,
139
+ lineHeight,
140
+ letterSpacing,
141
+ reverseTheme = false,
142
+ hideFromScreenReader = false,
143
+ accessibilityRole = "text",
144
+ strikeThrough = false,
145
+ selectable = true,
146
+ }: TypographyProps<T>): JSX.Element {
147
+ const sizeAndHeight = getSizeAndHeightStyle(size, lineHeight);
148
+ const style: StyleProp<ViewStyle>[] = [
149
+ getFontStyle(fontFamily, fontStyle, fontWeight),
150
+ getColorStyle(color, reverseTheme),
151
+ getAlignStyle(align),
152
+ sizeAndHeight,
153
+ getLetterSpacingStyle(letterSpacing),
154
+ ];
155
+
156
+ if (strikeThrough) {
157
+ style.push(styles.strikeThrough);
158
+ }
159
+ const numberOfLinesForNativeText = maxNumberOfLines[maxLines];
160
+
161
+ const text = getTransformedText(children, transform);
162
+ const accessibilityProps: AccessibilityProps = hideFromScreenReader
163
+ ? {
164
+ accessibilityRole: "none",
165
+ accessible: false,
166
+ importantForAccessibility: "no-hide-descendants",
167
+ }
168
+ : { accessibilityRole };
169
+
170
+ return (
171
+ <TypographyGestureDetector>
172
+ <Text
173
+ {...{
174
+ allowFontScaling,
175
+ adjustsFontSizeToFit,
176
+ style,
177
+ numberOfLines: numberOfLinesForNativeText,
178
+ }}
179
+ {...accessibilityProps}
180
+ maxFontSizeMultiplier={getScaleMultiplier(
181
+ maxFontScaleSize,
182
+ sizeAndHeight.fontSize,
183
+ )}
184
+ selectable={selectable}
185
+ selectionColor={tokens["color-brand--highlight"]}
186
+ >
187
+ {text}
188
+ </Text>
189
+ </TypographyGestureDetector>
190
+ );
191
+ }
192
+
193
+ function getScaleMultiplier(maxFontScaleSize = 0, size = 1) {
194
+ if (maxFontScaleSize === 0) return undefined;
195
+
196
+ return maxFontScaleSize / size;
197
+ }
198
+
199
+ function getFontStyle(
200
+ fontFamily: FontFamily = "base",
201
+ fontStyle: FontStyle = "regular",
202
+ fontWeight: FontWeight = "regular",
203
+ ) {
204
+ const defaultBaseFontStyling = styles.baseRegularRegular;
205
+ const defaultDisplayFontStyling = styles.displayRegularBold;
206
+ const styleKey = `${fontFamily}${capitalize(fontStyle)}${capitalize(
207
+ fontWeight,
208
+ )}`;
209
+ const fontStyling = styles[styleKey];
210
+ if (fontStyling) {
211
+ return fontStyling;
212
+ } else {
213
+ return fontFamily === "display"
214
+ ? defaultDisplayFontStyling
215
+ : defaultBaseFontStyling;
216
+ }
217
+ }
218
+
219
+ function getTransformedText(text?: string, transform?: TextTransform) {
220
+ switch (transform) {
221
+ case "lowercase":
222
+ return text?.toLocaleLowerCase();
223
+ case "uppercase":
224
+ return text?.toLocaleUpperCase();
225
+ case "capitalize":
226
+ return capitalize(text || "");
227
+ default:
228
+ return text;
229
+ }
230
+ }
231
+
232
+ function getColorStyle(color?: TextColor, reverseTheme?: boolean) {
233
+ if (color === "default" || !color) {
234
+ return styles.greyBlue;
235
+ }
236
+ const colorStyleKey = `${color}${reverseTheme ? "Reverse" : ""}`;
237
+ return styles[`${colorStyleKey}`];
238
+ }
239
+
240
+ function getAlignStyle(
241
+ alignStyle: TextAlign = I18nManager.isRTL ? "end" : "start",
242
+ ) {
243
+ return styles[`${alignStyle}Align`];
244
+ }
245
+
246
+ function getSizeAndHeightStyle(
247
+ textSize: TextSize,
248
+ lineHeightOverwrite?: LineHeight,
249
+ ) {
250
+ const fontSize = styles[`${textSize}Size`];
251
+ if (lineHeightOverwrite) {
252
+ const lineHeight = styles[`${lineHeightOverwrite}LineHeight`];
253
+ return StyleSheet.flatten([fontSize, lineHeight]);
254
+ }
255
+ return fontSize;
256
+ }
257
+
258
+ function getLetterSpacingStyle(letterSpacing: LetterSpacing = "base") {
259
+ return styles[`${letterSpacing}LetterSpacing`];
260
+ }
261
+
262
+ export type FontFamily = "base" | "display";
263
+ export type FontStyle = "regular" | "italic";
264
+ export type FontWeight =
265
+ | "regular"
266
+ | "medium"
267
+ | "bold"
268
+ | "semiBold"
269
+ | "extraBold"
270
+ | "black";
271
+
272
+ export type BaseWeight = Extract<
273
+ FontWeight,
274
+ "regular" | "medium" | "semiBold" | "bold" | "extraBold"
275
+ >;
276
+
277
+ export type DisplayWeight = Extract<
278
+ FontWeight,
279
+ "semiBold" | "bold" | "extraBold" | "black"
280
+ >;
281
+
282
+ export type BaseStyle = FontStyle;
283
+ export type DisplayStyle = Extract<FontStyle, "regular">;
284
+
285
+ export type TextColor =
286
+ | TextVariation
287
+ | "default"
288
+ | "blue"
289
+ | "blueDark"
290
+ | "white"
291
+ | "green"
292
+ | "greenDark"
293
+ | "grey"
294
+ | "greyDark"
295
+ | "greyBlue"
296
+ | "greyBlueDark"
297
+ | "lightBlue"
298
+ | "lightBlueDark"
299
+ | "red"
300
+ | "redDark"
301
+ | "yellow"
302
+ | "yellowDark"
303
+ | "yellowGreenDark"
304
+ | "orangeDark"
305
+ | "navyDark"
306
+ | "limeDark"
307
+ | "purpleDark"
308
+ | "pinkDark"
309
+ | "tealDark"
310
+ | "indigoDark"
311
+ | "navy"
312
+ | "text"
313
+ | "heading"
314
+ | "textSecondary"
315
+ | "textReverse"
316
+ | "textReverseSecondary"
317
+ | "interactive"
318
+ | "destructive"
319
+ | "learning"
320
+ | "subtle"
321
+ | "onPrimary";
322
+
323
+ export type TextVariation =
324
+ | "success"
325
+ | "interactive"
326
+ | "error"
327
+ | "base"
328
+ | "subdued"
329
+ | "warn"
330
+ | "info"
331
+ | "disabled"
332
+ | "critical";
333
+
334
+ export type TextTransform = "uppercase" | "lowercase" | "capitalize" | "none";
335
+
336
+ export type TextSize =
337
+ | "smallest"
338
+ | "smaller"
339
+ | "small"
340
+ | "default"
341
+ | "large"
342
+ | "larger"
343
+ | "largest"
344
+ | "jumbo"
345
+ | "extravagant";
346
+
347
+ export type TextAlign = "start" | "end" | "center" | "justify";
348
+
349
+ export type LineHeight =
350
+ | "extravagant"
351
+ | "jumbo"
352
+ | "largest"
353
+ | "larger"
354
+ | "large"
355
+ | "base"
356
+ | "tight";
357
+
358
+ export type LetterSpacing = "base" | "loose";
359
+
360
+ export type TextAccessibilityRole = "text" | "header";
361
+
362
+ export type TruncateLength =
363
+ | "single"
364
+ | "small"
365
+ | "base"
366
+ | "large"
367
+ | "extraLarge"
368
+ | "unlimited";
@@ -0,0 +1,13 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
3
+
4
+ /**
5
+ * This is a workaround suggested by react-native-gesture-handler to prevent
6
+ * accidental highlighting of text in Android devices
7
+ * https://github.com/software-mansion/react-native-gesture-handler/issues/1372
8
+ */
9
+ export function TypographyGestureDetector(
10
+ props: PropsWithChildren<unknown>,
11
+ ): JSX.Element {
12
+ return <GestureDetector {...props} gesture={Gesture.Native()} />;
13
+ }
@@ -0,0 +1,22 @@
1
+ export { Typography } from "./Typography";
2
+ export type {
3
+ FontFamily,
4
+ FontStyle,
5
+ FontWeight,
6
+ BaseWeight,
7
+ DisplayWeight,
8
+ BaseStyle,
9
+ DisplayStyle,
10
+ TextColor,
11
+ TextTransform,
12
+ TextSize,
13
+ TextAlign,
14
+ LineHeight,
15
+ LetterSpacing,
16
+ TextAccessibilityRole,
17
+ TextVariation,
18
+ TypographyProps,
19
+ TruncateLength,
20
+ } from "./Typography";
21
+ export { typographyStyles } from "./Typography.style";
22
+ export { TypographyGestureDetector } from "./TypographyGestureDetector";
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./Icon";
2
2
  export * from "./Divider";
3
+ export * from "./Typography";
@@ -0,0 +1,8 @@
1
+ import { JobberStyle as mobileFoundationBase } from "@jobber/design/foundation.native";
2
+ import { Platform } from "react-native";
3
+
4
+ export const tokens: typeof mobileFoundationBase = Platform.select({
5
+ ios: () => require("@jobber/design/foundation.ios").JobberStyle,
6
+ android: () => require("@jobber/design/foundation.android").JobberStyle,
7
+ default: () => require("@jobber/design/foundation.native").JobberStyle,
8
+ })();
@@ -0,0 +1,6 @@
1
+ export function capitalize(text: string): string {
2
+ if (!text) {
3
+ return "";
4
+ }
5
+ return text.charAt(0).toLocaleUpperCase("en-US") + text.slice(1);
6
+ }
@@ -0,0 +1 @@
1
+ export { capitalize } from "./capitalize";