@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.
- package/dist/src/ActionLabel/ActionLabel.js +27 -0
- package/dist/src/ActionLabel/index.js +1 -0
- package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.js +41 -0
- package/dist/src/ErrorMessageWrapper/ErrorMessageWrapper.style.js +31 -0
- package/dist/src/ErrorMessageWrapper/context/ErrorMessageContext.js +11 -0
- package/dist/src/ErrorMessageWrapper/context/ErrorMessageProvider.js +38 -0
- package/dist/src/ErrorMessageWrapper/context/index.js +2 -0
- package/dist/src/ErrorMessageWrapper/context/types.js +1 -0
- package/dist/src/ErrorMessageWrapper/index.js +2 -0
- package/dist/src/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/ActionLabel/ActionLabel.d.ts +28 -0
- package/dist/types/src/ActionLabel/index.d.ts +1 -0
- package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.d.ts +21 -0
- package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.style.d.ts +29 -0
- package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageContext.d.ts +4 -0
- package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageProvider.d.ts +6 -0
- package/dist/types/src/ErrorMessageWrapper/context/index.d.ts +2 -0
- package/dist/types/src/ErrorMessageWrapper/context/types.d.ts +62 -0
- package/dist/types/src/ErrorMessageWrapper/index.d.ts +2 -0
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +4 -2
- package/src/ActionLabel/ActionLabel.test.tsx +129 -0
- package/src/ActionLabel/ActionLabel.tsx +88 -0
- package/src/ActionLabel/index.ts +1 -0
- package/src/ErrorMessageWrapper/ErrorMessageWrapper.style.ts +32 -0
- package/src/ErrorMessageWrapper/ErrorMessageWrapper.test.tsx +48 -0
- package/src/ErrorMessageWrapper/ErrorMessageWrapper.tsx +88 -0
- package/src/ErrorMessageWrapper/context/ErrorMessageContext.tsx +15 -0
- package/src/ErrorMessageWrapper/context/ErrorMessageProvider.tsx +71 -0
- package/src/ErrorMessageWrapper/context/index.ts +2 -0
- package/src/ErrorMessageWrapper/context/types.ts +71 -0
- package/src/ErrorMessageWrapper/index.ts +6 -0
- 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,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 {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "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": "
|
|
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
|
+
}
|