diff --git a/package-lock.json b/package-lock.json index 1d4f3728..bac03f3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1278,6 +1278,7 @@ "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "dev": true, "requires": { "@emotion/sheet": "0.9.4", "@emotion/stylis": "0.8.5", @@ -1289,6 +1290,7 @@ "version": "10.0.28", "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz", "integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==", + "dev": true, "requires": { "@babel/runtime": "^7.5.5", "@emotion/cache": "^10.0.27", @@ -1302,6 +1304,7 @@ "version": "10.0.27", "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "dev": true, "requires": { "@emotion/serialize": "^0.11.15", "@emotion/utils": "0.11.3", @@ -1311,7 +1314,8 @@ "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true }, "@emotion/is-prop-valid": { "version": "0.8.8", @@ -1325,12 +1329,14 @@ "@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "dev": true }, "@emotion/serialize": { "version": "0.11.16", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, "requires": { "@emotion/hash": "0.8.0", "@emotion/memoize": "0.7.4", @@ -1342,7 +1348,8 @@ "@emotion/sheet": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", - "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "dev": true }, "@emotion/styled": { "version": "10.0.27", @@ -1369,22 +1376,26 @@ "@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "dev": true }, "@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "dev": true }, "@emotion/utils": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true }, "@emotion/weak-memoize": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "dev": true }, "@hapi/address": { "version": "2.1.4", @@ -7168,6 +7179,7 @@ "version": "10.0.33", "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@emotion/hash": "0.8.0", @@ -7382,7 +7394,8 @@ "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", + "dev": true }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", @@ -11185,7 +11198,8 @@ "csstype": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.11.tgz", - "integrity": "sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==" + "integrity": "sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==", + "dev": true }, "cyclist": { "version": "1.0.1", @@ -13249,7 +13263,8 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true }, "find-up": { "version": "3.0.0", @@ -20724,14 +20739,6 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "dev": true }, - "react-loading-skeleton": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-2.1.1.tgz", - "integrity": "sha512-+fGvgG9ieUw4D5QVgpqJkJ75jhzUdz96GRsA0HjTlR0Mpj9DJUEFc0AKELs7ZkqWVH8/DiroaaufSrOPld1kGA==", - "requires": { - "@emotion/core": "^10.0.22" - } - }, "react-popper": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", diff --git a/package.json b/package.json index 323b1b28..98011794 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-jss": "^10.4.0", - "react-loading-skeleton": "^2.1.1", "react-scripts": "^3.4.3", "typescript": "^3.9.7" }, @@ -42,6 +41,11 @@ "eslintConfig": { "extends": "react-app" }, + "jest": { + "coveragePathIgnorePatterns": [ + ".stories.tsx" + ] + }, "browserslist": { "production": [ ">0.2%", diff --git a/src/__snapshots__/storybook.test.ts.snap b/src/__snapshots__/storybook.test.ts.snap index 807c3bc5..7be44637 100644 --- a/src/__snapshots__/storybook.test.ts.snap +++ b/src/__snapshots__/storybook.test.ts.snap @@ -126,7 +126,7 @@ exports[`Storyshots InputField Default 1`] = ` exports[`Storyshots InputField Error 1`] = `
- - - ‌ - + +  
- - - ‌ - + +  
@@ -243,6 +229,52 @@ exports[`Storyshots Link Href 1`] = ` `; +exports[`Storyshots Skeleton Circle 1`] = ` + +   + +`; + +exports[`Storyshots Skeleton Count 1`] = ` +Array [ + +   + , + +   + , + +   + , + +   + , + +   + , +] +`; + +exports[`Storyshots Skeleton Default 1`] = ` + +   + +`; + exports[`Storyshots Tag Colored 1`] = ` { expect(wrapper.simulate('click')) expect(mockClick).toHaveBeenCalledTimes(1) }) + + it('should pass type primary if primary is passed as true', () => { + wrapper = shallow( + + ) + expect(wrapper.find(AntDButton).props().type).toEqual('primary') + }) + + it('should have a type default by default', () => { + expect(wrapper.find(AntDButton).props().type).toEqual('default') + }) }) describe('Disabled Button', () => { diff --git a/src/components/Icon/Icon.test.tsx b/src/components/Icon/Icon.test.tsx index ea654ad0..78bc7f36 100644 --- a/src/components/Icon/Icon.test.tsx +++ b/src/components/Icon/Icon.test.tsx @@ -31,6 +31,12 @@ describe('Predefined Icon', () => { it('has the correct height', () => { expect(wrapper.getDOMNode().getAttribute('height')).toBe('64') }) + + it('renders with a default height of 32', () => { + wrapper = mount() + + expect(wrapper.getDOMNode().getAttribute('height')).toBe('32') + }) }) describe('Custom Icon', () => { diff --git a/src/components/InputField/InputField.test.tsx b/src/components/InputField/InputField.test.tsx index 6e089062..a6c70961 100644 --- a/src/components/InputField/InputField.test.tsx +++ b/src/components/InputField/InputField.test.tsx @@ -1,6 +1,6 @@ import { Input } from 'antd' import React from 'react' -import Skeleton from 'react-loading-skeleton' +import Skeleton from '../Skeleton' import InputField, { InputFieldProps } from './index' import { mount, ReactWrapper, shallow } from 'enzyme' @@ -98,10 +98,7 @@ describe('InputField', () => { attachTo: document.getElementById('container') }) - const element = document.getElementsByClassName( - wrapper.getDOMNode().className - )[0] - const style = window.getComputedStyle(element) + const style = window.getComputedStyle(wrapper.getDOMNode()) expect(style.width).toEqual('100%') }) @@ -111,10 +108,7 @@ describe('InputField', () => { attachTo: document.getElementById('container') }) - const element = document.getElementsByClassName( - wrapper.getDOMNode().className - )[0] - const style = window.getComputedStyle(element) + const style = window.getComputedStyle(wrapper.getDOMNode()) expect(style.width).not.toEqual('100%') }) diff --git a/src/components/InputField/index.tsx b/src/components/InputField/index.tsx index 26d55cd1..6da0bdba 100644 --- a/src/components/InputField/index.tsx +++ b/src/components/InputField/index.tsx @@ -2,7 +2,7 @@ import 'antd/lib/input/style/index.css' import cn from 'classnames' import { createUseStyles } from 'react-jss' import { Input } from 'antd' -import Skeleton from 'react-loading-skeleton' +import Skeleton from '../Skeleton' import React, { FC } from 'react' const useStyles = createUseStyles({ @@ -61,12 +61,12 @@ const InputFieldSkeleton: FC = (props: InputFieldProps) => { export interface InputFieldProps { /** - * Array of classes to pass to button. + * Array of classes to pass to input * @default [] */ classes?: string[] /** - * Adds the disabled attribute and styles (opacity, gray scale filter, no pointer events). + * Adds the disabled attribute and styles (opacity, gray scale filter, no pointer events) * @default false */ disabled?: boolean diff --git a/src/components/Link/Link.test.tsx b/src/components/Link/Link.test.tsx index b2ea2514..ecf09c17 100644 --- a/src/components/Link/Link.test.tsx +++ b/src/components/Link/Link.test.tsx @@ -30,6 +30,12 @@ describe('Link with href', () => { mockProps.target ) }) + + it('has a default target of _self if none is passed in', () => { + wrapper = mount(Test) + + expect(wrapper.getDOMNode().getAttribute('target')).toBe('_self') + }) }) describe('Link', () => { diff --git a/src/components/Skeleton/Skeleton.stories.tsx b/src/components/Skeleton/Skeleton.stories.tsx new file mode 100644 index 00000000..e5365d5e --- /dev/null +++ b/src/components/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { Meta, Story } from '@storybook/react/types-6-0' +import Skeleton, { SkeletonProps } from './index' + +export default { + component: Skeleton, + title: 'Skeleton' +} as Meta + +const Template: Story = args => + +export const Default = Template.bind({}) + +export const Circle = Template.bind({}) +Circle.args = { circle: true, height: 50, width: 50 } + +export const Count = Template.bind({}) +Count.args = { count: 5, width: 300 } diff --git a/src/components/Skeleton/Skeleton.test.tsx b/src/components/Skeleton/Skeleton.test.tsx new file mode 100644 index 00000000..42755690 --- /dev/null +++ b/src/components/Skeleton/Skeleton.test.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import { mount, shallow, ShallowWrapper } from 'enzyme' +import Skeleton, { SkeletonProps } from './index' + +let wrapper: ShallowWrapper + +beforeEach(() => { + wrapper = shallow() +}) + +describe('Skeleton', () => { + it('renders', () => { + expect(wrapper).toHaveLength(1) + }) + + it('renders the correct number of rows when passed a count', () => { + wrapper = shallow() + + expect(wrapper.find('span').length).toEqual(5) + }) + + it('correctly passes custom classes', () => { + wrapper = shallow() + + expect(wrapper.find('span').props().className).toContain('test') + }) + + describe('conditional CSS properties', () => { + beforeEach(() => { + // Mounting to document.body throws a React error, so create a temporary container div for the tests to mount the element to + const div = document.createElement('div') + div.setAttribute('id', 'container') + document.body.appendChild(div) + }) + + afterEach(() => { + const div = document.getElementById('container') + + if (div) { + document.body.removeChild(div) + } + }) + + describe('circle or ellipse', () => { + it('should have rounded edges', () => { + const skeleton = mount( + , + { + attachTo: document.getElementById('container') + } + ) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.borderRadius).toEqual('50%') + }) + }) + + describe('duration', () => { + it('should default to 1.2 seconds if no duration is passed', () => { + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.animation).toContain('1.2') + }) + + it('should correctly pass the duration if one is provided', () => { + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.animation).toContain('3.2') + }) + }) + + describe('dimensions', () => { + it('should span the width of the container by default', () => { + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.width).toEqual('100%') + }) + + it('should apply width prop if passed in', () => { + const mockWidth = 250 + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.width).toEqual(`${mockWidth}px`) + }) + + it('should span the height of the container by default', () => { + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.height).toEqual('100%') + }) + + it('should apply height prop if passed in', () => { + const mockHeight = 250 + const skeleton = mount(, { + attachTo: document.getElementById('container') + }) + + const style = window.getComputedStyle(skeleton.getDOMNode()) + + expect(style.height).toEqual(`${mockHeight}px`) + }) + }) + }) +}) diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx new file mode 100644 index 00000000..62d73cdb --- /dev/null +++ b/src/components/Skeleton/index.tsx @@ -0,0 +1,93 @@ +import cn from 'classnames' +import { createUseStyles } from 'react-jss' +import React, { FC } from 'react' + +const useStyles = createUseStyles({ + '@global': { + '@keyframes skeleton': { + '0%': { backgroundPosition: '-200px 0' }, + '100%': { backgroundPosition: 'calc(200px + 100%) 0' } + } + }, + container: { + animation: props => `skeleton ${props.duration}s ease-in-out infinite`, + backgroundColor: '#EEEEEE', + backgroundImage: 'linear-gradient(90deg, #EEEEEE, #F5F5F5, #EEEEEE)', + backgroundRepeat: 'no-repeat', + backgroundSize: '200px 100%', + borderRadius: props => (props.circle ? '50%' : '4px'), + display: props => (props.count > 1 ? 'block' : 'inline-block'), + height: props => (props.height ? props.height : '100%'), + lineHeight: 1, + marginBottom: props => (props.count > 1 ? 5 : 0), + width: props => (props.width ? `${props.width}px` : '100%') + } +}) + +export type SkeletonProps = DefaultSkeletonProps | CircleSkeletonProps + +interface DefaultSkeletonProps { + /** + * Whether or not to render circle skeleton. **Only works if height and width are set to be the same number** + */ + circle?: false + /** + * Array of classes to pass to skeleton + */ + classes?: string[] + /** + * Number of skeleton rows to render + */ + count?: number + /** + * Animation duration + */ + duration?: number + /** + * Skeleton height. If undefined, skeleton will span the height of parent container or 16px - whichever is greater. **Note:** height is a required prop for a circle skeleton. + */ + height?: number + /** + * Skeleton width. If undefined, skeleton will span the width of parent container. **Note**: width is a required prop for a circle skeleton. + */ + width?: number +} + +interface CircleSkeletonProps + extends Omit { + circle: true + height: number + width: number +} + +const Skeleton: FC = (props: SkeletonProps) => { + const { classes: customClasses, count } = props + + const classes = useStyles(props) + + const skeletonClasses = cn( + { + [classes.container]: true + }, + customClasses + ) + + return ( + <> + {[...Array(count)].map((_, i) => ( + +   + + ))} + + ) +} + +Skeleton.defaultProps = { + circle: false, + classes: [], + count: 1, + duration: 1.2 +} + +export default Skeleton diff --git a/src/components/index.ts b/src/components/index.ts index c159beb6..be529c46 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,5 +2,6 @@ export { default as Button } from './Button' export { default as InputField } from './InputField' export { default as Icon } from './Icon' export { default as Link } from './Link' +export { default as Skeleton } from './Skeleton' export { default as Tag } from './Tag' export { default as Toggle } from './Toggle'