diff --git a/package-lock.json b/package-lock.json
index e8aa0f8d..796567e7 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",
@@ -7152,6 +7163,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",
@@ -7366,7 +7378,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",
@@ -11168,7 +11181,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",
@@ -13229,7 +13243,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",
@@ -20694,14 +20709,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 fbf2bf7f..7db26e50 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,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",
"rollup": "^2.23.0",
"rollup-plugin-typescript2": "^0.27.1",
diff --git a/src/__snapshots__/storybook.test.ts.snap b/src/__snapshots__/storybook.test.ts.snap
index bbab594d..7be44637 100644
--- a/src/__snapshots__/storybook.test.ts.snap
+++ b/src/__snapshots__/storybook.test.ts.snap
@@ -103,10 +103,10 @@ exports[`Storyshots Icon Predefined 1`] = `
exports[`Storyshots InputField Default 1`] = `
Field Label
@@ -126,15 +126,15 @@ exports[`Storyshots InputField Default 1`] = `
exports[`Storyshots InputField Error 1`] = `
Field Label
Field Label
@@ -172,38 +172,24 @@ exports[`Storyshots InputField Full Width 1`] = `
exports[`Storyshots InputField Loading 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`] = `
{
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 cbdc02f0..6da0bdba 100644
--- a/src/components/InputField/index.tsx
+++ b/src/components/InputField/index.tsx
@@ -2,14 +2,16 @@ 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({
- '@keyframes shake': {
- '0%, 100%': { left: '0rem' },
- '20%, 60%': { left: '0.5rem' },
- '40%, 80%': { left: '-0.5rem' }
+ '@global': {
+ '@keyframes shake': {
+ '0%, 100%': { left: '0rem' },
+ '20%, 60%': { left: '0.5rem' },
+ '40%, 80%': { left: '-0.5rem' }
+ }
},
container: {
display: 'flex',
@@ -59,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/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..3ab534da
--- /dev/null
+++ b/src/components/Skeleton/Skeleton.test.tsx
@@ -0,0 +1,110 @@
+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('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('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('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..68a3b5d5
--- /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: '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
+ */
+ height?: number
+ /**
+ * Width of skeleton. If undefined, skeleton will span the width of parent container
+ */
+ 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