diff --git a/.babelrc b/.babelrc index 59fac07ef..fbf43a8ea 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,10 @@ "presets": [ ["@babel/preset-env", { "modules": "cjs" }], "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-transform-regenerator", + "@babel/plugin-transform-runtime" ] }, "build": { diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7d646af50..629c76267 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -43,6 +43,15 @@ jobs: - name: unit test run: | yarn test + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage/clover.xml + flags: unittests + name: codecov-umbrella + yml: ./codecov.yml + fail_ci_if_error: true visual-tests: name: visual regression tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index bada37a8a..7e48dcc95 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /coverage # production +/coverage /dist /build /storybook-static diff --git a/.storybook/main.js b/.storybook/main.js index 647e95537..ca07930e9 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -21,15 +21,13 @@ const scssRules = { const fileLoaderRules = { test: /\.(png|jpe?g|gif)$/i, - use: [ - { - loader: 'file-loader', - }, - ], + use: [{ + loader: 'file-loader', + }, ], }; module.exports = { - stories: ['../src/**/*.stories.[tj]sx'], + stories: ['../src/**/*.stories.([tj]sx|mdx)'], addons: [ '@storybook/addon-actions/register', '@storybook/addon-a11y/register', @@ -40,7 +38,9 @@ module.exports = { webpackFinal: (config) => { config.module.rules.push(scssRules); config.module.rules.push(fileLoaderRules); - const assetRule = config.module.rules.find(({ test }) => test.test(".svg")); + const assetRule = config.module.rules.find(({ + test + }) => test.test(".svg")); const assetLoader = { loader: assetRule.loader, diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e9d858d19 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,20 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "80...100" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: no \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 9a8db2f64..d54afb866 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,4 +11,6 @@ module.exports = { 'identity-obj-proxy', '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, + coverageDirectory: './coverage/', + collectCoverage: true, }; diff --git a/package.json b/package.json index be1b1f580..fb6f86ec0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@palmetto/palmetto-components", "author": "@palmetto", - "version": "0.1.6", + "version": "0.1.7", "private": false, "main": "dist/index.js", "module": "dist/index.esm.js", @@ -45,6 +45,8 @@ }, "devDependencies": { "@babel/core": "^7.10.3", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.10.4", "@babel/polyfill": "^7.10.4", "@babel/preset-react": "^7.10.4", "@rollup/plugin-babel": "^5.0.4", @@ -68,6 +70,7 @@ "chromatic": "^4.0.3", "classnames": "^2.2.6", "cleave.js": "^1.6.0", + "codecov": "^3.7.0", "css-loader": "^3.6.0", "eslint": "^7.3.1", "eslint-config-airbnb": "^18.2.0", @@ -81,12 +84,14 @@ "identity-obj-proxy": "^3.0.0", "jest": "^24.9.0", "mini-css-extract-plugin": "^0.9.0", + "mutationobserver-shim": "^0.3.7", "node-sass": "^4.14.1", "postcss-loader": "^3.0.0", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-is": "^16.13.1", + "react-select-event": "^4.1.4", "rollup": "^1.27.4", "rollup-plugin-cleaner": "^1.0.0", "rollup-plugin-copy": "^3.3.0", diff --git a/src/components/Button/Button.jsx b/src/components/Button/Button.jsx index fbb1c3f40..acb9a1fe2 100644 --- a/src/components/Button/Button.jsx +++ b/src/components/Button/Button.jsx @@ -94,10 +94,7 @@ Button.propTypes = { /** * Contents of the button */ - children: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node, - ]).isRequired, + children: PropTypes.node.isRequired, /** * Disables the button, making it inoperable */ @@ -107,7 +104,7 @@ Button.propTypes = { */ isLoading: PropTypes.bool, /** - * Callback when button receives focus + * Allow the button to grow to its container's full width */ fullWidth: PropTypes.bool, /** diff --git a/src/components/Button/Button.stories.jsx b/src/components/Button/Button.stories.jsx deleted file mode 100644 index b222f0f9c..000000000 --- a/src/components/Button/Button.stories.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { withA11y } from '@storybook/addon-a11y'; -import Button from './Button'; - -export default { - title: 'Button', - component: Button, - decorators: [withA11y], -}; - -export const All = () => { - const handleClick = event => { - action('clicked')(event); - }; - - const handleBlur = event => { - action('blurred')(event); - }; - - const handleFocus = event => { - action('focused')(event); - }; - - return ( - <> -
- -
-
- -
-
- -
-
- -
- - ); -}; diff --git a/src/components/Button/Button.stories.mdx b/src/components/Button/Button.stories.mdx new file mode 100644 index 000000000..01758494c --- /dev/null +++ b/src/components/Button/Button.stories.mdx @@ -0,0 +1,113 @@ +import Button from './Button'; +import { action } from '@storybook/addon-actions'; +import { withA11y } from '@storybook/addon-a11y'; +import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; + + + + +# Button + +Buttons are used as triggers for actions. +They are used in forms, toolbars, modal dialogs and as stand-alone action triggers. +Actions almost always occur on the same page. + + + + + +## Props + + +## Appearance + + + + + + + +## Full Width + +Use the `fullWidth` prop to allow the button to grow to its container's full width. + + + + + + + +## Loading + +Setting the `isLoading` prop to true will replace the button text with loading indicator and disable the button to prevent unintended submissions. +The width of the button will remain consistent to prevent content from shifting. + + + + <> +
+ +
+
+ +
+
+ +
+ +
+
+ +## Disabled + +Use for actions that aren’t currently available, such as immediately after a form submission. +The interface should make it clear why the button is disabled and what needs to be done to enable it. + + + + <> +
+ +
+
+ +
+ +
+
+ +## Event Callbacks + +Callback functions can be passed to `onClick`, `onBlur`, and `onFocus` events. + + + + + + diff --git a/src/components/CheckboxInput/CheckboxInput.jsx b/src/components/CheckboxInput/CheckboxInput.jsx index 45510067d..06ea43780 100644 --- a/src/components/CheckboxInput/CheckboxInput.jsx +++ b/src/components/CheckboxInput/CheckboxInput.jsx @@ -16,17 +16,22 @@ const CheckboxInput = ({ isChecked, isDisabled, isRequired, + onBlur, onChange, + onFocus, label, }) => { + const handleBlur = e => { + if (onBlur) onBlur(e); + }; + const handleChange = e => { - onChange(e.target.checked); + onChange(e); }; - // const labelClasses = classNames( - // 'checkboxInputInstructions', - // { error }, - // ); + const handleFocus = e => { + if (onFocus) onFocus(e); + }; const labelProps = { isFieldRequired: isRequired, @@ -40,10 +45,14 @@ const CheckboxInput = ({
@@ -60,6 +69,8 @@ CheckboxInput.defaultProps = { isChecked: false, isDisabled: false, isRequired: false, + onBlur: undefined, + onFocus: undefined, }; CheckboxInput.propTypes = { @@ -92,10 +103,18 @@ CheckboxInput.propTypes = { * Determines if input is required or not. (Label will have an asterisk if required) */ isRequired: PropTypes.bool, + /** + * Callback function when input is blurred. + */ + onBlur: PropTypes.func, /** * Callback function when input is changed */ onChange: PropTypes.func.isRequired, + /** + * Callback function when input is focused + */ + onFocus: PropTypes.func, /** * Custom content to be displayed to right of checkbox. Can be any valid node/tree, anchors, etc. */ diff --git a/src/components/CheckboxInput/CheckboxInput.stories.jsx b/src/components/CheckboxInput/CheckboxInput.stories.jsx index 3cf4befbb..3fb225c64 100644 --- a/src/components/CheckboxInput/CheckboxInput.stories.jsx +++ b/src/components/CheckboxInput/CheckboxInput.stories.jsx @@ -5,7 +5,7 @@ import { withA11y } from '@storybook/addon-a11y'; import CheckboxInput from './CheckboxInput'; export default { - title: 'Forms/Inputs/CheckboxInput', + title: 'Components/Form Inputs/CheckboxInput', component: CheckboxInput, decorators: [withA11y], }; @@ -22,9 +22,9 @@ export const All = () => { withErrorRequired: false, }); - const handleChange = (value, key) => { - action('change')(value); - store.set({ [key]: value }); + const handleChange = (event, key) => { + action('change')(event); + store.set({ [key]: event.target.checked }); }; return ( diff --git a/src/components/CheckboxInput/CheckboxInput.test.jsx b/src/components/CheckboxInput/CheckboxInput.test.jsx index b14e1ae1e..9959003fb 100644 --- a/src/components/CheckboxInput/CheckboxInput.test.jsx +++ b/src/components/CheckboxInput/CheckboxInput.test.jsx @@ -12,7 +12,7 @@ describe('CheckboxInput', () => { null} />, ); @@ -28,8 +28,8 @@ describe('CheckboxInput', () => { null} + isChecked={false} />, ); expect(getByLabelText('test checkbox')).toBeDefined(); @@ -40,7 +40,6 @@ describe('CheckboxInput', () => { null} isChecked />, @@ -54,15 +53,27 @@ describe('CheckboxInput', () => { null} isChecked={false} + onChange={() => null} />, ); const checkbox = getByLabelText('test checkbox'); expect(checkbox.checked).toEqual(false); }); + test('assigns the "aria-labelledby" attribute and renders a label with correct id, when a label is provided', () => { + const { getByLabelText } = render( + null} + />, + ); + expect(getByLabelText('test label')).toHaveAttribute('aria-labelledby', 'testInputLabel'); + expect(document.getElementById('testInputLabel')).toBeInTheDocument(); + }); + describe('onChange', () => { test('onChange event fires callback function', () => { const mockedHandleChange = jest.fn(() => null); @@ -71,7 +82,7 @@ describe('CheckboxInput', () => { , ); @@ -80,37 +91,22 @@ describe('CheckboxInput', () => { expect(mockedHandleChange).toHaveBeenCalledTimes(1); }); - test('calls onChange with true when isCheck is false', () => { - const mockedHandleChange = jest.fn(() => null); - - const { getByLabelText } = render( - , - ); - const checkbox = getByLabelText('test checkbox'); - fireEvent.click(checkbox); - expect(mockedHandleChange).toHaveBeenCalledWith(true); - }); - - test('calls onChange with false when isChecked when true', () => { - const mockedHandleChange = jest.fn(() => null); + test('calls onChange and passes checked value in event', () => { + let value = true; + const mockedHandleChange = jest.fn(event => { value = event.target.checked; }); const { getByLabelText } = render( , ); const checkbox = getByLabelText('test checkbox'); fireEvent.click(checkbox); - expect(mockedHandleChange).toHaveBeenCalledWith(false); + expect(mockedHandleChange).toBeCalledTimes(1); + expect(value).toBe(false); }); }); }); diff --git a/src/components/FormLabel/FormLabel.jsx b/src/components/FormLabel/FormLabel.jsx index 09e84f1e3..48ab3a97f 100644 --- a/src/components/FormLabel/FormLabel.jsx +++ b/src/components/FormLabel/FormLabel.jsx @@ -17,7 +17,11 @@ const FormLabel = ({ ); return ( -