@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/package.json +138 -0
- package/src/components/Alert/Alert.constants.ts +19 -0
- package/src/components/Alert/Alert.mdx +29 -0
- package/src/components/Alert/Alert.module.scss +74 -0
- package/src/components/Alert/Alert.stories.tsx +102 -0
- package/src/components/Alert/Alert.test.tsx +187 -0
- package/src/components/Alert/Alert.tsx +157 -0
- package/src/components/Alert/Alert.types.ts +14 -0
- package/src/components/Badge/Badge.mdx +28 -0
- package/src/components/Badge/Badge.module.scss +155 -0
- package/src/components/Badge/Badge.stories.tsx +52 -0
- package/src/components/Badge/Badge.test.tsx +74 -0
- package/src/components/Badge/Badge.tsx +70 -0
- package/src/components/Box/Box.mdx +259 -0
- package/src/components/Box/Box.module.scss +16 -0
- package/src/components/Box/Box.stories.tsx +1679 -0
- package/src/components/Box/Box.test.tsx +478 -0
- package/src/components/Box/Box.tsx +636 -0
- package/src/components/Button/Button.constants.ts +10 -0
- package/src/components/Button/Button.mdx +71 -0
- package/src/components/Button/Button.module.scss +312 -0
- package/src/components/Button/Button.stories.tsx +117 -0
- package/src/components/Button/Button.test.tsx +460 -0
- package/src/components/Button/Button.tsx +241 -0
- package/src/components/Card/Card.mdx +46 -0
- package/src/components/Card/Card.module.scss +6 -0
- package/src/components/Card/Card.stories.tsx +101 -0
- package/src/components/Card/Card.test.tsx +11 -0
- package/src/components/Card/Card.tsx +61 -0
- package/src/components/Card/components/CardFooter/CardFooter.test.tsx +11 -0
- package/src/components/Card/components/CardFooter/CardFooter.tsx +35 -0
- package/src/components/Card/components/CardHeader/CardHeader.test.tsx +23 -0
- package/src/components/Card/components/CardHeader/CardHeader.tsx +54 -0
- package/src/components/Card/components/CardSection/CardSection.test.tsx +28 -0
- package/src/components/Card/components/CardSection/CardSection.tsx +102 -0
- package/src/components/Card/components/index.ts +3 -0
- package/src/components/CheckboxInput/CheckboxInput.mdx +98 -0
- package/src/components/CheckboxInput/CheckboxInput.stories.tsx +254 -0
- package/src/components/CheckboxInput/CheckboxInput.test.tsx +436 -0
- package/src/components/CheckboxInput/CheckboxInput.tsx +171 -0
- package/src/components/CheckboxInput/components/Checkbox.module.scss +174 -0
- package/src/components/CheckboxInput/components/Checkbox.test.tsx +201 -0
- package/src/components/CheckboxInput/components/Checkbox.tsx +176 -0
- package/src/components/CheckboxInput/components/CheckboxIcon.tsx +71 -0
- package/src/components/DateInput/DateInput.mdx +61 -0
- package/src/components/DateInput/DateInput.stories.tsx +168 -0
- package/src/components/DateInput/DateInput.test.tsx +258 -0
- package/src/components/DateInput/DateInput.tsx +189 -0
- package/src/components/DatePicker/DatePicker.mdx +52 -0
- package/src/components/DatePicker/DatePicker.module.scss +603 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +199 -0
- package/src/components/DatePicker/DatePicker.test.tsx +26 -0
- package/src/components/DatePicker/DatePicker.tsx +138 -0
- package/src/components/Details/Details.mdx +30 -0
- package/src/components/Details/Details.module.scss +32 -0
- package/src/components/Details/Details.stories.tsx +38 -0
- package/src/components/Details/Details.test.tsx +189 -0
- package/src/components/Details/Details.tsx +51 -0
- package/src/components/Details/DetailsSummary.tsx +65 -0
- package/src/components/Drawer/Drawer.mdx +117 -0
- package/src/components/Drawer/Drawer.module.scss +96 -0
- package/src/components/Drawer/Drawer.stories.tsx +380 -0
- package/src/components/Drawer/Drawer.test.tsx +90 -0
- package/src/components/Drawer/Drawer.tsx +249 -0
- package/src/components/FormControl/FormControl.tsx +78 -0
- package/src/components/FormLabel/FormLabel.mdx +24 -0
- package/src/components/FormLabel/FormLabel.module.scss +19 -0
- package/src/components/FormLabel/FormLabel.stories.tsx +20 -0
- package/src/components/FormLabel/FormLabel.test.tsx +35 -0
- package/src/components/FormLabel/FormLabel.tsx +96 -0
- package/src/components/Formik/Formik.mdx +10 -0
- package/src/components/Formik/Formik.stories.tsx +307 -0
- package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.test.tsx +172 -0
- package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.tsx +41 -0
- package/src/components/Formik/FormikRadioGroup/FormikRadioGroup.test.tsx +205 -0
- package/src/components/Formik/FormikRadioGroup/FormikRadioGroup.tsx +37 -0
- package/src/components/Formik/FormikSelectInput/FormikSelectInput.test.tsx +210 -0
- package/src/components/Formik/FormikSelectInput/FormikSelectInput.tsx +41 -0
- package/src/components/Formik/FormikSelectInputInset/FormikSelectInputInset.test.tsx +153 -0
- package/src/components/Formik/FormikSelectInputInset/FormikSelectInputInset.tsx +44 -0
- package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.test.tsx +161 -0
- package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.tsx +46 -0
- package/src/components/Formik/FormikTextInput/FormikTextInput.test.tsx +176 -0
- package/src/components/Formik/FormikTextInput/FormikTextInput.tsx +38 -0
- package/src/components/Formik/FormikTextInputInset/FormikTextInputInset.test.tsx +170 -0
- package/src/components/Formik/FormikTextInputInset/FormikTextInputInset.tsx +42 -0
- package/src/components/Formik/FormikTextareaInput/FormikTextareaInput.test.tsx +186 -0
- package/src/components/Formik/FormikTextareaInput/FormikTextareaInput.tsx +42 -0
- package/src/components/Formik/FormikTextareaInputInset/FormikTextareaInputInset.test.tsx +179 -0
- package/src/components/Formik/FormikTextareaInputInset/FormikTextareaInputInset.tsx +42 -0
- package/src/components/Formik/FormikTimePicker/FormikTimePicker.test.tsx +224 -0
- package/src/components/Formik/FormikTimePicker/FormikTimePicker.tsx +37 -0
- package/src/components/Formik/FormikTimePickerNative/FormikTimePickerNative.test.tsx +175 -0
- package/src/components/Formik/FormikTimePickerNative/FormikTimePickerNative.tsx +38 -0
- package/src/components/Formik/FormikToggle/FormikToggle.test.tsx +172 -0
- package/src/components/Formik/FormikToggle/FormikToggle.tsx +38 -0
- package/src/components/Heading/Heading.constants.ts +19 -0
- package/src/components/Heading/Heading.mdx +35 -0
- package/src/components/Heading/Heading.module.scss +5 -0
- package/src/components/Heading/Heading.stories.tsx +90 -0
- package/src/components/Heading/Heading.test.tsx +67 -0
- package/src/components/Heading/Heading.tsx +67 -0
- package/src/components/HelpText/HelpText.module.scss +14 -0
- package/src/components/HelpText/HelpText.tsx +33 -0
- package/src/components/Icon/Icon.mdx +40 -0
- package/src/components/Icon/Icon.stories.tsx +72 -0
- package/src/components/Icon/Icon.test.tsx +30 -0
- package/src/components/Icon/Icon.tsx +61 -0
- package/src/components/InputValidationMessage/InputValidationMessage.module.scss +3 -0
- package/src/components/InputValidationMessage/InputValidationMessage.tsx +27 -0
- package/src/components/Modal/Modal.mdx +60 -0
- package/src/components/Modal/Modal.module.scss +135 -0
- package/src/components/Modal/Modal.stories.tsx +194 -0
- package/src/components/Modal/Modal.test.tsx +81 -0
- package/src/components/Modal/Modal.tsx +174 -0
- package/src/components/Modal/components/ModalBody/ModalBody.test.tsx +20 -0
- package/src/components/Modal/components/ModalBody/ModalBody.tsx +24 -0
- package/src/components/Modal/components/ModalFooter/ModalFooter.test.tsx +32 -0
- package/src/components/Modal/components/ModalFooter/ModalFooter.tsx +37 -0
- package/src/components/Modal/components/ModalHeader/ModalHeader.test.tsx +29 -0
- package/src/components/Modal/components/ModalHeader/ModalHeader.tsx +58 -0
- package/src/components/Modal/components/index.ts +5 -0
- package/src/components/Pagination/Pagination.mdx +26 -0
- package/src/components/Pagination/Pagination.stories.tsx +55 -0
- package/src/components/Pagination/Pagination.test.tsx +225 -0
- package/src/components/Pagination/Pagination.tsx +162 -0
- package/src/components/Pagination/Pagination.utilities.test.ts +133 -0
- package/src/components/Pagination/Pagination.utilities.ts +101 -0
- package/src/components/Popover/Popover.mdx +104 -0
- package/src/components/Popover/Popover.module.scss +74 -0
- package/src/components/Popover/Popover.stories.tsx +471 -0
- package/src/components/Popover/Popover.test.tsx +128 -0
- package/src/components/Popover/Popover.tsx +277 -0
- package/src/components/RadioGroup/RadioGroup.mdx +81 -0
- package/src/components/RadioGroup/RadioGroup.module.scss +23 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +375 -0
- package/src/components/RadioGroup/RadioGroup.test.tsx +282 -0
- package/src/components/RadioGroup/RadioGroup.tsx +145 -0
- package/src/components/RadioGroup/RadioInput/RadioInput.module.scss +114 -0
- package/src/components/RadioGroup/RadioInput/RadioInput.test.tsx +156 -0
- package/src/components/RadioGroup/RadioInput/RadioInput.tsx +148 -0
- package/src/components/RadioGroup/RadioInput/RadioInputIcon.tsx +59 -0
- package/src/components/ResponsiveProvider/ResponsiveProvider.mdx +36 -0
- package/src/components/ResponsiveProvider/ResponsiveProvider.stories.tsx +54 -0
- package/src/components/ResponsiveProvider/ResponsiveProvider.test.tsx +70 -0
- package/src/components/ResponsiveProvider/ResponsiveProvider.tsx +73 -0
- package/src/components/SelectInput/SelectInput.mdx +115 -0
- package/src/components/SelectInput/SelectInput.module.scss +357 -0
- package/src/components/SelectInput/SelectInput.stories.tsx +373 -0
- package/src/components/SelectInput/SelectInput.test.tsx +403 -0
- package/src/components/SelectInput/SelectInput.tsx +245 -0
- package/src/components/SelectInputInset/SelectInputInset.mdx +56 -0
- package/src/components/SelectInputInset/SelectInputInset.module.scss +397 -0
- package/src/components/SelectInputInset/SelectInputInset.stories.tsx +189 -0
- package/src/components/SelectInputInset/SelectInputInset.test.tsx +305 -0
- package/src/components/SelectInputInset/SelectInputInset.tsx +235 -0
- package/src/components/SelectInputNative/SelectInputNative.mdx +87 -0
- package/src/components/SelectInputNative/SelectInputNative.module.scss +356 -0
- package/src/components/SelectInputNative/SelectInputNative.stories.tsx +282 -0
- package/src/components/SelectInputNative/SelectInputNative.test.tsx +341 -0
- package/src/components/SelectInputNative/SelectInputNative.tsx +121 -0
- package/src/components/Spinner/Spinner.mdx +29 -0
- package/src/components/Spinner/Spinner.module.scss +16 -0
- package/src/components/Spinner/Spinner.stories.tsx +48 -0
- package/src/components/Spinner/Spinner.test.tsx +47 -0
- package/src/components/Spinner/Spinner.tsx +116 -0
- package/src/components/Table/Table.mdx +216 -0
- package/src/components/Table/Table.module.scss +61 -0
- package/src/components/Table/Table.stories.tsx +884 -0
- package/src/components/Table/Table.test.tsx +437 -0
- package/src/components/Table/Table.tsx +171 -0
- package/src/components/Table/TableBody/TableBody.module.scss +19 -0
- package/src/components/Table/TableBody/TableBody.test.tsx +38 -0
- package/src/components/Table/TableBody/TableBody.tsx +96 -0
- package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.module.scss +47 -0
- package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.test.tsx +81 -0
- package/src/components/Table/TableBody/TableBodyCell/TableBodyCell.tsx +94 -0
- package/src/components/Table/TableHead/TableHead.test.tsx +20 -0
- package/src/components/Table/TableHead/TableHead.tsx +78 -0
- package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.module.scss +72 -0
- package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.test.tsx +187 -0
- package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.tsx +192 -0
- package/src/components/Table/common/TableRow/TableRow.module.scss +5 -0
- package/src/components/Table/common/TableRow/TableRow.test.tsx +52 -0
- package/src/components/Table/common/TableRow/TableRow.tsx +155 -0
- package/src/components/TextInput/TextInput.mdx +96 -0
- package/src/components/TextInput/TextInput.module.scss +405 -0
- package/src/components/TextInput/TextInput.stories.tsx +268 -0
- package/src/components/TextInput/TextInput.test.tsx +231 -0
- package/src/components/TextInput/TextInput.tsx +263 -0
- package/src/components/TextInputInset/TextInputInset.mdx +62 -0
- package/src/components/TextInputInset/TextInputInset.module.scss +418 -0
- package/src/components/TextInputInset/TextInputInset.stories.tsx +213 -0
- package/src/components/TextInputInset/TextInputInset.test.tsx +222 -0
- package/src/components/TextInputInset/TextInputInset.tsx +261 -0
- package/src/components/TextareaInput/TextareaInput.mdx +117 -0
- package/src/components/TextareaInput/TextareaInput.module.scss +275 -0
- package/src/components/TextareaInput/TextareaInput.stories.tsx +293 -0
- package/src/components/TextareaInput/TextareaInput.test.tsx +195 -0
- package/src/components/TextareaInput/TextareaInput.tsx +182 -0
- package/src/components/TextareaInputInset/TextareaInputInset.mdx +55 -0
- package/src/components/TextareaInputInset/TextareaInputInset.module.scss +337 -0
- package/src/components/TextareaInputInset/TextareaInputInset.stories.tsx +160 -0
- package/src/components/TextareaInputInset/TextareaInputInset.test.tsx +199 -0
- package/src/components/TextareaInputInset/TextareaInputInset.tsx +213 -0
- package/src/components/ThemeProvider/ThemeProvider.mdx +11 -0
- package/src/components/ThemeProvider/ThemeProvider.stories.tsx +56 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +75 -0
- package/src/components/TimePicker/TimePicker.mdx +75 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +149 -0
- package/src/components/TimePicker/TimePicker.test.tsx +97 -0
- package/src/components/TimePicker/TimePicker.tsx +83 -0
- package/src/components/TimePickerNative/TimePickerNative.mdx +67 -0
- package/src/components/TimePickerNative/TimePickerNative.stories.tsx +151 -0
- package/src/components/TimePickerNative/TimePickerNative.test.tsx +117 -0
- package/src/components/TimePickerNative/TimePickerNative.tsx +93 -0
- package/src/components/Toast/Toast.mdx +134 -0
- package/src/components/Toast/Toast.store.ts +280 -0
- package/src/components/Toast/Toast.stories.tsx +283 -0
- package/src/components/Toast/Toast.test.tsx +784 -0
- package/src/components/Toast/Toast.types.ts +98 -0
- package/src/components/Toast/ToastContainer.tsx +170 -0
- package/src/components/Toast/ToastNotification.module.scss +63 -0
- package/src/components/Toast/ToastNotification.tsx +176 -0
- package/src/components/Toast/index.ts +4 -0
- package/src/components/Toast/toast.ts +102 -0
- package/src/components/Toast/useToasts.ts +102 -0
- package/src/components/Toggle/Toggle.mdx +51 -0
- package/src/components/Toggle/Toggle.module.scss +294 -0
- package/src/components/Toggle/Toggle.stories.tsx +128 -0
- package/src/components/Toggle/Toggle.test.tsx +308 -0
- package/src/components/Toggle/Toggle.tsx +184 -0
- package/src/constants/keyCodes.ts +2 -0
- package/src/docs/Brands.mdx +153 -0
- package/src/docs/Colors.mdx +158 -0
- package/src/docs/DesignTokens.mdx +415 -0
- package/src/docs/GetStarted.mdx +47 -0
- package/src/docs/intro.mdx +12 -0
- package/src/fonts/AvenirBold.otf +0 -0
- package/src/fonts/AvenirBold.woff +0 -0
- package/src/fonts/AvenirBold.woff2 +0 -0
- package/src/fonts/AvenirLight.otf +0 -0
- package/src/fonts/AvenirLight.woff +0 -0
- package/src/fonts/AvenirLight.woff2 +0 -0
- package/src/fonts/AvenirRegular.otf +0 -0
- package/src/fonts/AvenirRegular.woff +0 -0
- package/src/fonts/AvenirRegular.woff2 +0 -0
- package/src/fonts/Geist-Bold.otf +0 -0
- package/src/fonts/Geist-Bold.woff +0 -0
- package/src/fonts/Geist-Bold.woff2 +0 -0
- package/src/fonts/Geist-Medium.otf +0 -0
- package/src/fonts/Geist-Medium.woff +0 -0
- package/src/fonts/Geist-Medium.woff2 +0 -0
- package/src/fonts/Geist-Regular.otf +0 -0
- package/src/fonts/Geist-Regular.woff +0 -0
- package/src/fonts/Geist-Regular.woff2 +0 -0
- package/src/fonts/Geist-SemiBold.otf +0 -0
- package/src/fonts/Geist-SemiBold.woff +0 -0
- package/src/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/fonts/GeistMono-Regular.otf +0 -0
- package/src/fonts/GeistMono-Regular.woff +0 -0
- package/src/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useBreakpoint/useBreakpoint.mdx +26 -0
- package/src/hooks/useBreakpoint/useBreakpoint.stories.tsx +30 -0
- package/src/hooks/useBreakpoint/useBreakpoint.test.tsx +19 -0
- package/src/hooks/useBreakpoint/useBreakpoint.ts +50 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayouEffect.ts +5 -0
- package/src/hooks/useOpenClose/useOpenClose.mdx +15 -0
- package/src/hooks/useOpenClose/useOpenClose.stories.tsx +41 -0
- package/src/hooks/useOpenClose/useOpenClose.test.tsx +119 -0
- package/src/hooks/useOpenClose/useOpenClose.tsx +95 -0
- package/src/hooks/useWindowSize/useWindowSize.mdx +25 -0
- package/src/hooks/useWindowSize/useWindowSize.stories.tsx +35 -0
- package/src/hooks/useWindowSize/useWindowSize.ts +24 -0
- package/src/index.ts +48 -0
- package/src/lib/cssShorthandToClasses.test.ts +149 -0
- package/src/lib/cssShorthandToClasses.ts +133 -0
- package/src/lib/doesStringIncludeCssUnit.ts +6 -0
- package/src/lib/generateResponsiveClasses.test.ts +24 -0
- package/src/lib/generateResponsiveClasses.ts +30 -0
- package/src/lib/getAutoCompleteValue.test.ts +27 -0
- package/src/lib/getAutoCompleteValue.ts +12 -0
- package/src/lib/getColumnKeys.ts +27 -0
- package/src/lib/getDimensionCss.test.ts +148 -0
- package/src/lib/getDimensionCss.ts +73 -0
- package/src/lib/getElementType.test.tsx +42 -0
- package/src/lib/getElementType.ts +42 -0
- package/src/lib/getFlexCss.test.ts +122 -0
- package/src/lib/getFlexCss.ts +67 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/isFunction.ts +6 -0
- package/src/lib/mergeRefs.ts +15 -0
- package/src/lib/prefersReducedMotion.ts +12 -0
- package/src/lib/react-children-utilities/filter.ts +12 -0
- package/src/lib/react-children-utilities/index.ts +1 -0
- package/src/lib/reactRouterClickHandler.ts +37 -0
- package/src/lib/resolveValue.ts +7 -0
- package/src/lib/tokens.ts +139 -0
- package/src/modes.ts +8 -0
- package/src/styles/animation.scss +152 -0
- package/src/styles/cursor.scss +43 -0
- package/src/styles/display.scss +119 -0
- package/src/styles/flex.scss +453 -0
- package/src/styles/fonts.scss +44 -0
- package/src/styles/globals/utilities.scss +4 -0
- package/src/styles/mixins.scss +14 -0
- package/src/styles/overflow.scss +41 -0
- package/src/styles/position.scss +45 -0
- package/src/styles/reset.scss +108 -0
- package/src/styles/text-align.scss +21 -0
- package/src/styles/utilities.scss +9 -0
- package/src/styles/variables/forms.scss +71 -0
- package/src/styles/variables/index.scss +3 -0
- package/src/styles/white-space.scss +21 -0
- package/src/types/index.ts +201 -0
- 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
|
+
);
|