@onewelcome/react-lib-components 1.6.0 → 1.8.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/Form/FileUpload/FileItem/FileItem.d.ts +17 -0
- package/dist/Form/FileUpload/FileUpload.d.ts +26 -0
- package/dist/Icon/Icon.d.ts +4 -1
- package/dist/ProgressBar/ProgressBar.d.ts +2 -1
- package/dist/Typography/Typography.d.ts +1 -1
- package/dist/_BaseStyling_/BaseStyling.d.ts +23 -0
- package/dist/hooks/useUploadFile.d.ts +22 -0
- package/dist/react-lib-components.cjs.development.js +130 -98
- 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 +130 -98
- package/dist/react-lib-components.esm.js.map +1 -1
- package/dist/util/helper.d.ts +7 -0
- package/package.json +24 -21
- package/src/Breadcrumbs/Breadcrumbs.module.scss +2 -2
- package/src/Button/Button.module.scss +14 -2
- package/src/ContextMenu/ContextMenuItem.module.scss +1 -0
- package/src/DataGrid/DataGridHeader/DataGridHeader.module.scss +1 -1
- package/src/DataGrid/DataGridHeader/DataGridHeaderCell.module.scss +1 -1
- package/src/Form/Fieldset/Fieldset.module.scss +8 -1
- 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 +9 -3
- package/src/Form/Select/Select.module.scss +26 -3
- package/src/Form/Wrapper/InputWrapper/InputWrapper.tsx +3 -1
- package/src/Form/Wrapper/SelectWrapper/SelectWrapper.module.scss +9 -1
- package/src/Form/Wrapper/Wrapper/Wrapper.module.scss +11 -2
- package/src/Icon/Icon.module.scss +12 -0
- package/src/Icon/Icon.tsx +4 -1
- package/src/Link/Link.module.scss +1 -1
- package/src/Notifications/Banner/Banner.module.scss +2 -2
- package/src/Pagination/Pagination.module.scss +1 -0
- 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/Tabs/TabButton.module.scss +3 -3
- package/src/Tabs/Tabs.module.scss +1 -0
- package/src/Typography/Typography.module.scss +4 -4
- package/src/Typography/Typography.tsx +1 -1
- package/src/Wizard/BaseWizardSteps/BaseWizardSteps.module.scss +17 -7
- package/src/_BaseStyling_/BaseStyling.tsx +73 -27
- package/src/hooks/useUploadFile.test.ts +211 -0
- package/src/hooks/useUploadFile.tsx +136 -0
- package/src/mixins.module.scss +26 -7
- package/src/util/helper.test.tsx +188 -16
- package/src/util/helper.tsx +38 -0
- package/src/variables.scss +18 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useUploadFile } from "./useUploadFile";
|
|
2
|
+
import { renderHook } from "@testing-library/react-hooks";
|
|
3
|
+
import { waitFor } from "@testing-library/react";
|
|
4
|
+
import { FileType } from "../Form/FileUpload/FileUpload";
|
|
5
|
+
|
|
6
|
+
const DONE = 4;
|
|
7
|
+
|
|
8
|
+
const mockXhrRequest = (status: number, readyState?: number, response?: Object) => {
|
|
9
|
+
return {
|
|
10
|
+
open: jest.fn(),
|
|
11
|
+
send: jest.fn(),
|
|
12
|
+
addEventListener: jest.fn(),
|
|
13
|
+
onprogress: jest.fn(),
|
|
14
|
+
responseText: response,
|
|
15
|
+
onreadystatechange: jest.fn(),
|
|
16
|
+
getResponseHeader: jest.fn(),
|
|
17
|
+
upload: {
|
|
18
|
+
addEventListener: jest.fn()
|
|
19
|
+
},
|
|
20
|
+
DONE,
|
|
21
|
+
setRequestHeader: jest.fn(),
|
|
22
|
+
readyState: readyState || 4,
|
|
23
|
+
status
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type mockRequestParams = Parameters<typeof mockXhrRequest>;
|
|
28
|
+
|
|
29
|
+
const progressData = {
|
|
30
|
+
loaded: 12,
|
|
31
|
+
total: 100
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const requestInfo = { url: "https://www.test.io", withCredentials: true };
|
|
35
|
+
|
|
36
|
+
const file = {
|
|
37
|
+
name: "test.txt",
|
|
38
|
+
data: new File([""], "test.txt"),
|
|
39
|
+
size: 5,
|
|
40
|
+
type: ""
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const setupXhrEnvironment = (mockParams: mockRequestParams) => {
|
|
44
|
+
const mock = mockXhrRequest(...mockParams);
|
|
45
|
+
const mockClass = () => mock;
|
|
46
|
+
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
window.XMLHttpRequest = jest.fn().mockImplementation(mockClass);
|
|
49
|
+
|
|
50
|
+
return mock;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const expectReadyStateInfo = async (
|
|
54
|
+
files: FileType[],
|
|
55
|
+
mock: ReturnType<typeof mockXhrRequest>,
|
|
56
|
+
response: { code: number; body: { message: string } }
|
|
57
|
+
) => {
|
|
58
|
+
const { result } = renderHook(() =>
|
|
59
|
+
useUploadFile(files, { ...requestInfo, responseErrorPath: "body.message" })
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(mock.addEventListener).toHaveBeenCalled();
|
|
63
|
+
const [[, readystatechange]] = mock.addEventListener.mock.calls;
|
|
64
|
+
await waitFor(() => readystatechange());
|
|
65
|
+
const currentFile = result.current.updatedFiles[0];
|
|
66
|
+
expect(currentFile.status).toEqual("error");
|
|
67
|
+
expect(currentFile.error).toEqual(response.body.message);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("it should perform upload", () => {
|
|
71
|
+
it("should register the correct progress", async () => {
|
|
72
|
+
const mock = setupXhrEnvironment([200]);
|
|
73
|
+
const { result } = renderHook(() => useUploadFile([file], requestInfo));
|
|
74
|
+
expect(result.current).toBeDefined();
|
|
75
|
+
expect(mock.upload.addEventListener).toHaveBeenCalled();
|
|
76
|
+
const [[, progress]] = mock.upload.addEventListener.mock.calls;
|
|
77
|
+
await waitFor(() => progress(progressData));
|
|
78
|
+
|
|
79
|
+
const targetFile = result.current.updatedFiles[0];
|
|
80
|
+
expect(targetFile.name).toEqual(file.name);
|
|
81
|
+
expect(targetFile.progress).toEqual(12);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should contain a file with status of retry", async () => {
|
|
85
|
+
const mock = setupXhrEnvironment([0, DONE]);
|
|
86
|
+
const files2 = [
|
|
87
|
+
{
|
|
88
|
+
name: "test2.txt",
|
|
89
|
+
data: new File([""], "test2.txt"),
|
|
90
|
+
size: 5,
|
|
91
|
+
type: ""
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
const { result } = renderHook(() => useUploadFile(files2, requestInfo));
|
|
95
|
+
expect(result.current).toBeDefined();
|
|
96
|
+
expect(mock.addEventListener).toHaveBeenCalled();
|
|
97
|
+
const [[, readystatechange]] = mock.addEventListener.mock.calls;
|
|
98
|
+
await waitFor(() => readystatechange());
|
|
99
|
+
expect(result.current.updatedFiles[0].status).toEqual("retry");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should contain a file with status of error", async () => {
|
|
103
|
+
const response = { code: 404, body: { message: "Error test" } };
|
|
104
|
+
const mock = setupXhrEnvironment([404, DONE, response]);
|
|
105
|
+
const files3 = [
|
|
106
|
+
{
|
|
107
|
+
name: "test3.txt",
|
|
108
|
+
data: new File([""], "test3.txt"),
|
|
109
|
+
size: 5,
|
|
110
|
+
type: ""
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
await expectReadyStateInfo(files3, mock, response);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should contain a file with status of server error", async () => {
|
|
117
|
+
const response = { code: 500, body: { message: "Error test" } };
|
|
118
|
+
const mock = setupXhrEnvironment([500, DONE, response]);
|
|
119
|
+
const files6 = [
|
|
120
|
+
{
|
|
121
|
+
name: "test6.txt",
|
|
122
|
+
data: new File([""], "test3.txt"),
|
|
123
|
+
size: 5,
|
|
124
|
+
type: ""
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
await expectReadyStateInfo(files6, mock, response);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should contain a file with status of success", async () => {
|
|
131
|
+
const mock = setupXhrEnvironment([200, DONE]);
|
|
132
|
+
const files3 = [
|
|
133
|
+
{
|
|
134
|
+
name: "test3.txt",
|
|
135
|
+
data: new File([""], "test3.txt"),
|
|
136
|
+
size: 5,
|
|
137
|
+
type: ""
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
const { result } = renderHook(() => useUploadFile(files3, requestInfo));
|
|
141
|
+
expect(result.current).toBeDefined();
|
|
142
|
+
expect(mock.addEventListener).toHaveBeenCalled();
|
|
143
|
+
const [[, readystatechange]] = mock.addEventListener.mock.calls;
|
|
144
|
+
await waitFor(() => readystatechange());
|
|
145
|
+
const currentFile = result.current.updatedFiles[0];
|
|
146
|
+
expect(currentFile.status).toEqual("completed");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("should return data according to the parameters", () => {
|
|
151
|
+
it("should call custom callbacks", async () => {
|
|
152
|
+
const onComplete = jest.fn();
|
|
153
|
+
const onProgress = jest.fn();
|
|
154
|
+
const mock = setupXhrEnvironment([200, DONE]);
|
|
155
|
+
const files4 = [
|
|
156
|
+
{
|
|
157
|
+
name: "test4.txt",
|
|
158
|
+
data: new File([""], "test3.txt"),
|
|
159
|
+
size: 5,
|
|
160
|
+
type: ""
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
renderHook(() => useUploadFile(files4, requestInfo, { onProgress, onComplete }));
|
|
164
|
+
|
|
165
|
+
const [[, progress]] = mock.upload.addEventListener.mock.calls;
|
|
166
|
+
await waitFor(() => progress(progressData));
|
|
167
|
+
|
|
168
|
+
const [[, readystatechange]] = mock.addEventListener.mock.calls;
|
|
169
|
+
await waitFor(() => readystatechange());
|
|
170
|
+
|
|
171
|
+
expect(mock.addEventListener).toHaveBeenCalled();
|
|
172
|
+
expect(onComplete).toHaveBeenCalled();
|
|
173
|
+
expect(onComplete).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should add the correct error message", async () => {
|
|
177
|
+
const mock = setupXhrEnvironment([0, DONE]);
|
|
178
|
+
const files5 = [
|
|
179
|
+
{
|
|
180
|
+
name: "test5.txt",
|
|
181
|
+
data: new File([""], "test5.txt"),
|
|
182
|
+
size: 5,
|
|
183
|
+
type: ""
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
const error = "Network error";
|
|
187
|
+
const { result } = renderHook(() =>
|
|
188
|
+
useUploadFile(files5, { ...requestInfo, networkErrorText: error })
|
|
189
|
+
);
|
|
190
|
+
const [[, readystatechange]] = mock.addEventListener.mock.calls;
|
|
191
|
+
await waitFor(() => readystatechange());
|
|
192
|
+
|
|
193
|
+
expect(mock.addEventListener).toHaveBeenCalled();
|
|
194
|
+
const file = result.current.updatedFiles[0];
|
|
195
|
+
expect(file.error).toEqual(error);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("useFileUpload hook should not fire when url and array of files is not present", () => {
|
|
200
|
+
it("should return an empty array if url is empty", () => {
|
|
201
|
+
const { result } = renderHook(() => useUploadFile([], { url: "https://yoohoo.co" }));
|
|
202
|
+
|
|
203
|
+
expect(result.current.uploadingFiles.length).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should return an empty array if url is empty", () => {
|
|
207
|
+
const { result } = renderHook(() => useUploadFile([file], { url: "" }));
|
|
208
|
+
|
|
209
|
+
expect(result.current.uploadingFiles.length).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { FileType } from "../Form/FileUpload/FileUpload";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { getValueByPath } from "../util/helper";
|
|
20
|
+
|
|
21
|
+
export interface UploadResponseType {
|
|
22
|
+
fileList: FileType[];
|
|
23
|
+
status: XMLHttpRequest["status"];
|
|
24
|
+
}
|
|
25
|
+
export interface UseUploadFileCallback {
|
|
26
|
+
onComplete?: (response: UploadResponseType) => void;
|
|
27
|
+
onProgress?: Function;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UploadFileRequestParams {
|
|
31
|
+
url: string;
|
|
32
|
+
headers?: Headers;
|
|
33
|
+
responseErrorPath?: string;
|
|
34
|
+
networkErrorText?: string;
|
|
35
|
+
withCredentials?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useUploadFile = (
|
|
39
|
+
files: FileType[],
|
|
40
|
+
request: UploadFileRequestParams,
|
|
41
|
+
callbacks?: UseUploadFileCallback
|
|
42
|
+
) => {
|
|
43
|
+
const { url, headers, withCredentials, networkErrorText, responseErrorPath } = request;
|
|
44
|
+
|
|
45
|
+
const { onComplete, onProgress } = callbacks || {};
|
|
46
|
+
|
|
47
|
+
const [uploadingFiles, setUploadingFiles] = useState<FileType[]>([]);
|
|
48
|
+
const [updatedFiles, setUpdatedFiles] = useState<FileType[]>([...files]);
|
|
49
|
+
|
|
50
|
+
const getUpdatedList = (
|
|
51
|
+
fileName: string,
|
|
52
|
+
fileStatus: FileType["status"],
|
|
53
|
+
progress?: number,
|
|
54
|
+
error?: string
|
|
55
|
+
) => {
|
|
56
|
+
return files.map(file => {
|
|
57
|
+
if (file.name === fileName) {
|
|
58
|
+
file.progress = progress;
|
|
59
|
+
file.error = error;
|
|
60
|
+
file.status = fileStatus;
|
|
61
|
+
}
|
|
62
|
+
return { ...file };
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const recordProgress = (e: ProgressEvent<XMLHttpRequestEventTarget>, fileName: string) => {
|
|
67
|
+
const progress = (e.loaded / e.total) * 100;
|
|
68
|
+
const updatedData = getUpdatedList(fileName, "uploading", progress);
|
|
69
|
+
setUpdatedFiles(updatedData);
|
|
70
|
+
onProgress && onProgress(fileName, progress);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getFileStatus = (
|
|
74
|
+
requestStatus: XMLHttpRequest["status"],
|
|
75
|
+
responseText: XMLHttpRequest["responseText"]
|
|
76
|
+
) => {
|
|
77
|
+
let fileStatus: FileType["status"] = undefined;
|
|
78
|
+
let error = "";
|
|
79
|
+
if (requestStatus >= 200 && requestStatus < 400) {
|
|
80
|
+
fileStatus = "completed";
|
|
81
|
+
} else if (requestStatus === 0) {
|
|
82
|
+
fileStatus = "retry";
|
|
83
|
+
error =
|
|
84
|
+
networkErrorText || "Network error. Check internet connection and retry uploading the file";
|
|
85
|
+
} else if (requestStatus >= 400 && requestStatus < 500) {
|
|
86
|
+
const response = responseText && JSON.parse(JSON.stringify(responseText));
|
|
87
|
+
fileStatus = "error";
|
|
88
|
+
error = responseErrorPath ? getValueByPath(response, responseErrorPath) : "Bad request";
|
|
89
|
+
} else if (requestStatus >= 500) {
|
|
90
|
+
const response = responseText && JSON.parse(JSON.stringify(responseText));
|
|
91
|
+
fileStatus = "error";
|
|
92
|
+
error = responseErrorPath ? getValueByPath(response, responseErrorPath) : "Server Error";
|
|
93
|
+
}
|
|
94
|
+
return { fileStatus, error };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleOnComplete = (xhr: XMLHttpRequest, fileName: string) => {
|
|
98
|
+
const { status, readyState, responseText } = xhr;
|
|
99
|
+
if (readyState === xhr.DONE) {
|
|
100
|
+
const { fileStatus, error } = getFileStatus(status, responseText);
|
|
101
|
+
const updatedList = getUpdatedList(fileName, fileStatus, undefined, error);
|
|
102
|
+
setUpdatedFiles(updatedList);
|
|
103
|
+
const response = { fileList: updatedList, status };
|
|
104
|
+
setUploadingFiles(prevState => prevState.filter(selected => selected.name === fileName));
|
|
105
|
+
onComplete && onComplete(response);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const uploadFile = (file: FileType) => {
|
|
110
|
+
const xhr = new XMLHttpRequest();
|
|
111
|
+
xhr.upload.addEventListener("progress", e => recordProgress(e, file.name));
|
|
112
|
+
xhr.addEventListener("readystatechange", () => handleOnComplete(xhr, file.name));
|
|
113
|
+
headers && headers.forEach((value, key) => xhr.setRequestHeader(key, value));
|
|
114
|
+
withCredentials && (xhr.withCredentials = true);
|
|
115
|
+
xhr.open("POST", url, true);
|
|
116
|
+
const formData = new FormData();
|
|
117
|
+
formData.append("file", file.data as File);
|
|
118
|
+
formData.append("name", file.name);
|
|
119
|
+
xhr.send(formData);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!url || !files.length) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
files.forEach(file => {
|
|
128
|
+
if (!file.status && !uploadingFiles.find(selected => selected.name === file.name)) {
|
|
129
|
+
setUploadingFiles(prevState => [...prevState, file]);
|
|
130
|
+
uploadFile(file);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}, [url, files]);
|
|
134
|
+
|
|
135
|
+
return { updatedFiles, setUpdatedFiles, uploadingFiles };
|
|
136
|
+
};
|
package/src/mixins.module.scss
CHANGED
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
color: var(--button-fill-text-color);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
font-weight:
|
|
33
|
+
font-weight: 500;
|
|
34
34
|
} @else if $variant == "outline" {
|
|
35
35
|
background-color: var(--white);
|
|
36
|
-
font-weight:
|
|
36
|
+
font-weight: 500;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
&.primary:not(#{$disabledSelector}) {
|
|
@@ -156,17 +156,26 @@
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
@mixin outline
|
|
159
|
+
@mixin outline(
|
|
160
|
+
$color: var(--light-grey-border),
|
|
161
|
+
$style: var(--input-border-style),
|
|
162
|
+
$width: var(--input-border-width),
|
|
163
|
+
$borderRadius: var(--input-border-radius),
|
|
164
|
+
$backgroundColor: ""
|
|
165
|
+
) {
|
|
160
166
|
.outline {
|
|
161
167
|
pointer-events: none;
|
|
162
168
|
position: absolute;
|
|
163
169
|
margin: 0;
|
|
164
170
|
padding: 0;
|
|
165
171
|
inset: 0;
|
|
166
|
-
border-color:
|
|
167
|
-
border-style:
|
|
168
|
-
border-width:
|
|
169
|
-
border-radius:
|
|
172
|
+
border-color: $color;
|
|
173
|
+
border-style: $style;
|
|
174
|
+
border-width: $width;
|
|
175
|
+
border-radius: $borderRadius;
|
|
176
|
+
@if ($backgroundColor != "") {
|
|
177
|
+
background-color: $backgroundColor;
|
|
178
|
+
}
|
|
170
179
|
@include transition(all, 0.2s, ease-in-out);
|
|
171
180
|
}
|
|
172
181
|
}
|
|
@@ -249,3 +258,13 @@
|
|
|
249
258
|
transition-duration: 0.1ms;
|
|
250
259
|
}
|
|
251
260
|
}
|
|
261
|
+
|
|
262
|
+
@mixin width-size($size) {
|
|
263
|
+
.w-#{$size} {
|
|
264
|
+
width: $size * 1%;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@for $i from 1 through 100 {
|
|
269
|
+
@include width-size($i + 5);
|
|
270
|
+
}
|
package/src/util/helper.test.tsx
CHANGED
|
@@ -16,7 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import React, { useCallback, useEffect, useState } from "react";
|
|
18
18
|
import { fireEvent, waitFor } from "@testing-library/dom";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
debounce,
|
|
21
|
+
filterProps,
|
|
22
|
+
generateID,
|
|
23
|
+
remToPx,
|
|
24
|
+
throttle,
|
|
25
|
+
areArraysDifferent,
|
|
26
|
+
getValueByPath,
|
|
27
|
+
isEqual
|
|
28
|
+
} from "./helper";
|
|
20
29
|
import { render } from "@testing-library/react";
|
|
21
30
|
|
|
22
31
|
/* Generate an ID of 20 characters with a string woven in */
|
|
@@ -76,11 +85,11 @@ describe("debounce function", () => {
|
|
|
76
85
|
|
|
77
86
|
window.addEventListener("resize", debounce(debouncedFunction, 200));
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
fireEvent.resize(window);
|
|
89
|
+
fireEvent.resize(window);
|
|
90
|
+
fireEvent.resize(window);
|
|
91
|
+
fireEvent.resize(window);
|
|
92
|
+
fireEvent.resize(window);
|
|
84
93
|
|
|
85
94
|
await waitFor(() => expect(debouncedFunction).toHaveBeenCalledTimes(1));
|
|
86
95
|
});
|
|
@@ -150,18 +159,181 @@ describe("throttling works", () => {
|
|
|
150
159
|
|
|
151
160
|
render(<ExampleComponent throttledFunction={exampleFunction} />);
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
fireEvent.resize(window);
|
|
163
|
+
fireEvent.resize(window);
|
|
164
|
+
fireEvent.resize(window);
|
|
165
|
+
fireEvent.resize(window);
|
|
166
|
+
fireEvent.resize(window);
|
|
167
|
+
fireEvent.resize(window);
|
|
168
|
+
fireEvent.resize(window);
|
|
169
|
+
fireEvent.resize(window);
|
|
170
|
+
fireEvent.resize(window);
|
|
171
|
+
fireEvent.resize(window);
|
|
163
172
|
|
|
164
173
|
expect(exampleFunction).not.toHaveBeenCalledTimes(1);
|
|
165
174
|
expect(exampleFunction).not.toHaveBeenCalledTimes(10);
|
|
166
175
|
});
|
|
167
176
|
});
|
|
177
|
+
|
|
178
|
+
describe("areArraysDifferent works as expected", () => {
|
|
179
|
+
it("should return true for different arrays", () => {
|
|
180
|
+
const arr1 = [
|
|
181
|
+
{
|
|
182
|
+
name: "test1"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "test2"
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const arr2 = [
|
|
190
|
+
{
|
|
191
|
+
name: "test1"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "test3"
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
const result = areArraysDifferent(arr1, arr2, "name");
|
|
199
|
+
expect(result).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should return false for arrays with same values", () => {
|
|
203
|
+
const arr1 = [
|
|
204
|
+
{
|
|
205
|
+
name: "test1"
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const arr2 = [
|
|
210
|
+
{
|
|
211
|
+
name: "test1"
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const result = areArraysDifferent(arr1, arr2, "name");
|
|
216
|
+
expect(result).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should return false for falsy values", () => {
|
|
220
|
+
const arr1 = [
|
|
221
|
+
{
|
|
222
|
+
name: "test1"
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const arr2 = [
|
|
227
|
+
{
|
|
228
|
+
label: "test1"
|
|
229
|
+
}
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const result = areArraysDifferent(arr1, arr2, "name");
|
|
233
|
+
expect(result).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("return correct values from getValueByPath", () => {
|
|
238
|
+
it("should return the correct value form a multi layered object", () => {
|
|
239
|
+
const val = "test";
|
|
240
|
+
const obj = {
|
|
241
|
+
firstNode: {
|
|
242
|
+
secondNode: {
|
|
243
|
+
thirdNode: {
|
|
244
|
+
val
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = getValueByPath(obj, "firstNode.secondNode.thirdNode.val");
|
|
251
|
+
expect(result).toBe(val);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("verifies if isEqual returns the correct value", () => {
|
|
256
|
+
it("should return true for equal values objects", () => {
|
|
257
|
+
const obj1 = {
|
|
258
|
+
name1: "test1",
|
|
259
|
+
name2: {
|
|
260
|
+
val: "test2"
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const obj2 = {
|
|
265
|
+
name1: "test1",
|
|
266
|
+
name2: {
|
|
267
|
+
val: "test2"
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const res = isEqual(obj1, obj2);
|
|
272
|
+
expect(res).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should return false for unequal values objects", () => {
|
|
276
|
+
const obj1 = {
|
|
277
|
+
name1: "test1",
|
|
278
|
+
name2: {
|
|
279
|
+
val: "test2"
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const obj2 = {
|
|
284
|
+
name1: "test1"
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const res = isEqual(obj1, obj2);
|
|
288
|
+
expect(res).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should return false for falsy values", () => {
|
|
292
|
+
const obj1 = {
|
|
293
|
+
name1: "test1",
|
|
294
|
+
name2: {
|
|
295
|
+
val: "test2"
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const obj2 = null;
|
|
299
|
+
|
|
300
|
+
const res = isEqual(obj1, obj2);
|
|
301
|
+
expect(res).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should return false for different types", () => {
|
|
305
|
+
const obj1 = [
|
|
306
|
+
{
|
|
307
|
+
name1: "test1",
|
|
308
|
+
name2: {
|
|
309
|
+
val: "test2"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
const obj2 = {
|
|
314
|
+
name1: "test1",
|
|
315
|
+
name2: {
|
|
316
|
+
val: "test2"
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const res = isEqual(obj1, obj2);
|
|
321
|
+
expect(res).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("pixel to rem function works", () => {
|
|
326
|
+
document.documentElement.style.setProperty("font-size", "16px");
|
|
327
|
+
|
|
328
|
+
it.each([
|
|
329
|
+
[1, 16],
|
|
330
|
+
[1.25, 20],
|
|
331
|
+
[1.5, 24],
|
|
332
|
+
[1.75, 28],
|
|
333
|
+
[2, 32]
|
|
334
|
+
])("%p rem equals %p px when font-size is 16px", async (rem: number, px: number) => {
|
|
335
|
+
const result = remToPx(rem);
|
|
336
|
+
|
|
337
|
+
expect(result).toBe(px);
|
|
338
|
+
});
|
|
339
|
+
});
|
package/src/util/helper.tsx
CHANGED
|
@@ -143,3 +143,41 @@ export const throttle = (fn: (...args: unknown[]) => unknown, delay: number) =>
|
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
};
|
|
146
|
+
|
|
147
|
+
export const isEqual = (x: any, y: any): boolean => {
|
|
148
|
+
const typesCoincide = x && y && typeof x === "object" && typeof y === "object";
|
|
149
|
+
return typesCoincide
|
|
150
|
+
? Object.keys(x).length === Object.keys(y).length &&
|
|
151
|
+
Object.keys(x).every(key => isEqual(x[key], y[key]))
|
|
152
|
+
: x === y;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const areArraysDifferent = (
|
|
156
|
+
arr1: Record<string, any>[],
|
|
157
|
+
arr2: Record<string, any>[],
|
|
158
|
+
key: string
|
|
159
|
+
) => {
|
|
160
|
+
if (arr1.length !== arr2.length) {
|
|
161
|
+
return true;
|
|
162
|
+
} else {
|
|
163
|
+
const firstFilteredArray = arr1.filter(arr1Item =>
|
|
164
|
+
arr2.some((arr2Item: { [x: string]: any }) => !isEqual(arr1Item[key], arr2Item[key]))
|
|
165
|
+
);
|
|
166
|
+
const secondFilteredArray = arr2.filter(arr2Item =>
|
|
167
|
+
arr1.some((arr1Item: { [x: string]: any }) => !isEqual(arr1Item[key], arr2Item[key]))
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return !!firstFilteredArray.length || !!secondFilteredArray.length;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const getValueByPath = (obj: { [key: string]: any }, path: string): any => {
|
|
175
|
+
return path.split(".").reduce((res, prop) => {
|
|
176
|
+
return res[prop];
|
|
177
|
+
}, obj);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Source: https://stackoverflow.com/a/42769683/5084110 */
|
|
181
|
+
export const remToPx = (rem: number): number => {
|
|
182
|
+
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
183
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
$form-element-horizontal-padding-mobile: 1rem;
|
|
18
|
+
$form-element-horizontal-padding-desktop: 1.25rem;
|