@hyphen/hyphen-components 2.9.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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/package.json +138 -0
  4. package/src/components/Alert/Alert.constants.ts +19 -0
  5. package/src/components/Alert/Alert.mdx +29 -0
  6. package/src/components/Alert/Alert.module.scss +74 -0
  7. package/src/components/Alert/Alert.stories.tsx +102 -0
  8. package/src/components/Alert/Alert.test.tsx +187 -0
  9. package/src/components/Alert/Alert.tsx +157 -0
  10. package/src/components/Alert/Alert.types.ts +14 -0
  11. package/src/components/Badge/Badge.mdx +28 -0
  12. package/src/components/Badge/Badge.module.scss +155 -0
  13. package/src/components/Badge/Badge.stories.tsx +52 -0
  14. package/src/components/Badge/Badge.test.tsx +74 -0
  15. package/src/components/Badge/Badge.tsx +70 -0
  16. package/src/components/Box/Box.mdx +259 -0
  17. package/src/components/Box/Box.module.scss +16 -0
  18. package/src/components/Box/Box.stories.tsx +1679 -0
  19. package/src/components/Box/Box.test.tsx +478 -0
  20. package/src/components/Box/Box.tsx +636 -0
  21. package/src/components/Button/Button.constants.ts +10 -0
  22. package/src/components/Button/Button.mdx +71 -0
  23. package/src/components/Button/Button.module.scss +312 -0
  24. package/src/components/Button/Button.stories.tsx +117 -0
  25. package/src/components/Button/Button.test.tsx +460 -0
  26. package/src/components/Button/Button.tsx +241 -0
  27. package/src/components/Card/Card.mdx +46 -0
  28. package/src/components/Card/Card.module.scss +6 -0
  29. package/src/components/Card/Card.stories.tsx +101 -0
  30. package/src/components/Card/Card.test.tsx +11 -0
  31. package/src/components/Card/Card.tsx +61 -0
  32. package/src/components/Card/components/CardFooter/CardFooter.test.tsx +11 -0
  33. package/src/components/Card/components/CardFooter/CardFooter.tsx +35 -0
  34. package/src/components/Card/components/CardHeader/CardHeader.test.tsx +23 -0
  35. package/src/components/Card/components/CardHeader/CardHeader.tsx +54 -0
  36. package/src/components/Card/components/CardSection/CardSection.test.tsx +28 -0
  37. package/src/components/Card/components/CardSection/CardSection.tsx +102 -0
  38. package/src/components/Card/components/index.ts +3 -0
  39. package/src/components/CheckboxInput/CheckboxInput.mdx +98 -0
  40. package/src/components/CheckboxInput/CheckboxInput.stories.tsx +254 -0
  41. package/src/components/CheckboxInput/CheckboxInput.test.tsx +436 -0
  42. package/src/components/CheckboxInput/CheckboxInput.tsx +171 -0
  43. package/src/components/CheckboxInput/components/Checkbox.module.scss +174 -0
  44. package/src/components/CheckboxInput/components/Checkbox.test.tsx +201 -0
  45. package/src/components/CheckboxInput/components/Checkbox.tsx +176 -0
  46. package/src/components/CheckboxInput/components/CheckboxIcon.tsx +71 -0
  47. package/src/components/DateInput/DateInput.mdx +61 -0
  48. package/src/components/DateInput/DateInput.stories.tsx +168 -0
  49. package/src/components/DateInput/DateInput.test.tsx +258 -0
  50. package/src/components/DateInput/DateInput.tsx +189 -0
  51. package/src/components/DatePicker/DatePicker.mdx +52 -0
  52. package/src/components/DatePicker/DatePicker.module.scss +603 -0
  53. package/src/components/DatePicker/DatePicker.stories.tsx +199 -0
  54. package/src/components/DatePicker/DatePicker.test.tsx +26 -0
  55. package/src/components/DatePicker/DatePicker.tsx +138 -0
  56. package/src/components/Details/Details.mdx +30 -0
  57. package/src/components/Details/Details.module.scss +32 -0
  58. package/src/components/Details/Details.stories.tsx +38 -0
  59. package/src/components/Details/Details.test.tsx +189 -0
  60. package/src/components/Details/Details.tsx +51 -0
  61. package/src/components/Details/DetailsSummary.tsx +65 -0
  62. package/src/components/Drawer/Drawer.mdx +117 -0
  63. package/src/components/Drawer/Drawer.module.scss +96 -0
  64. package/src/components/Drawer/Drawer.stories.tsx +380 -0
  65. package/src/components/Drawer/Drawer.test.tsx +90 -0
  66. package/src/components/Drawer/Drawer.tsx +249 -0
  67. package/src/components/FormControl/FormControl.tsx +78 -0
  68. package/src/components/FormLabel/FormLabel.mdx +24 -0
  69. package/src/components/FormLabel/FormLabel.module.scss +19 -0
  70. package/src/components/FormLabel/FormLabel.stories.tsx +20 -0
  71. package/src/components/FormLabel/FormLabel.test.tsx +35 -0
  72. package/src/components/FormLabel/FormLabel.tsx +96 -0
  73. package/src/components/Formik/Formik.mdx +10 -0
  74. package/src/components/Formik/Formik.stories.tsx +307 -0
  75. package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.test.tsx +172 -0
  76. package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.tsx +41 -0
  77. package/src/components/Formik/FormikRadioGroup/FormikRadioGroup.test.tsx +205 -0
  78. package/src/components/Formik/FormikRadioGroup/FormikRadioGroup.tsx +37 -0
  79. package/src/components/Formik/FormikSelectInput/FormikSelectInput.test.tsx +210 -0
  80. package/src/components/Formik/FormikSelectInput/FormikSelectInput.tsx +41 -0
  81. package/src/components/Formik/FormikSelectInputInset/FormikSelectInputInset.test.tsx +153 -0
  82. package/src/components/Formik/FormikSelectInputInset/FormikSelectInputInset.tsx +44 -0
  83. package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.test.tsx +161 -0
  84. package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.tsx +46 -0
  85. package/src/components/Formik/FormikTextInput/FormikTextInput.test.tsx +176 -0
  86. package/src/components/Formik/FormikTextInput/FormikTextInput.tsx +38 -0
  87. package/src/components/Formik/FormikTextInputInset/FormikTextInputInset.test.tsx +170 -0
  88. package/src/components/Formik/FormikTextInputInset/FormikTextInputInset.tsx +42 -0
  89. package/src/components/Formik/FormikTextareaInput/FormikTextareaInput.test.tsx +186 -0
  90. package/src/components/Formik/FormikTextareaInput/FormikTextareaInput.tsx +42 -0
  91. package/src/components/Formik/FormikTextareaInputInset/FormikTextareaInputInset.test.tsx +179 -0
  92. package/src/components/Formik/FormikTextareaInputInset/FormikTextareaInputInset.tsx +42 -0
  93. package/src/components/Formik/FormikTimePicker/FormikTimePicker.test.tsx +224 -0
  94. package/src/components/Formik/FormikTimePicker/FormikTimePicker.tsx +37 -0
  95. package/src/components/Formik/FormikTimePickerNative/FormikTimePickerNative.test.tsx +175 -0
  96. package/src/components/Formik/FormikTimePickerNative/FormikTimePickerNative.tsx +38 -0
  97. package/src/components/Formik/FormikToggle/FormikToggle.test.tsx +172 -0
  98. package/src/components/Formik/FormikToggle/FormikToggle.tsx +38 -0
  99. package/src/components/Heading/Heading.constants.ts +19 -0
  100. package/src/components/Heading/Heading.mdx +35 -0
  101. package/src/components/Heading/Heading.module.scss +5 -0
  102. package/src/components/Heading/Heading.stories.tsx +90 -0
  103. package/src/components/Heading/Heading.test.tsx +67 -0
  104. package/src/components/Heading/Heading.tsx +67 -0
  105. package/src/components/HelpText/HelpText.module.scss +14 -0
  106. package/src/components/HelpText/HelpText.tsx +33 -0
  107. package/src/components/Icon/Icon.mdx +40 -0
  108. package/src/components/Icon/Icon.stories.tsx +72 -0
  109. package/src/components/Icon/Icon.test.tsx +30 -0
  110. package/src/components/Icon/Icon.tsx +61 -0
  111. package/src/components/InputValidationMessage/InputValidationMessage.module.scss +3 -0
  112. package/src/components/InputValidationMessage/InputValidationMessage.tsx +27 -0
  113. package/src/components/Modal/Modal.mdx +60 -0
  114. package/src/components/Modal/Modal.module.scss +135 -0
  115. package/src/components/Modal/Modal.stories.tsx +194 -0
  116. package/src/components/Modal/Modal.test.tsx +81 -0
  117. package/src/components/Modal/Modal.tsx +174 -0
  118. package/src/components/Modal/components/ModalBody/ModalBody.test.tsx +20 -0
  119. package/src/components/Modal/components/ModalBody/ModalBody.tsx +24 -0
  120. package/src/components/Modal/components/ModalFooter/ModalFooter.test.tsx +32 -0
  121. package/src/components/Modal/components/ModalFooter/ModalFooter.tsx +37 -0
  122. package/src/components/Modal/components/ModalHeader/ModalHeader.test.tsx +29 -0
  123. package/src/components/Modal/components/ModalHeader/ModalHeader.tsx +58 -0
  124. package/src/components/Modal/components/index.ts +5 -0
  125. package/src/components/Pagination/Pagination.mdx +26 -0
  126. package/src/components/Pagination/Pagination.stories.tsx +55 -0
  127. package/src/components/Pagination/Pagination.test.tsx +225 -0
  128. package/src/components/Pagination/Pagination.tsx +162 -0
  129. package/src/components/Pagination/Pagination.utilities.test.ts +133 -0
  130. package/src/components/Pagination/Pagination.utilities.ts +101 -0
  131. package/src/components/Popover/Popover.mdx +104 -0
  132. package/src/components/Popover/Popover.module.scss +74 -0
  133. package/src/components/Popover/Popover.stories.tsx +471 -0
  134. package/src/components/Popover/Popover.test.tsx +128 -0
  135. package/src/components/Popover/Popover.tsx +277 -0
  136. package/src/components/RadioGroup/RadioGroup.mdx +81 -0
  137. package/src/components/RadioGroup/RadioGroup.module.scss +23 -0
  138. package/src/components/RadioGroup/RadioGroup.stories.tsx +375 -0
  139. package/src/components/RadioGroup/RadioGroup.test.tsx +282 -0
  140. package/src/components/RadioGroup/RadioGroup.tsx +145 -0
  141. package/src/components/RadioGroup/RadioInput/RadioInput.module.scss +114 -0
  142. package/src/components/RadioGroup/RadioInput/RadioInput.test.tsx +156 -0
  143. package/src/components/RadioGroup/RadioInput/RadioInput.tsx +148 -0
  144. package/src/components/RadioGroup/RadioInput/RadioInputIcon.tsx +59 -0
  145. package/src/components/ResponsiveProvider/ResponsiveProvider.mdx +36 -0
  146. package/src/components/ResponsiveProvider/ResponsiveProvider.stories.tsx +54 -0
  147. package/src/components/ResponsiveProvider/ResponsiveProvider.test.tsx +70 -0
  148. package/src/components/ResponsiveProvider/ResponsiveProvider.tsx +73 -0
  149. package/src/components/SelectInput/SelectInput.mdx +115 -0
  150. package/src/components/SelectInput/SelectInput.module.scss +357 -0
  151. package/src/components/SelectInput/SelectInput.stories.tsx +373 -0
  152. package/src/components/SelectInput/SelectInput.test.tsx +403 -0
  153. package/src/components/SelectInput/SelectInput.tsx +245 -0
  154. package/src/components/SelectInputInset/SelectInputInset.mdx +56 -0
  155. package/src/components/SelectInputInset/SelectInputInset.module.scss +397 -0
  156. package/src/components/SelectInputInset/SelectInputInset.stories.tsx +189 -0
  157. package/src/components/SelectInputInset/SelectInputInset.test.tsx +305 -0
  158. package/src/components/SelectInputInset/SelectInputInset.tsx +235 -0
  159. package/src/components/SelectInputNative/SelectInputNative.mdx +87 -0
  160. package/src/components/SelectInputNative/SelectInputNative.module.scss +356 -0
  161. package/src/components/SelectInputNative/SelectInputNative.stories.tsx +282 -0
  162. package/src/components/SelectInputNative/SelectInputNative.test.tsx +341 -0
  163. package/src/components/SelectInputNative/SelectInputNative.tsx +121 -0
  164. package/src/components/Spinner/Spinner.mdx +29 -0
  165. package/src/components/Spinner/Spinner.module.scss +16 -0
  166. package/src/components/Spinner/Spinner.stories.tsx +48 -0
  167. package/src/components/Spinner/Spinner.test.tsx +47 -0
  168. package/src/components/Spinner/Spinner.tsx +116 -0
  169. package/src/components/Table/Table.mdx +216 -0
  170. package/src/components/Table/Table.module.scss +61 -0
  171. package/src/components/Table/Table.stories.tsx +884 -0
  172. package/src/components/Table/Table.test.tsx +437 -0
  173. package/src/components/Table/Table.tsx +171 -0
  174. package/src/components/Table/TableBody/TableBody.module.scss +19 -0
  175. package/src/components/Table/TableBody/TableBody.test.tsx +38 -0
  176. package/src/components/Table/TableBody/TableBody.tsx +96 -0
  177. package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.module.scss +47 -0
  178. package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.test.tsx +81 -0
  179. package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.tsx +94 -0
  180. package/src/components/Table/TableHead/TableHead.test.tsx +20 -0
  181. package/src/components/Table/TableHead/TableHead.tsx +78 -0
  182. package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.module.scss +72 -0
  183. package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.test.tsx +187 -0
  184. package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.tsx +192 -0
  185. package/src/components/Table/common/TableRow/TableRow.module.scss +5 -0
  186. package/src/components/Table/common/TableRow/TableRow.test.tsx +52 -0
  187. package/src/components/Table/common/TableRow/TableRow.tsx +155 -0
  188. package/src/components/TextInput/TextInput.mdx +96 -0
  189. package/src/components/TextInput/TextInput.module.scss +405 -0
  190. package/src/components/TextInput/TextInput.stories.tsx +268 -0
  191. package/src/components/TextInput/TextInput.test.tsx +231 -0
  192. package/src/components/TextInput/TextInput.tsx +263 -0
  193. package/src/components/TextInputInset/TextInputInset.mdx +62 -0
  194. package/src/components/TextInputInset/TextInputInset.module.scss +418 -0
  195. package/src/components/TextInputInset/TextInputInset.stories.tsx +213 -0
  196. package/src/components/TextInputInset/TextInputInset.test.tsx +222 -0
  197. package/src/components/TextInputInset/TextInputInset.tsx +261 -0
  198. package/src/components/TextareaInput/TextareaInput.mdx +117 -0
  199. package/src/components/TextareaInput/TextareaInput.module.scss +275 -0
  200. package/src/components/TextareaInput/TextareaInput.stories.tsx +293 -0
  201. package/src/components/TextareaInput/TextareaInput.test.tsx +195 -0
  202. package/src/components/TextareaInput/TextareaInput.tsx +182 -0
  203. package/src/components/TextareaInputInset/TextareaInputInset.mdx +55 -0
  204. package/src/components/TextareaInputInset/TextareaInputInset.module.scss +337 -0
  205. package/src/components/TextareaInputInset/TextareaInputInset.stories.tsx +160 -0
  206. package/src/components/TextareaInputInset/TextareaInputInset.test.tsx +199 -0
  207. package/src/components/TextareaInputInset/TextareaInputInset.tsx +213 -0
  208. package/src/components/ThemeProvider/ThemeProvider.mdx +11 -0
  209. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +56 -0
  210. package/src/components/ThemeProvider/ThemeProvider.tsx +75 -0
  211. package/src/components/TimePicker/TimePicker.mdx +75 -0
  212. package/src/components/TimePicker/TimePicker.stories.tsx +149 -0
  213. package/src/components/TimePicker/TimePicker.test.tsx +97 -0
  214. package/src/components/TimePicker/TimePicker.tsx +83 -0
  215. package/src/components/TimePickerNative/TimePickerNative.mdx +67 -0
  216. package/src/components/TimePickerNative/TimePickerNative.stories.tsx +151 -0
  217. package/src/components/TimePickerNative/TimePickerNative.test.tsx +117 -0
  218. package/src/components/TimePickerNative/TimePickerNative.tsx +93 -0
  219. package/src/components/Toast/Toast.mdx +134 -0
  220. package/src/components/Toast/Toast.store.ts +280 -0
  221. package/src/components/Toast/Toast.stories.tsx +283 -0
  222. package/src/components/Toast/Toast.test.tsx +784 -0
  223. package/src/components/Toast/Toast.types.ts +98 -0
  224. package/src/components/Toast/ToastContainer.tsx +170 -0
  225. package/src/components/Toast/ToastNotification.module.scss +63 -0
  226. package/src/components/Toast/ToastNotification.tsx +176 -0
  227. package/src/components/Toast/index.ts +4 -0
  228. package/src/components/Toast/toast.ts +102 -0
  229. package/src/components/Toast/useToasts.ts +102 -0
  230. package/src/components/Toggle/Toggle.mdx +51 -0
  231. package/src/components/Toggle/Toggle.module.scss +294 -0
  232. package/src/components/Toggle/Toggle.stories.tsx +128 -0
  233. package/src/components/Toggle/Toggle.test.tsx +308 -0
  234. package/src/components/Toggle/Toggle.tsx +184 -0
  235. package/src/constants/keyCodes.ts +2 -0
  236. package/src/docs/Brands.mdx +153 -0
  237. package/src/docs/Colors.mdx +158 -0
  238. package/src/docs/DesignTokens.mdx +415 -0
  239. package/src/docs/GetStarted.mdx +47 -0
  240. package/src/docs/intro.mdx +12 -0
  241. package/src/fonts/AvenirBold.otf +0 -0
  242. package/src/fonts/AvenirBold.woff +0 -0
  243. package/src/fonts/AvenirBold.woff2 +0 -0
  244. package/src/fonts/AvenirLight.otf +0 -0
  245. package/src/fonts/AvenirLight.woff +0 -0
  246. package/src/fonts/AvenirLight.woff2 +0 -0
  247. package/src/fonts/AvenirRegular.otf +0 -0
  248. package/src/fonts/AvenirRegular.woff +0 -0
  249. package/src/fonts/AvenirRegular.woff2 +0 -0
  250. package/src/fonts/Geist-Bold.otf +0 -0
  251. package/src/fonts/Geist-Bold.woff +0 -0
  252. package/src/fonts/Geist-Bold.woff2 +0 -0
  253. package/src/fonts/Geist-Medium.otf +0 -0
  254. package/src/fonts/Geist-Medium.woff +0 -0
  255. package/src/fonts/Geist-Medium.woff2 +0 -0
  256. package/src/fonts/Geist-Regular.otf +0 -0
  257. package/src/fonts/Geist-Regular.woff +0 -0
  258. package/src/fonts/Geist-Regular.woff2 +0 -0
  259. package/src/fonts/Geist-SemiBold.otf +0 -0
  260. package/src/fonts/Geist-SemiBold.woff +0 -0
  261. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  262. package/src/fonts/GeistMono-Regular.otf +0 -0
  263. package/src/fonts/GeistMono-Regular.woff +0 -0
  264. package/src/fonts/GeistMono-Regular.woff2 +0 -0
  265. package/src/hooks/index.ts +4 -0
  266. package/src/hooks/useBreakpoint/useBreakpoint.mdx +26 -0
  267. package/src/hooks/useBreakpoint/useBreakpoint.stories.tsx +30 -0
  268. package/src/hooks/useBreakpoint/useBreakpoint.test.tsx +19 -0
  269. package/src/hooks/useBreakpoint/useBreakpoint.ts +50 -0
  270. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayouEffect.ts +5 -0
  271. package/src/hooks/useOpenClose/useOpenClose.mdx +15 -0
  272. package/src/hooks/useOpenClose/useOpenClose.stories.tsx +41 -0
  273. package/src/hooks/useOpenClose/useOpenClose.test.tsx +119 -0
  274. package/src/hooks/useOpenClose/useOpenClose.tsx +95 -0
  275. package/src/hooks/useWindowSize/useWindowSize.mdx +25 -0
  276. package/src/hooks/useWindowSize/useWindowSize.stories.tsx +35 -0
  277. package/src/hooks/useWindowSize/useWindowSize.ts +24 -0
  278. package/src/index.ts +48 -0
  279. package/src/lib/cssShorthandToClasses.test.ts +149 -0
  280. package/src/lib/cssShorthandToClasses.ts +133 -0
  281. package/src/lib/doesStringIncludeCssUnit.ts +6 -0
  282. package/src/lib/generateResponsiveClasses.test.ts +24 -0
  283. package/src/lib/generateResponsiveClasses.ts +30 -0
  284. package/src/lib/getAutoCompleteValue.test.ts +27 -0
  285. package/src/lib/getAutoCompleteValue.ts +12 -0
  286. package/src/lib/getColumnKeys.ts +27 -0
  287. package/src/lib/getDimensionCss.test.ts +148 -0
  288. package/src/lib/getDimensionCss.ts +73 -0
  289. package/src/lib/getElementType.test.tsx +42 -0
  290. package/src/lib/getElementType.ts +42 -0
  291. package/src/lib/getFlexCss.test.ts +122 -0
  292. package/src/lib/getFlexCss.ts +67 -0
  293. package/src/lib/index.ts +15 -0
  294. package/src/lib/isFunction.ts +6 -0
  295. package/src/lib/mergeRefs.ts +15 -0
  296. package/src/lib/prefersReducedMotion.ts +12 -0
  297. package/src/lib/react-children-utilities/filter.ts +12 -0
  298. package/src/lib/react-children-utilities/index.ts +1 -0
  299. package/src/lib/reactRouterClickHandler.ts +37 -0
  300. package/src/lib/resolveValue.ts +7 -0
  301. package/src/lib/tokens.ts +139 -0
  302. package/src/modes.ts +8 -0
  303. package/src/styles/animation.scss +152 -0
  304. package/src/styles/cursor.scss +43 -0
  305. package/src/styles/display.scss +119 -0
  306. package/src/styles/flex.scss +453 -0
  307. package/src/styles/fonts.scss +44 -0
  308. package/src/styles/globals/utilities.scss +4 -0
  309. package/src/styles/mixins.scss +14 -0
  310. package/src/styles/overflow.scss +41 -0
  311. package/src/styles/position.scss +45 -0
  312. package/src/styles/reset.scss +108 -0
  313. package/src/styles/text-align.scss +21 -0
  314. package/src/styles/utilities.scss +9 -0
  315. package/src/styles/variables/forms.scss +71 -0
  316. package/src/styles/variables/index.scss +3 -0
  317. package/src/styles/white-space.scss +21 -0
  318. package/src/types/index.ts +201 -0
  319. package/src/types/lib.types.ts +3 -0
@@ -0,0 +1,460 @@
1
+ import { BUTTON_SIZES, BUTTON_VARIANTS } from './Button.constants';
2
+ import { Button, ButtonVariant } from './Button';
3
+ import { fireEvent, render, screen } from '@testing-library/react';
4
+
5
+ import React from 'react';
6
+
7
+ describe('Button', () => {
8
+ describe('html button type', () => {
9
+ test('is set to button', () => {
10
+ render(<Button>Button</Button>);
11
+ const testBtn = screen.getByRole('button');
12
+ expect(testBtn.getAttribute('type')).toBe('button');
13
+ });
14
+
15
+ test('is set to "submit" if specified', () => {
16
+ render(<Button type="submit">Submit Button</Button>);
17
+
18
+ const testBtn = screen.getByRole('button');
19
+ expect(testBtn.getAttribute('type')).toBe('submit');
20
+ });
21
+
22
+ test('is set to "reset" if specified', () => {
23
+ render(<Button type="reset">Reset Button</Button>);
24
+
25
+ const testBtn = screen.getByRole('button');
26
+ expect(testBtn.getAttribute('type')).toBe('reset');
27
+ });
28
+
29
+ test('is not set if "as" prop is an anchor tag', () => {
30
+ render(
31
+ <Button as="a" href="https://www.hyphen.ai">
32
+ link button
33
+ </Button>
34
+ );
35
+ const testBtn = screen.getByText('link button').parentElement;
36
+ expect(testBtn).not.toHaveAttribute('type');
37
+ });
38
+ });
39
+
40
+ describe('with Icon', () => {
41
+ test('Renders an icon prefix if specified', () => {
42
+ render(<Button iconPrefix="alarm">Alarm Button</Button>);
43
+ expect(screen.getByTestId('prefixIcon')).toBeInTheDocument();
44
+ });
45
+
46
+ test('Renders an icon suffix if specified', () => {
47
+ render(<Button iconSuffix="alarm">Alarm Button</Button>);
48
+ expect(screen.getByTestId('suffixIcon')).toBeInTheDocument();
49
+ });
50
+
51
+ test('Renders icon prefix and suffix if specified', () => {
52
+ render(
53
+ <Button iconPrefix="alarm" iconSuffix="check">
54
+ Suffix Prefix Icon Button
55
+ </Button>
56
+ );
57
+ expect(screen.getByTestId('prefixIcon')).toBeInTheDocument();
58
+ expect(screen.getByTestId('suffixIcon')).toBeInTheDocument();
59
+ });
60
+ });
61
+
62
+ describe('Sizes', () => {
63
+ BUTTON_SIZES.map((size) =>
64
+ describe(`${BUTTON_SIZES}`, () => {
65
+ test(`it has a ${size} class applied to it`, () => {
66
+ render(<Button size={size}>{`${size} Button`}</Button>);
67
+
68
+ const btn = screen.getByText(`${size} Button`).closest('button');
69
+
70
+ expect(btn?.getAttribute('class')).toContain(`size-${size}`);
71
+ });
72
+ })
73
+ );
74
+
75
+ test('It applies responsive classes', () => {
76
+ render(
77
+ <Button size={{ base: 'lg', tablet: 'sm', desktop: 'md', hd: 'lg' }}>
78
+ button
79
+ </Button>
80
+ );
81
+
82
+ const btn = screen.getByText('button').closest('button');
83
+
84
+ expect(btn?.getAttribute('class')).toContain('size-lg');
85
+ expect(btn?.getAttribute('class')).toContain('size-sm-tablet');
86
+ expect(btn?.getAttribute('class')).toContain('size-md-desktop');
87
+ expect(btn?.getAttribute('class')).toContain('size-lg-hd');
88
+ });
89
+ });
90
+
91
+ describe('Variants', () => {
92
+ BUTTON_VARIANTS.map((variant) =>
93
+ describe(`${BUTTON_VARIANTS}`, () => {
94
+ test(`it has a ${variant} class applied to it`, () => {
95
+ render(<Button variant={variant}>{`${variant} Button`}</Button>);
96
+
97
+ const btn = screen.getByText(`${variant} Button`).closest('button');
98
+
99
+ expect(btn?.getAttribute('class')).toContain(variant);
100
+ });
101
+ })
102
+ );
103
+ });
104
+
105
+ describe('Callback Handling', () => {
106
+ describe('onClick', () => {
107
+ test('it fires onClick callback', () => {
108
+ const mockedHandleClick = jest.fn();
109
+
110
+ render(<Button onClick={mockedHandleClick}>Click</Button>);
111
+
112
+ const buttonElement = screen.getByText('Click').closest('button');
113
+ if (buttonElement) {
114
+ fireEvent.click(buttonElement);
115
+ }
116
+
117
+ expect(mockedHandleClick).toBeCalledTimes(1);
118
+ });
119
+
120
+ test('it does not fire function if onClick callback not provided', () => {
121
+ const mockedHandleClick = jest.fn();
122
+
123
+ render(<Button>Click</Button>);
124
+
125
+ const buttonElement = screen.getByText('Click').closest('button');
126
+ if (buttonElement) {
127
+ fireEvent.click(buttonElement);
128
+ }
129
+
130
+ expect(mockedHandleClick).toBeCalledTimes(0);
131
+ });
132
+
133
+ test('it prevents default event behavior if specified by onClick', async () => {
134
+ const mockedHandleClick = jest.fn((event) => event.preventDefault());
135
+ const mockedNavigate = jest.fn(() => null);
136
+
137
+ render(
138
+ <Button navigate={mockedNavigate} onClick={mockedHandleClick}>
139
+ Click
140
+ </Button>
141
+ );
142
+ const buttonElement = screen.getByText('Click').closest('button');
143
+ if (buttonElement) {
144
+ fireEvent.click(buttonElement);
145
+ }
146
+
147
+ expect(mockedHandleClick).toBeCalledTimes(1);
148
+ expect(mockedNavigate).not.toBeCalled();
149
+ });
150
+ });
151
+
152
+ describe('onFocus', () => {
153
+ test('it fires onFocus callback', () => {
154
+ const mockedHandleFocus = jest.fn();
155
+
156
+ render(<Button onFocus={mockedHandleFocus}>Focus</Button>);
157
+
158
+ const buttonElement = screen.getByText('Focus').closest('button');
159
+ if (buttonElement) {
160
+ fireEvent.focus(buttonElement);
161
+ }
162
+
163
+ expect(mockedHandleFocus).toBeCalledTimes(1);
164
+ });
165
+
166
+ test('it does not fire function of onFocus callback not provided', () => {
167
+ const mockedHandleFocus = jest.fn();
168
+
169
+ render(<Button>Focus</Button>);
170
+
171
+ const buttonElement = screen.getByText('Focus').closest('button');
172
+ if (buttonElement) {
173
+ fireEvent.focus(buttonElement);
174
+ }
175
+
176
+ expect(mockedHandleFocus).toBeCalledTimes(0);
177
+ });
178
+ });
179
+
180
+ describe('onBlur', () => {
181
+ test('it fires onBlur callback', () => {
182
+ const mockedHandleBlur = jest.fn();
183
+
184
+ render(<Button onBlur={mockedHandleBlur}>Blur</Button>);
185
+
186
+ const buttonElement = screen.getByText('Blur').closest('button');
187
+ if (buttonElement) {
188
+ fireEvent.blur(buttonElement);
189
+ }
190
+
191
+ expect(mockedHandleBlur).toBeCalledTimes(1);
192
+ });
193
+
194
+ test('it does not fire onBlur callback if not provided', () => {
195
+ const mockedHandleBlur = jest.fn();
196
+
197
+ render(<Button>Blur</Button>);
198
+
199
+ const buttonElement = screen.getByText('Blur').closest('button');
200
+ if (buttonElement) {
201
+ fireEvent.blur(buttonElement);
202
+ }
203
+
204
+ expect(mockedHandleBlur).toBeCalledTimes(0);
205
+ });
206
+ });
207
+ });
208
+
209
+ describe('States', () => {
210
+ describe('Default', () => {
211
+ test('it renders the button with simple text', () => {
212
+ render(<Button>Button!</Button>);
213
+ const buttonElement = screen.getByText('Button!');
214
+
215
+ expect(buttonElement).toBeInTheDocument();
216
+ });
217
+
218
+ test('it renders the button with nested dom nodes', () => {
219
+ render(
220
+ <Button>
221
+ <div className="buttonLoadingIndicator">
222
+ <div>Im a nested dom node!</div>
223
+ </div>
224
+ </Button>
225
+ );
226
+ const buttonElement = screen.getByText('Im a nested dom node!');
227
+
228
+ expect(buttonElement).toBeInTheDocument();
229
+ });
230
+
231
+ test('it does not have a disabled attribute', () => {
232
+ render(<Button>Not Disabled Button</Button>);
233
+
234
+ expect(
235
+ screen.getByText('Not Disabled Button').closest('button')
236
+ ).not.toBeDisabled();
237
+ });
238
+
239
+ test('it renders an empty button when no children are passed', () => {
240
+ render(<Button />);
241
+ const buttonElement = screen.getByRole('button');
242
+
243
+ expect(buttonElement).toBeInTheDocument();
244
+ expect(buttonElement.innerText).toBe(undefined);
245
+ });
246
+ });
247
+
248
+ describe('Full Width', () => {
249
+ test('it has a fullWidth class applied to it', () => {
250
+ render(<Button fullWidth>Full Width Button</Button>);
251
+
252
+ const fullWidthBtn = screen
253
+ .getByText('Full Width Button')
254
+ .closest('button');
255
+
256
+ expect(fullWidthBtn?.getAttribute('class')).toContain('full-width');
257
+ });
258
+ });
259
+
260
+ describe('Custom ClassName', () => {
261
+ test('if a ClassName is provided, its added to the button', () => {
262
+ render(<Button className="custom-class">Custom ClassName</Button>);
263
+
264
+ const customClassNameBtn = screen
265
+ .getByText('Custom ClassName')
266
+ .closest('button');
267
+
268
+ expect(customClassNameBtn?.getAttribute('class')).toContain(
269
+ 'custom-class'
270
+ );
271
+ });
272
+ });
273
+
274
+ describe('Disabled', () => {
275
+ test('it has a disabled attribute', () => {
276
+ render(<Button isDisabled>Disabled Button</Button>);
277
+
278
+ expect(
279
+ screen.getByText('Disabled Button').closest('button')
280
+ ).toBeDisabled();
281
+ });
282
+ });
283
+
284
+ describe('Loading', () => {
285
+ test('it renders the spinning loading indicator', () => {
286
+ render(<Button isLoading>Button is loading</Button>);
287
+ const spinnerElement = document.getElementsByClassName('spinner')[0];
288
+ expect(spinnerElement).toBeInTheDocument();
289
+ });
290
+
291
+ test('it renders the grey spinning indicator if button variant is secondary', () => {
292
+ render(
293
+ <Button isLoading variant="secondary">
294
+ Button is loading
295
+ </Button>
296
+ );
297
+ const spinnerElement = document.getElementsByClassName('spinner')[0];
298
+ expect(spinnerElement).toBeInTheDocument();
299
+ });
300
+
301
+ test('it keeps the button text in the dom so the button width does not change', () => {
302
+ render(<Button isLoading>Button is loading</Button>);
303
+ expect(screen.getByText('Button is loading')).toBeInTheDocument();
304
+ });
305
+
306
+ test('it renders white spinning indicator when button is primary', () => {
307
+ render(
308
+ <Button variant="primary" isLoading>
309
+ Button is loading
310
+ </Button>
311
+ );
312
+ const spinnerElement = document.getElementsByClassName('spinner')[0];
313
+ expect(spinnerElement).toBeInTheDocument();
314
+ });
315
+
316
+ test('it renders white spinning indicator when button is danger', () => {
317
+ render(
318
+ <Button variant="danger" isLoading>
319
+ Button is loading
320
+ </Button>
321
+ );
322
+ const spinnerElement = document.getElementsByClassName('spinner')[0];
323
+ expect(spinnerElement).toBeInTheDocument();
324
+ });
325
+ });
326
+
327
+ describe('Disabled and Loading', () => {
328
+ test('it has a disabled attribute', () => {
329
+ render(
330
+ <Button isDisabled isLoading>
331
+ Disabled and Loading Button
332
+ </Button>
333
+ );
334
+
335
+ expect(
336
+ screen.getByText('Disabled and Loading Button').closest('button')
337
+ ).toBeDisabled();
338
+ });
339
+ });
340
+
341
+ describe('Color Variations', () => {
342
+ test('Renders button with default variant neutral', () => {
343
+ render(<Button>primary</Button>);
344
+
345
+ expect(screen.getByText('primary').closest('button')).toHaveClass(
346
+ 'primary'
347
+ );
348
+ });
349
+
350
+ const variants = [
351
+ 'primary',
352
+ 'secondary',
353
+ 'tertiary',
354
+ 'danger',
355
+ ] as ButtonVariant[];
356
+ variants.forEach((variant) => {
357
+ test(`It renders component with variant: ${variant} when passed`, () => {
358
+ render(<Button variant={variant}>{variant}</Button>);
359
+ expect(screen.getByText(variant).closest('button')).toHaveClass(
360
+ variant
361
+ );
362
+ });
363
+ });
364
+ });
365
+
366
+ describe('Anchor', () => {
367
+ test('it renders an anchor tag if as prop `a` is passed', () => {
368
+ render(
369
+ <Button href="http://hyphen.ai" as="a">
370
+ hey there
371
+ </Button>
372
+ );
373
+ const buttonElement = screen.getByRole('link');
374
+
375
+ expect(buttonElement).toBeInTheDocument();
376
+ });
377
+
378
+ test('it does not have a button type attribute if as prop `a` is passed', () => {
379
+ render(
380
+ <Button href="http://hyphen.ai" as="a">
381
+ hey there
382
+ </Button>
383
+ );
384
+ const buttonElement = screen.getByRole('link');
385
+
386
+ expect(buttonElement.getAttribute('type')).toBe(null);
387
+ });
388
+
389
+ test('it renders a target attribute if one is passed, the element is an anchor, and there is a href', () => {
390
+ render(
391
+ <Button href="http://hyphen.ai" as="a" target="_blank">
392
+ hey there
393
+ </Button>
394
+ );
395
+ const buttonElement = screen.getByRole('link');
396
+
397
+ expect(buttonElement).toBeInTheDocument();
398
+ expect(buttonElement).toHaveAttribute('target');
399
+ });
400
+
401
+ test('it does not render a target attribute if the element is not an anchor', () => {
402
+ render(
403
+ <Button href="http://hyphen.ai" target="_blank">
404
+ hey there
405
+ </Button>
406
+ );
407
+ const buttonElement = screen.getByRole('button');
408
+
409
+ expect(buttonElement).toBeInTheDocument();
410
+ expect(buttonElement).not.toHaveAttribute('target');
411
+ });
412
+
413
+ test('it does not render a target attribute if the element does not have an href', () => {
414
+ render(
415
+ <Button as="a" target="_blank">
416
+ hey there
417
+ </Button>
418
+ );
419
+ const buttonElement = screen.getByText('hey there');
420
+
421
+ expect(buttonElement).toBeInTheDocument();
422
+ expect(buttonElement).not.toHaveAttribute('target');
423
+ });
424
+ });
425
+ });
426
+
427
+ describe('React Router', () => {
428
+ it('fires navigate callback when included', () => {
429
+ const mockedNavigate = jest.fn(() => {});
430
+ render(
431
+ <Button as="a" navigate={mockedNavigate} href="/">
432
+ react router link
433
+ </Button>
434
+ );
435
+
436
+ const anchorElement = screen.getByText('react router link').closest('a');
437
+ if (anchorElement) {
438
+ fireEvent.click(anchorElement);
439
+ }
440
+
441
+ expect(mockedNavigate).toBeCalledTimes(1);
442
+ });
443
+
444
+ it('does not fire navigate callback if target is _blank', () => {
445
+ const mockedNavigate = jest.fn(() => {});
446
+ render(
447
+ <Button as="a" navigate={mockedNavigate} href="/" target="_blank">
448
+ react router link
449
+ </Button>
450
+ );
451
+
452
+ const anchorElement = screen.getByText('react router link').closest('a');
453
+ if (anchorElement) {
454
+ fireEvent.click(anchorElement);
455
+ }
456
+
457
+ expect(mockedNavigate).toBeCalledTimes(0);
458
+ });
459
+ });
460
+ });
@@ -0,0 +1,241 @@
1
+ import { IconName, ResponsiveProp } from '../../types';
2
+ import React, {
3
+ AnchorHTMLAttributes,
4
+ ButtonHTMLAttributes,
5
+ FocusEvent,
6
+ MouseEvent,
7
+ ReactNode,
8
+ createElement,
9
+ forwardRef,
10
+ } from 'react';
11
+
12
+ import { Box } from '../Box/Box';
13
+ import { Icon } from '../Icon/Icon';
14
+ import { Spinner } from '../Spinner/Spinner';
15
+ import classNames from 'classnames';
16
+ import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses';
17
+ import { getElementType } from '../../lib/getElementType';
18
+ import { handleReactRouterClick } from '../../lib/reactRouterClickHandler';
19
+ import styles from './Button.module.scss';
20
+
21
+ export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger';
22
+
23
+ export type ButtonSize = 'sm' | 'md' | 'lg';
24
+
25
+ export interface BaseButtonProps {
26
+ /**
27
+ * Contents of the button.
28
+ */
29
+ children?: ReactNode;
30
+ /**
31
+ * Additional ClassNames to add to button.
32
+ */
33
+ className?: string;
34
+ /**
35
+ * Button takes up the full width of its parent container.
36
+ */
37
+ fullWidth?: boolean;
38
+ /**
39
+ * Name of the icon to include before the button text
40
+ */
41
+ iconPrefix?: IconName;
42
+ /**
43
+ * Name of the icon to include after the button text
44
+ */
45
+ iconSuffix?: IconName;
46
+ /**
47
+ * A unique identifier for the button.
48
+ */
49
+ id?: string;
50
+ /**
51
+ * URL to navigate to when clicked. Passing this attribute automatically
52
+ * renders an anchor <a> tag, NOT a <button> element.
53
+ */
54
+ href?: string;
55
+ /**
56
+ * Disables the button, making it inoperable.
57
+ */
58
+ isDisabled?: boolean;
59
+ /**
60
+ * Replaces the button text with a loading indicator and disables the button.
61
+ */
62
+ isLoading?: boolean;
63
+ /**
64
+ * Prop reserved for when component is wrapped by `<Link>` from react-router.
65
+ */
66
+ navigate?: () => void;
67
+ /**
68
+ * Callback when Button is pressed.
69
+ */
70
+ onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
71
+ /**
72
+ * Callback when focus leaves Button.
73
+ */
74
+ onBlur?: (event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
75
+ /**
76
+ * Callback when Button receives focus.
77
+ */
78
+ onFocus?: (event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
79
+ /**
80
+ * Specify the tabIndex of the button.
81
+ */
82
+ tabIndex?: number;
83
+ /**
84
+ * Useful when using button as an anchor tag.
85
+ */
86
+ target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
87
+ /**
88
+ * The size of the button.
89
+ */
90
+ size?: ButtonSize | ResponsiveProp<ButtonSize>;
91
+ /**
92
+ * The color variant of the button
93
+ */
94
+ variant?: ButtonVariant;
95
+ /**
96
+ * ref - currently cannot be typed due to limitations of using the `as` prop
97
+ */
98
+ }
99
+
100
+ export type AnchorButtonProps = { as: 'a' } & BaseButtonProps &
101
+ Omit<
102
+ React.DetailedHTMLProps<
103
+ AnchorHTMLAttributes<HTMLAnchorElement>,
104
+ HTMLAnchorElement
105
+ >,
106
+ 'ref'
107
+ >;
108
+
109
+ export type NormalButtonProps = { as?: 'button' } & BaseButtonProps &
110
+ Omit<
111
+ React.DetailedHTMLProps<
112
+ ButtonHTMLAttributes<HTMLButtonElement>,
113
+ HTMLButtonElement
114
+ >,
115
+ 'ref'
116
+ >;
117
+
118
+ export type ButtonProps = NormalButtonProps | AnchorButtonProps;
119
+
120
+ export const Button = forwardRef<
121
+ HTMLAnchorElement | HTMLButtonElement,
122
+ ButtonProps
123
+ >(
124
+ (
125
+ {
126
+ children = undefined,
127
+ as = 'button',
128
+ className = '',
129
+ fullWidth = false,
130
+ id = undefined,
131
+ href = undefined,
132
+ iconPrefix = undefined,
133
+ iconSuffix = undefined,
134
+ isDisabled = false,
135
+ isLoading = false,
136
+ navigate = undefined,
137
+ onClick = undefined,
138
+ onFocus = undefined,
139
+ onBlur = undefined,
140
+ tabIndex = undefined,
141
+ target = undefined,
142
+ type = undefined,
143
+ size = 'md',
144
+ variant = 'primary',
145
+ ...restProps
146
+ },
147
+ ref
148
+ ) => {
149
+ const disabled = isLoading || isDisabled;
150
+
151
+ const responsiveClasses = generateResponsiveClasses('size', size).map(
152
+ (c) => styles[c]
153
+ );
154
+
155
+ const buttonClasses = classNames(
156
+ 'hyphen-components__variables__form-control',
157
+ styles.button,
158
+ className,
159
+ responsiveClasses,
160
+ {
161
+ [styles.loading]: isLoading,
162
+ [styles[variant]]: variant,
163
+ [styles['full-width']]: fullWidth,
164
+ }
165
+ );
166
+
167
+ const handleClick = handleReactRouterClick;
168
+
169
+ const handleFocus = (
170
+ event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>
171
+ ) => {
172
+ if (onFocus) onFocus(event);
173
+ };
174
+
175
+ const handleBlur = (
176
+ event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>
177
+ ) => {
178
+ if (onBlur) onBlur(event);
179
+ };
180
+
181
+ const buttonContent =
182
+ iconPrefix || iconSuffix ? (
183
+ <Box display="inline-flex" direction="row" alignItems="center" gap="xs">
184
+ {isLoading && <Spinner className={styles['spinner-wrapper']} />}
185
+ {iconPrefix && (
186
+ <Icon
187
+ className={styles.label}
188
+ name={iconPrefix}
189
+ aria-hidden="true"
190
+ focusable="false"
191
+ data-testid="prefixIcon"
192
+ size={size === 'md' ? 'sm' : size}
193
+ />
194
+ )}
195
+ {children && <span className={styles.label}>{children}</span>}
196
+ {iconSuffix && (
197
+ <Icon
198
+ className={styles.label}
199
+ name={iconSuffix}
200
+ aria-hidden="true"
201
+ focusable="false"
202
+ data-testid="suffixIcon"
203
+ size={size === 'md' ? 'sm' : size}
204
+ />
205
+ )}
206
+ </Box>
207
+ ) : (
208
+ <>
209
+ {isLoading && <Spinner className={styles['spinner-wrapper']} />}
210
+ {(() => {
211
+ if (children) {
212
+ return <span className={styles.label}>{children}</span>;
213
+ }
214
+ return null;
215
+ })()}
216
+ </>
217
+ );
218
+
219
+ const buttonElement = getElementType(Button, { as });
220
+
221
+ return createElement(
222
+ buttonElement,
223
+ {
224
+ id,
225
+ href,
226
+ className: buttonClasses,
227
+ disabled,
228
+ target: as === 'a' && href ? target : null,
229
+ onBlur: handleBlur,
230
+ onClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) =>
231
+ handleClick(event, onClick, target, navigate),
232
+ onFocus: handleFocus,
233
+ ref,
234
+ type: type || (as !== 'a' && !href ? 'button' : undefined),
235
+ tabIndex,
236
+ ...restProps,
237
+ },
238
+ buttonContent
239
+ );
240
+ }
241
+ );