@jobber/components-native 0.5.0 → 0.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 (27) hide show
  1. package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.js +41 -0
  2. package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.style.js +31 -0
  3. package/dist/src/ErrorMessageWrapper/context/ErrorMessageContext.js +11 -0
  4. package/dist/src/ErrorMessageWrapper/context/ErrorMessageProvider.js +38 -0
  5. package/dist/src/ErrorMessageWrapper/context/index.js +2 -0
  6. package/dist/src/ErrorMessageWrapper/context/types.js +1 -0
  7. package/dist/src/ErrorMessageWrapper/index.js +2 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.d.ts +21 -0
  11. package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.style.d.ts +29 -0
  12. package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageContext.d.ts +4 -0
  13. package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageProvider.d.ts +6 -0
  14. package/dist/types/src/ErrorMessageWrapper/context/index.d.ts +2 -0
  15. package/dist/types/src/ErrorMessageWrapper/context/types.d.ts +62 -0
  16. package/dist/types/src/ErrorMessageWrapper/index.d.ts +2 -0
  17. package/dist/types/src/index.d.ts +1 -0
  18. package/package.json +4 -2
  19. package/src/ErrorMessageWrapper/ErrorMessageWrapper.style.ts +32 -0
  20. package/src/ErrorMessageWrapper/ErrorMessageWrapper.test.tsx +48 -0
  21. package/src/ErrorMessageWrapper/ErrorMessageWrapper.tsx +88 -0
  22. package/src/ErrorMessageWrapper/context/ErrorMessageContext.tsx +15 -0
  23. package/src/ErrorMessageWrapper/context/ErrorMessageProvider.tsx +71 -0
  24. package/src/ErrorMessageWrapper/context/index.ts +2 -0
  25. package/src/ErrorMessageWrapper/context/types.ts +71 -0
  26. package/src/ErrorMessageWrapper/index.ts +6 -0
  27. package/src/index.ts +1 -0
@@ -0,0 +1,21 @@
1
+ import { ReactNode } from "react";
2
+ type WrapForTypes = "card" | "default";
3
+ interface ErrorMessageWrapperProps {
4
+ /**
5
+ * The message that shows up below the children
6
+ */
7
+ readonly message?: string;
8
+ /**
9
+ * Changes how it gets laid out on the UI
10
+ */
11
+ readonly wrapFor?: WrapForTypes;
12
+ readonly children: ReactNode;
13
+ }
14
+ /**
15
+ * Adds an error message below the children but ensure the message gets read
16
+ * out first.
17
+ *
18
+ * This component is internal to Atlantis and shouldn't be used outside of it.
19
+ */
20
+ export declare function ErrorMessageWrapper({ message, wrapFor, children, }: ErrorMessageWrapperProps): JSX.Element;
21
+ export {};
@@ -0,0 +1,29 @@
1
+ export declare const styles: {
2
+ wrapper: {
3
+ position: "relative";
4
+ width: string;
5
+ };
6
+ wrapForCard: {
7
+ paddingHorizontal: number;
8
+ paddingVertical: number;
9
+ };
10
+ messageWrapper: {
11
+ flexDirection: "row";
12
+ };
13
+ messageWrapperIcon: {
14
+ flex: number;
15
+ flexBasis: string;
16
+ paddingTop: number;
17
+ paddingRight: number;
18
+ };
19
+ messageWrapperContent: {
20
+ flex: number;
21
+ };
22
+ screenReaderMessage: {
23
+ position: "absolute";
24
+ top: number;
25
+ left: number;
26
+ width: string;
27
+ height: string;
28
+ };
29
+ };
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ import { ErrorMessageContextProps } from "./types";
3
+ export declare const ErrorMessageContext: import("react").Context<ErrorMessageContextProps>;
4
+ export declare function useErrorMessageContext(): ErrorMessageContextProps;
@@ -0,0 +1,6 @@
1
+ import { ReactNode } from "react";
2
+ interface ErrorMessageProviderProps {
3
+ readonly children: ReactNode;
4
+ }
5
+ export declare function ErrorMessageProvider({ children, }: ErrorMessageProviderProps): JSX.Element;
6
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from "./ErrorMessageContext";
2
+ export * from "./ErrorMessageProvider";
@@ -0,0 +1,62 @@
1
+ import { RefObject } from "react";
2
+ import { NativeMethods, View } from "react-native";
3
+ interface Methods {
4
+ /**
5
+ * Requires the method that returns
6
+ * - x
7
+ * - y
8
+ * - width
9
+ * - height
10
+ *
11
+ * This determines the location of the element on screen.
12
+ */
13
+ readonly measure: NativeMethods["measureLayout"];
14
+ /**
15
+ * Requires a method that makes accessible element be focused.
16
+ *
17
+ * **Example**
18
+ * ```
19
+ * function accessibilityFocus() {
20
+ * const reactTag = findNodeHandle(ref.current);
21
+ * AccessibilityInfo.setAccessibilityFocus(reactTag);
22
+ * }
23
+ * ```
24
+ */
25
+ readonly accessibilityFocus: () => void;
26
+ /**
27
+ * Check if the registered element has an error.
28
+ */
29
+ readonly hasErrorMessage: boolean;
30
+ }
31
+ export interface Element {
32
+ /**
33
+ * Used to easily identify the registered element so it's easier to modify or
34
+ * unregister it.
35
+ */
36
+ readonly id: string;
37
+ /**
38
+ * Information about the element that you can access.
39
+ */
40
+ readonly methods: Methods;
41
+ }
42
+ type ElementID = Element["id"];
43
+ export interface ErrorMessageContextRegisterParams {
44
+ readonly id: ElementID;
45
+ readonly hasErrorMessage: Methods["hasErrorMessage"];
46
+ readonly ref: RefObject<View>;
47
+ }
48
+ export interface ErrorMessageContextProps {
49
+ /**
50
+ * Registered elements.
51
+ */
52
+ readonly elements: Record<ElementID, Element["methods"]>;
53
+ /**
54
+ * Registers the element to the context.
55
+ */
56
+ readonly register: (params: ErrorMessageContextRegisterParams) => void;
57
+ /**
58
+ * Un-registers the element from the context.
59
+ */
60
+ readonly unregister: (id: ElementID) => void;
61
+ }
62
+ export {};
@@ -0,0 +1,2 @@
1
+ export { ErrorMessageWrapper } from "./ErrorMessageWrapper";
2
+ export { useErrorMessageContext, ErrorMessageContext, ErrorMessageProvider, } from "./context";
@@ -2,6 +2,7 @@ export * from "./Icon";
2
2
  export * from "./Divider";
3
3
  export * from "./Typography";
4
4
  export * from "./Text";
5
+ export * from "./ErrorMessageWrapper";
5
6
  export * from "./ActionLabel";
6
7
  export * from "./Content";
7
8
  export * from "./ActivityIndicator";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/src/index.js",
6
6
  "module": "dist/src/index.js",
@@ -24,6 +24,7 @@
24
24
  "@jobber/design": "^0.39.0",
25
25
  "react-native-gesture-handler": "^2.5.0",
26
26
  "react-native-svg": "^13.9.0",
27
+ "react-native-uuid": "^1.4.9",
27
28
  "ts-xor": "^1.1.0"
28
29
  },
29
30
  "devDependencies": {
@@ -31,6 +32,7 @@
31
32
  "@testing-library/react-native": "^12.0.1",
32
33
  "@types/react": "^18.0.28",
33
34
  "@types/react-native": "^0.71.6",
35
+ "@types/react-native-uuid": "^1.4.0",
34
36
  "metro-react-native-babel-preset": "^0.76.0",
35
37
  "react-test-renderer": "^18.2.0",
36
38
  "typescript": "^4.9.5"
@@ -40,5 +42,5 @@
40
42
  "react": "^18",
41
43
  "react-native": ">=0.69.2"
42
44
  },
43
- "gitHead": "28a0933c17617009875afd1895e58131f5036337"
45
+ "gitHead": "e74d1b1bae286e2c18d1b84592f2ef569d0cfe36"
44
46
  }
@@ -0,0 +1,32 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ wrapper: {
6
+ position: "relative",
7
+ width: "100%",
8
+ },
9
+ wrapForCard: {
10
+ paddingHorizontal: tokens["space-base"],
11
+ paddingVertical: tokens["space-small"],
12
+ },
13
+ messageWrapper: {
14
+ flexDirection: "row",
15
+ },
16
+ messageWrapperIcon: {
17
+ flex: 0,
18
+ flexBasis: "auto",
19
+ paddingTop: tokens["space-minuscule"],
20
+ paddingRight: tokens["space-smaller"],
21
+ },
22
+ messageWrapperContent: {
23
+ flex: 1,
24
+ },
25
+ screenReaderMessage: {
26
+ position: "absolute",
27
+ top: 0,
28
+ left: 0,
29
+ width: "100%",
30
+ height: "100%",
31
+ },
32
+ });
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { ErrorMessageWrapper } from "./ErrorMessageWrapper";
4
+ import { Text } from "../Text";
5
+
6
+ describe("ErrorMessageWrapper", () => {
7
+ it("should show the child, an error message, and an icon", () => {
8
+ const errorMessage = "This is an error message";
9
+ const childText = "Howdy";
10
+ const screen = render(
11
+ <ErrorMessageWrapper message={errorMessage}>
12
+ <Text>{childText}</Text>
13
+ </ErrorMessageWrapper>,
14
+ );
15
+
16
+ expect(screen.getByText(childText)).toBeDefined();
17
+ expect(
18
+ screen.getByText(errorMessage, { includeHiddenElements: true }),
19
+ ).toBeDefined();
20
+ expect(screen.getByTestId("alert")).toBeDefined();
21
+ });
22
+
23
+ it("should show the child, but not an error message and an icon", () => {
24
+ const errorMessage = "This is an error message part 2";
25
+ const childText = "I'm still here";
26
+
27
+ const screen = render(
28
+ <ErrorMessageWrapper message={errorMessage}>
29
+ <Text>{childText}</Text>
30
+ </ErrorMessageWrapper>,
31
+ );
32
+
33
+ expect(screen.getByText(childText)).toBeDefined();
34
+ expect(
35
+ screen.getByText(errorMessage, { includeHiddenElements: true }),
36
+ ).toBeDefined();
37
+ expect(screen.getByTestId("alert")).toBeDefined();
38
+
39
+ screen.rerender(
40
+ <ErrorMessageWrapper>
41
+ <Text>{childText}</Text>
42
+ </ErrorMessageWrapper>,
43
+ );
44
+ expect(screen.getByText(childText)).toBeDefined();
45
+ expect(screen.queryByText(errorMessage)).toBeNull();
46
+ expect(screen.queryByTestId("alert")).toBeNull();
47
+ });
48
+ });
@@ -0,0 +1,88 @@
1
+ import React, { ReactNode, useEffect, useRef } from "react";
2
+ import { View, ViewStyle } from "react-native";
3
+ import { v4 } from "react-native-uuid";
4
+ import { useErrorMessageContext } from "./context";
5
+ import { styles } from "./ErrorMessageWrapper.style";
6
+ import { Icon } from "../Icon";
7
+ import { Text } from "../Text";
8
+
9
+ type WrapForTypes = "card" | "default";
10
+
11
+ interface ErrorMessageWrapperProps {
12
+ /**
13
+ * The message that shows up below the children
14
+ */
15
+ readonly message?: string;
16
+
17
+ /**
18
+ * Changes how it gets laid out on the UI
19
+ */
20
+ readonly wrapFor?: WrapForTypes;
21
+
22
+ readonly children: ReactNode;
23
+ }
24
+
25
+ const wrapForStyle: Record<WrapForTypes, ViewStyle | undefined> = {
26
+ card: styles.wrapForCard,
27
+ default: undefined,
28
+ };
29
+
30
+ /**
31
+ * Adds an error message below the children but ensure the message gets read
32
+ * out first.
33
+ *
34
+ * This component is internal to Atlantis and shouldn't be used outside of it.
35
+ */
36
+ export function ErrorMessageWrapper({
37
+ message,
38
+ wrapFor = "default",
39
+ children,
40
+ }: ErrorMessageWrapperProps): JSX.Element {
41
+ const errorMessageContext = useErrorMessageContext();
42
+ const register = errorMessageContext?.register;
43
+ const unregister = errorMessageContext?.unregister;
44
+ const a11yMessageRef = useRef<View>(null);
45
+ const { current: uuid } = useRef(v4());
46
+
47
+ const hasErrorMessage = Boolean(message);
48
+
49
+ useEffect(() => {
50
+ if (register) {
51
+ register({ id: uuid, ref: a11yMessageRef, hasErrorMessage });
52
+ }
53
+
54
+ if (unregister) {
55
+ return () => unregister(uuid);
56
+ }
57
+ }, [uuid, hasErrorMessage, register, unregister]);
58
+
59
+ return (
60
+ <View style={[styles.wrapper]}>
61
+ {hasErrorMessage && (
62
+ <View
63
+ ref={a11yMessageRef}
64
+ accessible={true}
65
+ accessibilityRole="text"
66
+ accessibilityLabel={message}
67
+ pointerEvents="none"
68
+ style={styles.screenReaderMessage}
69
+ />
70
+ )}
71
+
72
+ {children}
73
+
74
+ {hasErrorMessage && (
75
+ <View style={[styles.messageWrapper, wrapForStyle[wrapFor]]}>
76
+ <View style={styles.messageWrapperIcon}>
77
+ <Icon name="alert" size="small" color="critical" />
78
+ </View>
79
+ <View style={styles.messageWrapperContent}>
80
+ <Text variation="error" level="textSupporting" hideFromScreenReader>
81
+ {message}
82
+ </Text>
83
+ </View>
84
+ </View>
85
+ )}
86
+ </View>
87
+ );
88
+ }
@@ -0,0 +1,15 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { createContext, useContext } from "react";
3
+ import { ErrorMessageContextProps } from "./types";
4
+
5
+ const defaultValues: ErrorMessageContextProps = {
6
+ elements: {},
7
+ register: _ => undefined,
8
+ unregister: _ => undefined,
9
+ };
10
+
11
+ export const ErrorMessageContext = createContext(defaultValues);
12
+
13
+ export function useErrorMessageContext(): ErrorMessageContextProps {
14
+ return useContext(ErrorMessageContext);
15
+ }
@@ -0,0 +1,71 @@
1
+ import React, { ReactNode, RefObject, useState } from "react";
2
+ import {
3
+ AccessibilityInfo,
4
+ NativeMethods,
5
+ View,
6
+ findNodeHandle,
7
+ } from "react-native";
8
+ import { ErrorMessageContext } from "./ErrorMessageContext";
9
+ import {
10
+ Element,
11
+ ErrorMessageContextProps,
12
+ ErrorMessageContextRegisterParams,
13
+ } from "./types";
14
+
15
+ interface ErrorMessageProviderProps {
16
+ readonly children: ReactNode;
17
+ }
18
+
19
+ export function ErrorMessageProvider({
20
+ children,
21
+ }: ErrorMessageProviderProps): JSX.Element {
22
+ const [elements, setElements] = useState<
23
+ ErrorMessageContextProps["elements"]
24
+ >({});
25
+
26
+ return (
27
+ <ErrorMessageContext.Provider
28
+ value={{
29
+ elements,
30
+ register: handleRegister,
31
+ unregister: handleUnregister,
32
+ }}
33
+ >
34
+ {children}
35
+ </ErrorMessageContext.Provider>
36
+ );
37
+
38
+ function handleRegister({
39
+ id,
40
+ ref,
41
+ hasErrorMessage,
42
+ }: ErrorMessageContextRegisterParams) {
43
+ elements[id] = {
44
+ measure: getMeasure(ref),
45
+ accessibilityFocus: getAccessibilityFocus(ref),
46
+ hasErrorMessage,
47
+ };
48
+ setElements(elements);
49
+ }
50
+
51
+ function handleUnregister(id: Element["id"]) {
52
+ delete elements[id];
53
+ setElements(elements);
54
+ }
55
+ }
56
+
57
+ function getMeasure(ref: RefObject<View>) {
58
+ return function measure(...args: Parameters<NativeMethods["measureLayout"]>) {
59
+ ref.current?.measureLayout(...args);
60
+ };
61
+ }
62
+
63
+ function getAccessibilityFocus(ref: RefObject<View>) {
64
+ return function accessibilityFocus() {
65
+ const reactTag = findNodeHandle(ref.current);
66
+ reactTag &&
67
+ setTimeout(() => {
68
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
69
+ }, 0);
70
+ };
71
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./ErrorMessageContext";
2
+ export * from "./ErrorMessageProvider";
@@ -0,0 +1,71 @@
1
+ import { RefObject } from "react";
2
+ import { NativeMethods, View } from "react-native";
3
+
4
+ interface Methods {
5
+ /**
6
+ * Requires the method that returns
7
+ * - x
8
+ * - y
9
+ * - width
10
+ * - height
11
+ *
12
+ * This determines the location of the element on screen.
13
+ */
14
+ readonly measure: NativeMethods["measureLayout"];
15
+
16
+ /**
17
+ * Requires a method that makes accessible element be focused.
18
+ *
19
+ * **Example**
20
+ * ```
21
+ * function accessibilityFocus() {
22
+ * const reactTag = findNodeHandle(ref.current);
23
+ * AccessibilityInfo.setAccessibilityFocus(reactTag);
24
+ * }
25
+ * ```
26
+ */
27
+ readonly accessibilityFocus: () => void;
28
+
29
+ /**
30
+ * Check if the registered element has an error.
31
+ */
32
+ readonly hasErrorMessage: boolean;
33
+ }
34
+
35
+ export interface Element {
36
+ /**
37
+ * Used to easily identify the registered element so it's easier to modify or
38
+ * unregister it.
39
+ */
40
+ readonly id: string;
41
+
42
+ /**
43
+ * Information about the element that you can access.
44
+ */
45
+ readonly methods: Methods;
46
+ }
47
+
48
+ type ElementID = Element["id"];
49
+
50
+ export interface ErrorMessageContextRegisterParams {
51
+ readonly id: ElementID;
52
+ readonly hasErrorMessage: Methods["hasErrorMessage"];
53
+ readonly ref: RefObject<View>;
54
+ }
55
+
56
+ export interface ErrorMessageContextProps {
57
+ /**
58
+ * Registered elements.
59
+ */
60
+ readonly elements: Record<ElementID, Element["methods"]>;
61
+
62
+ /**
63
+ * Registers the element to the context.
64
+ */
65
+ readonly register: (params: ErrorMessageContextRegisterParams) => void;
66
+
67
+ /**
68
+ * Un-registers the element from the context.
69
+ */
70
+ readonly unregister: (id: ElementID) => void;
71
+ }
@@ -0,0 +1,6 @@
1
+ export { ErrorMessageWrapper } from "./ErrorMessageWrapper";
2
+ export {
3
+ useErrorMessageContext,
4
+ ErrorMessageContext,
5
+ ErrorMessageProvider,
6
+ } from "./context";
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./Icon";
2
2
  export * from "./Divider";
3
3
  export * from "./Typography";
4
4
  export * from "./Text";
5
+ export * from "./ErrorMessageWrapper";
5
6
  export * from "./ActionLabel";
6
7
  export * from "./Content";
7
8
  export * from "./ActivityIndicator";