@jobber/components-native 0.19.0 → 0.20.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,71 @@
1
+ import React, { Ref } from "react";
2
+ import { IconNames } from "@jobber/design";
3
+ import { FieldError } from "react-hook-form";
4
+ import { Text as NativeText } from "react-native";
5
+ import { Clearable } from "../InputFieldWrapper";
6
+ interface InputPressableProps {
7
+ /**
8
+ * Current value of the component
9
+ */
10
+ readonly value?: string;
11
+ /**
12
+ * Placeholder item shown until a selection is made
13
+ */
14
+ readonly placeholder?: string;
15
+ /**
16
+ * Disables input selection
17
+ */
18
+ readonly disabled?: boolean;
19
+ /**
20
+ * Indicates if there is an validation error
21
+ */
22
+ readonly error?: FieldError;
23
+ /**
24
+ * Indicates the current selection is invalid
25
+ */
26
+ readonly invalid?: boolean | string;
27
+ /**
28
+ * Callback that is called when the text input is focused
29
+ * @param event
30
+ */
31
+ readonly onPress?: () => void;
32
+ /**
33
+ * VoiceOver will read this string when a user selects the element
34
+ */
35
+ readonly accessibilityLabel?: string;
36
+ /**
37
+ * Helps users understand what will happen when they perform an action
38
+ */
39
+ readonly accessibilityHint?: string;
40
+ /**
41
+ * Symbol to display before the text input
42
+ */
43
+ readonly prefix?: {
44
+ icon?: IconNames;
45
+ label?: string;
46
+ };
47
+ /**
48
+ * Symbol to display after the text input
49
+ */
50
+ readonly suffix?: {
51
+ icon?: IconNames;
52
+ label?: string;
53
+ };
54
+ /**
55
+ * Add a clear action on the input that clears the value.
56
+ *
57
+ * Since the input value isn't editable (i.e. `InputDateTime`) you can
58
+ * set it to `always`. If you set it to `always` you must also provide an
59
+ * onClear to clear the input's value
60
+ */
61
+ readonly clearable?: Clearable;
62
+ /**
63
+ * Callback called when the user clicks the ClearAction button. Should clear
64
+ * the value passed. To disallow clearing set the clearable prop to never
65
+ */
66
+ readonly onClear?: () => void;
67
+ }
68
+ export type InputPressableRef = NativeText;
69
+ export declare const InputPressable: React.ForwardRefExoticComponent<InputPressableProps & React.RefAttributes<NativeText>>;
70
+ export declare function InputPressableInternal({ value, placeholder, disabled, invalid, error, onPress, accessibilityLabel, accessibilityHint, prefix, suffix, clearable, onClear, }: InputPressableProps, ref: Ref<InputPressableRef>): JSX.Element;
71
+ export {};
@@ -0,0 +1,18 @@
1
+ export declare const styles: {
2
+ pressable: {
3
+ flex: number;
4
+ };
5
+ inputPressableStyles: {
6
+ paddingTop: number;
7
+ lineHeight: number | undefined;
8
+ };
9
+ inputEmpty: {
10
+ paddingTop: number;
11
+ };
12
+ inputDisabled: {
13
+ color: import("react-native").ColorValue | undefined;
14
+ };
15
+ inputInvalid: {
16
+ borderColor: string;
17
+ };
18
+ };
@@ -0,0 +1,2 @@
1
+ export { InputPressable } from "./InputPressable";
2
+ export type { InputPressableRef } from "./InputPressable";
@@ -13,6 +13,7 @@ export * from "./Heading";
13
13
  export * from "./Icon";
14
14
  export * from "./IconButton";
15
15
  export * from "./InputFieldWrapper";
16
+ export * from "./InputPressable";
16
17
  export * from "./ProgressBar";
17
18
  export * from "./StatusLabel";
18
19
  export * from "./Text";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/src/index.js",
6
6
  "module": "dist/src/index.js",
@@ -49,5 +49,5 @@
49
49
  "react": "^18",
50
50
  "react-native": ">=0.69.2"
51
51
  },
52
- "gitHead": "e112ab079fb049e3ef003bb4c27ccae64cd75991"
52
+ "gitHead": "d9b821d3c4b8557d631813a0199c3d0946d192b9"
53
53
  }
@@ -0,0 +1,29 @@
1
+ import { JobberStyle } from "@jobber/design/foundation";
2
+ import { StyleSheet } from "react-native";
3
+ import { typographyStyles } from "../Typography/Typography.style";
4
+
5
+ const miniLabelFontSize = typographyStyles.smallSize.fontSize || 0;
6
+ const miniLabelPadding = JobberStyle["space-small"];
7
+
8
+ export const styles = StyleSheet.create({
9
+ pressable: {
10
+ flex: 1,
11
+ },
12
+
13
+ inputPressableStyles: {
14
+ paddingTop: miniLabelPadding + miniLabelFontSize,
15
+ lineHeight: typographyStyles.defaultSize.lineHeight,
16
+ },
17
+
18
+ inputEmpty: {
19
+ paddingTop: 0,
20
+ },
21
+
22
+ inputDisabled: {
23
+ color: typographyStyles.disabled.color,
24
+ },
25
+
26
+ inputInvalid: {
27
+ borderColor: JobberStyle["color-critical"],
28
+ },
29
+ });
@@ -0,0 +1,144 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { InputPressable } from ".";
4
+ import { InputFieldWrapperProps } from "../InputFieldWrapper";
5
+
6
+ const MockInputFieldWrapper = jest.fn();
7
+ jest.mock("../InputFieldWrapper", () => ({
8
+ ...jest.requireActual("../InputFieldWrapper"),
9
+ InputFieldWrapper: function Mock(props: InputFieldWrapperProps) {
10
+ MockInputFieldWrapper(props);
11
+ return jest.requireActual("../InputFieldWrapper").InputFieldWrapper(props);
12
+ },
13
+ }));
14
+
15
+ describe("InputPressable", () => {
16
+ describe("InputFieldWrapper gets the expected props", () => {
17
+ it("renders an invalid InputPressable", () => {
18
+ const props = { invalid: true };
19
+ render(<InputPressable {...props} />);
20
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
21
+ expect.objectContaining(props),
22
+ );
23
+ });
24
+
25
+ it("renders an invalid InputPressable with text", () => {
26
+ const props = { invalid: "this test is invalid" };
27
+ render(<InputPressable {...props} />);
28
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
29
+ expect.objectContaining(props),
30
+ );
31
+ });
32
+
33
+ it("renders a valid InputText with empty string", () => {
34
+ const props = { invalid: "" };
35
+ render(<InputPressable {...props} />);
36
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
37
+ expect.objectContaining(props),
38
+ );
39
+ });
40
+ it("renders a disabled InputPressable", () => {
41
+ const props = { disabled: true };
42
+ render(<InputPressable {...props} />);
43
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
44
+ expect.objectContaining(props),
45
+ );
46
+ });
47
+
48
+ it("renders an InputPressable with a placeholder", () => {
49
+ const props = { placeholder: "test placeholder" };
50
+ render(<InputPressable {...props} />);
51
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
52
+ expect.objectContaining(props),
53
+ );
54
+ });
55
+ });
56
+
57
+ it("renders an InputPressable with a value", () => {
58
+ const value = "test value";
59
+ const { getByText } = render(<InputPressable value={value} />);
60
+ expect(getByText(value, { includeHiddenElements: true })).toBeTruthy();
61
+ });
62
+
63
+ it("renders a prefix label when specified", () => {
64
+ const label = "test label";
65
+ const { getByText } = render(
66
+ <InputPressable prefix={{ label }} value="hey" />,
67
+ );
68
+ expect(getByText(label)).toBeDefined();
69
+ });
70
+
71
+ it("renders a prefix icon when specified", () => {
72
+ const { getByTestId } = render(
73
+ <InputPressable prefix={{ icon: "calendar" }} />,
74
+ );
75
+ expect(getByTestId("calendar")).toBeDefined();
76
+ });
77
+
78
+ it("renders a suffix icon when specified", () => {
79
+ const { getByTestId } = render(
80
+ <InputPressable suffix={{ icon: "calendar" }} />,
81
+ );
82
+ expect(getByTestId("calendar")).toBeDefined();
83
+ });
84
+
85
+ it("calls onPress when pressed", () => {
86
+ const onPressMock = jest.fn();
87
+ const a11yLabel = "test InputPressable";
88
+ const { getByLabelText } = render(
89
+ <InputPressable
90
+ onPress={onPressMock}
91
+ value="test value"
92
+ accessibilityLabel={a11yLabel}
93
+ />,
94
+ );
95
+
96
+ fireEvent.press(getByLabelText(a11yLabel));
97
+ expect(onPressMock).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ describe("when given a value", () => {
101
+ const value = "A value";
102
+
103
+ it("renders an InputPressable with the value", () => {
104
+ const { getByText } = render(<InputPressable value={value} />);
105
+ expect(getByText(value, { includeHiddenElements: true })).toBeDefined();
106
+ });
107
+
108
+ it("renders a prefix label when specified", () => {
109
+ const { getByText } = render(
110
+ <InputPressable prefix={{ label: "test" }} value={value} />,
111
+ );
112
+ expect(getByText(value, { includeHiddenElements: true })).toBeDefined();
113
+ });
114
+
115
+ it("renders a suffix label when specified", () => {
116
+ const { getByText } = render(
117
+ <InputPressable suffix={{ label: "test" }} value={value} />,
118
+ );
119
+ expect(getByText(value, { includeHiddenElements: true })).toBeDefined();
120
+ });
121
+ });
122
+ });
123
+
124
+ describe("accessibilityLabel", () => {
125
+ it("uses accessibilityLabel if specified", () => {
126
+ const { getByLabelText } = render(
127
+ <InputPressable
128
+ value="test value"
129
+ placeholder="placeholder"
130
+ accessibilityLabel="accessibilityLabel"
131
+ />,
132
+ );
133
+
134
+ expect(getByLabelText("accessibilityLabel")).toBeTruthy();
135
+ });
136
+
137
+ it("uses placeholder if unspecified", () => {
138
+ const { getByLabelText } = render(
139
+ <InputPressable value="test value" placeholder="placeholder" />,
140
+ );
141
+
142
+ expect(getByLabelText("placeholder")).toBeTruthy();
143
+ });
144
+ });
@@ -0,0 +1,161 @@
1
+ import React, { Ref, forwardRef, useEffect, useState } from "react";
2
+ import { IconNames } from "@jobber/design";
3
+ import { FieldError } from "react-hook-form";
4
+ import { Text as NativeText, Pressable } from "react-native";
5
+ import { styles } from "./InputPressable.style";
6
+ import {
7
+ Clearable,
8
+ InputFieldWrapper,
9
+ commonInputStyles,
10
+ useShowClear,
11
+ } from "../InputFieldWrapper";
12
+
13
+ interface InputPressableProps {
14
+ /**
15
+ * Current value of the component
16
+ */
17
+ readonly value?: string;
18
+
19
+ /**
20
+ * Placeholder item shown until a selection is made
21
+ */
22
+ readonly placeholder?: string;
23
+
24
+ /**
25
+ * Disables input selection
26
+ */
27
+ readonly disabled?: boolean;
28
+
29
+ /**
30
+ * Indicates if there is an validation error
31
+ */
32
+ readonly error?: FieldError;
33
+
34
+ /**
35
+ * Indicates the current selection is invalid
36
+ */
37
+ readonly invalid?: boolean | string;
38
+
39
+ /**
40
+ * Callback that is called when the text input is focused
41
+ * @param event
42
+ */
43
+ readonly onPress?: () => void;
44
+
45
+ /**
46
+ * VoiceOver will read this string when a user selects the element
47
+ */
48
+ readonly accessibilityLabel?: string;
49
+
50
+ /**
51
+ * Helps users understand what will happen when they perform an action
52
+ */
53
+ readonly accessibilityHint?: string;
54
+
55
+ /**
56
+ * Symbol to display before the text input
57
+ */
58
+ readonly prefix?: {
59
+ icon?: IconNames;
60
+ label?: string;
61
+ };
62
+
63
+ /**
64
+ * Symbol to display after the text input
65
+ */
66
+ readonly suffix?: {
67
+ icon?: IconNames;
68
+ label?: string;
69
+ };
70
+ /**
71
+ * Add a clear action on the input that clears the value.
72
+ *
73
+ * Since the input value isn't editable (i.e. `InputDateTime`) you can
74
+ * set it to `always`. If you set it to `always` you must also provide an
75
+ * onClear to clear the input's value
76
+ */
77
+ readonly clearable?: Clearable;
78
+ /**
79
+ * Callback called when the user clicks the ClearAction button. Should clear
80
+ * the value passed. To disallow clearing set the clearable prop to never
81
+ */
82
+ readonly onClear?: () => void;
83
+ }
84
+
85
+ export type InputPressableRef = NativeText;
86
+ export const InputPressable = forwardRef(InputPressableInternal);
87
+
88
+ export function InputPressableInternal(
89
+ {
90
+ value,
91
+ placeholder,
92
+ disabled,
93
+ invalid,
94
+ error,
95
+ onPress,
96
+ accessibilityLabel,
97
+ accessibilityHint,
98
+ prefix,
99
+ suffix,
100
+ clearable = "never",
101
+ onClear,
102
+ }: InputPressableProps,
103
+ ref: Ref<InputPressableRef>,
104
+ ): JSX.Element {
105
+ const hasValue = !!value;
106
+ const [hasMiniLabel, setHasMiniLabel] = useState(Boolean(value));
107
+
108
+ useEffect(() => {
109
+ setHasMiniLabel(Boolean(value));
110
+ }, [value]);
111
+
112
+ const showClear = useShowClear({
113
+ clearable,
114
+ multiline: false,
115
+ focused: false,
116
+ hasValue,
117
+ disabled,
118
+ });
119
+
120
+ return (
121
+ <InputFieldWrapper
122
+ prefix={prefix}
123
+ suffix={suffix}
124
+ hasValue={hasValue}
125
+ hasMiniLabel={hasMiniLabel}
126
+ focused={false}
127
+ error={error}
128
+ invalid={invalid}
129
+ placeholder={placeholder}
130
+ disabled={disabled}
131
+ showClearAction={showClear}
132
+ onClear={onClear}
133
+ >
134
+ <Pressable
135
+ style={styles.pressable}
136
+ onPress={onPress}
137
+ disabled={disabled}
138
+ accessibilityLabel={accessibilityLabel || placeholder}
139
+ accessibilityHint={accessibilityHint}
140
+ accessibilityValue={{ text: value }}
141
+ >
142
+ <NativeText
143
+ style={[
144
+ commonInputStyles.input,
145
+ styles.inputPressableStyles,
146
+ !hasMiniLabel && commonInputStyles.inputEmpty,
147
+ disabled && commonInputStyles.inputDisabled,
148
+ (Boolean(invalid) || error) && styles.inputInvalid,
149
+ ]}
150
+ testID="inputPressableText"
151
+ ref={ref}
152
+ accessibilityRole="none"
153
+ accessible={false}
154
+ importantForAccessibility="no-hide-descendants"
155
+ >
156
+ {value}
157
+ </NativeText>
158
+ </Pressable>
159
+ </InputFieldWrapper>
160
+ );
161
+ }
@@ -0,0 +1,2 @@
1
+ export { InputPressable } from "./InputPressable";
2
+ export type { InputPressableRef } from "./InputPressable";
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from "./Heading";
13
13
  export * from "./Icon";
14
14
  export * from "./IconButton";
15
15
  export * from "./InputFieldWrapper";
16
+ export * from "./InputPressable";
16
17
  export * from "./ProgressBar";
17
18
  export * from "./StatusLabel";
18
19
  export * from "./Text";