@onewelcome/react-lib-components 0.1.0-alpha → 0.1.3-alpha

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 (240) hide show
  1. package/README.md +16 -1
  2. package/dist/Breadcrumbs/Breadcrumbs.d.ts +3 -3
  3. package/dist/Button/BaseButton.d.ts +3 -4
  4. package/dist/Button/Button.d.ts +3 -4
  5. package/dist/Button/IconButton.d.ts +4 -5
  6. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  7. package/dist/Form/Checkbox/Checkbox.d.ts +5 -5
  8. package/dist/Form/Fieldset/Fieldset.d.ts +9 -7
  9. package/dist/Form/FormControl/FormControl.d.ts +6 -5
  10. package/dist/Form/FormGroup/FormGroup.d.ts +4 -4
  11. package/dist/Form/FormHelperText/FormHelperText.d.ts +4 -5
  12. package/dist/Form/FormSelectorWrapper/FormSelectorWrapper.d.ts +8 -12
  13. package/dist/Form/Input/Input.d.ts +7 -6
  14. package/dist/Form/Label/Label.d.ts +4 -5
  15. package/dist/Form/Radio/Radio.d.ts +5 -5
  16. package/dist/Form/Select/Option.d.ts +3 -4
  17. package/dist/Form/Select/Select.d.ts +4 -4
  18. package/dist/Form/Textarea/Textarea.d.ts +9 -5
  19. package/dist/Form/Toggle/Toggle.d.ts +3 -3
  20. package/dist/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.d.ts +4 -3
  21. package/dist/Form/Wrapper/InputWrapper/InputWrapper.d.ts +5 -5
  22. package/dist/Form/Wrapper/RadioWrapper/RadioWrapper.d.ts +4 -4
  23. package/dist/Form/Wrapper/SelectWrapper/SelectWrapper.d.ts +8 -4
  24. package/dist/Form/Wrapper/TextareaWrapper/TextareaWrapper.d.ts +3 -3
  25. package/dist/Form/Wrapper/Wrapper/Wrapper.d.ts +6 -6
  26. package/dist/Form/form.interfaces.d.ts +4 -3
  27. package/dist/Icon/Icon.d.ts +4 -4
  28. package/dist/Link/Link.d.ts +3 -5
  29. package/dist/Notifications/BaseModal/BaseModal.d.ts +17 -0
  30. package/dist/Notifications/BaseModal/BaseModalActions/BaseModalActions.d.ts +5 -0
  31. package/dist/Notifications/BaseModal/BaseModalContent/BaseModalContent.d.ts +8 -0
  32. package/dist/{BaseModal → Notifications/BaseModal}/BaseModalContext.d.ts +0 -0
  33. package/dist/Notifications/BaseModal/BaseModalHeader/BaseModalHeader.d.ts +8 -0
  34. package/dist/Notifications/Dialog/Dialog.d.ts +19 -0
  35. package/dist/Notifications/Dialog/DialogActions/DialogActions.d.ts +6 -0
  36. package/dist/Notifications/Dialog/DialogTitle/DialogTitle.d.ts +6 -0
  37. package/dist/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.d.ts +13 -0
  38. package/dist/Notifications/DiscardChangesModal/DiscardChangesModal.d.ts +13 -0
  39. package/dist/{Modal → Notifications/Modal}/Modal.d.ts +0 -0
  40. package/dist/{Modal → Notifications/Modal}/ModalActions/ModalActions.d.ts +0 -0
  41. package/dist/{Modal → Notifications/Modal}/ModalContent/ModalContent.d.ts +0 -0
  42. package/dist/{Modal → Notifications/Modal}/ModalHeader/ModalHeader.d.ts +0 -0
  43. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.d.ts +0 -0
  44. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.d.ts +0 -0
  45. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.d.ts +1 -1
  46. package/dist/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarStateProvider.d.ts +0 -0
  47. package/dist/{Snackbar → Notifications/Snackbar}/interfaces.d.ts +0 -0
  48. package/dist/{Snackbar → Notifications/Snackbar}/useSnackbar.d.ts +0 -0
  49. package/dist/Pagination/Pagination.d.ts +19 -0
  50. package/dist/Popover/Popover.d.ts +3 -3
  51. package/dist/Tabs/Tab.d.ts +11 -0
  52. package/dist/Tabs/TabButton.d.ts +10 -0
  53. package/dist/Tabs/TabPanel.d.ts +8 -0
  54. package/dist/Tabs/Tabs.d.ts +9 -0
  55. package/dist/TextEllipsis/TextEllipsis.d.ts +6 -0
  56. package/dist/Tiles/Tile.d.ts +6 -7
  57. package/dist/Tiles/Tiles.d.ts +3 -3
  58. package/dist/Tooltip/Tooltip.d.ts +3 -3
  59. package/dist/Typography/Typography.d.ts +6 -4
  60. package/dist/Wizard/BaseWizardSteps/BaseWizardSteps.d.ts +3 -3
  61. package/dist/Wizard/WizardSteps/WizardSteps.d.ts +3 -3
  62. package/dist/_BaseStyling_/BaseStyling.d.ts +9 -0
  63. package/dist/hooks/useRepeater.d.ts +10 -0
  64. package/dist/hooks/useSpacing.d.ts +2 -2
  65. package/dist/hooks/useWrapper.d.ts +1 -1
  66. package/dist/index.d.ts +12 -7
  67. package/dist/interfaces.d.ts +2 -11
  68. package/dist/react-lib-components.cjs.development.js +1861 -1287
  69. package/dist/react-lib-components.cjs.development.js.map +1 -1
  70. package/dist/react-lib-components.cjs.production.min.js +1 -1
  71. package/dist/react-lib-components.cjs.production.min.js.map +1 -1
  72. package/dist/react-lib-components.esm.js +1858 -1289
  73. package/dist/react-lib-components.esm.js.map +1 -1
  74. package/dist/util/helper.d.ts +6 -1
  75. package/package.json +30 -24
  76. package/src/Breadcrumbs/Breadcrumbs.tsx +39 -37
  77. package/src/Button/BaseButton.test.tsx +65 -19
  78. package/src/Button/BaseButton.tsx +2 -3
  79. package/src/Button/Button.test.tsx +63 -17
  80. package/src/Button/Button.tsx +15 -4
  81. package/src/Button/IconButton.test.tsx +57 -22
  82. package/src/Button/IconButton.tsx +21 -12
  83. package/src/ContextMenu/ContextMenu.test.tsx +27 -1
  84. package/src/ContextMenu/ContextMenu.tsx +70 -65
  85. package/src/Form/Checkbox/Checkbox.module.scss +4 -0
  86. package/src/Form/Checkbox/Checkbox.test.tsx +28 -2
  87. package/src/Form/Checkbox/Checkbox.tsx +132 -117
  88. package/src/Form/Fieldset/Fieldset.module.scss +11 -1
  89. package/src/Form/Fieldset/Fieldset.test.tsx +30 -4
  90. package/src/Form/Fieldset/Fieldset.tsx +101 -43
  91. package/src/Form/FormControl/FormControl.test.tsx +27 -1
  92. package/src/Form/FormControl/FormControl.tsx +37 -37
  93. package/src/Form/FormGroup/FormGroup.test.tsx +27 -1
  94. package/src/Form/FormGroup/FormGroup.tsx +64 -58
  95. package/src/Form/FormHelperText/FormHelperText.test.tsx +27 -1
  96. package/src/Form/FormHelperText/FormHelperText.tsx +20 -16
  97. package/src/Form/FormSelectorWrapper/FormSelectorWrapper.test.tsx +78 -0
  98. package/src/Form/FormSelectorWrapper/FormSelectorWrapper.tsx +61 -55
  99. package/src/Form/Input/Input.module.scss +34 -15
  100. package/src/Form/Input/Input.test.tsx +27 -1
  101. package/src/Form/Input/Input.tsx +88 -47
  102. package/src/Form/Label/Label.test.tsx +27 -1
  103. package/src/Form/Label/Label.tsx +18 -14
  104. package/src/Form/Radio/Radio.module.scss +4 -0
  105. package/src/Form/Radio/Radio.test.tsx +28 -2
  106. package/src/Form/Radio/Radio.tsx +98 -80
  107. package/src/Form/Select/Option.test.tsx +27 -1
  108. package/src/Form/Select/Option.tsx +49 -42
  109. package/src/Form/Select/Select.module.scss +5 -1
  110. package/src/Form/Select/Select.test.tsx +224 -30
  111. package/src/Form/Select/Select.tsx +248 -182
  112. package/src/Form/Textarea/Textarea.module.scss +2 -1
  113. package/src/Form/Textarea/Textarea.test.tsx +28 -2
  114. package/src/Form/Textarea/Textarea.tsx +44 -29
  115. package/src/Form/Toggle/Toggle.module.scss +9 -0
  116. package/src/Form/Toggle/Toggle.test.tsx +27 -1
  117. package/src/Form/Toggle/Toggle.tsx +25 -12
  118. package/src/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.test.tsx +28 -2
  119. package/src/Form/Wrapper/CheckboxWrapper/CheckboxWrapper.tsx +45 -48
  120. package/src/Form/Wrapper/InputWrapper/InputWrapper.module.scss +17 -1
  121. package/src/Form/Wrapper/InputWrapper/InputWrapper.test.tsx +89 -1
  122. package/src/Form/Wrapper/InputWrapper/InputWrapper.tsx +134 -74
  123. package/src/Form/Wrapper/RadioWrapper/RadioWrapper.test.tsx +1 -1
  124. package/src/Form/Wrapper/RadioWrapper/RadioWrapper.tsx +64 -59
  125. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.module.scss +1 -1
  126. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.test.tsx +43 -1
  127. package/src/Form/Wrapper/SelectWrapper/SelectWrapper.tsx +55 -44
  128. package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.module.scss +5 -7
  129. package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.test.tsx +43 -1
  130. package/src/Form/Wrapper/TextareaWrapper/TextareaWrapper.tsx +100 -85
  131. package/src/Form/Wrapper/Wrapper/Wrapper.module.scss +1 -1
  132. package/src/Form/Wrapper/Wrapper/Wrapper.test.tsx +27 -1
  133. package/src/Form/Wrapper/Wrapper/Wrapper.tsx +76 -71
  134. package/src/Form/form.interfaces.ts +4 -3
  135. package/src/Icon/Icon.module.scss +4 -0
  136. package/src/Icon/Icon.test.tsx +30 -2
  137. package/src/Icon/Icon.tsx +5 -5
  138. package/src/Link/Link.test.tsx +27 -1
  139. package/src/Link/Link.tsx +4 -6
  140. package/src/{BaseModal → Notifications/BaseModal}/BaseModal.module.scss +0 -0
  141. package/src/{BaseModal → Notifications/BaseModal}/BaseModal.test.tsx +35 -16
  142. package/src/Notifications/BaseModal/BaseModal.tsx +105 -0
  143. package/src/{BaseModal → Notifications/BaseModal}/BaseModalActions/BaseModalActions.module.scss +0 -0
  144. package/src/Notifications/BaseModal/BaseModalActions/BaseModalActions.test.tsx +42 -0
  145. package/src/Notifications/BaseModal/BaseModalActions/BaseModalActions.tsx +16 -0
  146. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.module.scss +0 -0
  147. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContent/BaseModalContent.test.tsx +27 -1
  148. package/src/Notifications/BaseModal/BaseModalContent/BaseModalContent.tsx +36 -0
  149. package/src/{BaseModal → Notifications/BaseModal}/BaseModalContext.ts +0 -0
  150. package/src/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.module.scss +0 -0
  151. package/src/{BaseModal → Notifications/BaseModal}/BaseModalHeader/BaseModalHeader.test.tsx +29 -1
  152. package/src/Notifications/BaseModal/BaseModalHeader/BaseModalHeader.tsx +30 -0
  153. package/src/{Dialog → Notifications/Dialog}/Dialog.module.scss +0 -0
  154. package/src/{Dialog → Notifications/Dialog}/Dialog.test.tsx +52 -17
  155. package/src/Notifications/Dialog/Dialog.tsx +113 -0
  156. package/src/{Dialog → Notifications/Dialog}/DialogActions/DialogActions.module.scss +0 -0
  157. package/src/Notifications/Dialog/DialogActions/DialogActions.test.tsx +51 -0
  158. package/src/Notifications/Dialog/DialogActions/DialogActions.tsx +24 -0
  159. package/src/{Dialog → Notifications/Dialog}/DialogTitle/DialogTitle.module.scss +0 -0
  160. package/src/Notifications/Dialog/DialogTitle/DialogTitle.test.tsx +44 -0
  161. package/src/Notifications/Dialog/DialogTitle/DialogTitle.tsx +20 -0
  162. package/src/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.test.tsx +95 -0
  163. package/src/Notifications/DiscardChangesModal/DiscardChangesDialog/DiscardChangesDialog.tsx +55 -0
  164. package/src/Notifications/DiscardChangesModal/DiscardChangesModal.test.tsx +162 -0
  165. package/src/Notifications/DiscardChangesModal/DiscardChangesModal.tsx +61 -0
  166. package/src/{Modal → Notifications/Modal}/Modal.test.tsx +0 -0
  167. package/src/{Modal → Notifications/Modal}/Modal.tsx +0 -0
  168. package/src/{Modal → Notifications/Modal}/ModalActions/ModalActions.tsx +0 -0
  169. package/src/{Modal → Notifications/Modal}/ModalContent/ModalContent.tsx +0 -0
  170. package/src/{Modal → Notifications/Modal}/ModalHeader/ModalHeader.tsx +0 -0
  171. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.module.scss +0 -0
  172. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.test.tsx +0 -0
  173. package/src/{Snackbar → Notifications/Snackbar}/SnackbarContainer/SnackbarContainer.tsx +0 -0
  174. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.module.scss +1 -1
  175. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.test.tsx +0 -0
  176. package/src/{Snackbar → Notifications/Snackbar}/SnackbarItem/SnackbarItem.tsx +6 -7
  177. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.test.tsx +0 -0
  178. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarProvider.tsx +2 -2
  179. package/src/{Snackbar → Notifications/Snackbar}/SnackbarProvider/SnackbarStateProvider.tsx +0 -0
  180. package/src/{Snackbar → Notifications/Snackbar}/interfaces.ts +0 -0
  181. package/src/{Snackbar → Notifications/Snackbar}/useSnackbar.ts +0 -0
  182. package/src/Pagination/Pagination.module.scss +120 -0
  183. package/src/Pagination/Pagination.test.tsx +176 -0
  184. package/src/Pagination/Pagination.tsx +205 -0
  185. package/src/Popover/Popover.test.tsx +3 -3
  186. package/src/Popover/Popover.tsx +3 -3
  187. package/src/Tabs/Tab.test.tsx +71 -0
  188. package/src/Tabs/Tab.tsx +17 -0
  189. package/src/Tabs/TabButton.module.scss +36 -0
  190. package/src/Tabs/TabButton.test.tsx +77 -0
  191. package/src/Tabs/TabButton.tsx +58 -0
  192. package/src/Tabs/TabPanel.module.scss +7 -0
  193. package/src/Tabs/TabPanel.test.tsx +76 -0
  194. package/src/Tabs/TabPanel.tsx +27 -0
  195. package/src/Tabs/Tabs.module.scss +41 -0
  196. package/src/Tabs/Tabs.test.tsx +268 -0
  197. package/src/Tabs/Tabs.tsx +149 -0
  198. package/src/TextEllipsis/TextEllipsis.module.scss +18 -0
  199. package/src/TextEllipsis/TextEllipsis.test.tsx +80 -0
  200. package/src/TextEllipsis/TextEllipsis.tsx +55 -0
  201. package/src/Tiles/Tile.module.scss +1 -1
  202. package/src/Tiles/Tile.test.tsx +48 -12
  203. package/src/Tiles/Tile.tsx +68 -34
  204. package/src/Tiles/Tiles.test.tsx +38 -10
  205. package/src/Tiles/Tiles.tsx +42 -39
  206. package/src/Tooltip/Tooltip.test.tsx +27 -1
  207. package/src/Tooltip/Tooltip.tsx +104 -92
  208. package/src/Typography/Typography.test.tsx +27 -1
  209. package/src/Typography/Typography.tsx +66 -68
  210. package/src/Wizard/BaseWizardSteps/BaseWizardSteps.tsx +67 -62
  211. package/src/Wizard/Wizard.tsx +2 -2
  212. package/src/Wizard/WizardActions/WizardActions.tsx +3 -3
  213. package/src/Wizard/WizardSteps/WizardSteps.tsx +24 -21
  214. package/src/_BaseStyling_/BaseStyling.tsx +19 -1
  215. package/src/hooks/usePosition.test.tsx +3 -3
  216. package/src/hooks/useRepeater.test.tsx +139 -0
  217. package/src/hooks/useRepeater.ts +34 -0
  218. package/src/hooks/useSpacing.ts +1 -1
  219. package/src/hooks/useWrapper.ts +7 -2
  220. package/src/index.ts +20 -8
  221. package/src/interfaces.ts +2 -12
  222. package/src/util/helper.test.tsx +38 -1
  223. package/src/util/helper.tsx +21 -0
  224. package/dist/BaseModal/BaseModal.d.ts +0 -16
  225. package/dist/BaseModal/BaseModalActions/BaseModalActions.d.ts +0 -5
  226. package/dist/BaseModal/BaseModalContent/BaseModalContent.d.ts +0 -8
  227. package/dist/BaseModal/BaseModalHeader/BaseModalHeader.d.ts +0 -8
  228. package/dist/Dialog/Dialog.d.ts +0 -18
  229. package/dist/Dialog/DialogActions/DialogActions.d.ts +0 -6
  230. package/dist/Dialog/DialogTitle/DialogTitle.d.ts +0 -6
  231. package/src/BaseModal/BaseModal.tsx +0 -113
  232. package/src/BaseModal/BaseModalActions/BaseModalActions.test.tsx +0 -17
  233. package/src/BaseModal/BaseModalActions/BaseModalActions.tsx +0 -14
  234. package/src/BaseModal/BaseModalContent/BaseModalContent.tsx +0 -35
  235. package/src/BaseModal/BaseModalHeader/BaseModalHeader.tsx +0 -28
  236. package/src/Dialog/Dialog.tsx +0 -96
  237. package/src/Dialog/DialogActions/DialogActions.test.tsx +0 -25
  238. package/src/Dialog/DialogActions/DialogActions.tsx +0 -21
  239. package/src/Dialog/DialogTitle/DialogTitle.test.tsx +0 -18
  240. package/src/Dialog/DialogTitle/DialogTitle.tsx +0 -18
@@ -1,10 +1,11 @@
1
1
  import classes from './Select.module.scss';
2
2
 
3
3
  import React, {
4
- HTMLProps,
4
+ ComponentPropsWithRef,
5
+ Fragment,
5
6
  ReactElement,
7
+ RefObject,
6
8
  useEffect,
7
- useLayoutEffect,
8
9
  useRef,
9
10
  useState,
10
11
  } from 'react';
@@ -12,10 +13,10 @@ import { Input } from '../Input/Input';
12
13
  import { Icon, Icons } from '../../Icon/Icon';
13
14
  import { FormElement } from '../form.interfaces';
14
15
  import { useBodyClick } from '../../hooks/useBodyClick';
15
- import { Position } from '../../hooks/usePosition';
16
- import { useScroll } from '../../hooks/useScroll';
16
+ import readyclasses from '../../readyclasses.module.scss';
17
+ import { filterProps } from '../../util/helper';
17
18
 
18
- export interface Props extends FormElement<HTMLSelectElement> {
19
+ export interface Props extends ComponentPropsWithRef<'select'>, FormElement {
19
20
  children: ReactElement[];
20
21
  name?: string;
21
22
  labeledBy?: string;
@@ -23,195 +24,260 @@ export interface Props extends FormElement<HTMLSelectElement> {
23
24
  placeholder?: string;
24
25
  searchPlaceholder?: string;
25
26
  className?: string;
26
- value?: string;
27
+ value: string;
27
28
  onChange?: (event: React.ChangeEvent<HTMLSelectElement>, child?: ReactElement) => void;
28
29
  onClear?: (event: React.MouseEvent<HTMLDivElement>) => void;
29
30
  }
30
31
 
31
- export const Select = ({
32
- children,
33
- name,
34
- disabled = false,
35
- labeledBy,
36
- placeholder,
37
- describedBy,
38
- searchPlaceholder = 'Search item',
39
- className,
40
- error = false,
41
- value = '',
42
- onChange,
43
- onClear,
44
- ...rest
45
- }: Props) => {
46
- const [expanded, setExpanded] = useState(false);
47
- const [filter, setFilter] = useState('');
48
- const [display, setDisplay] = useState('');
49
- const [listPosition, setListPosition] = useState<Partial<Position>>({});
50
- const containerReference = useRef<HTMLDivElement>(null);
51
- const optionListReference = useRef<HTMLDivElement>(null);
52
-
53
- useBodyClick(
54
- (event: MouseEvent) => !(event.target as Element).closest('.custom-select') && expanded,
55
- () => {
56
- setExpanded(!expanded);
57
- },
58
- expanded
59
- );
60
-
61
- const rePositionList = () => {
62
- if (!expanded || !optionListReference.current || !containerReference.current) {
63
- return;
64
- }
65
- const windowHeight = window.innerHeight;
66
- const containerTopWithListHeight =
67
- containerReference.current.getBoundingClientRect().bottom -
68
- containerReference.current.getBoundingClientRect().height +
69
- optionListReference.current.getBoundingClientRect().height;
70
-
71
- if (containerTopWithListHeight > windowHeight) {
72
- setListPosition({ top: 'initial', bottom: 0 });
73
- } else {
74
- setListPosition({ top: 0, bottom: 'initial' });
75
- }
76
- };
77
-
78
- useScroll(rePositionList, [expanded]);
79
-
80
- useLayoutEffect(() => {
81
- rePositionList();
82
- }, [expanded]);
83
-
84
- const onOptionChangeHandler = (child: ReactElement) => (event: React.ChangeEvent) => {
32
+ type Position = {
33
+ top: 0 | 'initial';
34
+ bottom: 0 | 'initial';
35
+ };
36
+
37
+ export const Select = React.forwardRef<HTMLSelectElement, Props>(
38
+ (
39
+ {
40
+ children,
41
+ name,
42
+ disabled = false,
43
+ labeledBy,
44
+ placeholder,
45
+ describedBy,
46
+ searchPlaceholder = 'Search item',
47
+ className,
48
+ error = false,
49
+ value,
50
+ onChange,
51
+ onClear,
52
+ ...rest
53
+ }: Props,
54
+ ref
55
+ ) => {
56
+ const [expanded, setExpanded] = useState(false);
57
+ const [opacity, setOpacity] = useState(0); // We set opacity because other wise if we calculate the max height you see the list full height for a split second and then it shortens.
58
+ const [filter, setFilter] = useState('');
59
+ const [display, setDisplay] = useState('');
60
+ const [listPosition, setListPosition] = useState<Partial<Position>>({});
61
+ const [optionsListMaxHeight, setOptionsListMaxHeight] = useState('none');
62
+ const containerReference = useRef<HTMLDivElement>(null);
63
+ const optionListReference = useRef<HTMLDivElement>(null);
64
+
65
+ const nativeSelect = useRef<HTMLSelectElement>(null);
66
+
67
+ const syncDisplayValue = (val: string) => {
68
+ React.Children.forEach(children, (child) => {
69
+ if (child.props.value === val) {
70
+ setDisplay(child.props.children);
71
+ }
72
+ });
73
+ };
74
+
75
+ const rePositionList = () => {
76
+ if (!expanded || !optionListReference.current || !containerReference.current) {
77
+ return;
78
+ }
79
+
80
+ // Check whether there is more space above or below the select
81
+ // Check space between the bottom of select and top of viewport
82
+ const spaceOnTopOfSelect = containerReference.current.getBoundingClientRect().bottom;
83
+
84
+ // Check space between the top of the select and bottom of viewport
85
+ const spaceOnBottomOfSelect =
86
+ window.innerHeight - containerReference.current.getBoundingClientRect().top;
87
+
88
+ // Set position as if there's more space on the bottom
89
+ let position: Position = { top: 0, bottom: 'initial' };
90
+
91
+ // Set the position of the select
92
+ if (spaceOnTopOfSelect > spaceOnBottomOfSelect) {
93
+ position = { top: 'initial', bottom: 0 };
94
+ }
95
+
96
+ setListPosition(position);
97
+
98
+ // Calculate the potential max height of the options list
99
+ calculateOptionListMaxHeight(position);
100
+ };
101
+
102
+ const calculateOptionListMaxHeight = (position: Position) => {
103
+ // Calculate max height if there's more space below the select
104
+ const listHeight = optionListReference.current!.getBoundingClientRect().height;
105
+ const transformOrigin = position.top !== 'initial' ? 'top' : 'bottom';
106
+
107
+ const availableSpace =
108
+ transformOrigin === 'top'
109
+ ? window.innerHeight -
110
+ containerReference.current!.getBoundingClientRect()[transformOrigin] -
111
+ 16
112
+ : containerReference.current!.getBoundingClientRect()[transformOrigin] - 16;
113
+
114
+ if (availableSpace < listHeight) {
115
+ setOptionsListMaxHeight(`${availableSpace}px`);
116
+ setOpacity(100);
117
+ return;
118
+ }
119
+
120
+ setOptionsListMaxHeight('none');
121
+ setOpacity(100);
122
+ };
123
+
124
+ const onOptionChangeHandler = (event: React.MouseEvent<HTMLLIElement>) => {
125
+ // We need to set value and the fire change event. If a custom ref has been given we pass that value, otherwise we use the ref we've created ourselves when the component was instantiated.
126
+ if (nativeSelect.current) {
127
+ nativeSelect.current.value = event.currentTarget.dataset.value!;
128
+ nativeSelect.current.dispatchEvent(new Event('change', { bubbles: true }));
129
+ } else if (ref) {
130
+ (ref as RefObject<HTMLSelectElement>).current!.value = event.currentTarget.dataset.value!;
131
+ (ref as RefObject<HTMLSelectElement>).current!.dispatchEvent(
132
+ new Event('change', { bubbles: true })
133
+ );
134
+ }
135
+ setExpanded(false);
136
+ };
137
+
85
138
  /**
86
- * We expose this to the outside inside of the onChange function as a parameter along with an optional second
87
- * parameter of the option that was clicked.
139
+ * @description We have to modify the children (Option component) to have a additional props that allows us to keep track of which one is selected at all times and if a filter is active.
140
+ * The `children` prop can be either a single object (1 child) or an array of multiple children.
88
141
  */
142
+ const renderOptions = () =>
143
+ React.Children.map(children, (child) =>
144
+ React.cloneElement(child, {
145
+ onOptionSelect: onOptionChangeHandler,
146
+ selected: child.props.value === value,
147
+ filter: filter,
148
+ })
149
+ );
150
+
151
+ const renderSearch = () => (
152
+ <Input
153
+ autoFocus
154
+ onChange={filterResults}
155
+ className={classes['select-search']}
156
+ wrapperProps={{ className: classes['select-search-wrapper'] }}
157
+ type="text"
158
+ name="search-option"
159
+ placeholder={searchPlaceholder}
160
+ />
161
+ );
162
+
163
+ const filterResults = (event: React.ChangeEvent<HTMLInputElement>) => {
164
+ setFilter(event.currentTarget.value);
165
+ };
89
166
 
90
- setDisplay(child.props.children);
167
+ const statusIcon = () => {
168
+ if (error) {
169
+ return <Icon className={classes['warning']} icon={Icons.Warning} />;
170
+ }
91
171
 
92
- let newValue;
93
- let multiple = false; // Potential support for future multiple select. This should be a prop obviously.
172
+ if (value?.length !== 0 && onClear) {
173
+ return (
174
+ <Icon
175
+ tag="div"
176
+ data-clear
177
+ icon={Icons.TimesThin}
178
+ onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
179
+ e.preventDefault();
180
+ e.stopPropagation();
181
+ onClear(e);
182
+ }}
183
+ />
184
+ );
185
+ }
186
+ return null;
187
+ };
94
188
 
95
- if (multiple) {
96
- /** We will implement the mulitple select in the next iteration */
97
- } else {
98
- newValue = child.props.value;
99
- }
189
+ const nativeOnChangeHandler = (event: React.ChangeEvent<HTMLSelectElement>) => {
190
+ onChange && onChange(event);
191
+ };
100
192
 
101
- if (onChange) {
102
- // Redefine target to allow name and value to be read.
103
- // This allows seamless integration with the most popular form libraries.
104
- // Clone the event to not override `target` of the original event.
105
- // Don't know how to fix this any.. compiler whines that it can't construct it otherwise.
106
- const nativeEvent: any = event.nativeEvent || event;
107
- const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent);
193
+ useEffect(() => {
194
+ syncDisplayValue(value);
195
+ }, [value]);
108
196
 
109
- Object.defineProperty(clonedEvent as React.ChangeEvent<HTMLSelectElement>, 'target', {
110
- writable: true,
111
- value: { value: newValue },
112
- });
197
+ useEffect(() => {
198
+ rePositionList();
199
+ }, [expanded]);
113
200
 
114
- onChange(clonedEvent as React.ChangeEvent<HTMLSelectElement>, child);
115
- }
116
-
117
- setDisplay((event.currentTarget as HTMLElement).innerText);
118
- setExpanded(false);
119
- };
120
-
121
- /**
122
- * @description We have to modify the children (Option component) to have a additional props that allows us to keep track of which one is selected at all times and if a filter is active.
123
- * The `children` prop can be either a single object (1 child) or an array of multiple children.
124
- */
125
- const renderOptions = () =>
126
- React.Children.map(children, (child) =>
127
- React.cloneElement(child, {
128
- onOptionSelect: onOptionChangeHandler(child),
129
- selected: child.props.value === value,
130
- filter: filter,
131
- })
201
+ useBodyClick(
202
+ (event: MouseEvent) => !(event.target as Element).closest('.custom-select') && expanded,
203
+ () => {
204
+ setExpanded(!expanded);
205
+ setListPosition({ top: 0, bottom: 'initial' });
206
+ setOpacity(0);
207
+ },
208
+ expanded
132
209
  );
133
210
 
134
- const renderSearch = () => (
135
- <Input
136
- autoFocus
137
- onChange={filterResults}
138
- className={classes['select-search']}
139
- wrapperProps={{ className: classes['select-search-wrapper'] }}
140
- type="text"
141
- name="search-option"
142
- placeholder={searchPlaceholder}
143
- />
144
- );
145
-
146
- const filterResults = (event: React.ChangeEvent<HTMLInputElement>) => {
147
- setFilter(event.currentTarget.value);
148
- };
149
-
150
- const statusIcon = () => {
151
- if (error) {
152
- return <Icon className={classes['warning']} icon={Icons.Warning} />;
153
- }
154
- if (value.length !== 0 && onClear) {
155
- return <Icon tag="div" icon={Icons.TimesThin} onClick={onClear} />;
156
- }
157
- return null;
158
- };
159
-
160
- /** Set initial display value */
161
- useEffect(() => {
162
- for (let child of children) {
163
- if (child.props.value === value) {
164
- setDisplay(child.props.children);
165
- }
166
- }
167
- }, []);
168
-
169
- const additionalClasses = [];
170
- expanded && additionalClasses.push(classes.expanded);
171
- error && additionalClasses.push(classes.error);
172
- disabled && additionalClasses.push(classes.disabled);
173
-
174
- return (
175
- <div
176
- {...(rest as HTMLProps<HTMLDivElement>)}
177
- ref={containerReference}
178
- className={`custom-select ${classes.select} ${additionalClasses.join(' ')} ${
179
- className ?? ''
180
- }`}
181
- >
182
- <button
183
- onClick={() => setExpanded(!expanded)}
184
- type="button"
185
- name={name}
186
- disabled={disabled}
187
- aria-disabled={disabled}
188
- aria-invalid={error}
189
- aria-expanded={expanded}
190
- aria-haspopup="listbox"
191
- aria-labelledby={labeledBy}
192
- aria-describedby={describedBy}
193
- >
194
- <div data-display className={classes['selected']}>
195
- {value.length === 0 && placeholder && (
196
- <span className={classes['placeholder']}>{placeholder}</span>
197
- )}
198
- {value.length > 0 && <span>{display}</span>}
199
- </div>
200
- <div className={classes['status']}>
201
- {statusIcon()}
202
- <Icon className={classes['triangle-down']} icon={Icons.TriangleDown} />
211
+ const additionalClasses = [];
212
+ expanded && additionalClasses.push(classes.expanded);
213
+ error && additionalClasses.push(classes.error);
214
+ disabled && additionalClasses.push(classes.disabled);
215
+ className && additionalClasses.push(className);
216
+
217
+ /** 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. */
218
+ return (
219
+ <Fragment>
220
+ <select
221
+ {...filterProps(rest, /^data-/, false)}
222
+ tabIndex={-1}
223
+ aria-hidden="true"
224
+ ref={ref || nativeSelect}
225
+ name={name}
226
+ onChange={nativeOnChangeHandler}
227
+ className={readyclasses['sr-only']}
228
+ >
229
+ <option value=""></option>
230
+ {React.Children.map(children, (child) => (
231
+ <option value={child.props.value}></option>
232
+ ))}
233
+ </select>
234
+ <div
235
+ {...filterProps(rest, /^data-/)}
236
+ ref={containerReference}
237
+ className={`custom-select ${classes.select} ${additionalClasses.join(' ')} ${
238
+ className ?? ''
239
+ }`}
240
+ >
241
+ <button
242
+ onClick={() => setExpanded(!expanded)}
243
+ type="button"
244
+ name={name}
245
+ disabled={disabled}
246
+ aria-disabled={disabled}
247
+ aria-invalid={error}
248
+ aria-expanded={expanded}
249
+ aria-haspopup="listbox"
250
+ aria-labelledby={labeledBy}
251
+ aria-describedby={describedBy}
252
+ >
253
+ <div data-display className={classes['selected']}>
254
+ {!value && placeholder && (
255
+ <span className={classes['placeholder']}>{placeholder}</span>
256
+ )}
257
+ {value?.length > 0 && <span>{display}</span>}
258
+ </div>
259
+ <div className={classes['status']}>
260
+ {statusIcon()}
261
+ <Icon className={classes['triangle-down']} icon={Icons.TriangleDown} />
262
+ </div>
263
+ </button>
264
+ <div
265
+ ref={optionListReference}
266
+ className={`list-wrapper ${classes['list-wrapper']}`}
267
+ style={{
268
+ display: expanded ? 'block' : 'none',
269
+ opacity: opacity,
270
+ maxHeight: optionsListMaxHeight,
271
+ ...listPosition,
272
+ }}
273
+ >
274
+ {Array.isArray(children) && children.length > 10 && renderSearch()}
275
+ <ul role="listbox" tabIndex={-1}>
276
+ {renderOptions()}
277
+ </ul>
278
+ </div>
203
279
  </div>
204
- </button>
205
- <div
206
- ref={optionListReference}
207
- className={`list-wrapper ${classes['list-wrapper']}`}
208
- style={{ display: expanded ? 'block' : 'none', ...listPosition }}
209
- >
210
- {Array.isArray(children) && children.length > 10 && renderSearch()}
211
- <ul role="listbox" tabIndex={-1}>
212
- {renderOptions()}
213
- </ul>
214
- </div>
215
- </div>
216
- );
217
- };
280
+ </Fragment>
281
+ );
282
+ }
283
+ );
@@ -6,6 +6,7 @@
6
6
 
7
7
  .textarea {
8
8
  padding: 0.75rem 3.75rem 0.75rem 1.25rem;
9
+ box-sizing: border-box;
9
10
  border-color: var(--input-border-color);
10
11
  border-style: var(--input-border-style);
11
12
  border-width: var(--input-border-width);
@@ -13,8 +14,8 @@
13
14
  transition: all 0.2s ease-in-out;
14
15
  font-family: var(--font-family);
15
16
  font-size: var(--font-size);
17
+ color: var(--greyed-out);
16
18
  width: 100%;
17
- box-sizing: border-box;
18
19
  vertical-align: top;
19
20
  resize: vertical;
20
21
 
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { Textarea, Props } from './Textarea';
3
3
  import { render } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
@@ -20,6 +20,32 @@ describe('Textarea should render', () => {
20
20
  });
21
21
  });
22
22
 
23
+ describe('ref should work', () => {
24
+ it('should give back the proper data prop, this also checks if the component propagates ...rest properly', () => {
25
+ const ExampleComponent = ({
26
+ propagateRef,
27
+ }: {
28
+ propagateRef?: (ref: React.RefObject<HTMLElement>) => void;
29
+ }) => {
30
+ const ref = useRef(null);
31
+
32
+ useEffect(() => {
33
+ if (ref.current) {
34
+ propagateRef && propagateRef(ref);
35
+ }
36
+ }, [ref]);
37
+
38
+ return <Textarea data-ref="testing" ref={ref} />;
39
+ };
40
+
41
+ const refCheck = (ref: React.RefObject<HTMLElement>) => {
42
+ expect(ref.current).toHaveAttribute('data-ref', 'testing');
43
+ };
44
+
45
+ render(<ExampleComponent propagateRef={refCheck} />);
46
+ });
47
+ });
48
+
23
49
  describe('Textarea properties', () => {
24
50
  it('is disabled', () => {
25
51
  const { textarea } = createTextarea({ disabled: true });
@@ -71,6 +97,6 @@ describe('Error status', () => {
71
97
  const { textarea } = createTextarea({ error: true });
72
98
 
73
99
  expect(textarea).toHaveClass('error');
74
- expect(textarea.nextElementSibling).toHaveClass('icon-warning');
100
+ expect(textarea.nextElementSibling).toHaveClass('icon-error-circle');
75
101
  });
76
102
  });
@@ -1,33 +1,48 @@
1
- import React from 'react';
2
- import { Icon, Icons } from '../../Icon/Icon';
3
- import { FormElement } from '../form.interfaces';
1
+ import React, { ComponentPropsWithRef } from 'react';
2
+ import { Icon, Props as IconProps, Icons } from '../../Icon/Icon';
4
3
  import classes from './Textarea.module.scss';
4
+ import { FormElement } from '../form.interfaces';
5
+
6
+ interface IconPropsPartial extends Omit<Partial<IconProps>, 'ref'> {}
5
7
 
6
- export interface Props extends FormElement<HTMLTextAreaElement> {
7
- wrapperClassName?: string;
8
- errorClassName?: string;
8
+ export interface Props extends ComponentPropsWithRef<'textarea'>, FormElement {
9
+ wrapperProps?: ComponentPropsWithRef<'div'>;
10
+ errorProps?: IconPropsPartial;
9
11
  }
10
12
 
11
- export const Textarea = ({
12
- error = false,
13
- disabled = false,
14
- className,
15
- rows = 4,
16
- wrapperClassName,
17
- errorClassName,
18
- ...rest
19
- }: Props) => {
20
- return (
21
- <div className={`${classes['textarea-wrapper']} ${wrapperClassName ? wrapperClassName : ''}`}>
22
- <textarea
23
- {...rest}
24
- rows={rows}
25
- className={`${error ? classes['error'] : ''} ${classes['textarea']} ${className ?? ''}`}
26
- disabled={disabled}
27
- />
28
- {error && (
29
- <Icon className={`${classes['warning']} ${errorClassName ?? ''}`} icon={Icons.Warning} />
30
- )}
31
- </div>
32
- );
33
- };
13
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, Props>(
14
+ (
15
+ {
16
+ error = false,
17
+ disabled = false,
18
+ className,
19
+ rows = 4,
20
+ wrapperProps,
21
+ errorProps,
22
+ ...rest
23
+ }: Props,
24
+ ref
25
+ ) => {
26
+ return (
27
+ <div
28
+ {...wrapperProps}
29
+ className={`${classes['textarea-wrapper']} ${wrapperProps?.className ?? ''}`}
30
+ >
31
+ <textarea
32
+ {...rest}
33
+ ref={ref}
34
+ rows={rows}
35
+ className={`${error ? classes['error'] : ''} ${classes['textarea']} ${className ?? ''}`}
36
+ disabled={disabled}
37
+ />
38
+ {error && (
39
+ <Icon
40
+ {...errorProps}
41
+ className={`${classes['warning']} ${errorProps?.className ?? ''}`}
42
+ icon={Icons.Error}
43
+ />
44
+ )}
45
+ </div>
46
+ );
47
+ }
48
+ );
@@ -16,6 +16,11 @@ $borderRadius: 2.5rem;
16
16
  display: block;
17
17
  pointer-events: none;
18
18
 
19
+ &.disabled {
20
+ opacity: 0.25;
21
+ cursor: not-allowed;
22
+ }
23
+
19
24
  &:before {
20
25
  content: '';
21
26
  width: 1rem;
@@ -38,6 +43,10 @@ $borderRadius: 2.5rem;
38
43
  }
39
44
  }
40
45
 
46
+ .toggle-helper {
47
+ margin-left: 2.5rem;
48
+ }
49
+
41
50
  .checkbox {
42
51
  z-index: 10;
43
52
  position: static;