diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 548557b..c28c8d9 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -10,6 +10,7 @@ describe('Index', () => { 'TabSet', 'Tag', 'Tooltip', + 'Loading', 'WarningIcon', ]); }); diff --git a/src/all.scss b/src/all.scss index cac9fdd..1d4c211 100644 --- a/src/all.scss +++ b/src/all.scss @@ -13,9 +13,14 @@ @import './components/tooltip/Tooltip'; @import './components/ribbon-link/RibbonLink'; +// LoadingSpinner Component +@import './components/loading-spinner/Loading'; + // Masked Input Depends on standard Input Styling - Include here and // not in the components.scss @import '~nhsuk-frontend/packages/components/error-message/error-message'; @import '~nhsuk-frontend/packages/components/label/label'; @import '~nhsuk-frontend/packages/components/hint/hint'; @import '~nhsuk-frontend/packages/components/input/input'; + +@import '../node_modules/nhsuk-frontend/packages/components/panel/panel'; diff --git a/src/components.scss b/src/components.scss index 1e5fd47..7f3baed 100644 --- a/src/components.scss +++ b/src/components.scss @@ -9,3 +9,6 @@ @import './components/tab-set/TabSet'; @import './components/tooltip/Tooltip'; @import './components/ribbon-link/RibbonLink'; + +//Loading Component +@import './components/loading/Loading'; \ No newline at end of file diff --git a/src/components/loading-spinner/Loading.tsx b/src/components/loading-spinner/Loading.tsx new file mode 100644 index 0000000..5bff9f4 --- /dev/null +++ b/src/components/loading-spinner/Loading.tsx @@ -0,0 +1,31 @@ +import React, { HTMLProps } from 'react'; +import { Panel } from 'nhsuk-react-components'; + +interface Props extends HTMLProps { + text?: string | false; + backdrop?: boolean; + shown?: boolean; +} + +const Loading: React.FC = ({ backdrop, shown, text, width, height }) => { + if (!shown) return null; + + const baseSpinner = ( + +
+ {text &&

{text}

} + + ); + + return backdrop ?
{baseSpinner}
: baseSpinner; +}; + +Loading.defaultProps = { + text: 'Loading...', + width: 200, + height: 200, + shown: true, + backdrop: true, +}; + +export default Loading; diff --git a/src/components/loading-spinner/_Loading.scss b/src/components/loading-spinner/_Loading.scss new file mode 100644 index 0000000..acafe41 --- /dev/null +++ b/src/components/loading-spinner/_Loading.scss @@ -0,0 +1,62 @@ +@keyframes loadingSpin { + 100% { + transform: rotate(360deg); + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.nhsuk-loader { + border: 5px solid transparent; + border-color: $color_nhsuk-grey-3; + border-top-color: $color_nhsuk-blue; + border-radius: 50%; + pointer-events: none; + width: 100%; + height: 100%; + animation: loadingSpin 1s linear infinite; + + &__container { + display: flex; + justify-content: center; + text-align: center; + } + + &__text { + @include nhsuk-responsive-margin(4, 'top'); + font-weight: 400; + text-align: center; + } + + &__backdrop { + position: absolute; + width: 100%; + height: 100%; + overflow: none; + background-color: rgba($color_nhsuk-black, 0.5); + display: flex; + justify-content: center; + align-items: center; + opacity: 1; + animation: fadeIn 200ms ease-in-out forwards; + transition: opacity 200ms ease-in-out; + + &--fade { + opacity: 0; + } + } + + &__panel { + max-width: fit-content; + display: flex; + flex-direction: column; + align-items: center; + } +} diff --git a/src/components/loading-spinner/__tests__/Loading.tests.tsx b/src/components/loading-spinner/__tests__/Loading.tests.tsx new file mode 100644 index 0000000..61c61d4 --- /dev/null +++ b/src/components/loading-spinner/__tests__/Loading.tests.tsx @@ -0,0 +1,28 @@ +import Loading from '..'; +import React from 'react'; +import { shallow } from 'enzyme'; + +describe('loading', () => { + it('renders correctly given text', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + expect(component.text()).toBe("I am testing the loading component"); + component.unmount(); + }); + + it('renders with default text if none supplied', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + expect(component.text()).toBe("Loading..."); + component.unmount(); + }); + + it('renders without text if false value supplied', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + expect(component.children('p')).toHaveLength(0); + component.unmount(); + }); +}); diff --git a/src/components/loading-spinner/__tests__/__snapshots__/Loading.tests.tsx.snap b/src/components/loading-spinner/__tests__/__snapshots__/Loading.tests.tsx.snap new file mode 100644 index 0000000..c0632aa --- /dev/null +++ b/src/components/loading-spinner/__tests__/__snapshots__/Loading.tests.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loading renders correctly given text 1`] = ` +
+
+

+ I am testing the loading component +

+
+
+
+
+
+`; + +exports[`loading renders with default text if none supplied 1`] = ` +
+
+

+ Loading... +

+
+
+
+
+
+`; + +exports[`loading renders without text if false value supplied 1`] = ` +
+
+
+
+
+
+`; diff --git a/src/components/loading-spinner/index.ts b/src/components/loading-spinner/index.ts new file mode 100644 index 0000000..a57e397 --- /dev/null +++ b/src/components/loading-spinner/index.ts @@ -0,0 +1,3 @@ +import Loading from './Loading'; + +export default Loading; diff --git a/src/index.ts b/src/index.ts index e3a62e7..b8cb3d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,5 @@ export { default as SubNavigation } from './components/sub-navigation'; export { default as TabSet } from './components/tab-set'; export { default as Tag } from './components/tag'; export { default as Tooltip } from './components/tooltip'; +export { default as Loading } from './components/loading-spinner'; export { WarningIcon } from './components/icons'; diff --git a/stories/Loading.stories.tsx b/stories/Loading.stories.tsx new file mode 100644 index 0000000..97a298e --- /dev/null +++ b/stories/Loading.stories.tsx @@ -0,0 +1,22 @@ +import React, { useState, useEffect } from 'react'; +import { storiesOf } from '@storybook/react'; +import { Loading } from '../src'; + +const stories = storiesOf('Loading spinner', module); + +stories + .add('Basic', () => ) + .add('With custom text', () => ) + .add('Without text', () => ) + .add('Without backdrop', () => ) + .add('With custom sizing', () => ) + .add('With programmatic control', () => { + const [isShown, setIsShown] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => setIsShown(!isShown), 3000); + return () => clearTimeout(timeout); + }, [isShown]); + + return ; + });