@jobber/components-native 0.45.1 → 0.46.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.
@@ -1,7 +1,24 @@
1
1
  import en from "./locales/en.json";
2
2
  export type I18nKeys = keyof typeof en;
3
3
  export interface useAtlantisI18nValue {
4
+ /**
5
+ * The set locale based on the AtlantisContext.
6
+ */
4
7
  readonly locale: string;
8
+ /**
9
+ * Returns the translated string depending on the locale. This accepts a 2nd
10
+ * param for string interpolation.
11
+ */
5
12
  readonly t: (message: I18nKeys, values?: Record<string, string>) => string;
13
+ /**
14
+ * Returns a formatted date string based on the locale and the `dateFormat`
15
+ * set in AtlantisContext.
16
+ */
17
+ readonly formatDate: (date: Date) => string;
18
+ /**
19
+ * Returns a formatted time string based on the locale and the `timeFormat`
20
+ * set in AtlantisContext.
21
+ */
22
+ readonly formatTime: (date: Date) => string;
6
23
  }
7
24
  export declare function useAtlantisI18n(): useAtlantisI18nValue;
@@ -0,0 +1,6 @@
1
+ interface DateFormatterOptions {
2
+ readonly locale: string;
3
+ readonly timeZone: string;
4
+ }
5
+ export declare function dateFormatter(date: Date, dateTimeFormat: string, { locale, timeZone }: DateFormatterOptions): string;
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -84,5 +84,5 @@
84
84
  "react-native-reanimated": "^2.17.0",
85
85
  "react-native-safe-area-context": "^4.5.2"
86
86
  },
87
- "gitHead": "8764198c93cfc2812f504b6f6509595ad6b3195c"
87
+ "gitHead": "6ff59b16b38fdf784fa15bcfa41eb97181d2f6b0"
88
88
  }
@@ -3,12 +3,9 @@ import DateTimePicker from "react-native-modal-datetime-picker";
3
3
  import { Platform } from "react-native";
4
4
  import { FieldError, UseControllerProps } from "react-hook-form";
5
5
  import { XOR } from "ts-xor";
6
- import { utcToZonedTime } from "date-fns-tz";
7
- import { format as formatDate } from "date-fns";
8
6
  import { Clearable, InputFieldWrapperProps } from "../InputFieldWrapper";
9
7
  import { FormField } from "../FormField";
10
8
  import { InputPressable } from "../InputPressable";
11
- import { useAtlantisContext } from "../AtlantisContext";
12
9
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
13
10
 
14
11
  interface BaseInputDateProps
@@ -153,8 +150,7 @@ function InternalInputDate({
153
150
  accessibilityHint,
154
151
  }: InputDateProps): JSX.Element {
155
152
  const [showPicker, setShowPicker] = useState(false);
156
- const { t, locale } = useAtlantisI18n();
157
- const { timeZone, dateFormat } = useAtlantisContext();
153
+ const { t, locale, formatDate } = useAtlantisI18n();
158
154
 
159
155
  const date = useMemo(() => {
160
156
  if (typeof value === "string") return new Date(value);
@@ -163,12 +159,11 @@ function InternalInputDate({
163
159
 
164
160
  const formattedDate = useMemo(() => {
165
161
  if (date) {
166
- const zonedTime = utcToZonedTime(date, timeZone);
167
- return formatDate(zonedTime, dateFormat);
162
+ return formatDate(date);
168
163
  }
169
164
 
170
165
  return emptyValueLabel;
171
- }, [date, emptyValueLabel, timeZone, dateFormat]);
166
+ }, [date, emptyValueLabel]);
172
167
 
173
168
  const canClearDate = formattedDate === emptyValueLabel ? "never" : clearable;
174
169
 
@@ -3,8 +3,6 @@ import { FieldError, UseControllerProps } from "react-hook-form";
3
3
  import { XOR } from "ts-xor";
4
4
  import DateTimePicker from "react-native-modal-datetime-picker";
5
5
  import { View } from "react-native";
6
- import { utcToZonedTime } from "date-fns-tz";
7
- import { format as formatTime } from "date-fns";
8
6
  import { styles } from "./InputTime.style";
9
7
  import { getTimeZoneOffsetInMinutes, roundUpToNearestMinutes } from "./utils";
10
8
  import { useAtlantisContext } from "../AtlantisContext";
@@ -134,9 +132,9 @@ function InternalInputTime({
134
132
  showIcon = true,
135
133
  }: InputTimeProps): JSX.Element {
136
134
  const [showPicker, setShowPicker] = useState(false);
137
- const { t } = useAtlantisI18n();
138
-
135
+ const { t, formatTime } = useAtlantisI18n();
139
136
  const { timeZone, timeFormat } = useAtlantisContext();
137
+
140
138
  const is24Hour = timeFormat === "HH:mm";
141
139
 
142
140
  const dateTime = useMemo(
@@ -146,12 +144,11 @@ function InternalInputTime({
146
144
 
147
145
  const formattedTime = useMemo(() => {
148
146
  if (dateTime) {
149
- const zonedTime = utcToZonedTime(dateTime, timeZone);
150
- return formatTime(zonedTime, timeFormat);
147
+ return formatTime(dateTime);
151
148
  }
152
149
 
153
150
  return emptyValueLabel;
154
- }, [dateTime, emptyValueLabel, timeZone, timeFormat]);
151
+ }, [dateTime, emptyValueLabel]);
155
152
 
156
153
  const canClearTime = formattedTime === emptyValueLabel ? "never" : clearable;
157
154
 
@@ -10,6 +10,15 @@ jest.mock("../../AtlantisContext", () => ({
10
10
  ...jest.requireActual("../../AtlantisContext"),
11
11
  }));
12
12
 
13
+ const spy = jest.spyOn(context, "useAtlantisContext");
14
+ const testDate = new Date("2020-01-01T00:00:00.000Z");
15
+ const dateAfterSpringForward = new Date("2020-04-10T00:00:00.000Z");
16
+
17
+ beforeEach(() => {
18
+ jest.useFakeTimers();
19
+ jest.setSystemTime(testDate);
20
+ });
21
+
13
22
  describe("useAtlantisI18n", () => {
14
23
  it("should return english by default", () => {
15
24
  const { result } = renderHook(useAtlantisI18n);
@@ -27,7 +36,6 @@ describe("useAtlantisI18n", () => {
27
36
 
28
37
  describe("Español", () => {
29
38
  it("should return español", () => {
30
- const spy = jest.spyOn(context, "useAtlantisContext");
31
39
  spy.mockReturnValueOnce({ ...context.defaultValues, locale: "es" });
32
40
  const { result } = renderHook(useAtlantisI18n);
33
41
 
@@ -37,7 +45,6 @@ describe("useAtlantisI18n", () => {
37
45
 
38
46
  describe("Unsupported language", () => {
39
47
  it("should return the english translation", () => {
40
- const spy = jest.spyOn(context, "useAtlantisContext");
41
48
  spy.mockReturnValueOnce({ ...context.defaultValues, locale: "fr" });
42
49
  const { result } = renderHook(useAtlantisI18n);
43
50
 
@@ -50,4 +57,77 @@ describe("useAtlantisI18n", () => {
50
57
  expect(Object.keys(en)).toEqual(Object.keys(es));
51
58
  });
52
59
  });
60
+
61
+ describe("formatDate", () => {
62
+ it("should return the formatted date", () => {
63
+ const { result } = renderHook(useAtlantisI18n);
64
+ expect(result.current.formatDate(testDate)).toBe("Jan 1, 2020");
65
+ });
66
+
67
+ it("should return the date formatted for es", () => {
68
+ spy.mockReturnValueOnce({ ...context.defaultValues, locale: "es" });
69
+
70
+ const { result } = renderHook(useAtlantisI18n);
71
+ expect(result.current.formatDate(testDate)).toBe("1 ene 2020");
72
+ });
73
+
74
+ describe("Timezone", () => {
75
+ it.each([
76
+ ["America/New_York", "Dec 31, 2019"],
77
+ ["America/Chicago", "Dec 31, 2019"],
78
+ ["America/Denver", "Dec 31, 2019"],
79
+ ["Europe/London", "Jan 1, 2020"],
80
+ ["Australia/Sydney", "Jan 1, 2020"],
81
+ ])("should return the %s time", (timeZone, expected) => {
82
+ spy.mockReturnValueOnce({ ...context.defaultValues, timeZone });
83
+
84
+ const { result } = renderHook(useAtlantisI18n);
85
+ expect(result.current.formatDate(testDate)).toBe(expected);
86
+ });
87
+ });
88
+ });
89
+
90
+ describe("formatTime", () => {
91
+ it("should return the formatted time", () => {
92
+ const { result } = renderHook(useAtlantisI18n);
93
+ expect(result.current.formatTime(testDate)).toBe("12:00 AM");
94
+ });
95
+
96
+ it("should return the date formatted for es", () => {
97
+ spy.mockReturnValueOnce({ ...context.defaultValues, locale: "es" });
98
+
99
+ const { result } = renderHook(useAtlantisI18n);
100
+ expect(result.current.formatTime(testDate)).toBe("00:00");
101
+ });
102
+
103
+ describe("Timezone", () => {
104
+ it.each([
105
+ ["America/New_York", "7:00 PM"],
106
+ ["America/Chicago", "6:00 PM"],
107
+ ["America/Denver", "5:00 PM"],
108
+ ["Europe/London", "12:00 AM"],
109
+ ["Australia/Sydney", "11:00 AM"],
110
+ ])("should return the %s zoned time", (timeZone, expected) => {
111
+ spy.mockReturnValueOnce({ ...context.defaultValues, timeZone });
112
+
113
+ const { result } = renderHook(useAtlantisI18n);
114
+ expect(result.current.formatTime(testDate)).toBe(expected);
115
+ });
116
+
117
+ it.each([
118
+ ["America/New_York", "8:00 PM"],
119
+ ["America/Chicago", "7:00 PM"],
120
+ ["America/Denver", "6:00 PM"],
121
+ ["Europe/London", "1:00 AM"],
122
+ ["Australia/Sydney", "10:00 AM"],
123
+ ])("should return the %s spring zoned time", (timeZone, expected) => {
124
+ spy.mockReturnValueOnce({ ...context.defaultValues, timeZone });
125
+
126
+ const { result } = renderHook(useAtlantisI18n);
127
+ expect(result.current.formatTime(dateAfterSpringForward)).toBe(
128
+ expected,
129
+ );
130
+ });
131
+ });
132
+ });
53
133
  });
@@ -1,20 +1,56 @@
1
+ import { useCallback } from "react";
1
2
  import en from "./locales/en.json";
2
3
  import es from "./locales/es.json";
4
+ import { dateFormatter } from "./utils/dateFormatter";
3
5
  import { useAtlantisContext } from "../../AtlantisContext";
4
6
 
5
7
  export type I18nKeys = keyof typeof en;
6
8
 
7
9
  export interface useAtlantisI18nValue {
10
+ /**
11
+ * The set locale based on the AtlantisContext.
12
+ */
8
13
  readonly locale: string;
14
+
15
+ /**
16
+ * Returns the translated string depending on the locale. This accepts a 2nd
17
+ * param for string interpolation.
18
+ */
9
19
  readonly t: (message: I18nKeys, values?: Record<string, string>) => string;
20
+
21
+ /**
22
+ * Returns a formatted date string based on the locale and the `dateFormat`
23
+ * set in AtlantisContext.
24
+ */
25
+ readonly formatDate: (date: Date) => string;
26
+
27
+ /**
28
+ * Returns a formatted time string based on the locale and the `timeFormat`
29
+ * set in AtlantisContext.
30
+ */
31
+ readonly formatTime: (date: Date) => string;
10
32
  }
11
33
 
12
34
  export function useAtlantisI18n(): useAtlantisI18nValue {
13
- const { locale } = useAtlantisContext();
14
- const t = (messageKey: keyof typeof en, values?: Record<string, string>) =>
15
- formatMessage(messageKey, values, locale);
35
+ const { locale, dateFormat, timeFormat, timeZone } = useAtlantisContext();
36
+
37
+ const t = useCallback(
38
+ (messageKey: keyof typeof en, values?: Record<string, string>) =>
39
+ formatMessage(messageKey, values, locale),
40
+ [formatMessage, locale],
41
+ );
42
+
43
+ const formatDate = useCallback(
44
+ (date: Date) => dateFormatter(date, dateFormat, { locale, timeZone }),
45
+ [dateFormatter, locale],
46
+ );
47
+
48
+ const formatTime = useCallback(
49
+ (date: Date) => dateFormatter(date, timeFormat, { locale, timeZone }),
50
+ [dateFormatter, locale],
51
+ );
16
52
 
17
- return { locale, t };
53
+ return { locale, t, formatDate, formatTime };
18
54
  }
19
55
 
20
56
  function getLocalizedStrings(locale: string): typeof en {
@@ -0,0 +1,31 @@
1
+ import { format } from "date-fns";
2
+ import { utcToZonedTime } from "date-fns-tz";
3
+ import { es } from "date-fns/locale";
4
+
5
+ interface DateFormatterOptions {
6
+ readonly locale: string;
7
+ readonly timeZone: string;
8
+ }
9
+
10
+ export function dateFormatter(
11
+ date: Date,
12
+ dateTimeFormat: string,
13
+ { locale, timeZone }: DateFormatterOptions,
14
+ ): string {
15
+ const zonedTime = utcToZonedTime(date, timeZone);
16
+ return format(zonedTime, dateTimeFormat, {
17
+ locale: getDateFnsLocale(locale),
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Change locale string to date-fns locale object.
23
+ */
24
+ function getDateFnsLocale(locale?: string): Locale | undefined {
25
+ switch (locale) {
26
+ case "es":
27
+ return es;
28
+ default:
29
+ return;
30
+ }
31
+ }