@nuskin/foundation-ui-components 1.0.0-cft-35.13 → 1.0.0-develop.10

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 (247) hide show
  1. package/package.json +21 -11
  2. package/src/common/accordion/NsAccordion.tsx +69 -0
  3. package/src/common/accordion/NsAccordionHeader.tsx +77 -0
  4. package/src/common/accordion/NsAccordionItem.tsx +59 -0
  5. package/src/common/accordion/NsCollapse.tsx +110 -0
  6. package/src/common/accordion/NsToggleSection.tsx +43 -0
  7. package/src/common/accordion/ToggleSection.styled.tsx +32 -0
  8. package/src/common/accordion/colorMap.ts +117 -0
  9. package/src/common/accordion/enums.ts +8 -0
  10. package/src/common/accordion/index.ts +7 -0
  11. package/src/common/accordion/specs/NsAccordion.spec.tsx +163 -0
  12. package/src/common/accordion/specs/NsAccordionHeader.spec.tsx +130 -0
  13. package/src/common/accordion/specs/NsAccordionItem.spec.tsx +79 -0
  14. package/src/common/accordion/specs/NsCollapse.spec.tsx +129 -0
  15. package/src/common/accordion/specs/NsToggleSection.spec.tsx +74 -0
  16. package/src/common/accordion/stories/NsAccordion.stories.tsx +65 -0
  17. package/src/common/accordion/stories/NsAccordionHeader.stories.tsx +44 -0
  18. package/src/common/accordion/stories/NsAccordionItem.stories.tsx +45 -0
  19. package/src/common/accordion/stories/NsCollapse.stories.tsx +50 -0
  20. package/src/common/accordion/stories/NsToggleSection.stories.tsx +26 -0
  21. package/src/common/accordion/stories/data.tsx +143 -0
  22. package/src/common/accordion/types.d.ts +81 -0
  23. package/src/common/alert/Header.tsx +29 -0
  24. package/src/common/alert/NsAlert.spec.tsx +62 -29
  25. package/src/common/alert/NsAlert.stories.tsx +18 -17
  26. package/src/common/alert/NsAlert.styled.tsx +124 -0
  27. package/src/common/alert/NsAlert.tsx +27 -110
  28. package/src/common/alert/index.ts +2 -2
  29. package/src/common/alert/types.d.ts +23 -23
  30. package/src/common/auth-dropdown/NsAuthDropdown.spec.tsx +24 -6
  31. package/src/common/auth-dropdown/NsAuthDropdown.stories.tsx +2 -2
  32. package/src/common/auth-dropdown/NsAuthDropdown.styled.ts +21 -16
  33. package/src/common/auth-dropdown/NsAuthDropdown.tsx +7 -6
  34. package/src/common/auth-dropdown/index.ts +1 -1
  35. package/src/common/auth-dropdown/types.d.ts +3 -1
  36. package/src/common/autocomplete/NsAutocomplete.spec.tsx +69 -24
  37. package/src/common/autocomplete/NsAutocomplete.stories.tsx +64 -7
  38. package/src/common/autocomplete/NsAutocomplete.styled.tsx +12 -0
  39. package/src/common/autocomplete/NsAutocomplete.tsx +100 -46
  40. package/src/common/autocomplete/index.ts +2 -1
  41. package/src/common/autocomplete/mock/MockData.ts +50 -7
  42. package/src/common/autocomplete/mock/MockUI.tsx +2 -1
  43. package/src/common/autocomplete/{types.ts → types.d.ts} +18 -8
  44. package/src/common/banner/NsBanner.spec.tsx +13 -4
  45. package/src/common/banner/NsBanner.stories.tsx +2 -2
  46. package/src/common/banner/NsBanner.styled.tsx +1 -1
  47. package/src/common/banner/NsBanner.tsx +1 -1
  48. package/src/common/banner/index.ts +1 -1
  49. package/src/common/brand-components/NsBrand.stories.tsx +2 -2
  50. package/src/common/brand-components/NsBrandComponent.tsx +6 -3
  51. package/src/common/brand-components/index.ts +1 -1
  52. package/src/common/breadcrumb/NsBreadcrumb.spec.tsx +8 -9
  53. package/src/common/breadcrumb/NsBreadcrumb.stories.tsx +3 -5
  54. package/src/common/breadcrumb/NsBreadcrumb.tsx +5 -3
  55. package/src/common/breadcrumb/data.ts +10 -0
  56. package/src/common/breadcrumb/index.ts +1 -1
  57. package/src/common/button/NsButton.spec.tsx +66 -51
  58. package/src/common/button/NsButton.stories.tsx +61 -4
  59. package/src/common/button/NsButton.styled.tsx +54 -72
  60. package/src/common/button/NsButton.tsx +9 -3
  61. package/src/common/button/helper.ts +82 -0
  62. package/src/common/button/index.ts +1 -1
  63. package/src/common/button/types.d.ts +3 -0
  64. package/src/common/card/NsFfgCard.tsx +14 -8
  65. package/src/common/card/specs/NsFfgCard.spec.tsx +33 -0
  66. package/src/common/card/stories/NsBgCard.stories.tsx +2 -4
  67. package/src/common/card/stories/NsBrandProductCard.stories.tsx +2 -2
  68. package/src/common/card/stories/NsFfgCard.stories.tsx +49 -16
  69. package/src/common/card/stories/{data.js → data.ts} +1 -25
  70. package/src/common/card/styled/NsFfgStyled.tsx +9 -7
  71. package/src/common/card/types.d.ts +7 -4
  72. package/src/common/carousal/NsScrollingCarousal.tsx +61 -0
  73. package/src/common/carousal/index.ts +3 -0
  74. package/src/common/carousal/{tests → specs}/NsCarousal.spec.tsx +2 -2
  75. package/src/common/carousal/specs/NsScrollingCarousal.spec.tsx +61 -0
  76. package/src/common/carousal/stories/NSCarousal.stories.tsx +2 -2
  77. package/src/common/carousal/stories/NsScrollingCarousal.stories.tsx +52 -0
  78. package/src/common/carousal/types.d.ts +23 -0
  79. package/src/common/cart-icon/NsCartIcon.spec.tsx +1 -1
  80. package/src/common/cart-icon/NsCartIcon.stories.tsx +2 -2
  81. package/src/common/cart-icon/NsCartIcon.tsx +1 -1
  82. package/src/common/cart-icon/index.ts +1 -1
  83. package/src/common/checkbox/NsCheckbox.spec.tsx +2 -2
  84. package/src/common/checkbox/NsCheckbox.stories.tsx +2 -2
  85. package/src/common/checkbox/NsCheckbox.styled.tsx +15 -16
  86. package/src/common/checkbox/NsCheckbox.tsx +2 -2
  87. package/src/common/checkbox/index.ts +1 -1
  88. package/src/common/checkbox/types.d.ts +1 -7
  89. package/src/common/column-control/NsColumnControl.tsx +1 -1
  90. package/src/common/column-control/NsFlexColumns.tsx +22 -0
  91. package/src/common/column-control/NsGridColumns.tsx +22 -0
  92. package/src/common/column-control/index.ts +3 -1
  93. package/src/common/column-control/{NsColumnControl.spec.tsx → specs/NsColumnControl.spec.tsx} +3 -3
  94. package/src/common/column-control/specs/NsFlexColumns.spec.tsx +24 -0
  95. package/src/common/column-control/specs/NsGridColumns.spec.tsx +24 -0
  96. package/src/common/column-control/stories/DivArray.stories.tsx +18 -0
  97. package/src/common/column-control/stories/DivArray.tsx +63 -0
  98. package/src/common/column-control/{NsColumnControl.stories.tsx → stories/NsColumnControl.stories.tsx} +9 -16
  99. package/src/common/column-control/stories/NsFlexColumns.stories.tsx +63 -0
  100. package/src/common/column-control/stories/NsGridColumns.stories.tsx +63 -0
  101. package/src/common/column-control/stories/common.tsx +14 -0
  102. package/src/common/column-control/styled/StyledFlex.tsx +45 -0
  103. package/src/common/column-control/styled/StyledGrid.tsx +40 -0
  104. package/src/common/column-control/styled/common.ts +27 -0
  105. package/src/common/column-control/types.d.ts +15 -16
  106. package/src/common/column-control/utils.ts +5 -5
  107. package/src/common/common.types.d.ts +21 -0
  108. package/src/common/dropdown/NsDropdown.spec.tsx +1 -1
  109. package/src/common/dropdown/NsDropdown.styled.tsx +23 -18
  110. package/src/common/dropdown/NsDropdown.tsx +1 -1
  111. package/src/common/dropdown/NsDropdownOption.tsx +1 -1
  112. package/src/common/dropdown/index.ts +1 -1
  113. package/src/common/dropdown/stories/NsDropdown.stories.tsx +2 -2
  114. package/src/common/dropdown/stories/NsDropdownCountry.stories.tsx +2 -2
  115. package/src/common/dropdown/types.d.ts +3 -1
  116. package/src/common/email-simple/NsEmailSimple.spec.tsx +145 -0
  117. package/src/common/email-simple/NsEmailSimple.stories.tsx +61 -0
  118. package/src/common/email-simple/NsEmailSimple.tsx +71 -0
  119. package/src/common/email-simple/index.ts +2 -0
  120. package/src/common/email-simple/types.d.ts +14 -0
  121. package/src/common/flag/NsFlag.spec.tsx +5 -25
  122. package/src/common/flag/NsFlag.stories.tsx +2 -2
  123. package/src/common/flag/NsFlag.styled.tsx +7 -9
  124. package/src/common/flag/NsFlag.tsx +1 -3
  125. package/src/common/flag/index.ts +1 -1
  126. package/src/common/flag/types.d.ts +0 -2
  127. package/src/common/hooks/index.ts +2 -0
  128. package/src/common/hooks/specs/useWindowSize.spec.tsx +19 -0
  129. package/src/common/hooks/useAccordionMap.tsx +39 -0
  130. package/src/common/hooks/useWindowSize.tsx +30 -0
  131. package/src/common/icon/NsIcon.spec.tsx +1 -1
  132. package/src/common/icon/NsIcon.stories.tsx +2 -2
  133. package/src/common/icon/NsIcon.styled.tsx +1 -1
  134. package/src/common/icon/NsIcon.tsx +1 -1
  135. package/src/common/icon/NsIconVariants.ts +9 -4
  136. package/src/common/icon/icons/AlertErrorIcon.tsx +15 -6
  137. package/src/common/icon/icons/HamburgerIcon.tsx +29 -0
  138. package/src/common/icon/icons/UserIcon.tsx +30 -0
  139. package/src/common/icon/icons/WarningIcon.tsx +14 -5
  140. package/src/common/icon/index.ts +1 -1
  141. package/src/common/icon/types.d.ts +1 -0
  142. package/src/common/image/NsImage.spec.tsx +57 -7
  143. package/src/common/image/NsImage.stories.tsx +2 -2
  144. package/src/common/image/NsImage.tsx +24 -12
  145. package/src/common/image/NsNextImage.spec.tsx +1 -1
  146. package/src/common/image/NsNextImage.stories.tsx +2 -2
  147. package/src/common/image/NsNextImage.tsx +1 -1
  148. package/src/common/image/index.ts +1 -1
  149. package/src/common/image/types.d.ts +1 -0
  150. package/src/common/index.ts +14 -10
  151. package/src/common/input/InputFieldWrapper.tsx +4 -4
  152. package/src/common/input/InputWrapper.tsx +12 -7
  153. package/src/common/input/NsInputField.stories.tsx +13 -10
  154. package/src/common/input/NsInputField.tsx +13 -6
  155. package/src/common/input/getAdornmentProps.tsx +5 -1
  156. package/src/common/input/index.ts +1 -1
  157. package/src/common/input/types.d.ts +18 -1
  158. package/src/common/label/NsLabel.stories.tsx +6 -1
  159. package/src/common/label/NsLabel.tsx +21 -20
  160. package/src/common/label/index.ts +1 -1
  161. package/src/common/label/types.d.ts +2 -1
  162. package/src/common/link/NsLink.spec.tsx +1 -1
  163. package/src/common/link/NsLink.stories.tsx +2 -2
  164. package/src/common/link/NsLink.tsx +1 -1
  165. package/src/common/link/index.ts +1 -1
  166. package/src/common/modal/NsModal.spec.tsx +46 -0
  167. package/src/common/modal/NsModal.styled.tsx +28 -0
  168. package/src/common/modal/NsModal.tsx +37 -0
  169. package/src/common/modal/helper-component/NsModalContentBox.tsx +32 -0
  170. package/src/common/modal/helper-component/NsModalDefault.tsx +25 -0
  171. package/src/common/modal/helper-component/NsModalStatus.tsx +15 -0
  172. package/src/common/modal/helper-component/NsModalStatusIcon.tsx +14 -0
  173. package/src/common/modal/helper-component/NsModalTitle.tsx +26 -0
  174. package/src/common/modal/index.ts +2 -0
  175. package/src/common/modal/model/NsModalVariantMap.tsx +24 -0
  176. package/src/common/modal/stories/NsModal.stories.tsx +86 -0
  177. package/src/common/modal/stories/mock.tsx +46 -0
  178. package/src/common/modal/types.ts +56 -0
  179. package/src/common/nav-product-card/NsNavProductCard.spec.tsx +1 -1
  180. package/src/common/nav-product-card/NsNavProductCard.stories.tsx +2 -2
  181. package/src/common/nav-product-card/NsNavProductCard.tsx +1 -1
  182. package/src/common/nav-product-card/index.ts +1 -1
  183. package/src/common/nav-tab/NsNavTab.spec.tsx +2 -7
  184. package/src/common/nav-tab/NsNavTab.stories.tsx +2 -2
  185. package/src/common/nav-tab/NsNavTab.styled.tsx +7 -4
  186. package/src/common/nav-tab/NsNavTab.tsx +1 -1
  187. package/src/common/nav-tab/index.ts +1 -1
  188. package/src/common/nav-tab/types.d.ts +2 -2
  189. package/src/common/radiogroup/NsRadioGroup.spec.tsx +63 -0
  190. package/src/common/radiogroup/NsRadioGroup.stories.tsx +84 -0
  191. package/src/common/radiogroup/NsRadioGroup.styled.tsx +52 -0
  192. package/src/common/radiogroup/NsRadioGroup.tsx +62 -0
  193. package/src/common/radiogroup/index.ts +2 -0
  194. package/src/common/radiogroup/types.d.ts +20 -0
  195. package/src/common/search-bar/NsSeachBar.spec.tsx +1 -1
  196. package/src/common/search-bar/NsSearchBar.tsx +1 -3
  197. package/src/common/select/NsSelect.spec.tsx +193 -0
  198. package/src/common/select/NsSelect.styled.tsx +104 -0
  199. package/src/common/select/NsSelect.tsx +117 -0
  200. package/src/common/select/NsSelectOption.tsx +30 -0
  201. package/src/common/select/index.ts +3 -0
  202. package/src/common/select/stories/NsSelect.stories.tsx +134 -0
  203. package/src/common/select/types.d.ts +34 -0
  204. package/src/common/spinner/NsSpinner.spec.tsx +21 -0
  205. package/src/common/spinner/NsSpinner.stories.tsx +26 -0
  206. package/src/common/spinner/NsSpinner.styled.tsx +344 -0
  207. package/src/common/spinner/NsSpinner.tsx +14 -0
  208. package/src/common/spinner/icons/Spinner.tsx +161 -0
  209. package/src/common/spinner/index.ts +2 -0
  210. package/src/common/spinner/types.ts +5 -0
  211. package/src/common/table/NsExpandableRow.tsx +68 -0
  212. package/src/common/table/NsTable.spec.tsx +174 -0
  213. package/src/common/table/NsTable.stories.tsx +161 -0
  214. package/src/common/table/NsTable.styled.ts +127 -0
  215. package/src/common/table/NsTable.tsx +133 -0
  216. package/src/common/table/NsTableHeader.tsx +41 -0
  217. package/src/common/table/NsTablePagination.tsx +28 -0
  218. package/src/common/table/helper.ts +33 -0
  219. package/src/common/table/index.ts +2 -0
  220. package/src/common/table/types.d.ts +57 -0
  221. package/src/common/text-strip/NsTextStrip.spec.tsx +30 -0
  222. package/src/common/text-strip/NsTextStrip.stories.tsx +33 -0
  223. package/src/common/text-strip/NsTextStrip.tsx +47 -0
  224. package/src/common/text-strip/index.ts +2 -0
  225. package/src/common/text-strip/types.d.ts +17 -0
  226. package/src/common/typography/NsTypography.stories.tsx +2 -2
  227. package/src/common/typography/NsTypography.tsx +1 -1
  228. package/src/common/typography/Typography.styled.tsx +4 -4
  229. package/src/common/typography/index.ts +1 -1
  230. package/src/common/util.ts +1 -0
  231. package/src/common/utility/Show.tsx +13 -0
  232. package/src/common/utility/index.ts +1 -0
  233. package/src/common/validations/regex.ts +3 -0
  234. package/src/common/videos/NsVideo.stories.tsx +2 -2
  235. package/src/common/videos/NsVideo.tsx +1 -1
  236. package/src/common/videos/index.ts +1 -1
  237. package/src/common/videos/types.d.ts +1 -1
  238. package/src/common/alert/NsAlert.styled.ts +0 -124
  239. package/src/common/card/tests/NsFfgCard.spec.tsx +0 -23
  240. package/src/common/column-control/NDivs.story.tsx +0 -35
  241. package/src/common/common.types.ts +0 -10
  242. /package/src/common/brand-components/{NsBrandContanier.styled.tsx → NsBrandContainer.styled.tsx} +0 -0
  243. /package/src/common/card/{tests → specs}/BrandLogo.spec.tsx +0 -0
  244. /package/src/common/card/{tests → specs}/NsBgCard.spec.tsx +0 -0
  245. /package/src/common/card/{tests → specs}/NsBrandProductCard.spec.tsx +0 -0
  246. /package/src/common/carousal/{tests → specs}/Chevron.spec.tsx +0 -0
  247. /package/src/common/carousal/{tests → specs}/RenderBrand.spec.tsx +0 -0
@@ -0,0 +1,193 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@/test-utils/index';
3
+ import NsSelect from './NsSelect';
4
+ import NsSelectOption from './NsSelectOption';
5
+
6
+ function createSelectOptions(options: { value: string; label: string }[]) {
7
+ return options.map((option) => <NsSelectOption key={option.value} value={option.value} label={option.label} />);
8
+ }
9
+
10
+ describe('NsSelectOption', () => {
11
+ test('renders NsSelectOption component without errors', () => {
12
+ const value = 'option1';
13
+ const label = 'Option 1';
14
+ const { getByTestId, getByText } = render(<NsSelectOption value={value} label={label} />);
15
+
16
+ // Check if the option container element exists
17
+ const optionContainer = getByTestId(`option-${value}`);
18
+ expect(optionContainer).toBeInTheDocument();
19
+
20
+ // Check if the option label is rendered correctly
21
+ const optionLabel = getByText(label);
22
+ expect(optionLabel).toBeInTheDocument();
23
+ });
24
+ test('clicking on NsSelectOption selects the option', () => {
25
+ const value = 'option1';
26
+ const label = 'Option 1';
27
+ const onSelect = jest.fn();
28
+ const { getByTestId } = render(<NsSelectOption value={value} label={label} onSelect={onSelect} />);
29
+ const optionElement = getByTestId(`option-${value}`);
30
+ fireEvent.click(optionElement);
31
+ expect(onSelect).toHaveBeenCalledWith(value);
32
+ });
33
+ });
34
+
35
+ describe('NsSelect', () => {
36
+ const onSelect = jest.fn();
37
+ const selectedValue = '';
38
+ const selectedLabel = '';
39
+
40
+ const options = [
41
+ { value: 'Option1', label: 'Option 1' },
42
+ { value: 'Option2', label: 'Option 2' },
43
+ { value: 'Option3', label: 'Option 3' },
44
+ ];
45
+
46
+ it('renders the placeholder text when no value is selected', () => {
47
+ render(
48
+ <NsSelect
49
+ placeholder="Select an option"
50
+ selectedValue={selectedValue}
51
+ selectedLabel={selectedLabel}
52
+ onSelect={onSelect}
53
+ >
54
+ {createSelectOptions(options)}
55
+ </NsSelect>
56
+ );
57
+
58
+ expect(screen.getByText('Select an option')).toBeInTheDocument();
59
+ });
60
+
61
+ test('clicking outside the select container closes the dropdown', () => {
62
+ // const value = 'option1';
63
+ // const label = 'Option 1';
64
+ const onSelect = jest.fn();
65
+ const selectedLabel = 'Option 1';
66
+ const { getByTestId, queryByTestId } = render(
67
+ <NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect} />
68
+ );
69
+ const selectButton = getByTestId('select-head');
70
+ fireEvent.mouseDown(selectButton);
71
+ const outsideElement = document.createElement('div');
72
+ fireEvent.mouseDown(outsideElement);
73
+ // Add assertions to check if the dropdown is closed (isOpen state is false)
74
+ expect(queryByTestId('option-Option1')).toBeNull();
75
+ });
76
+
77
+ it('renders the selected value label', () => {
78
+ const selectedLabel = 'Option 1';
79
+ render(
80
+ <NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
81
+ {createSelectOptions(options)}
82
+ </NsSelect>
83
+ );
84
+
85
+ expect(screen.getByText(selectedLabel)).toBeInTheDocument();
86
+ });
87
+
88
+ test('clicking on select button opens the dropdown', () => {
89
+ const { getByTestId, getByText } = render(
90
+ <NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
91
+ {createSelectOptions(options)}
92
+ </NsSelect>
93
+ );
94
+ const selectButton = getByTestId('select-head');
95
+ fireEvent.mouseDown(selectButton);
96
+
97
+ // Check if the dropdown is opened (isOpen state is true)
98
+ const dropdownList = getByTestId('select-list');
99
+ expect(dropdownList).toBeInTheDocument();
100
+
101
+ // Check if the options are rendered in the dropdown
102
+ const option1 = getByText('Option 1');
103
+ const option2 = getByText('Option 2');
104
+ expect(option1).toBeInTheDocument();
105
+ expect(option2).toBeInTheDocument();
106
+ });
107
+
108
+ test('disabling the select component prevents interaction', () => {
109
+ const onSelect = jest.fn();
110
+ const { getByTestId, queryByTestId } = render(
111
+ <NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect} disabled>
112
+ <NsSelectOption value="option1" label="Option 1" onSelect={onSelect} />
113
+ <NsSelectOption value="option2" label="Option 2" onSelect={onSelect} />
114
+ </NsSelect>
115
+ );
116
+ const selectButton = getByTestId('select-head');
117
+ fireEvent.mouseDown(selectButton);
118
+
119
+ // Check if the dropdown does not open (select list is not present)
120
+ const dropdownList = queryByTestId('select-list');
121
+ expect(dropdownList).toBeNull();
122
+
123
+ // Check if the onSelect callback is not called
124
+ expect(onSelect).not.toHaveBeenCalled();
125
+ });
126
+
127
+ it('select closes on mouse leave when closeOnMouseLeave is set to true', async () => {
128
+ render(
129
+ <NsSelect closeOnMouseLeave selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
130
+ {createSelectOptions(options)}
131
+ </NsSelect>
132
+ );
133
+
134
+ const selectHead = screen.getByTestId('select-head');
135
+ fireEvent.click(selectHead);
136
+
137
+ // const selectList = screen.queryByTestId ('select-list');
138
+ const option1 = screen.queryByTestId('option-Option1');
139
+ const option2 = screen.queryByTestId('option-Option2');
140
+ const option3 = screen.queryByTestId('option-Option3');
141
+ fireEvent.click(selectHead);
142
+
143
+ await waitFor(() => {
144
+ expect(option1).not.toBeInTheDocument();
145
+ expect(option2).not.toBeInTheDocument();
146
+ expect(option3).not.toBeInTheDocument();
147
+ });
148
+ });
149
+ it('select does not open when disabled', () => {
150
+ render(
151
+ <NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} disabled>
152
+ {createSelectOptions(options)}
153
+ </NsSelect>
154
+ );
155
+
156
+ const selectHead = screen.getByTestId('select-head');
157
+ fireEvent.click(selectHead);
158
+
159
+ expect(screen.queryByTestId('select-list')).not.toBeInTheDocument();
160
+ });
161
+
162
+ it('opens the select on "ArrowDown" key press', () => {
163
+ render(
164
+ <NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
165
+ {createSelectOptions(options)}
166
+ </NsSelect>
167
+ );
168
+
169
+ const selectHead = screen.getByTestId('select-head');
170
+ fireEvent.keyDown(selectHead, { key: 'arrowDown' });
171
+
172
+ expect(selectHead).toBeInTheDocument();
173
+ });
174
+
175
+ test('renders SelectContainer with error styles when errorMessage is present', () => {
176
+ const errorMessage = 'Invalid value';
177
+ render(
178
+ <NsSelect
179
+ selectedValue={selectedValue}
180
+ selectedLabel={selectedLabel}
181
+ onSelect={onSelect}
182
+ errorMessage={errorMessage}
183
+ >
184
+ {createSelectOptions(options)}
185
+ </NsSelect>
186
+ );
187
+
188
+ const selectContainer = screen.getByTestId('select-container');
189
+
190
+ expect(selectContainer).toHaveStyle('borderColor: red');
191
+ expect(selectContainer).toHaveStyle('color: red');
192
+ });
193
+ });
@@ -0,0 +1,104 @@
1
+ import { styled, ColorUtils, FoundationTheme } from '@nuskin/foundation-theme';
2
+ import { Themed } from '../common.types';
3
+ import { getDropdownHeadBorder } from '../dropdown/NsDropdown.styled';
4
+ const { color, paletteColor, isDarkMode } = ColorUtils;
5
+
6
+ export const SelectContainer = styled.div<{ disabled: boolean }>`
7
+ position: relative;
8
+ display: inline-block;
9
+ opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
10
+ `;
11
+
12
+ export const SelectHead = styled.div<{ type: string; disabled: boolean }>`
13
+ cursor: ${({ disabled }) => (disabled ? 'inherit' : 'pointer')};
14
+ padding: 5px 10px;
15
+ display: flex;
16
+ justify-content: space-between;
17
+ align-items: center;
18
+ border: ${getDropdownHeadBorder}
19
+ color: ${paletteColor('primary')};
20
+ width: 207px;
21
+ position: relative;
22
+ z-index: 2;
23
+ `;
24
+
25
+ export const SelectButton = styled.div`
26
+ padding-right: 10px;
27
+ `;
28
+
29
+ export const SelectList = styled.div<{ borderColor: FoundationTheme.PaletteColors }>`
30
+ position: absolute;
31
+ z-index: 100;
32
+ border: 1px solid ${({ borderColor, theme }) => paletteColor(borderColor)({ theme })};
33
+ padding: 0;
34
+ margin: 0;
35
+ width: -webkit-fill-available;
36
+ color: ${paletteColor('primary')};
37
+ background-color: ${paletteColor('primary', 'contrastText')};
38
+ `;
39
+
40
+ const getDividerColor = (props: { theme: FoundationTheme.Theme }) => {
41
+ return !isDarkMode(props) ? color('neutral', '30')(props) : color('neutral', '20')(props);
42
+ };
43
+
44
+ const getListBgColor = (props: { theme: FoundationTheme.Theme }) => {
45
+ return !isDarkMode(props) ? color('accentColorB', '10')(props) : color('accentColorB', '50')(props);
46
+ };
47
+ type ContainerParam = { borderBottom: string; bgColor: string; color: string; hoveredBg: string };
48
+ function createListContainerStyle({ borderBottom, bgColor, color, hoveredBg }: ContainerParam) {
49
+ return `
50
+ cursor: pointer;
51
+ padding: 5px 0;
52
+ border-bottom: 1px solid ${borderBottom};
53
+ background-color: ${bgColor};
54
+ color: ${color};
55
+
56
+ &:hover {
57
+ background-color: ${hoveredBg};
58
+ }
59
+
60
+ &:last-child {
61
+ border-bottom: none;
62
+ }
63
+ `;
64
+ }
65
+ export const ListItemContainer = styled.div<{ showListDivider: boolean; isSelected: boolean }>`
66
+ ${({ showListDivider, isSelected, theme }) => {
67
+ const borderBottom = showListDivider ? getDividerColor({ theme }) : 'none';
68
+ const bgColor = isSelected ? paletteColor('primary')({ theme }) : 'white';
69
+ const color = isSelected ? 'white' : 'black';
70
+ const hoveredBg = getListBgColor({ theme });
71
+ return createListContainerStyle({ borderBottom, bgColor, color, hoveredBg });
72
+ }}
73
+ `;
74
+ const getListItemBgColor = ({ isSelected, theme }: { isSelected: boolean } & Themed) => {
75
+ if (isSelected) {
76
+ return paletteColor('primary')({ theme });
77
+ }
78
+ return getListBgColor({ theme });
79
+ };
80
+
81
+ const getListItemBorderLeftColor = ({ isSelected, theme }: { isSelected: boolean } & Themed) => {
82
+ if (isSelected) {
83
+ const color = paletteColor('primary')({ theme });
84
+ return `2px solid ${color}`;
85
+ }
86
+ return 'none';
87
+ };
88
+
89
+ export const ListItem = styled.div<{ isSelected: boolean }>(({ isSelected, theme }) => ({
90
+ padding: '5px 10px',
91
+ borderLeft: getListItemBorderLeftColor({ isSelected, theme }),
92
+ backgroundColor: isSelected ? paletteColor('primary', 'main')({ theme }) : 'white',
93
+ color: isSelected ? 'white' : 'black',
94
+ '&:hover': {
95
+ backgroundColor: getListItemBgColor({ isSelected, theme }),
96
+ fontWeight: 'bold',
97
+ },
98
+ }));
99
+
100
+ export const SelectLabel = styled.label`
101
+ display: block;
102
+ margin-bottom: 5px;
103
+ color: ${paletteColor('primary')} !important;
104
+ `;
@@ -0,0 +1,117 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { SelectContainer, SelectButton, SelectList, SelectHead, SelectLabel } from './NsSelect.styled';
3
+ import { NsIcon } from '../icon';
4
+ import { SelectProps } from './types.d';
5
+
6
+ const NsSelect: React.FC<SelectProps> = ({
7
+ selectedValue,
8
+ selectedLabel,
9
+ onSelect,
10
+ children,
11
+ disabled = false,
12
+ type = 'outlined',
13
+ placeholder = '',
14
+ selectBorderColor = 'accentColorB',
15
+ closeOnMouseLeave = false,
16
+ showListDivider = true,
17
+ label,
18
+ readOnly = false,
19
+ errorMessage,
20
+ }) => {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const [value, setValue] = useState(selectedValue || '');
23
+ const selectContainerRef = useRef<HTMLDivElement>(null);
24
+ const selectHeadRef = useRef<HTMLDivElement>(null);
25
+
26
+ const closeSelect = () => {
27
+ if (closeOnMouseLeave) {
28
+ setIsOpen(false);
29
+ }
30
+ };
31
+
32
+ const handleSelect = (input: string) => {
33
+ onSelect(input);
34
+ setIsOpen(false);
35
+ setValue(input);
36
+ };
37
+
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ const handleClickOutside = (event: MouseEvent) => {
40
+ if (
41
+ selectContainerRef.current &&
42
+ !selectContainerRef.current.contains(event.target as Node) &&
43
+ isOpen
44
+ ) {
45
+ setIsOpen(false);
46
+ }
47
+ };
48
+
49
+ const handleMouseClick = () => {
50
+ if (disabled) return;
51
+ setIsOpen(!isOpen);
52
+ if (!isOpen) {
53
+ setTimeout(() => {
54
+ selectHeadRef.current?.focus();
55
+ }, 0);
56
+ }
57
+ };
58
+
59
+ useEffect(() => {
60
+ document.addEventListener('click', handleClickOutside);
61
+ return () => {
62
+ document.removeEventListener('click', handleClickOutside);
63
+ };
64
+ }, [handleClickOutside, isOpen]);
65
+
66
+ return (
67
+ <SelectContainer
68
+ data-testid="select-container"
69
+ disabled={disabled || readOnly}
70
+ ref={selectContainerRef}
71
+ style={{
72
+ borderColor: errorMessage ? 'red' : undefined,
73
+ color: errorMessage ? 'red' : 'inherit',
74
+ }}
75
+ >
76
+ {label && <SelectLabel style={{ color: errorMessage ? 'red' : 'inherit' }}>{label}</SelectLabel>}
77
+ <SelectHead
78
+ data-testid="select-head"
79
+ disabled={disabled || readOnly}
80
+ type={type}
81
+ onMouseDown={handleMouseClick}
82
+ tabIndex={0}
83
+ ref={selectHeadRef}
84
+ >
85
+ <SelectButton>{selectedLabel || placeholder}</SelectButton>
86
+ {!readOnly && (
87
+ <NsIcon testId="chevron-icon" name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />
88
+ )}
89
+ </SelectHead>
90
+ {isOpen ? (
91
+ <SelectList
92
+ data-testid="select-list"
93
+ onMouseLeave={closeSelect}
94
+ borderColor={selectBorderColor}
95
+ className='selectList'
96
+ >
97
+ <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
98
+ {React.Children.map(children, (child) => {
99
+ if (React.isValidElement(child)) {
100
+ return React.cloneElement(child, {
101
+ // @ts-ignore
102
+ onSelect: handleSelect,
103
+ selectedValue: value,
104
+ showListDivider,
105
+ });
106
+ }
107
+ return null;
108
+ })}
109
+ </div>
110
+ </SelectList>
111
+ ) : null}
112
+ {errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
113
+ </SelectContainer>
114
+ );
115
+ };
116
+
117
+ export default NsSelect;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { ListItem, ListItemContainer } from './NsSelect.styled';
3
+ import { SelectOptionProps } from './types.d';
4
+
5
+ export const NsSelectOption: React.FC<SelectOptionProps> = ({
6
+ value,
7
+ label,
8
+ onSelect,
9
+ selectedValue,
10
+ showListDivider = true,
11
+ }) => {
12
+ const handleClick = () => {
13
+ if (onSelect && typeof onSelect === 'function') {
14
+ onSelect(value);
15
+ }
16
+ };
17
+
18
+ return (
19
+ <ListItemContainer
20
+ data-testid={`option-${value}`}
21
+ isSelected={value === selectedValue}
22
+ showListDivider={showListDivider}
23
+ onClick={handleClick}
24
+ >
25
+ <ListItem isSelected={value === selectedValue}>{label}</ListItem>
26
+ </ListItemContainer>
27
+
28
+ );
29
+ };
30
+ export default NsSelectOption;
@@ -0,0 +1,3 @@
1
+ export { default as NsSelect } from './NsSelect';
2
+ export { default as NsSelectOption } from './NsSelectOption';
3
+ export type { SelectOptionProps, SelectProps } from './types.d';
@@ -0,0 +1,134 @@
1
+ import React, { useState } from 'react';
2
+ import { ComponentStory, ComponentMeta } from '@storybook/react';
3
+ import NsSelect from '../NsSelect';
4
+ import NsSelectOption from '../NsSelectOption';
5
+
6
+ export default {
7
+ title: 'Components/Inputs/NsSelect',
8
+ decorators: [
9
+ (story) => <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{story()}</div>,
10
+ ],
11
+ component: NsSelect,
12
+ argTypes: {
13
+ type: { control: 'select', options: ['unfilled', 'outlined'] },
14
+ disabled: { control: 'boolean' },
15
+ selectBorderColor: {
16
+ control: 'select',
17
+ options: ['primary', 'secondary', 'accentColorA', 'accentColorB', 'accentColorC'],
18
+ },
19
+ placeholder: { control: 'text' },
20
+ closeOnMouseLeave: { control: 'boolean' },
21
+ showListDivider: { control: 'boolean' },
22
+ label: { control: 'text' },
23
+ name: { control: 'text' },
24
+ menuProps: { control: 'text' },
25
+ reverseArrowControls: { control: 'boolean' },
26
+ validateOnBlur: { control: 'boolean' },
27
+ autoComplete: { control: 'text' },
28
+ rules: { control: 'array' },
29
+ },
30
+ parameters: {
31
+ options: [
32
+ { label: 'Alabama', value: 'AL' },
33
+ { label: 'Alaska', value: 'AK' },
34
+ { label: 'Arizona', value: 'AZ' },
35
+ { label: 'Arkansas', value: 'AR' },
36
+ { label: 'California', value: 'CA' },
37
+ { label: 'Colorado', value: 'CO' },
38
+ { label: 'Connecticut', value: 'CT' },
39
+ { label: 'Delaware', value: 'DE' },
40
+ { label: 'District of Columbia', value: 'DC' },
41
+ { label: 'Florida', value: 'FL' },
42
+ { label: 'Georgia', value: 'GA' },
43
+ { label: 'Hawaii', value: 'HI' },
44
+ { label: 'Idaho', value: 'ID' },
45
+ { label: 'Illinois', value: 'IL' },
46
+ { label: 'Indiana', value: 'IN' },
47
+ { label: 'Iowa', value: 'IA' },
48
+ { label: 'Kansas', value: 'KA' },
49
+ { label: 'Kentucky', value: 'KY' },
50
+ { label: 'Louisiana', value: 'LA' },
51
+ { label: 'Maine', value: 'ME' },
52
+ { label: 'Maryland', value: 'MY' },
53
+ { label: 'Massachusetts', value: 'MA' },
54
+ { label: 'Michigan', value: 'MC' },
55
+ { label: 'Minnesota', value: 'MN' },
56
+ { label: 'Mississippi', value: 'MI' },
57
+ { label: 'Missouri', value: 'MO' },
58
+ { label: 'Montana', value: 'MT' },
59
+ { label: 'Nebraska', value: 'NE' },
60
+ { label: 'Nevada', value: 'NA' },
61
+ { label: 'New Hampshire', value: 'NH' },
62
+ { label: 'New Jersey', value: 'NJ' },
63
+ { label: 'New Mexico', value: 'NM' },
64
+ { label: 'New York', value: 'NY' },
65
+ { label: 'North Carolina', value: 'NC' },
66
+ { label: 'North Dakota', value: 'CD' },
67
+ { label: 'Ohio', value: 'OH' },
68
+ { label: 'Oklahoma', value: 'OK' },
69
+ { label: 'Oregon', value: 'OR' },
70
+ { label: 'Pennsylvania', value: 'PA' },
71
+ { label: 'Rhode Island', value: 'RI' },
72
+ { label: 'South Carolina', value: 'SC' },
73
+ { label: 'South Dakota', value: 'SD' },
74
+ { label: 'Tennessee', value: 'TN' },
75
+ { label: 'Texas', value: 'TX' },
76
+ { label: 'Utah', value: 'UT' },
77
+ { label: 'Vermont', value: 'VT' },
78
+ { label: 'Virginia', value: 'VA' },
79
+ { label: 'Washington', value: 'WA' },
80
+ { label: 'West Virginia', value: 'WV' },
81
+ { label: 'Wisconsin', value: 'WN' },
82
+ { label: 'Wyoming', value: 'WY' },
83
+ ],
84
+ },
85
+ args: {
86
+ type: 'outlined',
87
+ disabled: false,
88
+ selectBorderColor: 'primary',
89
+ closeOnMouseLeave: false,
90
+ showListDivider: true,
91
+ selectedValue: '',
92
+ selectedLabel: '',
93
+ label: 'State',
94
+ placeholder: '',
95
+ name: '',
96
+ menuProps: 'auto',
97
+ reverseArrowControls: false,
98
+ itemText: 'Text',
99
+ itemValue: 'Value',
100
+ validateOnBlur: false,
101
+ autoComplete: '',
102
+ rules: [],
103
+ },
104
+ } as ComponentMeta<typeof NsSelect>;
105
+
106
+ export const Select: ComponentStory<typeof NsSelect> = (args, context) => {
107
+ const [selectedLabel, setSelectedLabel] = useState(args.selectedLabel);
108
+ const [selectedValue, setSelectedValue] = useState(args.selectedValue);
109
+
110
+ const options = context.parameters.options;
111
+ const handleSelect = (value: string) => {
112
+ setSelectedValue(value);
113
+ options.find((option: any) => {
114
+ if (option.value === value) {
115
+ setSelectedLabel(option.label);
116
+ }
117
+ });
118
+ console.log('Selected:', value);
119
+ };
120
+
121
+ return (
122
+ <NsSelect
123
+ {...args}
124
+ selectedValue={selectedValue}
125
+ selectedLabel={selectedLabel}
126
+ onSelect={handleSelect}
127
+ label={args.label}
128
+ >
129
+ {options.map((option: any) => (
130
+ <NsSelectOption key={option.value} value={option.value} label={option.label} />
131
+ ))}
132
+ </NsSelect>
133
+ );
134
+ };
@@ -0,0 +1,34 @@
1
+ import { ReactNode } from 'react';
2
+ import { FoundationTheme } from '@nuskin/foundation-theme';
3
+
4
+ export interface SelectProps {
5
+ selectedValue: string;
6
+ selectedLabel: string;
7
+ onSelect: (arg: string) => void; // eslint-disable-line
8
+ children?: ReactNode | null;
9
+ disabled?: boolean;
10
+ type?: 'outlined' | 'unfilled';
11
+ placeholder?: string;
12
+ selectBorderColor?: FoundationTheme.PaletteColors;
13
+ closeOnMouseLeave?: boolean;
14
+ showListDivider?: boolean;
15
+ label?: string;
16
+ readOnly?: boolean;
17
+ name?: string;
18
+ errorMessage?: string;
19
+ menuProps?: string | object;
20
+ reverseArrowControls?: boolean;
21
+ itemText?: string;
22
+ itemValue?: string;
23
+ validateOnBlur?: boolean;
24
+ autoComplete?: string;
25
+ rules?: any[];
26
+ }
27
+
28
+ export interface SelectOptionProps {
29
+ value: string;
30
+ label: string;
31
+ selectedValue?: string;
32
+ onSelect?: (arg: string) => void; // eslint-disable-line
33
+ showListDivider?: boolean;
34
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@/test-utils/index';
3
+ import NsSpinner from './NsSpinner';
4
+
5
+ describe('NsSpinner', () => {
6
+ test('1.renders spinner', () => {
7
+ const { container } = render(<NsSpinner additionalProps={{ 'data-testid': 'spinner' }} />);
8
+
9
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
10
+ expect(container.getElementsByTagName('svg')).toHaveLength(1);
11
+ expect(screen.getByTestId('spinner')).toHaveStyle('width:100px');
12
+ });
13
+
14
+ test('2.renders spinner with different size', () => {
15
+ render(<NsSpinner size={200} additionalProps={{ 'data-testid': 'spinner' }} />);
16
+
17
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
18
+ expect(screen.getByTestId('spinner')).toHaveStyle('width:200px');
19
+ expect(screen.getByTestId('spinner')).toHaveStyle('height:200px');
20
+ });
21
+ });
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import NsSpinner from './NsSpinner';
3
+ import { withCenterDecorator } from '@/stories/decorators/withCenterDecorator';
4
+
5
+ type ComponentType = typeof NsSpinner;
6
+ type Story = StoryObj<ComponentType>;
7
+ type MetaStory = Meta<ComponentType>;
8
+
9
+ export default {
10
+ title: 'Components/Feedback/NsSpinner',
11
+ component: NsSpinner,
12
+ decorators: [
13
+ (Story) => (
14
+ <div className="flex place-content-center">
15
+ <Story />
16
+ </div>
17
+ ),
18
+ withCenterDecorator,
19
+ ],
20
+ argTypes: {},
21
+ args: {
22
+ size: 200,
23
+ },
24
+ } as MetaStory;
25
+
26
+ export const Spinner: Story = {};