@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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/Form/FileUpload/FileItem/FileItem.d.ts +17 -0
  3. package/dist/Form/FileUpload/FileUpload.d.ts +26 -0
  4. package/dist/Icon/Icon.d.ts +4 -1
  5. package/dist/ProgressBar/ProgressBar.d.ts +2 -1
  6. package/dist/Typography/Typography.d.ts +1 -1
  7. package/dist/_BaseStyling_/BaseStyling.d.ts +23 -0
  8. package/dist/hooks/useUploadFile.d.ts +22 -0
  9. package/dist/react-lib-components.cjs.development.js +130 -98
  10. package/dist/react-lib-components.cjs.development.js.map +1 -1
  11. package/dist/react-lib-components.cjs.production.min.js +1 -1
  12. package/dist/react-lib-components.cjs.production.min.js.map +1 -1
  13. package/dist/react-lib-components.esm.js +130 -98
  14. package/dist/react-lib-components.esm.js.map +1 -1
  15. package/dist/util/helper.d.ts +7 -0
  16. package/package.json +24 -21
  17. package/src/Breadcrumbs/Breadcrumbs.module.scss +2 -2
  18. package/src/Button/Button.module.scss +14 -2
  19. package/src/ContextMenu/ContextMenuItem.module.scss +1 -0
  20. package/src/DataGrid/DataGridHeader/DataGridHeader.module.scss +1 -1
  21. package/src/DataGrid/DataGridHeader/DataGridHeaderCell.module.scss +1 -1
  22. package/src/Form/Fieldset/Fieldset.module.scss +8 -1
  23. package/src/Form/FileUpload/FileItem/FileItem.modules.scss +75 -0
  24. package/src/Form/FileUpload/FileItem/FileItem.test.tsx +103 -0
  25. package/src/Form/FileUpload/FileItem/FileItem.tsx +141 -0
  26. package/src/Form/FileUpload/FileUpload.module.scss +106 -0
  27. package/src/Form/FileUpload/FileUpload.test.tsx +374 -0
  28. package/src/Form/FileUpload/FileUpload.tsx +251 -0
  29. package/src/Form/Input/Input.module.scss +9 -3
  30. package/src/Form/Select/Select.module.scss +26 -3
  31. package/src/Form/Wrapper/InputWrapper/InputWrapper.tsx +3 -1
  32. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.module.scss +9 -1
  33. package/src/Form/Wrapper/Wrapper/Wrapper.module.scss +11 -2
  34. package/src/Icon/Icon.module.scss +12 -0
  35. package/src/Icon/Icon.tsx +4 -1
  36. package/src/Link/Link.module.scss +1 -1
  37. package/src/Notifications/Banner/Banner.module.scss +2 -2
  38. package/src/Pagination/Pagination.module.scss +1 -0
  39. package/src/ProgressBar/ProgressBar.module.scss +11 -9
  40. package/src/ProgressBar/ProgressBar.test.tsx +21 -0
  41. package/src/ProgressBar/ProgressBar.tsx +7 -2
  42. package/src/Tabs/TabButton.module.scss +3 -3
  43. package/src/Tabs/Tabs.module.scss +1 -0
  44. package/src/Typography/Typography.module.scss +4 -4
  45. package/src/Typography/Typography.tsx +1 -1
  46. package/src/Wizard/BaseWizardSteps/BaseWizardSteps.module.scss +17 -7
  47. package/src/_BaseStyling_/BaseStyling.tsx +73 -27
  48. package/src/hooks/useUploadFile.test.ts +211 -0
  49. package/src/hooks/useUploadFile.tsx +136 -0
  50. package/src/mixins.module.scss +26 -7
  51. package/src/util/helper.test.tsx +188 -16
  52. package/src/util/helper.tsx +38 -0
  53. 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
+ };
@@ -30,10 +30,10 @@
30
30
  color: var(--button-fill-text-color);
31
31
  }
32
32
  }
33
- font-weight: bold;
33
+ font-weight: 500;
34
34
  } @else if $variant == "outline" {
35
35
  background-color: var(--white);
36
- font-weight: bold;
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: var(--light-grey-border);
167
- border-style: var(--input-border-style);
168
- border-width: var(--input-border-width);
169
- border-radius: var(--input-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
+ }
@@ -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 { generateID, filterProps, debounce, throttle } from "./helper";
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
- await fireEvent.resize(window);
80
- await fireEvent.resize(window);
81
- await fireEvent.resize(window);
82
- await fireEvent.resize(window);
83
- await fireEvent.resize(window);
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
- await fireEvent.resize(window);
154
- await fireEvent.resize(window);
155
- await fireEvent.resize(window);
156
- await fireEvent.resize(window);
157
- await fireEvent.resize(window);
158
- await fireEvent.resize(window);
159
- await fireEvent.resize(window);
160
- await fireEvent.resize(window);
161
- await fireEvent.resize(window);
162
- await fireEvent.resize(window);
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
+ });
@@ -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;