@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.
Files changed (101) hide show
  1. package/README.md +4 -4
  2. package/dist/Button/Button.d.ts +0 -1
  3. package/dist/DataGrid/datagrid.interfaces.d.ts +1 -0
  4. package/dist/Form/Checkbox/Checkbox.d.ts +1 -1
  5. package/dist/Form/FileUpload/FileItem/FileItem.d.ts +17 -0
  6. package/dist/Form/FileUpload/FileUpload.d.ts +26 -0
  7. package/dist/Form/FormHelperText/FormHelperText.d.ts +1 -1
  8. package/dist/Form/FormSelectorWrapper/FormSelectorWrapper.d.ts +1 -1
  9. package/dist/Form/Input/Input.d.ts +2 -2
  10. package/dist/Form/Radio/Radio.d.ts +1 -1
  11. package/dist/Form/Select/Select.d.ts +1 -1
  12. package/dist/Form/Textarea/Textarea.d.ts +1 -6
  13. package/dist/Form/Toggle/Toggle.d.ts +1 -1
  14. package/dist/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.d.ts +1 -1
  15. package/dist/Form/Wrapper/InputWrapper/InputWrapper.d.ts +1 -1
  16. package/dist/Form/Wrapper/RadioWrapper/RadioWrapper.d.ts +1 -1
  17. package/dist/Form/Wrapper/SelectWrapper/SelectWrapper.d.ts +1 -1
  18. package/dist/Form/Wrapper/TextareaWrapper/TextareaWrapper.d.ts +1 -1
  19. package/dist/Form/form.interfaces.d.ts +1 -0
  20. package/dist/Icon/Icon.d.ts +4 -1
  21. package/dist/Link/Link.d.ts +1 -2
  22. package/dist/Notifications/Banner/Banner.d.ts +11 -0
  23. package/dist/ProgressBar/ProgressBar.d.ts +2 -1
  24. package/dist/Tabs/TabButton.d.ts +0 -1
  25. package/dist/_BaseStyling_/BaseStyling.d.ts +5 -0
  26. package/dist/hooks/useDetermineStatusIcon.d.ts +3 -0
  27. package/dist/hooks/useUploadFile.d.ts +22 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/react-lib-components.cjs.development.js +431 -326
  30. package/dist/react-lib-components.cjs.development.js.map +1 -1
  31. package/dist/react-lib-components.cjs.production.min.js +1 -1
  32. package/dist/react-lib-components.cjs.production.min.js.map +1 -1
  33. package/dist/react-lib-components.esm.js +431 -327
  34. package/dist/react-lib-components.esm.js.map +1 -1
  35. package/dist/util/helper.d.ts +5 -0
  36. package/package.json +28 -25
  37. package/src/Button/BaseButton.module.scss +2 -2
  38. package/src/Button/Button.module.scss +4 -5
  39. package/src/Button/Button.tsx +0 -1
  40. package/src/Button/IconButton.module.scss +4 -5
  41. package/src/DataGrid/DataGrid.tsx +3 -2
  42. package/src/DataGrid/DataGridActions/DataGridActions.tsx +16 -9
  43. package/src/DataGrid/DataGridBody/DataGridCell.module.scss +2 -2
  44. package/src/DataGrid/DataGridHeader/DataGridHeader.test.tsx +8 -3
  45. package/src/DataGrid/DataGridHeader/DataGridHeader.tsx +3 -1
  46. package/src/DataGrid/datagrid.interfaces.ts +1 -0
  47. package/src/Form/FileUpload/FileItem/FileItem.modules.scss +75 -0
  48. package/src/Form/FileUpload/FileItem/FileItem.test.tsx +103 -0
  49. package/src/Form/FileUpload/FileItem/FileItem.tsx +141 -0
  50. package/src/Form/FileUpload/FileUpload.module.scss +106 -0
  51. package/src/Form/FileUpload/FileUpload.test.tsx +374 -0
  52. package/src/Form/FileUpload/FileUpload.tsx +251 -0
  53. package/src/Form/Input/Input.module.scss +36 -26
  54. package/src/Form/Input/Input.test.tsx +10 -0
  55. package/src/Form/Input/Input.tsx +7 -5
  56. package/src/Form/Select/Select.module.scss +9 -6
  57. package/src/Form/Select/Select.test.tsx +11 -0
  58. package/src/Form/Select/Select.tsx +5 -9
  59. package/src/Form/Select/SelectService.ts +2 -2
  60. package/src/Form/Textarea/Textarea.module.scss +21 -13
  61. package/src/Form/Textarea/Textarea.test.tsx +8 -0
  62. package/src/Form/Textarea/Textarea.tsx +6 -12
  63. package/src/Form/Toggle/Toggle.module.scss +3 -3
  64. package/src/Form/Wrapper/InputWrapper/InputWrapper.module.scss +7 -3
  65. package/src/Form/Wrapper/InputWrapper/InputWrapper.tsx +2 -0
  66. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.tsx +12 -1
  67. package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.module.scss +15 -14
  68. package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.tsx +2 -1
  69. package/src/Form/Wrapper/Wrapper/Wrapper.module.scss +2 -2
  70. package/src/Form/form.interfaces.ts +1 -0
  71. package/src/Icon/Icon.module.scss +12 -0
  72. package/src/Icon/Icon.tsx +4 -1
  73. package/src/Link/Link.module.scss +5 -5
  74. package/src/Link/Link.tsx +14 -13
  75. package/src/Notifications/Banner/Banner.module.scss +76 -0
  76. package/src/Notifications/Banner/Banner.test.tsx +84 -0
  77. package/src/Notifications/Banner/Banner.tsx +78 -0
  78. package/src/Notifications/BaseModal/BaseModal.module.scss +2 -2
  79. package/src/Notifications/Snackbar/SnackbarContainer/SnackbarContainer.module.scss +2 -2
  80. package/src/Notifications/Snackbar/SnackbarItem/SnackbarItem.module.scss +4 -4
  81. package/src/Notifications/Snackbar/SnackbarItem/SnackbarItem.tsx +3 -2
  82. package/src/Popover/Popover.module.scss +2 -2
  83. package/src/ProgressBar/ProgressBar.module.scss +11 -9
  84. package/src/ProgressBar/ProgressBar.test.tsx +21 -0
  85. package/src/ProgressBar/ProgressBar.tsx +7 -2
  86. package/src/Skeleton/Skeleton.module.scss +2 -2
  87. package/src/Tabs/TabButton.tsx +1 -2
  88. package/src/Tabs/Tabs.module.scss +2 -2
  89. package/src/Tabs/Tabs.tsx +13 -10
  90. package/src/Tiles/Tile.module.scss +4 -4
  91. package/src/Tooltip/Tooltip.module.scss +3 -3
  92. package/src/Typography/Typography.module.scss +2 -2
  93. package/src/_BaseStyling_/BaseStyling.tsx +13 -3
  94. package/src/hooks/useDetermineStatusIcon.test.ts +28 -0
  95. package/src/hooks/useDetermineStatusIcon.tsx +35 -0
  96. package/src/hooks/useUploadFile.test.ts +211 -0
  97. package/src/hooks/useUploadFile.tsx +136 -0
  98. package/src/index.ts +1 -0
  99. package/src/mixins.module.scss +24 -5
  100. package/src/util/helper.test.tsx +156 -1
  101. 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
- @import "../../mixins.module.scss";
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: 3.5rem;
128
+ padding-right: 1.25rem;
104
129
 
105
130
  &.remove-extra-indent {
106
131
  padding-right: 0;
107
132
  }
108
133
  }
109
-
110
- .warning {
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
  });
@@ -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
- @import "../../readyclasses.module.scss";
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
- .warning {
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 statusIcon = () => {
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
- {statusIcon()}
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!.getBoundingClientRect().height;
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
- @import "../../mixins.module.scss";
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
+ });