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'