@jobber/components-native 0.4.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 (34) hide show
  1. package/dist/src/ActionLabel/ActionLabel.js +27 -0
  2. package/dist/src/ActionLabel/index.js +1 -0
  3. package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.js +41 -0
  4. package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.style.js +31 -0
  5. package/dist/src/ErrorMessageWrapper/context/ErrorMessageContext.js +11 -0
  6. package/dist/src/ErrorMessageWrapper/context/ErrorMessageProvider.js +38 -0
  7. package/dist/src/ErrorMessageWrapper/context/index.js +2 -0
  8. package/dist/src/ErrorMessageWrapper/context/types.js +1 -0
  9. package/dist/src/ErrorMessageWrapper/index.js +2 -0
  10. package/dist/src/index.js +2 -0
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/dist/types/src/ActionLabel/ActionLabel.d.ts +28 -0
  13. package/dist/types/src/ActionLabel/index.d.ts +1 -0
  14. package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.d.ts +21 -0
  15. package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.style.d.ts +29 -0
  16. package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageContext.d.ts +4 -0
  17. package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageProvider.d.ts +6 -0
  18. package/dist/types/src/ErrorMessageWrapper/context/index.d.ts +2 -0
  19. package/dist/types/src/ErrorMessageWrapper/context/types.d.ts +62 -0
  20. package/dist/types/src/ErrorMessageWrapper/index.d.ts +2 -0
  21. package/dist/types/src/index.d.ts +2 -0
  22. package/package.json +4 -2
  23. package/src/ActionLabel/ActionLabel.test.tsx +129 -0
  24. package/src/ActionLabel/ActionLabel.tsx +88 -0
  25. package/src/ActionLabel/index.ts +1 -0
  26. package/src/ErrorMessageWrapper/ErrorMessageWrapper.style.ts +32 -0
  27. package/src/ErrorMessageWrapper/ErrorMessageWrapper.test.tsx +48 -0
  28. package/src/ErrorMessageWrapper/ErrorMessageWrapper.tsx +88 -0
  29. package/src/ErrorMessageWrapper/context/ErrorMessageContext.tsx +15 -0
  30. package/src/ErrorMessageWrapper/context/ErrorMessageProvider.tsx +71 -0
  31. package/src/ErrorMessageWrapper/context/index.ts +2 -0
  32. package/src/ErrorMessageWrapper/context/types.ts +71 -0
  33. package/src/ErrorMessageWrapper/index.ts +6 -0
  34. package/src/index.ts +2 -0
@@ -0,0 +1,28 @@
1
+ /// <reference types="react" />
2
+ import { TextAlign, TextColor } from "../Typography";
3
+ export type ActionLabelVariation = Extract<TextColor, "interactive" | "destructive" | "learning" | "subtle" | "onPrimary">;
4
+ type ActionLabelType = "default" | "cardTitle";
5
+ interface ActionLabelProps {
6
+ /**
7
+ * Text to display
8
+ */
9
+ readonly children?: string;
10
+ /**
11
+ * Set the display text to disabled color
12
+ */
13
+ readonly disabled?: boolean;
14
+ /**
15
+ * The text color
16
+ */
17
+ readonly variation?: ActionLabelVariation;
18
+ /**
19
+ * Changes the appearance to match the style of where it's getting used
20
+ */
21
+ readonly type?: ActionLabelType;
22
+ /**
23
+ * Alignment of action label
24
+ */
25
+ readonly align?: TextAlign;
26
+ }
27
+ export declare function ActionLabel({ children, variation, type, disabled, align, }: ActionLabelProps): JSX.Element;
28
+ export {};
@@ -0,0 +1 @@
1
+ export { ActionLabel, ActionLabelVariation } from "./ActionLabel";
@@ -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,5 +2,7 @@ export * from "./Icon";
2
2
  export * from "./Divider";
3
3
  export * from "./Typography";
4
4
  export * from "./Text";
5
+ export * from "./ErrorMessageWrapper";
6
+ export * from "./ActionLabel";
5
7
  export * from "./Content";
6
8
  export * from "./ActivityIndicator";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.4.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": "d9e4bbca01e3aa0a05ade2c3251681a416214789"
45
+ "gitHead": "e74d1b1bae286e2c18d1b84592f2ef569d0cfe36"
44
46
  }
@@ -0,0 +1,129 @@
1
+ import React, { CSSProperties } from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { ReactTestInstance } from "react-test-renderer";
4
+ import { ActionLabel } from "./ActionLabel";
5
+ import { tokens } from "../utils/design";
6
+
7
+ const defaultStyles = {
8
+ fontFamily: "inter-extrabold",
9
+ color: tokens["color-interactive"],
10
+ textAlign: "center",
11
+ fontSize: tokens["typography--fontSize-base"],
12
+ lineHeight: tokens["typography--lineHeight-tight"],
13
+ letterSpacing: tokens["typography--letterSpacing-loose"],
14
+ };
15
+
16
+ describe("ActionLabel", () => {
17
+ it("renders the default action label", () => {
18
+ const text = "Default Action Label";
19
+ const { getByText } = render(<ActionLabel>{text}</ActionLabel>);
20
+
21
+ const el = getByText(text);
22
+ expect(el).toBeDefined();
23
+ expect(getStyleObject(el)).toMatchObject(defaultStyles);
24
+ });
25
+
26
+ describe("Variations", () => {
27
+ it("renders a destructive variation", () => {
28
+ const text = "Destructive Action Label";
29
+ const { getByText } = render(
30
+ <ActionLabel variation="destructive">{text}</ActionLabel>,
31
+ );
32
+
33
+ expect(getStyleObject(getByText(text))).toMatchObject({
34
+ ...defaultStyles,
35
+ color: tokens["color-destructive"],
36
+ });
37
+ });
38
+
39
+ it("renders a learning variation", () => {
40
+ const text = "Learning Action Label";
41
+ const { getByText } = render(
42
+ <ActionLabel variation="learning">{text}</ActionLabel>,
43
+ );
44
+
45
+ expect(getStyleObject(getByText(text))).toMatchObject({
46
+ ...defaultStyles,
47
+ color: tokens["color-informative"],
48
+ });
49
+ });
50
+
51
+ it("renders a subtle variation", () => {
52
+ const text = "Subtle Action Label";
53
+ const { getByText } = render(
54
+ <ActionLabel variation="subtle">{text}</ActionLabel>,
55
+ );
56
+
57
+ expect(getStyleObject(getByText(text))).toMatchObject({
58
+ ...defaultStyles,
59
+ color: tokens["color-interactive--subtle"],
60
+ });
61
+ });
62
+
63
+ it("renders an onPrimary variation", () => {
64
+ const text = "onPrimary Action Label";
65
+ const { getByText } = render(
66
+ <ActionLabel variation="onPrimary">{text}</ActionLabel>,
67
+ );
68
+
69
+ expect(getStyleObject(getByText(text))).toMatchObject({
70
+ ...defaultStyles,
71
+ color: tokens["color-surface"],
72
+ });
73
+ });
74
+ });
75
+
76
+ describe("when action label is disabled", () => {
77
+ it("renders text with disabled color, overriding variation", () => {
78
+ const text = "Disabled Action Label";
79
+ const { getByText } = render(
80
+ <ActionLabel disabled variation="destructive">
81
+ {text}
82
+ </ActionLabel>,
83
+ );
84
+
85
+ const styles = getStyleObject(getByText(text));
86
+ expect(styles).toMatchObject({
87
+ ...defaultStyles,
88
+ color: tokens["color-disabled"],
89
+ });
90
+ expect(styles).not.toHaveProperty("color", tokens["color-destructive"]);
91
+ });
92
+ });
93
+
94
+ describe("when action label is aligned", () => {
95
+ it("renders text with left alignment", () => {
96
+ const text = "Left Aligned Action Label";
97
+ const { getByText } = render(
98
+ <ActionLabel align="start">{text}</ActionLabel>,
99
+ );
100
+
101
+ expect(getStyleObject(getByText(text))).toMatchObject({
102
+ ...defaultStyles,
103
+ textAlign: "left",
104
+ });
105
+ });
106
+
107
+ it("renders text with right alignment", () => {
108
+ const text = "Right Aligned Action Label";
109
+ const { getByText } = render(
110
+ <ActionLabel align="end">{text}</ActionLabel>,
111
+ );
112
+
113
+ expect(getStyleObject(getByText(text))).toMatchObject({
114
+ ...defaultStyles,
115
+ textAlign: "right",
116
+ });
117
+ });
118
+ });
119
+ });
120
+
121
+ function getStyleObject(el: ReactTestInstance) {
122
+ return el.props.style.reduce(
123
+ (mergedStyles: CSSProperties, additionalStyles: CSSProperties) => ({
124
+ ...mergedStyles,
125
+ ...additionalStyles,
126
+ }),
127
+ {},
128
+ );
129
+ }
@@ -0,0 +1,88 @@
1
+ import React from "react";
2
+ import { tokens } from "../utils/design";
3
+ import { TextAlign, TextColor, Typography } from "../Typography";
4
+
5
+ export type ActionLabelVariation = Extract<
6
+ TextColor,
7
+ "interactive" | "destructive" | "learning" | "subtle" | "onPrimary"
8
+ >;
9
+
10
+ type ActionLabelType = "default" | "cardTitle";
11
+
12
+ interface ActionLabelProps {
13
+ /**
14
+ * Text to display
15
+ */
16
+ readonly children?: string;
17
+
18
+ /**
19
+ * Set the display text to disabled color
20
+ */
21
+ readonly disabled?: boolean;
22
+
23
+ /**
24
+ * The text color
25
+ */
26
+ readonly variation?: ActionLabelVariation;
27
+
28
+ /**
29
+ * Changes the appearance to match the style of where it's getting used
30
+ */
31
+ readonly type?: ActionLabelType;
32
+
33
+ /**
34
+ * Alignment of action label
35
+ */
36
+ readonly align?: TextAlign;
37
+ }
38
+
39
+ export function ActionLabel({
40
+ children,
41
+ variation = "interactive",
42
+ type = "default",
43
+ disabled = false,
44
+ align = "center",
45
+ }: ActionLabelProps): JSX.Element {
46
+ return (
47
+ <Typography
48
+ color={getColor(variation, disabled)}
49
+ fontFamily="base"
50
+ size="default"
51
+ fontWeight={getFontWeight(type)}
52
+ align={align}
53
+ lineHeight="tight"
54
+ letterSpacing={getLetterSpacing(type)}
55
+ maxFontScaleSize={tokens["typography--fontSize-large"]}
56
+ selectable={false}
57
+ >
58
+ {children}
59
+ </Typography>
60
+ );
61
+ }
62
+
63
+ function getColor(variation: ActionLabelVariation, disabled: boolean) {
64
+ if (disabled) {
65
+ return "disabled";
66
+ }
67
+ if (variation) {
68
+ return variation;
69
+ }
70
+
71
+ return "interactive";
72
+ }
73
+
74
+ function getFontWeight(type: ActionLabelType) {
75
+ if (type === "cardTitle") {
76
+ return "bold";
77
+ }
78
+
79
+ return "extraBold";
80
+ }
81
+
82
+ function getLetterSpacing(type: ActionLabelType) {
83
+ if (type === "cardTitle") {
84
+ return "base";
85
+ }
86
+
87
+ return "loose";
88
+ }
@@ -0,0 +1 @@
1
+ export { ActionLabel, ActionLabelVariation } from "./ActionLabel";
@@ -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";