diff --git a/packages/pelagos/__tests__/components/ProgressBar-test.js b/packages/pelagos/__tests__/components/ProgressBar-test.js new file mode 100644 index 00000000..059abe39 --- /dev/null +++ b/packages/pelagos/__tests__/components/ProgressBar-test.js @@ -0,0 +1,40 @@ +import {shallow} from 'enzyme'; + +import ProgressBar from '../../src/components/ProgressBar'; +import useRandomId from '../../src/hooks/useRandomId'; + +jest.unmock('../../src/components/ProgressBar'); + +useRandomId.mockReturnValue('random-id'); + +describe('ProgressBar', () => { + describe('rendering', () => { + it('renders expected elements', () => { + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('renders expected elements when optional properties are set', () => { + const wrapper = shallow( + + ); + expect(wrapper.getElement()).toMatchSnapshot(); + expect(useRandomId.mock.calls).toEqual([['test']]); + }); + + it('renders expected elements when value is not set', () => { + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('renders expected elements when status is finished', () => { + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + it('renders expected elements when status is error', () => { + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap b/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap new file mode 100644 index 00000000..b1805e15 --- /dev/null +++ b/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProgressBar rendering renders expected elements 1`] = ` +
+
+ + Test label + +
+ +
+ +
+`; + +exports[`ProgressBar rendering renders expected elements when optional properties are set 1`] = ` +
+
+ + Test label + +
+ +
+ +
+ Test helper +
+
+`; + +exports[`ProgressBar rendering renders expected elements when status is error 1`] = ` +
+
+ + Test label + + +
+ +
+ +
+`; + +exports[`ProgressBar rendering renders expected elements when status is finished 1`] = ` +
+
+ + Test label + + +
+ +
+ +
+`; + +exports[`ProgressBar rendering renders expected elements when value is not set 1`] = ` +
+
+ + Test label + +
+ +
+ +
+`; diff --git a/packages/pelagos/src/components/ProgressBar.js b/packages/pelagos/src/components/ProgressBar.js new file mode 100644 index 00000000..c6450bcc --- /dev/null +++ b/packages/pelagos/src/components/ProgressBar.js @@ -0,0 +1,80 @@ +import PropTypes from 'prop-types'; +import {faCircleCheck} from '@fortawesome/free-solid-svg-icons'; + +import useRandomId from '../hooks/useRandomId'; +import rhombusExclamation from '../icons/rhombusExclamation'; + +import SvgIcon from './SvgIcon'; +import Layer from './Layer'; + +import './ProgressBar.less'; + +const icons = {finished: faCircleCheck, error: rhombusExclamation}; + +/** Displays the progress status for tasks that take a long time. */ +const ProgressBar = ({id, className, label, helperText, type, size, status, max, value}) => { + id = useRandomId(id); + const labelId = `${id}-label`; + const helperId = `${id}-helper`; + if (status === 'active' && (value === null || value === undefined)) { + status = 'indeterminate'; + } + return ( +
+
+ {label} + {status in icons && } +
+ +
+ + {helperText && ( +
+ {helperText} +
+ )} +
+ ); +}; + +ProgressBar.propTypes = { + /** The component id. */ + id: PropTypes.string, + /** The component class name(s). */ + className: PropTypes.string, + /** The label text. */ + label: PropTypes.string, + /** The textual representation of the current progress. */ + helperText: PropTypes.string, + /** The alignment type. */ + type: PropTypes.oneOf(['default', 'inline', 'indented']), + /** The size of the progress bar. */ + size: PropTypes.oneOf(['small', 'big']), + /** The progress bar status. */ + status: PropTypes.oneOf(['active', 'finished', 'error']), + /** The maximum value. */ + max: PropTypes.number, + /** The current value. */ + value: PropTypes.number, +}; + +ProgressBar.defaultProps = { + type: 'default', + size: 'big', + status: 'active', + max: 100, +}; + +export default ProgressBar; diff --git a/packages/pelagos/src/components/ProgressBar.less b/packages/pelagos/src/components/ProgressBar.less new file mode 100644 index 00000000..a816e79f --- /dev/null +++ b/packages/pelagos/src/components/ProgressBar.less @@ -0,0 +1,138 @@ +@import '../../less/fonts'; +@import '../../less/spacing'; +@import '../../less/utils'; + +@layer pelagos { + .ProgressBar { + gap: @sp-08; + + &__label { + @label-01(); + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: @sp-16; + + .ProgressBar--inline > & { + justify-content: flex-start; + } + + .ProgressBar--indented > & { + margin-left: @sp-16; + } + } + + &__labelText { + @base-ellipsis(); + } + + &__icon { + font-size: 16px; + + .ProgressBar--finished & { + color: var(--support-success); + } + + .ProgressBar--error & { + color: var(--support-error); + } + } + + &__track { + position: relative; + flex-direction: row; + background-color: var(--layer-accent); + overflow: hidden; + + .ProgressBar--inline > & { + flex: 1; + } + + .ProgressBar--big > & { + height: 8px; + } + + .ProgressBar--small > & { + height: 4px; + } + + .ProgressBar--indeterminate > &::after { + content: ''; + position: absolute; + width: 25%; + height: 100%; + background-color: var(--interactive); + animation: progress-bar-indeterminate 1.5s linear infinite; + will-change: transform; + + @media (prefers-reduced-motion) { + animation-duration: 6s; + } + } + + // stylelint-disable-next-line @bluecateng/selector-bem -- currently doesn't support :is + .ProgressBar--inline:is(.ProgressBar--finished, .ProgressBar--error) > & { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + } + } + + &__bar { + width: 100%; + transform: scaleX(0); + transform-origin: 0; + transition: transform 0.15s ease-out; + + .ProgressBar--active & { + background-color: var(--interactive); + } + + .ProgressBar--finished & { + background-color: var(--support-success); + transform: scaleX(1); + } + + .ProgressBar--error & { + background-color: var(--support-error); + transform: scaleX(1); + } + } + + &__helper { + @helper-text-01(); + color: var(--text-helper); + + .ProgressBar--inline > & { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + } + + .ProgressBar--indented > & { + margin-left: @sp-16; + } + + .ProgressBar--error > & { + color: var(--support-error); + } + } + + &--inline { + flex-direction: row; + align-items: center; + gap: @sp-16; + } + } + + @keyframes progress-bar-indeterminate { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } + } +} diff --git a/packages/pelagos/src/components/ProgressBar.stories.js b/packages/pelagos/src/components/ProgressBar.stories.js new file mode 100644 index 00000000..cec0f30f --- /dev/null +++ b/packages/pelagos/src/components/ProgressBar.stories.js @@ -0,0 +1,51 @@ +import WithLayers from '../../templates/WithLayers'; + +import ProgressBar from './ProgressBar'; + +export default { + title: 'Components/ProgressBar', + component: ProgressBar, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Default = { + args: {label: 'Default', helperText: 'Some helper text', type: 'default', size: 'big', status: 'active', value: 20}, +}; + +export const Inline = { + args: {...Default.args, label: 'Inline', type: 'inline'}, +}; + +export const Indented = { + args: {...Default.args, label: 'Indented', type: 'indented'}, +}; + +export const Small = { + args: {...Default.args, label: 'Small', size: 'small'}, +}; + +export const Indeterminate = { + args: {...Default.args, label: 'Indeterminate', value: null}, + parameters: {chromatic: {disableSnapshot: true}}, // avoid false positives due to the animation +}; + +export const Finished = { + args: {...Default.args, label: 'Finished', status: 'finished'}, +}; + +export const Error = { + args: {...Default.args, label: 'Error', status: 'error'}, +}; + +export const _WithLayers = { + render: () => {() => }, + parameters: { + controls: {hideNoControlsWarning: true}, + }, +}; diff --git a/packages/pelagos/src/components/index.js b/packages/pelagos/src/components/index.js index b3465c04..fce58db4 100644 --- a/packages/pelagos/src/components/index.js +++ b/packages/pelagos/src/components/index.js @@ -34,6 +34,7 @@ export {default as MenuArrow} from './MenuArrow'; export {default as ModalSpinner} from './ModalSpinner'; export {default as MultiColumn} from './MultiColumn'; export {default as Pagination} from './Pagination'; +export {default as ProgressBar} from './ProgressBar'; export {default as RadioButton} from './RadioButton'; export {default as RadioGroup} from './RadioGroup'; export {default as ScrollBox} from './ScrollBox';