diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
index e4614de9a5b5..6a8563890e75 100644
--- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
+++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
@@ -5149,6 +5149,16 @@ Map {
],
"type": "oneOf",
},
+ "status": Object {
+ "args": Array [
+ Array [
+ "active",
+ "finished",
+ "error",
+ ],
+ ],
+ "type": "oneOf",
+ },
"type": Object {
"args": Array [
Array [
diff --git a/packages/react/src/components/ProgressBar/ProgressBar-test.js b/packages/react/src/components/ProgressBar/ProgressBar-test.js
index 4c9bb1ec7893..80ae729752ea 100644
--- a/packages/react/src/components/ProgressBar/ProgressBar-test.js
+++ b/packages/react/src/components/ProgressBar/ProgressBar-test.js
@@ -24,7 +24,9 @@ describe('ProgressBar', () => {
describe('renders as expected', () => {
it('progress bar and label ids match', () => {
const bar = wrapper.getByRole('progressbar');
- const label = wrapper.container.querySelector('span');
+ const label = wrapper.container.querySelector(
+ `.${prefix}--progress-bar__label`
+ );
expect(bar.getAttribute('aria-labelledby')).toBe(label.id);
});
@@ -41,9 +43,11 @@ describe('ProgressBar', () => {
).toBe(helperText.id);
});
- it('still renders accessible when hideLabel is passed', () => {
+ it('still renders accessible label when hideLabel is passed', () => {
wrapper.rerender();
- const label = wrapper.container.querySelector('span');
+ const label = wrapper.container.querySelector(
+ `.${prefix}--progress-bar__label`
+ );
expect(label.textContent).toBe(props.label);
expect(label.classList.contains(`${prefix}--visually-hidden`)).toBe(true);
@@ -90,6 +94,38 @@ describe('ProgressBar', () => {
.classList.contains(className)
).toBe(true);
});
+
+ it('supports finished status', () => {
+ wrapper.rerender();
+
+ expect(
+ wrapper.container
+ .querySelector(`.${prefix}--progress-bar`)
+ .classList.contains(`${prefix}--progress-bar--finished`)
+ ).toBe(true);
+
+ expect(
+ wrapper.getByRole('progressbar').getAttribute('aria-valuenow')
+ ).toBe('100');
+ });
+
+ it('supports error status', () => {
+ wrapper.rerender();
+
+ expect(
+ wrapper.container
+ .querySelector(`.${prefix}--progress-bar`)
+ .classList.contains(`${prefix}--progress-bar--error`)
+ ).toBe(true);
+
+ expect(
+ wrapper.getByRole('progressbar').getAttribute('aria-valuenow')
+ ).toBe('0');
+
+ expect(
+ wrapper.getByRole('progressbar').getAttribute('aria-invalid')
+ ).toBe('true');
+ });
});
describe('behaves as expected', () => {
diff --git a/packages/react/src/components/ProgressBar/ProgressBar.js b/packages/react/src/components/ProgressBar/ProgressBar.js
index e8470a11d99f..c669c5e982e5 100644
--- a/packages/react/src/components/ProgressBar/ProgressBar.js
+++ b/packages/react/src/components/ProgressBar/ProgressBar.js
@@ -8,6 +8,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import { CheckmarkFilled, ErrorFilled } from '@carbon/icons-react';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';
@@ -18,6 +19,7 @@ function ProgressBar({
label,
max = 100,
size = 'big',
+ status = 'active',
type = 'default',
value,
}) {
@@ -25,7 +27,11 @@ function ProgressBar({
const helperId = useId('progress-bar-helper');
const prefix = usePrefix();
- const indeterminate = value === null || value === undefined;
+ const isFinished = status === 'finished';
+ const isError = status === 'error';
+
+ const indeterminate =
+ !isFinished && !isError && (value === null || value === undefined);
let cappedValue = value;
if (cappedValue > max) {
@@ -34,6 +40,11 @@ function ProgressBar({
if (cappedValue < 0) {
cappedValue = 0;
}
+ if (isError) {
+ cappedValue = 0;
+ } else if (isFinished) {
+ cappedValue = max;
+ }
const percentage = cappedValue / max;
@@ -43,6 +54,8 @@ function ProgressBar({
`${prefix}--progress-bar--${type}`,
{
[`${prefix}--progress-bar--indeterminate`]: indeterminate,
+ [`${prefix}--progress-bar--finished`]: isFinished,
+ [`${prefix}--progress-bar--error`]: isError,
},
className
);
@@ -51,14 +64,31 @@ function ProgressBar({
[`${prefix}--visually-hidden`]: hideLabel,
});
+ let StatusIcon = null;
+
+ if (isError) {
+ StatusIcon = React.forwardRef(function ErrorFilled16(props, ref) {
+ return ;
+ });
+ } else if (isFinished) {
+ StatusIcon = React.forwardRef(function CheckmarkFilled16(props, ref) {
+ return ;
+ });
+ }
+
return (
-
- {label}
-
+
+ {label}
+ {StatusIcon && (
+
+ )}
+
+ {/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}
{helperText && (
@@ -109,6 +143,11 @@ ProgressBar.propTypes = {
*/
size: PropTypes.oneOf(['small', 'big']),
+ /**
+ * Specify the status.
+ */
+ status: PropTypes.oneOf(['active', 'finished', 'error']),
+
/**
* Defines the alignment variant of the progress bar.
*/
diff --git a/packages/react/src/components/ProgressBar/next/ProgressBar.stories.js b/packages/react/src/components/ProgressBar/next/ProgressBar.stories.js
index 3a421b8b5d94..1780f1f95638 100644
--- a/packages/react/src/components/ProgressBar/next/ProgressBar.stories.js
+++ b/packages/react/src/components/ProgressBar/next/ProgressBar.stories.js
@@ -22,6 +22,23 @@ export const _ProgressBar = () => (
/>
);
+const PlaygroundStory = (args) => (
+
+);
+
+export const Playground = PlaygroundStory.bind({});
+
+Playground.argTypes = {
+ status: {
+ options: ['active', 'finished', 'error'],
+ control: { type: 'select' },
+ },
+};
+
export const Indeterminate = () => (
);
@@ -59,6 +76,7 @@ export const Example = () => {
diff --git a/packages/styles/scss/components/progress-bar/_progress-bar.scss b/packages/styles/scss/components/progress-bar/_progress-bar.scss
index fcb776109007..e6716aefb72f 100644
--- a/packages/styles/scss/components/progress-bar/_progress-bar.scss
+++ b/packages/styles/scss/components/progress-bar/_progress-bar.scss
@@ -20,14 +20,23 @@
.#{$prefix}--progress-bar__label {
@include type-style('body-compact-01');
- display: block;
+ display: flex;
+ min-width: rem(48px);
+ justify-content: space-between;
margin-bottom: $spacing-03;
color: $text-primary;
}
+ .#{$prefix}--progress-bar__label-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
.#{$prefix}--progress-bar__track {
position: relative;
width: 100%;
+ min-width: rem(48px);
height: rem(8px);
background-color: $layer;
}
@@ -44,7 +53,8 @@
display: block;
width: 100%;
height: 100%;
- background-color: $interactive;
+ background-color: currentColor;
+ color: $interactive;
transform: scaleX(0);
transform-origin: 0 center #{'/*rtl:100% center*/'};
transition: transform $duration-fast-02 motion(standard, productive);
@@ -74,10 +84,47 @@
.#{$prefix}--progress-bar__helper-text {
@include type-style('helper-text-01');
- margin-top: $spacing-02;
+ margin-top: $spacing-03;
color: $text-secondary;
}
+ .#{$prefix}--progress-bar__status-icon {
+ flex-shrink: 0;
+ margin-left: $spacing-05;
+ }
+
+ .#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__bar,
+ .#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__status-icon {
+ color: $support-success;
+ }
+
+ .#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__bar,
+ .#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__status-icon,
+ .#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__helper-text {
+ color: $support-error;
+ }
+
+ .#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__bar,
+ .#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__bar {
+ transform: scaleX(1);
+ }
+
+ .#{$prefix}--progress-bar--finished.#{$prefix}--progress-bar--inline
+ .#{$prefix}--progress-bar__track,
+ .#{$prefix}--progress-bar--error.#{$prefix}--progress-bar--inline
+ .#{$prefix}--progress-bar__track {
+ @include visually-hidden;
+ }
+
+ .#{$prefix}--progress-bar--finished.#{$prefix}--progress-bar--inline
+ .#{$prefix}--progress-bar__label,
+ .#{$prefix}--progress-bar--error.#{$prefix}--progress-bar--inline
+ .#{$prefix}--progress-bar__label {
+ flex-shrink: 1;
+ justify-content: flex-start;
+ margin-right: 0;
+ }
+
@keyframes progress-bar-indeterminate {
0% {
background-position-x: 25%;
@@ -100,6 +147,11 @@
margin-inline-end: $spacing-05;
}
+ .#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__track {
+ flex-basis: 0;
+ flex-grow: 1;
+ }
+
.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__helper-text {
@include visually-hidden;
}