@jobber/components-native 0.9.0 → 0.10.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/InputFieldWrapper/CommonInputStyles.style.js +33 -0
- package/dist/src/InputFieldWrapper/InputFieldWrapper.js +88 -0
- package/dist/src/InputFieldWrapper/InputFieldWrapper.style.js +79 -0
- package/dist/src/InputFieldWrapper/components/ClearAction/ClearAction.js +12 -0
- package/dist/src/InputFieldWrapper/components/ClearAction/ClearAction.style.js +25 -0
- package/dist/src/InputFieldWrapper/components/ClearAction/index.js +2 -0
- package/dist/src/InputFieldWrapper/components/ClearAction/messages.js +8 -0
- package/dist/src/InputFieldWrapper/components/Prefix/Prefix.js +34 -0
- package/dist/src/InputFieldWrapper/components/Suffix/Suffix.js +35 -0
- package/dist/src/InputFieldWrapper/hooks/useShowClear.js +15 -0
- package/dist/src/InputFieldWrapper/index.js +3 -0
- package/dist/src/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/InputFieldWrapper/CommonInputStyles.style.d.ts +30 -0
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +63 -0
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.style.d.ts +82 -0
- package/dist/types/src/InputFieldWrapper/components/ClearAction/ClearAction.d.ts +10 -0
- package/dist/types/src/InputFieldWrapper/components/ClearAction/ClearAction.style.d.ts +22 -0
- package/dist/types/src/InputFieldWrapper/components/ClearAction/index.d.ts +2 -0
- package/dist/types/src/InputFieldWrapper/components/ClearAction/messages.d.ts +7 -0
- package/dist/types/src/InputFieldWrapper/components/Prefix/Prefix.d.ts +23 -0
- package/dist/types/src/InputFieldWrapper/components/Suffix/Suffix.d.ts +25 -0
- package/dist/types/src/InputFieldWrapper/hooks/useShowClear.d.ts +10 -0
- package/dist/types/src/InputFieldWrapper/index.d.ts +4 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +4 -2
- package/src/InputFieldWrapper/CommonInputStyles.style.ts +37 -0
- package/src/InputFieldWrapper/InputFieldWrapper.style.ts +93 -0
- package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +243 -0
- package/src/InputFieldWrapper/InputFieldWrapper.tsx +317 -0
- package/src/InputFieldWrapper/components/ClearAction/ClearAction.style.ts +27 -0
- package/src/InputFieldWrapper/components/ClearAction/ClearAction.test.tsx +15 -0
- package/src/InputFieldWrapper/components/ClearAction/ClearAction.tsx +32 -0
- package/src/InputFieldWrapper/components/ClearAction/index.ts +2 -0
- package/src/InputFieldWrapper/components/ClearAction/messages.ts +9 -0
- package/src/InputFieldWrapper/components/Prefix/Prefix.test.tsx +221 -0
- package/src/InputFieldWrapper/components/Prefix/Prefix.tsx +104 -0
- package/src/InputFieldWrapper/components/Suffix/Suffix.test.tsx +101 -0
- package/src/InputFieldWrapper/components/Suffix/Suffix.tsx +113 -0
- package/src/InputFieldWrapper/hooks/useShowClear.test.ts +158 -0
- package/src/InputFieldWrapper/hooks/useShowClear.ts +31 -0
- package/src/InputFieldWrapper/index.ts +4 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { RenderAPI, fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
// eslint-disable-next-line no-restricted-imports
|
|
4
|
+
import { Text, ViewStyle } from "react-native";
|
|
5
|
+
import { useIntl } from "react-intl";
|
|
6
|
+
import {
|
|
7
|
+
InputFieldWrapper,
|
|
8
|
+
InputFieldWrapperProps,
|
|
9
|
+
commonInputStyles,
|
|
10
|
+
} from ".";
|
|
11
|
+
import { messages as clearMessages } from "./components/ClearAction";
|
|
12
|
+
import { styles } from "./InputFieldWrapper.style";
|
|
13
|
+
import { typographyStyles } from "../Typography";
|
|
14
|
+
|
|
15
|
+
const mockLabel = { label: "$" };
|
|
16
|
+
|
|
17
|
+
type InputFieldWrapperTestProps = Omit<InputFieldWrapperProps, "children">;
|
|
18
|
+
|
|
19
|
+
function renderInputFieldWrapper(props: InputFieldWrapperTestProps): RenderAPI {
|
|
20
|
+
return render(
|
|
21
|
+
<InputFieldWrapper {...props}>
|
|
22
|
+
<Text>Test</Text>
|
|
23
|
+
</InputFieldWrapper>,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderWithPrefixLabel(hasValue: boolean): RenderAPI {
|
|
28
|
+
return renderInputFieldWrapper({ prefix: mockLabel, hasValue });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderWithSuffixLabel(hasValue: boolean): RenderAPI {
|
|
32
|
+
return renderInputFieldWrapper({ suffix: mockLabel, hasValue });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("InputFieldWrapper", () => {
|
|
36
|
+
it("renders an invalid InputFieldWrapper", () => {
|
|
37
|
+
const { getByTestId } = renderInputFieldWrapper({ invalid: true });
|
|
38
|
+
|
|
39
|
+
expect(getByTestId("ATL-InputFieldWrapper").props.style).toContainEqual(
|
|
40
|
+
styles.inputInvalid,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders an invalid InputFieldWrapper with non-empty string", () => {
|
|
45
|
+
const invalid = "invalid string test";
|
|
46
|
+
const { getByText, getByTestId } = renderInputFieldWrapper({
|
|
47
|
+
invalid,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(getByTestId("ATL-InputFieldWrapper").props.style).toContainEqual(
|
|
51
|
+
styles.inputInvalid,
|
|
52
|
+
);
|
|
53
|
+
expect(getByText(invalid, { includeHiddenElements: true })).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders a valid InputFieldWrapper with empty string", () => {
|
|
57
|
+
const { getByTestId } = renderInputFieldWrapper({ invalid: "" });
|
|
58
|
+
|
|
59
|
+
expect(getByTestId("ATL-InputFieldWrapper").props.style).not.toContainEqual(
|
|
60
|
+
styles.inputInvalid,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders a disabled InputFieldWrapper", () => {
|
|
65
|
+
const { getByTestId } = renderInputFieldWrapper({ disabled: true });
|
|
66
|
+
|
|
67
|
+
expect(getByTestId("ATL-InputFieldWrapper").props.style).toContainEqual({
|
|
68
|
+
backgroundColor: "rgb(225, 225, 225)",
|
|
69
|
+
borderTopLeftRadius: 4,
|
|
70
|
+
borderTopRightRadius: 4,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders a InputFieldWrapper with placeholder", () => {
|
|
75
|
+
const placeholder = "test";
|
|
76
|
+
const { getByText } = renderInputFieldWrapper({ placeholder });
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
getByText(placeholder, { includeHiddenElements: true }),
|
|
80
|
+
).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders a prefix icon when specified", () => {
|
|
84
|
+
const { getByTestId } = renderInputFieldWrapper({
|
|
85
|
+
prefix: { icon: "invoice" },
|
|
86
|
+
});
|
|
87
|
+
expect(getByTestId("invoice")).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders a suffix icon when specified", () => {
|
|
91
|
+
const { getByTestId } = renderInputFieldWrapper({
|
|
92
|
+
suffix: { icon: "invoice" },
|
|
93
|
+
});
|
|
94
|
+
expect(getByTestId("invoice")).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("when hasValue", () => {
|
|
98
|
+
it("renders a prefix label", () => {
|
|
99
|
+
const { getByText } = renderWithPrefixLabel(true);
|
|
100
|
+
expect(getByText(mockLabel.label)).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders a suffix label", () => {
|
|
104
|
+
const { getByText } = renderWithSuffixLabel(true);
|
|
105
|
+
expect(getByText(mockLabel.label)).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("when not hasValue", () => {
|
|
110
|
+
it("does not render a prefix label", () => {
|
|
111
|
+
const { queryByText } = renderWithPrefixLabel(false);
|
|
112
|
+
expect(queryByText(mockLabel.label)).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does not render a suffix label", () => {
|
|
116
|
+
const { queryByText } = renderWithSuffixLabel(false);
|
|
117
|
+
expect(queryByText(mockLabel.label)).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("ClearAction", () => {
|
|
122
|
+
it("renders the Clear Action Button when showClearAction is true", () => {
|
|
123
|
+
const { formatMessage } = useIntl();
|
|
124
|
+
const { getByLabelText } = renderInputFieldWrapper({
|
|
125
|
+
showClearAction: true,
|
|
126
|
+
});
|
|
127
|
+
expect(
|
|
128
|
+
getByLabelText(formatMessage(clearMessages.clearTextLabel)),
|
|
129
|
+
).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does not render the Clear Action Button when showClearAction is false", () => {
|
|
133
|
+
const { formatMessage } = useIntl();
|
|
134
|
+
const { queryByLabelText } = renderInputFieldWrapper({
|
|
135
|
+
showClearAction: false,
|
|
136
|
+
});
|
|
137
|
+
expect(
|
|
138
|
+
queryByLabelText(formatMessage(clearMessages.clearTextLabel)),
|
|
139
|
+
).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("calls onClear when the Clear Action button is pressed", () => {
|
|
143
|
+
const { formatMessage } = useIntl();
|
|
144
|
+
const onClear = jest.fn();
|
|
145
|
+
const { getByLabelText } = renderInputFieldWrapper({
|
|
146
|
+
showClearAction: true,
|
|
147
|
+
onClear: onClear,
|
|
148
|
+
});
|
|
149
|
+
fireEvent(
|
|
150
|
+
getByLabelText(formatMessage(clearMessages.clearTextLabel)),
|
|
151
|
+
"press",
|
|
152
|
+
);
|
|
153
|
+
expect(onClear).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("Custom Styling", () => {
|
|
158
|
+
it("has the default container styling when no style override is provided", () => {
|
|
159
|
+
const { getByTestId } = renderInputFieldWrapper({});
|
|
160
|
+
const container = getByTestId("ATL-InputFieldWrapper");
|
|
161
|
+
expect(container.props.style).toContainEqual({
|
|
162
|
+
...commonInputStyles.container,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("shows the container style override when provided", () => {
|
|
167
|
+
const styleOverride = {
|
|
168
|
+
container: {
|
|
169
|
+
backgroundColor: "purple",
|
|
170
|
+
width: "50%",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const { getByTestId } = renderInputFieldWrapper({
|
|
174
|
+
styleOverride,
|
|
175
|
+
});
|
|
176
|
+
const container = getByTestId("ATL-InputFieldWrapper");
|
|
177
|
+
|
|
178
|
+
const flattenedStyle = container.props.style.reduce(
|
|
179
|
+
(style: ViewStyle, additionalStyles: ViewStyle) => ({
|
|
180
|
+
...style,
|
|
181
|
+
...additionalStyles,
|
|
182
|
+
}),
|
|
183
|
+
{},
|
|
184
|
+
);
|
|
185
|
+
expect(flattenedStyle.backgroundColor).toEqual(
|
|
186
|
+
styleOverride.container.backgroundColor,
|
|
187
|
+
);
|
|
188
|
+
expect(flattenedStyle.width).toEqual(styleOverride.container.width);
|
|
189
|
+
expect(flattenedStyle.borderColor).toEqual(
|
|
190
|
+
commonInputStyles.container.borderColor,
|
|
191
|
+
);
|
|
192
|
+
expect(flattenedStyle.marginVertical).toEqual(
|
|
193
|
+
commonInputStyles.container.marginVertical,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
it("has the default placeholder styling when no style override is provided", () => {
|
|
197
|
+
const placeholderText = "myplaceholder";
|
|
198
|
+
const { getByText } = renderInputFieldWrapper({
|
|
199
|
+
placeholder: placeholderText,
|
|
200
|
+
focused: true,
|
|
201
|
+
});
|
|
202
|
+
const placeholder = getByText(placeholderText, {
|
|
203
|
+
includeHiddenElements: true,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(placeholder.props.style).toContainEqual(
|
|
207
|
+
typographyStyles.interactive,
|
|
208
|
+
);
|
|
209
|
+
expect(placeholder.props.style).toContainEqual(
|
|
210
|
+
typographyStyles.defaultSize,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("shows the placeholder style override when provided", () => {
|
|
215
|
+
const placeholderText = "myplaceholder";
|
|
216
|
+
const styleOverride = {
|
|
217
|
+
placeholderText: {
|
|
218
|
+
color: "purple",
|
|
219
|
+
fontSize: 30,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const { getByText } = renderInputFieldWrapper({
|
|
223
|
+
placeholder: placeholderText,
|
|
224
|
+
styleOverride,
|
|
225
|
+
});
|
|
226
|
+
const placeholder = getByText(placeholderText, {
|
|
227
|
+
includeHiddenElements: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const flattenedStyle = placeholder.props.style.reduce(
|
|
231
|
+
(style: ViewStyle, additionalStyles: ViewStyle) => ({
|
|
232
|
+
...style,
|
|
233
|
+
...additionalStyles,
|
|
234
|
+
}),
|
|
235
|
+
{},
|
|
236
|
+
);
|
|
237
|
+
expect(flattenedStyle.color).toEqual(styleOverride.placeholderText.color);
|
|
238
|
+
expect(flattenedStyle.fontSize).toEqual(
|
|
239
|
+
styleOverride.placeholderText.fontSize,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
// eslint-disable-next-line no-restricted-imports
|
|
4
|
+
Text as RNText,
|
|
5
|
+
StyleProp,
|
|
6
|
+
TextStyle,
|
|
7
|
+
View,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { FieldError } from "react-hook-form";
|
|
11
|
+
import { IconNames } from "@jobber/design";
|
|
12
|
+
import { styles } from "./InputFieldWrapper.style";
|
|
13
|
+
import { PrefixIcon, PrefixLabel } from "./components/Prefix/Prefix";
|
|
14
|
+
import { SuffixIcon, SuffixLabel } from "./components/Suffix/Suffix";
|
|
15
|
+
import { ClearAction } from "./components/ClearAction";
|
|
16
|
+
import { ErrorMessageWrapper } from "../ErrorMessageWrapper";
|
|
17
|
+
import { TextVariation, typographyStyles } from "../Typography";
|
|
18
|
+
import { Text } from "../Text";
|
|
19
|
+
|
|
20
|
+
export type Clearable = "never" | "while-editing" | "always";
|
|
21
|
+
|
|
22
|
+
export interface InputFieldStyleOverride {
|
|
23
|
+
prefixLabel?: StyleProp<TextStyle>;
|
|
24
|
+
suffixLabel?: StyleProp<TextStyle>;
|
|
25
|
+
container?: StyleProp<ViewStyle>;
|
|
26
|
+
placeholderText?: StyleProp<TextStyle>;
|
|
27
|
+
}
|
|
28
|
+
export interface InputFieldWrapperProps {
|
|
29
|
+
/**
|
|
30
|
+
* Highlights the field red and shows message below (if string) to indicate an error
|
|
31
|
+
*/
|
|
32
|
+
readonly invalid?: boolean | string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Disable the input
|
|
36
|
+
*/
|
|
37
|
+
readonly disabled?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hint text that goes above the value once the field is filled out
|
|
41
|
+
*/
|
|
42
|
+
readonly placeholder?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Text that goes below the input to help the user understand the input
|
|
46
|
+
*/
|
|
47
|
+
readonly assistiveText?: string;
|
|
48
|
+
|
|
49
|
+
readonly hasMiniLabel?: boolean;
|
|
50
|
+
|
|
51
|
+
readonly hasValue?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Symbol to display before the text input
|
|
55
|
+
*/
|
|
56
|
+
readonly prefix?: {
|
|
57
|
+
icon?: IconNames;
|
|
58
|
+
label?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Symbol to display after the text input
|
|
63
|
+
*/
|
|
64
|
+
readonly suffix?: {
|
|
65
|
+
icon?: IconNames;
|
|
66
|
+
label?: string;
|
|
67
|
+
onPress?: () => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
readonly error?: FieldError;
|
|
71
|
+
|
|
72
|
+
readonly focused?: boolean;
|
|
73
|
+
|
|
74
|
+
readonly children: React.ReactNode;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Adds the ClearAction that will call the onClear handler when pressed
|
|
78
|
+
*/
|
|
79
|
+
readonly showClearAction?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Callback called when the user clicks the ClearAction button. Should clear the value passed.
|
|
83
|
+
* To disallow clearing set the clearable prop to never
|
|
84
|
+
*/
|
|
85
|
+
readonly onClear?: () => void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Custom styling to override default style of the input field
|
|
89
|
+
*/
|
|
90
|
+
readonly styleOverride?: InputFieldStyleOverride;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function InputFieldWrapper({
|
|
94
|
+
invalid,
|
|
95
|
+
disabled,
|
|
96
|
+
placeholder,
|
|
97
|
+
assistiveText,
|
|
98
|
+
prefix,
|
|
99
|
+
suffix,
|
|
100
|
+
hasMiniLabel = false,
|
|
101
|
+
hasValue = false,
|
|
102
|
+
error,
|
|
103
|
+
focused = false,
|
|
104
|
+
children,
|
|
105
|
+
onClear,
|
|
106
|
+
showClearAction = false,
|
|
107
|
+
styleOverride,
|
|
108
|
+
}: InputFieldWrapperProps): JSX.Element {
|
|
109
|
+
fieldAffixRequiredPropsCheck([prefix, suffix]);
|
|
110
|
+
const handleClear = onClear ?? noopClear;
|
|
111
|
+
warnIfClearActionWithNoOnClear(onClear, showClearAction);
|
|
112
|
+
const inputInvalid = Boolean(invalid) || Boolean(error);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ErrorMessageWrapper message={getMessage({ invalid, error })}>
|
|
116
|
+
<View
|
|
117
|
+
testID={"ATL-InputFieldWrapper"}
|
|
118
|
+
style={[
|
|
119
|
+
styles.container,
|
|
120
|
+
focused && styles.inputFocused,
|
|
121
|
+
(Boolean(invalid) || error) && styles.inputInvalid,
|
|
122
|
+
disabled && styles.disabled,
|
|
123
|
+
styleOverride?.container,
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{prefix?.icon && (
|
|
127
|
+
<PrefixIcon
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
focused={focused}
|
|
130
|
+
hasMiniLabel={hasMiniLabel}
|
|
131
|
+
inputInvalid={inputInvalid}
|
|
132
|
+
icon={prefix.icon}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
<View style={[styles.inputContainer]}>
|
|
136
|
+
<View
|
|
137
|
+
style={[
|
|
138
|
+
!!placeholder && styles.label,
|
|
139
|
+
hasMiniLabel && styles.miniLabel,
|
|
140
|
+
disabled && styles.disabled,
|
|
141
|
+
hasMiniLabel &&
|
|
142
|
+
showClearAction &&
|
|
143
|
+
styles.miniLabelShowClearAction,
|
|
144
|
+
]}
|
|
145
|
+
pointerEvents="none"
|
|
146
|
+
>
|
|
147
|
+
<Placeholder
|
|
148
|
+
placeholder={placeholder}
|
|
149
|
+
labelVariation={getLabelVariation(
|
|
150
|
+
error,
|
|
151
|
+
invalid,
|
|
152
|
+
focused,
|
|
153
|
+
disabled,
|
|
154
|
+
)}
|
|
155
|
+
hasMiniLabel={hasMiniLabel}
|
|
156
|
+
styleOverride={styleOverride?.placeholderText}
|
|
157
|
+
/>
|
|
158
|
+
</View>
|
|
159
|
+
{prefix?.label && hasValue && (
|
|
160
|
+
<PrefixLabel
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
focused={focused}
|
|
163
|
+
hasMiniLabel={hasMiniLabel}
|
|
164
|
+
inputInvalid={inputInvalid}
|
|
165
|
+
label={prefix.label}
|
|
166
|
+
styleOverride={styleOverride?.prefixLabel}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
{children}
|
|
170
|
+
{(showClearAction || suffix?.label || suffix?.icon) && (
|
|
171
|
+
<View style={styles.inputEndContainer}>
|
|
172
|
+
{showClearAction && (
|
|
173
|
+
<ClearAction
|
|
174
|
+
hasMarginRight={!!suffix?.icon || !!suffix?.label}
|
|
175
|
+
onPress={handleClear}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
{suffix?.label && hasValue && (
|
|
179
|
+
<SuffixLabel
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
focused={focused}
|
|
182
|
+
hasMiniLabel={hasMiniLabel}
|
|
183
|
+
inputInvalid={inputInvalid}
|
|
184
|
+
label={suffix.label}
|
|
185
|
+
hasLeftMargin={!showClearAction}
|
|
186
|
+
styleOverride={styleOverride?.suffixLabel}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
{suffix?.icon && (
|
|
190
|
+
<SuffixIcon
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
focused={focused}
|
|
193
|
+
hasMiniLabel={hasMiniLabel}
|
|
194
|
+
hasLeftMargin={!!(!showClearAction || suffix?.label)}
|
|
195
|
+
inputInvalid={inputInvalid}
|
|
196
|
+
icon={suffix.icon}
|
|
197
|
+
onPress={suffix.onPress}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</View>
|
|
201
|
+
)}
|
|
202
|
+
</View>
|
|
203
|
+
</View>
|
|
204
|
+
{assistiveText && !error && !invalid && (
|
|
205
|
+
<Text
|
|
206
|
+
level="textSupporting"
|
|
207
|
+
variation={
|
|
208
|
+
disabled ? "disabled" : focused ? "interactive" : "subdued"
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
{assistiveText}
|
|
212
|
+
</Text>
|
|
213
|
+
)}
|
|
214
|
+
</ErrorMessageWrapper>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getLabelVariation(
|
|
219
|
+
error?: FieldError,
|
|
220
|
+
invalid?: boolean | string,
|
|
221
|
+
focused?: boolean,
|
|
222
|
+
disabled?: boolean,
|
|
223
|
+
): TextVariation {
|
|
224
|
+
if (invalid || error) {
|
|
225
|
+
return "error";
|
|
226
|
+
} else if (disabled) {
|
|
227
|
+
return "disabled";
|
|
228
|
+
} else if (focused) {
|
|
229
|
+
return "interactive";
|
|
230
|
+
}
|
|
231
|
+
return "subdued";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function fieldAffixRequiredPropsCheck(
|
|
235
|
+
affixPair: [
|
|
236
|
+
InputFieldWrapperProps["prefix"],
|
|
237
|
+
InputFieldWrapperProps["suffix"],
|
|
238
|
+
],
|
|
239
|
+
) {
|
|
240
|
+
affixPair.map(affix => {
|
|
241
|
+
if (typeof affix !== "undefined" && !affix.icon && !affix.label) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`One of 'label' or 'icon' is required by the field affix component.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function warnIfClearActionWithNoOnClear(
|
|
250
|
+
onClear?: () => void,
|
|
251
|
+
showClearAction?: boolean,
|
|
252
|
+
): void {
|
|
253
|
+
if (showClearAction && !onClear && __DEV__) {
|
|
254
|
+
console.warn(
|
|
255
|
+
"Declare an `onClear` prop on your input. You can set `clearable` to never or `showClearAction` to false if you don't need a clearable input",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function noopClear() {
|
|
261
|
+
warnIfClearActionWithNoOnClear(undefined, true);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getMessage({
|
|
265
|
+
invalid,
|
|
266
|
+
error,
|
|
267
|
+
}: Pick<InputFieldWrapperProps, "invalid" | "error">): string | undefined {
|
|
268
|
+
const messages: string[] = [];
|
|
269
|
+
if (error?.message) messages.push(error.message);
|
|
270
|
+
if (invalid && typeof invalid === "string") messages.push(invalid);
|
|
271
|
+
|
|
272
|
+
return messages.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function Placeholder({
|
|
276
|
+
placeholder,
|
|
277
|
+
styleOverride,
|
|
278
|
+
labelVariation,
|
|
279
|
+
hasMiniLabel,
|
|
280
|
+
}: {
|
|
281
|
+
placeholder?: string;
|
|
282
|
+
styleOverride: StyleProp<TextStyle>;
|
|
283
|
+
labelVariation: TextVariation;
|
|
284
|
+
hasMiniLabel: boolean;
|
|
285
|
+
}) {
|
|
286
|
+
return (
|
|
287
|
+
<>
|
|
288
|
+
{!styleOverride ? (
|
|
289
|
+
<Text
|
|
290
|
+
hideFromScreenReader={true}
|
|
291
|
+
maxLines="single"
|
|
292
|
+
variation={labelVariation}
|
|
293
|
+
level={hasMiniLabel ? "textSupporting" : "text"}
|
|
294
|
+
>
|
|
295
|
+
{placeholder}
|
|
296
|
+
</Text>
|
|
297
|
+
) : (
|
|
298
|
+
<RNText
|
|
299
|
+
accessibilityRole="none"
|
|
300
|
+
accessible={false}
|
|
301
|
+
importantForAccessibility="no-hide-descendants"
|
|
302
|
+
numberOfLines={1}
|
|
303
|
+
style={[
|
|
304
|
+
typographyStyles[labelVariation],
|
|
305
|
+
typographyStyles.baseRegularRegular,
|
|
306
|
+
hasMiniLabel
|
|
307
|
+
? typographyStyles.smallSize
|
|
308
|
+
: typographyStyles.defaultSize,
|
|
309
|
+
styleOverride,
|
|
310
|
+
]}
|
|
311
|
+
>
|
|
312
|
+
{placeholder}
|
|
313
|
+
</RNText>
|
|
314
|
+
)}
|
|
315
|
+
</>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
|
|
4
|
+
const width = tokens["space-smaller"] + tokens["space-larger"];
|
|
5
|
+
|
|
6
|
+
export const styles = StyleSheet.create({
|
|
7
|
+
container: {
|
|
8
|
+
width,
|
|
9
|
+
height: "100%",
|
|
10
|
+
flexDirection: "row",
|
|
11
|
+
justifyContent: "center",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
alignSelf: "center",
|
|
14
|
+
},
|
|
15
|
+
circle: {
|
|
16
|
+
backgroundColor: tokens["color-surface--background"],
|
|
17
|
+
borderRadius: tokens["radius-circle"],
|
|
18
|
+
width: tokens["space-large"],
|
|
19
|
+
height: tokens["space-large"],
|
|
20
|
+
flexDirection: "row",
|
|
21
|
+
justifyContent: "center",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
},
|
|
24
|
+
addedMargin: {
|
|
25
|
+
marginRight: tokens["space-small"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { ClearAction } from "./ClearAction";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
|
|
7
|
+
it("should call the handler", () => {
|
|
8
|
+
const pressHandler = jest.fn();
|
|
9
|
+
const { formatMessage } = useIntl();
|
|
10
|
+
|
|
11
|
+
const { getByLabelText } = render(<ClearAction onPress={pressHandler} />);
|
|
12
|
+
|
|
13
|
+
fireEvent.press(getByLabelText(formatMessage(messages.clearTextLabel)));
|
|
14
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
15
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { styles } from "./ClearAction.style";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
import { Icon } from "../../../Icon";
|
|
7
|
+
|
|
8
|
+
interface ClearActionProps {
|
|
9
|
+
/**
|
|
10
|
+
* Press handler
|
|
11
|
+
*/
|
|
12
|
+
readonly onPress: () => void;
|
|
13
|
+
readonly hasMarginRight?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ClearAction({
|
|
17
|
+
onPress,
|
|
18
|
+
hasMarginRight = false,
|
|
19
|
+
}: ClearActionProps): JSX.Element {
|
|
20
|
+
const { formatMessage } = useIntl();
|
|
21
|
+
return (
|
|
22
|
+
<Pressable
|
|
23
|
+
style={[styles.container, hasMarginRight && styles.addedMargin]}
|
|
24
|
+
onPress={onPress}
|
|
25
|
+
accessibilityLabel={formatMessage(messages.clearTextLabel)}
|
|
26
|
+
>
|
|
27
|
+
<View style={styles.circle}>
|
|
28
|
+
<Icon size="small" name="cross" color="interactiveSubtle" />
|
|
29
|
+
</View>
|
|
30
|
+
</Pressable>
|
|
31
|
+
);
|
|
32
|
+
}
|