@onewelcome/react-lib-components 1.5.0 → 1.7.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/README.md +4 -4
- package/dist/Button/Button.d.ts +0 -1
- package/dist/DataGrid/datagrid.interfaces.d.ts +1 -0
- package/dist/Form/Checkbox/Checkbox.d.ts +1 -1
- package/dist/Form/FileUpload/FileItem/FileItem.d.ts +17 -0
- package/dist/Form/FileUpload/FileUpload.d.ts +26 -0
- package/dist/Form/FormHelperText/FormHelperText.d.ts +1 -1
- package/dist/Form/FormSelectorWrapper/FormSelectorWrapper.d.ts +1 -1
- package/dist/Form/Input/Input.d.ts +2 -2
- package/dist/Form/Radio/Radio.d.ts +1 -1
- package/dist/Form/Select/Select.d.ts +1 -1
- package/dist/Form/Textarea/Textarea.d.ts +1 -6
- package/dist/Form/Toggle/Toggle.d.ts +1 -1
- package/dist/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.d.ts +1 -1
- package/dist/Form/Wrapper/InputWrapper/InputWrapper.d.ts +1 -1
- package/dist/Form/Wrapper/RadioWrapper/RadioWrapper.d.ts +1 -1
- package/dist/Form/Wrapper/SelectWrapper/SelectWrapper.d.ts +1 -1
- package/dist/Form/Wrapper/TextareaWrapper/TextareaWrapper.d.ts +1 -1
- package/dist/Form/form.interfaces.d.ts +1 -0
- package/dist/Icon/Icon.d.ts +4 -1
- package/dist/Link/Link.d.ts +1 -2
- package/dist/Notifications/Banner/Banner.d.ts +11 -0
- package/dist/ProgressBar/ProgressBar.d.ts +2 -1
- package/dist/Tabs/TabButton.d.ts +0 -1
- package/dist/_BaseStyling_/BaseStyling.d.ts +5 -0
- package/dist/hooks/useDetermineStatusIcon.d.ts +3 -0
- package/dist/hooks/useUploadFile.d.ts +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/react-lib-components.cjs.development.js +431 -326
- package/dist/react-lib-components.cjs.development.js.map +1 -1
- package/dist/react-lib-components.cjs.production.min.js +1 -1
- package/dist/react-lib-components.cjs.production.min.js.map +1 -1
- package/dist/react-lib-components.esm.js +431 -327
- package/dist/react-lib-components.esm.js.map +1 -1
- package/dist/util/helper.d.ts +5 -0
- package/package.json +28 -25
- package/src/Button/BaseButton.module.scss +2 -2
- package/src/Button/Button.module.scss +4 -5
- package/src/Button/Button.tsx +0 -1
- package/src/Button/IconButton.module.scss +4 -5
- package/src/DataGrid/DataGrid.tsx +3 -2
- package/src/DataGrid/DataGridActions/DataGridActions.tsx +16 -9
- package/src/DataGrid/DataGridBody/DataGridCell.module.scss +2 -2
- package/src/DataGrid/DataGridHeader/DataGridHeader.test.tsx +8 -3
- package/src/DataGrid/DataGridHeader/DataGridHeader.tsx +3 -1
- package/src/DataGrid/datagrid.interfaces.ts +1 -0
- package/src/Form/FileUpload/FileItem/FileItem.modules.scss +75 -0
- package/src/Form/FileUpload/FileItem/FileItem.test.tsx +103 -0
- package/src/Form/FileUpload/FileItem/FileItem.tsx +141 -0
- package/src/Form/FileUpload/FileUpload.module.scss +106 -0
- package/src/Form/FileUpload/FileUpload.test.tsx +374 -0
- package/src/Form/FileUpload/FileUpload.tsx +251 -0
- package/src/Form/Input/Input.module.scss +36 -26
- package/src/Form/Input/Input.test.tsx +10 -0
- package/src/Form/Input/Input.tsx +7 -5
- package/src/Form/Select/Select.module.scss +9 -6
- package/src/Form/Select/Select.test.tsx +11 -0
- package/src/Form/Select/Select.tsx +5 -9
- package/src/Form/Select/SelectService.ts +2 -2
- package/src/Form/Textarea/Textarea.module.scss +21 -13
- package/src/Form/Textarea/Textarea.test.tsx +8 -0
- package/src/Form/Textarea/Textarea.tsx +6 -12
- package/src/Form/Toggle/Toggle.module.scss +3 -3
- package/src/Form/Wrapper/InputWrapper/InputWrapper.module.scss +7 -3
- package/src/Form/Wrapper/InputWrapper/InputWrapper.tsx +2 -0
- package/src/Form/Wrapper/SelectWrapper/SelectWrapper.tsx +12 -1
- package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.module.scss +15 -14
- package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.tsx +2 -1
- package/src/Form/Wrapper/Wrapper/Wrapper.module.scss +2 -2
- package/src/Form/form.interfaces.ts +1 -0
- package/src/Icon/Icon.module.scss +12 -0
- package/src/Icon/Icon.tsx +4 -1
- package/src/Link/Link.module.scss +5 -5
- package/src/Link/Link.tsx +14 -13
- package/src/Notifications/Banner/Banner.module.scss +76 -0
- package/src/Notifications/Banner/Banner.test.tsx +84 -0
- package/src/Notifications/Banner/Banner.tsx +78 -0
- package/src/Notifications/BaseModal/BaseModal.module.scss +2 -2
- package/src/Notifications/Snackbar/SnackbarContainer/SnackbarContainer.module.scss +2 -2
- package/src/Notifications/Snackbar/SnackbarItem/SnackbarItem.module.scss +4 -4
- package/src/Notifications/Snackbar/SnackbarItem/SnackbarItem.tsx +3 -2
- package/src/Popover/Popover.module.scss +2 -2
- package/src/ProgressBar/ProgressBar.module.scss +11 -9
- package/src/ProgressBar/ProgressBar.test.tsx +21 -0
- package/src/ProgressBar/ProgressBar.tsx +7 -2
- package/src/Skeleton/Skeleton.module.scss +2 -2
- package/src/Tabs/TabButton.tsx +1 -2
- package/src/Tabs/Tabs.module.scss +2 -2
- package/src/Tabs/Tabs.tsx +13 -10
- package/src/Tiles/Tile.module.scss +4 -4
- package/src/Tooltip/Tooltip.module.scss +3 -3
- package/src/Typography/Typography.module.scss +2 -2
- package/src/_BaseStyling_/BaseStyling.tsx +13 -3
- package/src/hooks/useDetermineStatusIcon.test.ts +28 -0
- package/src/hooks/useDetermineStatusIcon.tsx +35 -0
- package/src/hooks/useUploadFile.test.ts +211 -0
- package/src/hooks/useUploadFile.tsx +136 -0
- package/src/index.ts +1 -0
- package/src/mixins.module.scss +24 -5
- package/src/util/helper.test.tsx +156 -1
- package/src/util/helper.tsx +33 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2022 OneWelcome B.V.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, {
|
|
18
|
+
DragEvent,
|
|
19
|
+
DragEventHandler,
|
|
20
|
+
ForwardRefRenderFunction,
|
|
21
|
+
useEffect,
|
|
22
|
+
useRef,
|
|
23
|
+
useState
|
|
24
|
+
} from "react";
|
|
25
|
+
import { Button } from "../../Button/Button";
|
|
26
|
+
import { FILE_ACTION, FileItem, Props as FileConfig } from "./FileItem/FileItem";
|
|
27
|
+
import { Props as InputProps } from "../Input/Input";
|
|
28
|
+
import { Typography } from "../../Typography/Typography";
|
|
29
|
+
import classes from "./FileUpload.module.scss";
|
|
30
|
+
import { Icon, Icons } from "../../Icon/Icon";
|
|
31
|
+
import { useDetermineStatusIcon } from "../../hooks/useDetermineStatusIcon";
|
|
32
|
+
|
|
33
|
+
type FileUploadType = Omit<InputProps, "onDrop" | "type" | "onChange" | "suffix" | "prefix">;
|
|
34
|
+
export type FileType = Omit<FileConfig, "onRequestedFileAction"> &
|
|
35
|
+
Pick<File, "size" | "type"> & { data?: any };
|
|
36
|
+
|
|
37
|
+
export interface Props extends FileUploadType {
|
|
38
|
+
accept: string;
|
|
39
|
+
title: string;
|
|
40
|
+
multiple: boolean;
|
|
41
|
+
fileList: FileType[];
|
|
42
|
+
exceedingMaxSizeErrorText?: string;
|
|
43
|
+
maxFileSize?: number;
|
|
44
|
+
selectButtonText?: string;
|
|
45
|
+
dragAndDropText?: string;
|
|
46
|
+
subText?: string;
|
|
47
|
+
onDragOver?: DragEventHandler;
|
|
48
|
+
onDragEnter?: DragEventHandler;
|
|
49
|
+
onDragLeave?: DragEventHandler;
|
|
50
|
+
onDrop?: (e: FileType[]) => void;
|
|
51
|
+
onChange?: (e: FileType[]) => void;
|
|
52
|
+
onRequestedFileAction?: (action: FILE_ACTION, name: FileType["name"]) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const FileUploadComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
56
|
+
{
|
|
57
|
+
name,
|
|
58
|
+
accept,
|
|
59
|
+
error,
|
|
60
|
+
success,
|
|
61
|
+
maxFileSize,
|
|
62
|
+
multiple,
|
|
63
|
+
id,
|
|
64
|
+
title,
|
|
65
|
+
labeledBy,
|
|
66
|
+
disabled = false,
|
|
67
|
+
onChange,
|
|
68
|
+
dragAndDropText = "Drop file here or",
|
|
69
|
+
selectButtonText = "Select file",
|
|
70
|
+
onDragOver,
|
|
71
|
+
onDragLeave,
|
|
72
|
+
wrapperProps,
|
|
73
|
+
onDrop,
|
|
74
|
+
subText,
|
|
75
|
+
onRequestedFileAction,
|
|
76
|
+
exceedingMaxSizeErrorText,
|
|
77
|
+
fileList,
|
|
78
|
+
...rest
|
|
79
|
+
}: Props,
|
|
80
|
+
ref
|
|
81
|
+
) => {
|
|
82
|
+
const labelRef = useRef(null);
|
|
83
|
+
const [dragActive, setDragActive] = useState(false);
|
|
84
|
+
const [inputError, setInputError] = useState(false);
|
|
85
|
+
const icon = useDetermineStatusIcon({ success, error });
|
|
86
|
+
let dropzoneClassNames = [classes["file-dropzone"]];
|
|
87
|
+
let subTextClass = [classes["file-selector-sub-text"]];
|
|
88
|
+
dragActive && dropzoneClassNames.push(classes["drag-active"]);
|
|
89
|
+
inputError ||
|
|
90
|
+
(error && dropzoneClassNames.push(classes["error"]) && subTextClass.push(classes["error"]));
|
|
91
|
+
disabled && dropzoneClassNames.push(classes["disabled"]);
|
|
92
|
+
success && !error && dropzoneClassNames.push(classes["success"]);
|
|
93
|
+
|
|
94
|
+
const getFileList = (files: FileList | null): FileType[] => {
|
|
95
|
+
let savedFiles = fileList ? [...fileList] : [];
|
|
96
|
+
const fileNames = fileList.map(el => el.name);
|
|
97
|
+
files?.length &&
|
|
98
|
+
Array.from(files as ArrayLike<File>).forEach(el => {
|
|
99
|
+
if (!fileNames.includes(el.name)) {
|
|
100
|
+
savedFiles = multiple
|
|
101
|
+
? [
|
|
102
|
+
...savedFiles,
|
|
103
|
+
{
|
|
104
|
+
...validateUpload(el),
|
|
105
|
+
data: el
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
: [
|
|
109
|
+
{
|
|
110
|
+
...validateUpload(el),
|
|
111
|
+
data: el
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return savedFiles;
|
|
117
|
+
};
|
|
118
|
+
const onInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
let files = getFileList(e.target.files);
|
|
122
|
+
files.length && verifyExtensionValidity(files[files.length - 1]) && onChange && onChange(files);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const verifyExtensionValidity = (file: FileType) => {
|
|
126
|
+
const extension = file.name.split(".").pop();
|
|
127
|
+
return extension && accept.includes(extension);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const validateUpload = (file: FileType) => {
|
|
131
|
+
const result: FileType = {
|
|
132
|
+
name: file.name,
|
|
133
|
+
size: file.size,
|
|
134
|
+
type: file.type
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let err = false;
|
|
138
|
+
if (maxFileSize && file.size && file.size >= maxFileSize) {
|
|
139
|
+
const mb = (file.size / (1024 * 1024)).toFixed(2);
|
|
140
|
+
result.error =
|
|
141
|
+
exceedingMaxSizeErrorText ||
|
|
142
|
+
`The maximum allowed file size is ${mb}MB. Upload a smaller file.`;
|
|
143
|
+
result.status = "error";
|
|
144
|
+
err = true;
|
|
145
|
+
}
|
|
146
|
+
setInputError(err);
|
|
147
|
+
return result;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (fileList.length) {
|
|
152
|
+
const validatedFiles = fileList.map(file => validateUpload(file));
|
|
153
|
+
onChange && onChange(validatedFiles);
|
|
154
|
+
}
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
const handleOnDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
setDragActive(true);
|
|
161
|
+
onDragOver && onDragOver(e);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleOnDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
const target = e.target as HTMLElement;
|
|
168
|
+
if (target?.classList.contains(classes["file-dropzone"])) {
|
|
169
|
+
setDragActive(false);
|
|
170
|
+
}
|
|
171
|
+
onDragLeave && onDragLeave(e);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleOnDrop = async (e: DragEvent<HTMLDivElement>) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
if (e?.dataTransfer?.files && e.dataTransfer.files.length) {
|
|
178
|
+
const extension = e?.dataTransfer?.files[0].name.split(".").pop();
|
|
179
|
+
if (extension && accept && !accept.includes(extension)) {
|
|
180
|
+
setDragActive(false);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const validatedFiles = getFileList(e.dataTransfer.files);
|
|
184
|
+
onDrop && onDrop(validatedFiles);
|
|
185
|
+
}
|
|
186
|
+
setDragActive(false);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className={classes["file-upload-wrapper"]} {...wrapperProps}>
|
|
191
|
+
<div
|
|
192
|
+
className={dropzoneClassNames.join(" ")}
|
|
193
|
+
onDragOver={e => !disabled && handleOnDragOver(e)}
|
|
194
|
+
onDragLeave={e => !disabled && handleOnDragLeave(e)}
|
|
195
|
+
onDrop={e => !disabled && handleOnDrop(e)}
|
|
196
|
+
>
|
|
197
|
+
<Typography variant="body-bold" className={classes["file-upload-title"]} ref={labelRef}>
|
|
198
|
+
{title}
|
|
199
|
+
</Typography>
|
|
200
|
+
<div className={classes["file-select"]}>
|
|
201
|
+
<Icon className={"drop-file-icon"} icon={Icons.FileUpload} />
|
|
202
|
+
<Typography variant="body" className={"drag-and-drop-text"}>
|
|
203
|
+
{dragAndDropText}
|
|
204
|
+
</Typography>
|
|
205
|
+
<div className={classes["file-upload-btn"]}>
|
|
206
|
+
<Button variant="outline" disabled={disabled}>
|
|
207
|
+
{selectButtonText}
|
|
208
|
+
</Button>
|
|
209
|
+
<input
|
|
210
|
+
className={classes["upload-input"]}
|
|
211
|
+
{...rest}
|
|
212
|
+
ref={ref}
|
|
213
|
+
aria-labelledby={labeledBy}
|
|
214
|
+
type="file"
|
|
215
|
+
name={name}
|
|
216
|
+
{...(multiple && { multiple: true })}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
accept={accept}
|
|
219
|
+
onChange={onInputChange}
|
|
220
|
+
spellCheck={rest.spellCheck || false}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
{!disabled && icon}
|
|
224
|
+
<span className={classes["outline"]}></span>
|
|
225
|
+
</div>
|
|
226
|
+
{subText && (
|
|
227
|
+
<Typography variant={"sub-text"} className={subTextClass.join(" ")}>
|
|
228
|
+
{subText}
|
|
229
|
+
</Typography>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
{fileList?.length > 0 && (
|
|
233
|
+
<ul className={classes["file-list"]}>
|
|
234
|
+
{fileList.map(({ name, status, progress, error }) => (
|
|
235
|
+
<li key={name} className={status} id={name}>
|
|
236
|
+
<FileItem
|
|
237
|
+
name={name}
|
|
238
|
+
status={status}
|
|
239
|
+
progress={progress}
|
|
240
|
+
error={error}
|
|
241
|
+
onRequestedFileAction={onRequestedFileAction}
|
|
242
|
+
/>
|
|
243
|
+
</li>
|
|
244
|
+
))}
|
|
245
|
+
</ul>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const FileUpload = React.forwardRef(FileUploadComponent);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
@
|
|
17
|
+
@use "../../mixins.module.scss";
|
|
18
18
|
|
|
19
19
|
.input-wrapper {
|
|
20
20
|
position: relative;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
border-radius: var(--input-border-radius);
|
|
26
26
|
background-color: var(--input-background-color);
|
|
27
27
|
padding: 0 1.25rem;
|
|
28
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
28
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
29
29
|
|
|
30
30
|
// General autofill styles
|
|
31
31
|
input:-webkit-autofill,
|
|
@@ -64,7 +64,33 @@
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
@include outlineStates;
|
|
67
|
+
@include mixins.outlineStates;
|
|
68
|
+
|
|
69
|
+
[data-icon-status="success"],
|
|
70
|
+
[data-icon-status="error"] {
|
|
71
|
+
height: 100%;
|
|
72
|
+
width: 1.25rem;
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
padding-top: calc(1rem - (1.25rem - 1rem));
|
|
75
|
+
font-size: 1.25rem;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
z-index: 2;
|
|
80
|
+
|
|
81
|
+
&:before {
|
|
82
|
+
height: 1.25rem;
|
|
83
|
+
width: 1.25rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[data-icon-status="success"] {
|
|
88
|
+
color: var(--success);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[data-icon-status="error"] {
|
|
92
|
+
color: var(--error);
|
|
93
|
+
}
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
.input {
|
|
@@ -76,7 +102,7 @@
|
|
|
76
102
|
box-sizing: border-box;
|
|
77
103
|
border: 0;
|
|
78
104
|
border-radius: var(--input-border-radius);
|
|
79
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
105
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
80
106
|
background-color: transparent;
|
|
81
107
|
z-index: 1;
|
|
82
108
|
|
|
@@ -85,7 +111,6 @@
|
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
&:disabled {
|
|
88
|
-
background-color: var(--disabled);
|
|
89
114
|
cursor: not-allowed;
|
|
90
115
|
}
|
|
91
116
|
|
|
@@ -93,41 +118,26 @@
|
|
|
93
118
|
width: auto;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
|
-
@include browserOutlineDisabled;
|
|
121
|
+
@include mixins.browserOutlineDisabled;
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
@include outline;
|
|
124
|
+
@include mixins.outline;
|
|
100
125
|
|
|
101
126
|
.error input {
|
|
102
127
|
color: var(--error);
|
|
103
|
-
padding-right:
|
|
128
|
+
padding-right: 1.25rem;
|
|
104
129
|
|
|
105
130
|
&.remove-extra-indent {
|
|
106
131
|
padding-right: 0;
|
|
107
132
|
}
|
|
108
133
|
}
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
color: var(--error);
|
|
112
|
-
position: absolute;
|
|
113
|
-
height: 100%;
|
|
114
|
-
width: 1.25rem;
|
|
115
|
-
right: 1.25rem;
|
|
116
|
-
top: 0;
|
|
117
|
-
font-size: 1.125rem;
|
|
118
|
-
display: flex;
|
|
119
|
-
align-items: center;
|
|
120
|
-
justify-content: center;
|
|
121
|
-
z-index: 2;
|
|
122
|
-
|
|
123
|
-
&:before {
|
|
124
|
-
height: 1.3125rem;
|
|
125
|
-
}
|
|
134
|
+
.success input {
|
|
135
|
+
padding-right: 1.25rem;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
.input-wrapper [data-prefix],
|
|
129
139
|
.input-wrapper [data-suffix] {
|
|
130
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
140
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
131
141
|
display: block;
|
|
132
142
|
z-index: 1;
|
|
133
143
|
}
|
|
@@ -234,4 +234,14 @@ describe("It should render prefix and suffix ", () => {
|
|
|
234
234
|
expect(input.querySelector("icon-warning")).toBeDefined();
|
|
235
235
|
expect(getByText(suffix)).toBeDefined();
|
|
236
236
|
});
|
|
237
|
+
|
|
238
|
+
it("success icon should be visible", () => {
|
|
239
|
+
const { input } = createInput(defaultParams => ({
|
|
240
|
+
...defaultParams,
|
|
241
|
+
success: true
|
|
242
|
+
}));
|
|
243
|
+
const icon = input.querySelector(".icon-checkmark-circle-breakout");
|
|
244
|
+
expect(input.querySelector(".success")).toBeDefined();
|
|
245
|
+
expect(icon).toBeDefined();
|
|
246
|
+
});
|
|
237
247
|
});
|
package/src/Form/Input/Input.tsx
CHANGED
|
@@ -24,8 +24,8 @@ import React, {
|
|
|
24
24
|
} from "react";
|
|
25
25
|
import classes from "./Input.module.scss";
|
|
26
26
|
import readyclasses from "../../readyclasses.module.scss";
|
|
27
|
-
import { Icon, Icons } from "../../Icon/Icon";
|
|
28
27
|
import { FormElement } from "../form.interfaces";
|
|
28
|
+
import { useDetermineStatusIcon } from "../../hooks/useDetermineStatusIcon";
|
|
29
29
|
|
|
30
30
|
export const dateTypes = ["date", "time", "datetime-local"] as const;
|
|
31
31
|
|
|
@@ -39,7 +39,7 @@ export type Type =
|
|
|
39
39
|
| "tel"
|
|
40
40
|
| "url"
|
|
41
41
|
| "hidden"
|
|
42
|
-
| typeof dateTypes[number];
|
|
42
|
+
| (typeof dateTypes)[number];
|
|
43
43
|
|
|
44
44
|
export interface Props extends ComponentPropsWithRef<"input">, FormElement {
|
|
45
45
|
wrapperProps?: ComponentPropsWithRef<"div">;
|
|
@@ -52,6 +52,7 @@ export interface Props extends ComponentPropsWithRef<"input">, FormElement {
|
|
|
52
52
|
const InputComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
53
53
|
{
|
|
54
54
|
error = false,
|
|
55
|
+
success = false,
|
|
55
56
|
className,
|
|
56
57
|
name,
|
|
57
58
|
style,
|
|
@@ -69,7 +70,6 @@ const InputComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
|
69
70
|
) => {
|
|
70
71
|
const [focus, setFocus] = useState(false);
|
|
71
72
|
const inputWrapperRef = useRef<HTMLDivElement>(null);
|
|
72
|
-
const errorIconRef = useRef<HTMLDivElement>(null);
|
|
73
73
|
const suffixRef = useRef<HTMLDivElement>(null);
|
|
74
74
|
|
|
75
75
|
useEffect(() => {
|
|
@@ -84,7 +84,6 @@ const InputComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
|
84
84
|
inputClassNames.push(classes["shrink-width-for-date-icon"]);
|
|
85
85
|
className && inputClassNames.push(className);
|
|
86
86
|
|
|
87
|
-
const iconClassNames = [classes["warning"]];
|
|
88
87
|
const wrapperClasses = [classes["input-wrapper"]];
|
|
89
88
|
const outlineClasses = [classes["outline"]];
|
|
90
89
|
|
|
@@ -95,6 +94,9 @@ const InputComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
|
95
94
|
disabled && wrapperClasses.push(classes["disabled"]) && outlineClasses.push(classes["disabled"]);
|
|
96
95
|
error && wrapperClasses.push(classes["error"]) && outlineClasses.push(classes["error"]);
|
|
97
96
|
focus && wrapperClasses.push(classes["focus"]) && outlineClasses.push(classes["focus"]);
|
|
97
|
+
success && wrapperClasses.push(classes["success"]);
|
|
98
|
+
|
|
99
|
+
const icon = useDetermineStatusIcon({ success, error });
|
|
98
100
|
|
|
99
101
|
return (
|
|
100
102
|
<div
|
|
@@ -126,12 +128,12 @@ const InputComponent: ForwardRefRenderFunction<HTMLInputElement, Props> = (
|
|
|
126
128
|
className={inputClassNames.join(" ")}
|
|
127
129
|
spellCheck={rest.spellCheck || false}
|
|
128
130
|
/>
|
|
131
|
+
{icon}
|
|
129
132
|
{suffix && (
|
|
130
133
|
<div ref={suffixRef} data-suffix className={classes["suffix"]}>
|
|
131
134
|
<span>{suffix}</span>
|
|
132
135
|
</div>
|
|
133
136
|
)}
|
|
134
|
-
{error && <Icon ref={errorIconRef} className={iconClassNames.join(" ")} icon={Icons.Error} />}
|
|
135
137
|
<span className={outlineClasses.join(" ")}></span>
|
|
136
138
|
</div>
|
|
137
139
|
);
|
|
@@ -14,15 +14,14 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
@
|
|
18
|
-
@import "../../mixins.module.scss";
|
|
17
|
+
@use "../../mixins.module.scss";
|
|
19
18
|
|
|
20
19
|
$listItemHeight: 2.125rem;
|
|
21
20
|
|
|
22
21
|
.select {
|
|
23
22
|
position: relative;
|
|
24
23
|
box-sizing: border-box;
|
|
25
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
24
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
26
25
|
border: 0;
|
|
27
26
|
border-radius: var(--input-border-radius);
|
|
28
27
|
background-color: var(--input-background-color);
|
|
@@ -57,7 +56,6 @@ $listItemHeight: 2.125rem;
|
|
|
57
56
|
position: relative;
|
|
58
57
|
width: 100%;
|
|
59
58
|
min-height: calc(4rem - (2 * var(--input-border-width)));
|
|
60
|
-
border: 0;
|
|
61
59
|
padding: 0 1.25rem;
|
|
62
60
|
background-color: transparent;
|
|
63
61
|
border-color: var(--light-grey-border);
|
|
@@ -66,7 +64,7 @@ $listItemHeight: 2.125rem;
|
|
|
66
64
|
border-radius: var(--input-border-radius);
|
|
67
65
|
font-size: var(--font-size);
|
|
68
66
|
cursor: pointer;
|
|
69
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
67
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
70
68
|
|
|
71
69
|
&:focus {
|
|
72
70
|
outline: 0;
|
|
@@ -173,7 +171,12 @@ $listItemHeight: 2.125rem;
|
|
|
173
171
|
display: flex;
|
|
174
172
|
align-items: center;
|
|
175
173
|
|
|
176
|
-
|
|
174
|
+
[data-icon-status="success"] {
|
|
175
|
+
color: var(--success);
|
|
176
|
+
font-size: 1.25rem;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
[data-icon-status="error"] {
|
|
177
180
|
color: var(--error);
|
|
178
181
|
font-size: 1.25rem;
|
|
179
182
|
}
|
|
@@ -119,6 +119,17 @@ describe("Select should render", () => {
|
|
|
119
119
|
expect(button).toHaveAttribute("aria-invalid", "true");
|
|
120
120
|
expect(select.querySelector("[data-clear]")).not.toBeInTheDocument();
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
it("should have a success icon when success state", () => {
|
|
124
|
+
const { button } = createSelect(defaultParams => ({
|
|
125
|
+
...defaultParams,
|
|
126
|
+
success: true
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const icon = button.querySelector("[class*='icon-checkmark-circle-breakout']");
|
|
130
|
+
expect(button).toHaveClass("success");
|
|
131
|
+
expect(icon).toBeDefined();
|
|
132
|
+
});
|
|
122
133
|
});
|
|
123
134
|
|
|
124
135
|
describe("ref should work", () => {
|
|
@@ -33,6 +33,7 @@ import { useBodyClick } from "../../hooks/useBodyClick";
|
|
|
33
33
|
import readyclasses from "../../readyclasses.module.scss";
|
|
34
34
|
import { filterProps } from "../../util/helper";
|
|
35
35
|
import { useArrowNavigation, useSelectPositionList } from "./SelectService";
|
|
36
|
+
import { useDetermineStatusIcon } from "../../hooks/useDetermineStatusIcon";
|
|
36
37
|
|
|
37
38
|
type PartialInputProps = Partial<InputProps>;
|
|
38
39
|
|
|
@@ -67,6 +68,7 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, Props> = (
|
|
|
67
68
|
selectButtonProps,
|
|
68
69
|
className,
|
|
69
70
|
error = false,
|
|
71
|
+
success = false,
|
|
70
72
|
value,
|
|
71
73
|
clearLabel = "Clear selection",
|
|
72
74
|
onChange,
|
|
@@ -90,7 +92,6 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, Props> = (
|
|
|
90
92
|
const nativeSelect = (ref as React.RefObject<HTMLSelectElement>) || createRef();
|
|
91
93
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
92
94
|
const customSelectButtonRef = useRef<HTMLButtonElement>(null);
|
|
93
|
-
|
|
94
95
|
const { onArrowNavigation } = useArrowNavigation({
|
|
95
96
|
expanded,
|
|
96
97
|
setExpanded,
|
|
@@ -183,13 +184,7 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, Props> = (
|
|
|
183
184
|
setFilter(event.currentTarget.value);
|
|
184
185
|
};
|
|
185
186
|
|
|
186
|
-
const
|
|
187
|
-
if (error) {
|
|
188
|
-
return <Icon className={classes["warning"]} icon={Icons.Warning} />;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return null;
|
|
192
|
-
};
|
|
187
|
+
const icon = useDetermineStatusIcon({ success, error });
|
|
193
188
|
|
|
194
189
|
const nativeOnChangeHandler = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
195
190
|
onChange && onChange(event);
|
|
@@ -214,6 +209,7 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, Props> = (
|
|
|
214
209
|
error && additionalClasses.push(classes.error);
|
|
215
210
|
disabled && additionalClasses.push(classes.disabled);
|
|
216
211
|
className && additionalClasses.push(className);
|
|
212
|
+
success && additionalClasses.push(classes.success);
|
|
217
213
|
|
|
218
214
|
/** The native select is purely for external form libraries. We use it to emit an onChange with native select event object so they know exactly what's happening. */
|
|
219
215
|
return (
|
|
@@ -262,7 +258,7 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, Props> = (
|
|
|
262
258
|
{value?.length > 0 && <span data-display-inner>{display}</span>}
|
|
263
259
|
</div>
|
|
264
260
|
<div className={classes["status"]}>
|
|
265
|
-
{
|
|
261
|
+
{icon}
|
|
266
262
|
<Icon className={classes["triangle-down"]} icon={Icons.TriangleDown} />
|
|
267
263
|
</div>
|
|
268
264
|
</button>
|
|
@@ -174,7 +174,7 @@ export const useSelectPositionList = ({
|
|
|
174
174
|
|
|
175
175
|
const calculateOptionListMaxHeight = (position: Position) => {
|
|
176
176
|
// Calculate max height if there's more space below the select
|
|
177
|
-
const listHeight = optionListReference.current
|
|
177
|
+
const listHeight = optionListReference.current?.getBoundingClientRect().height;
|
|
178
178
|
const transformOrigin = position.top !== "initial" ? "top" : "bottom";
|
|
179
179
|
|
|
180
180
|
const availableSpace =
|
|
@@ -184,7 +184,7 @@ export const useSelectPositionList = ({
|
|
|
184
184
|
16
|
|
185
185
|
: containerReference.current!.getBoundingClientRect()[transformOrigin] - 16;
|
|
186
186
|
|
|
187
|
-
if (availableSpace < listHeight) {
|
|
187
|
+
if (listHeight && availableSpace < listHeight) {
|
|
188
188
|
setOptionsListMaxHeight(`${availableSpace}px`);
|
|
189
189
|
setOpacity(100);
|
|
190
190
|
return;
|
|
@@ -14,14 +14,30 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
@
|
|
17
|
+
@use "../../mixins.module.scss";
|
|
18
18
|
|
|
19
19
|
.textarea-wrapper {
|
|
20
20
|
position: relative;
|
|
21
21
|
box-sizing: border-box;
|
|
22
22
|
width: 100%;
|
|
23
|
+
[data-icon-status="success"],
|
|
24
|
+
[data-icon-status="error"] {
|
|
25
|
+
position: absolute;
|
|
26
|
+
right: 1.25rem;
|
|
27
|
+
z-index: 1;
|
|
28
|
+
top: 0.85rem;
|
|
29
|
+
font-size: 1.25rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-icon-status="success"] {
|
|
33
|
+
color: var(--success);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-icon-status="error"] {
|
|
37
|
+
color: var(--error);
|
|
38
|
+
}
|
|
23
39
|
|
|
24
|
-
@include outlineStates;
|
|
40
|
+
@include mixins.outlineStates();
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
.textarea {
|
|
@@ -29,7 +45,7 @@
|
|
|
29
45
|
box-sizing: border-box;
|
|
30
46
|
border: 0;
|
|
31
47
|
border-radius: var(--input-border-radius);
|
|
32
|
-
@include transition(all, 0.2s, ease-in-out);
|
|
48
|
+
@include mixins.transition(all, 0.2s, ease-in-out);
|
|
33
49
|
font-family: var(--font-family);
|
|
34
50
|
font-size: var(--font-size);
|
|
35
51
|
color: var(--greyed-out);
|
|
@@ -42,20 +58,12 @@
|
|
|
42
58
|
cursor: not-allowed;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
@include browserOutlineDisabled;
|
|
61
|
+
@include mixins.browserOutlineDisabled();
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
@include outline;
|
|
64
|
+
@include mixins.outline();
|
|
49
65
|
|
|
50
66
|
.error {
|
|
51
67
|
border-color: var(--error);
|
|
52
68
|
color: var(--error);
|
|
53
69
|
}
|
|
54
|
-
|
|
55
|
-
.warning {
|
|
56
|
-
color: var(--error);
|
|
57
|
-
position: absolute;
|
|
58
|
-
right: 1.25rem;
|
|
59
|
-
top: 0.75rem;
|
|
60
|
-
font-size: 1.25rem;
|
|
61
|
-
}
|
|
@@ -116,3 +116,11 @@ describe("Error status", () => {
|
|
|
116
116
|
expect(textarea.nextElementSibling).toHaveClass("icon-error-circle");
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
describe("Success status", () => {
|
|
121
|
+
it("success icon should be visible", () => {
|
|
122
|
+
const { textarea } = createTextarea({ success: true });
|
|
123
|
+
const icon = textarea.parentElement?.querySelector(".icon-checkmark-circle-breakout");
|
|
124
|
+
expect(icon).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
});
|