Skip to content

Commit fe600c1

Browse files
feat(progress-bar): add support for statuses (#10843)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 9db9b14 commit fe600c1

7 files changed

Lines changed: 208 additions & 15 deletions

File tree

packages/components/src/components/progress-bar/_progress-bar.scss

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@
1616
.#{$prefix}--progress-bar__label {
1717
@include type-style('body-short-01');
1818

19-
display: block;
19+
display: flex;
20+
min-width: rem(48px);
21+
justify-content: space-between;
2022
margin-bottom: $spacing-03;
2123
color: $text-primary;
2224
}
2325

26+
.#{$prefix}--progress-bar__label-text {
27+
overflow: hidden;
28+
text-overflow: ellipsis;
29+
white-space: nowrap;
30+
}
31+
2432
.#{$prefix}--progress-bar__track {
2533
position: relative;
2634
width: 100%;
35+
min-width: rem(48px);
2736
height: rem(8px);
2837
background-color: $layer;
2938
}
@@ -40,7 +49,8 @@
4049
display: block;
4150
width: 100%;
4251
height: 100%;
43-
background-color: $interactive;
52+
background-color: currentColor;
53+
color: $interactive;
4454
transform: scaleX(0);
4555
transform-origin: 0 center #{'/*rtl:100% center*/'};
4656
transition: transform $duration--fast-02 motion(standard, productive);
@@ -70,10 +80,47 @@
7080
.#{$prefix}--progress-bar__helper-text {
7181
@include type-style('helper-text-01');
7282

73-
margin-top: $spacing-02;
83+
margin-top: $spacing-03;
7484
color: $text-secondary;
7585
}
7686

87+
.#{$prefix}--progress-bar__status-icon {
88+
flex-shrink: 0;
89+
margin-left: $spacing-05;
90+
}
91+
92+
.#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__bar,
93+
.#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__status-icon {
94+
color: $support-success;
95+
}
96+
97+
.#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__bar,
98+
.#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__status-icon,
99+
.#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__helper-text {
100+
color: $support-error;
101+
}
102+
103+
.#{$prefix}--progress-bar--finished .#{$prefix}--progress-bar__bar,
104+
.#{$prefix}--progress-bar--error .#{$prefix}--progress-bar__bar {
105+
transform: scaleX(1);
106+
}
107+
108+
.#{$prefix}--progress-bar--finished.#{$prefix}--progress-bar--inline
109+
.#{$prefix}--progress-bar__track,
110+
.#{$prefix}--progress-bar--error.#{$prefix}--progress-bar--inline
111+
.#{$prefix}--progress-bar__track {
112+
@include hidden;
113+
}
114+
115+
.#{$prefix}--progress-bar--finished.#{$prefix}--progress-bar--inline
116+
.#{$prefix}--progress-bar__label,
117+
.#{$prefix}--progress-bar--error.#{$prefix}--progress-bar--inline
118+
.#{$prefix}--progress-bar__label {
119+
flex-shrink: 1;
120+
justify-content: flex-start;
121+
margin-right: 0;
122+
}
123+
77124
@keyframes progress-bar-indeterminate {
78125
0% {
79126
background-position-x: 25%;
@@ -91,11 +138,15 @@
91138
}
92139

93140
.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__label {
94-
flex-shrink: 0;
95141
margin-right: $spacing-05;
96142
margin-bottom: 0;
97143
}
98144

145+
.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__track {
146+
flex-basis: 0;
147+
flex-grow: 1;
148+
}
149+
99150
.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__helper-text {
100151
@include hidden;
101152
}

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8736,6 +8736,16 @@ Map {
87368736
],
87378737
"type": "oneOf",
87388738
},
8739+
"status": Object {
8740+
"args": Array [
8741+
Array [
8742+
"active",
8743+
"finished",
8744+
"error",
8745+
],
8746+
],
8747+
"type": "oneOf",
8748+
},
87398749
"type": Object {
87408750
"args": Array [
87418751
Array [

packages/react/src/components/ProgressBar/ProgressBar-story.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ const sizes = {
2121
'Big (big) - default': 'big',
2222
};
2323

24+
const statuses = {
25+
'Active (active) - default': 'active',
26+
'Finished (finished)': 'finished',
27+
'Error (error)': 'error',
28+
};
29+
2430
const types = {
2531
'Default (default)': 'default',
2632
'Inline (inline)': 'inline',
@@ -33,6 +39,7 @@ const props = () => ({
3339
label: text('Label text (label)', 'Progress bar label'),
3440
max: number('Maximum value (max)', 100),
3541
size: select('Size (size)', sizes, 'big'),
42+
status: select('Status (status)', statuses, 'active'),
3643
type: select('Type (type)', types, 'default'),
3744
value: number('Current value (value)', 75),
3845
});
@@ -89,6 +96,7 @@ export const Example = () => {
8996
<ProgressBar
9097
value={running ? progress : null}
9198
max={size}
99+
status={progress === size ? 'finished' : 'active'}
92100
label="Export data"
93101
helperText={helperText}
94102
/>

packages/react/src/components/ProgressBar/ProgressBar-test.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ describe('ProgressBar', () => {
2525
describe('renders as expected', () => {
2626
it('progress bar and label ids match', () => {
2727
const bar = wrapper.getByRole('progressbar');
28-
const label = wrapper.container.querySelector('span');
28+
const label = wrapper.container.querySelector(
29+
`.${prefix}--progress-bar__label`
30+
);
2931
expect(bar.getAttribute('aria-labelledby')).toBe(label.id);
3032
});
3133

@@ -42,9 +44,11 @@ describe('ProgressBar', () => {
4244
).toBe(helperText.id);
4345
});
4446

45-
it('still renders accessible when hideLabel is passed', () => {
47+
it('still renders accessible label when hideLabel is passed', () => {
4648
wrapper.rerender(<ProgressBar {...props} hideLabel />);
47-
const label = wrapper.container.querySelector('span');
49+
const label = wrapper.container.querySelector(
50+
`.${prefix}--progress-bar__label`
51+
);
4852

4953
expect(label.textContent).toBe(props.label);
5054
expect(label.classList.contains(`${prefix}--visually-hidden`)).toBe(true);
@@ -91,6 +95,38 @@ describe('ProgressBar', () => {
9195
.classList.contains(className)
9296
).toBe(true);
9397
});
98+
99+
it('supports finished status', () => {
100+
wrapper.rerender(<ProgressBar {...props} status="finished" />);
101+
102+
expect(
103+
wrapper.container
104+
.querySelector(`.${prefix}--progress-bar`)
105+
.classList.contains(`${prefix}--progress-bar--finished`)
106+
).toBe(true);
107+
108+
expect(
109+
wrapper.getByRole('progressbar').getAttribute('aria-valuenow')
110+
).toBe('100');
111+
});
112+
113+
it('supports error status', () => {
114+
wrapper.rerender(<ProgressBar {...props} status="error" />);
115+
116+
expect(
117+
wrapper.container
118+
.querySelector(`.${prefix}--progress-bar`)
119+
.classList.contains(`${prefix}--progress-bar--error`)
120+
).toBe(true);
121+
122+
expect(
123+
wrapper.getByRole('progressbar').getAttribute('aria-valuenow')
124+
).toBe('0');
125+
126+
expect(
127+
wrapper.getByRole('progressbar').getAttribute('aria-invalid')
128+
).toBe('true');
129+
});
94130
});
95131

96132
describe('behaves as expected', () => {

packages/react/src/components/ProgressBar/ProgressBar.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import React from 'react';
99
import PropTypes from 'prop-types';
1010
import classNames from 'classnames';
11+
import { CheckmarkFilled16, ErrorFilled16 } from '@carbon/icons-react';
1112
import { useId } from '../../internal/useId';
1213
import { usePrefix } from '../../internal/usePrefix';
1314

@@ -18,14 +19,19 @@ function ProgressBar({
1819
label,
1920
max = 100,
2021
size = 'big',
22+
status = 'active',
2123
type = 'default',
2224
value,
2325
}) {
2426
const labelId = useId('progress-bar');
2527
const helperId = useId('progress-bar-helper');
2628
const prefix = usePrefix();
2729

28-
const indeterminate = value === null || value === undefined;
30+
const isFinished = status === 'finished';
31+
const isError = status === 'error';
32+
33+
const indeterminate =
34+
!isFinished && !isError && (value === null || value === undefined);
2935

3036
let cappedValue = value;
3137
if (cappedValue > max) {
@@ -34,6 +40,11 @@ function ProgressBar({
3440
if (cappedValue < 0) {
3541
cappedValue = 0;
3642
}
43+
if (isError) {
44+
cappedValue = 0;
45+
} else if (isFinished) {
46+
cappedValue = max;
47+
}
3748

3849
const percentage = cappedValue / max;
3950

@@ -43,6 +54,8 @@ function ProgressBar({
4354
`${prefix}--progress-bar--${type}`,
4455
{
4556
[`${prefix}--progress-bar--indeterminate`]: indeterminate,
57+
[`${prefix}--progress-bar--finished`]: isFinished,
58+
[`${prefix}--progress-bar--error`]: isError,
4659
},
4760
className
4861
);
@@ -51,22 +64,39 @@ function ProgressBar({
5164
[`${prefix}--visually-hidden`]: hideLabel,
5265
});
5366

67+
let StatusIcon = null;
68+
69+
if (isError) {
70+
StatusIcon = ErrorFilled16;
71+
} else if (isFinished) {
72+
StatusIcon = CheckmarkFilled16;
73+
}
74+
5475
return (
5576
<div className={wrapperClasses}>
56-
<span className={labelClasses} id={labelId}>
57-
{label}
58-
</span>
77+
<div className={labelClasses} id={labelId}>
78+
<span className={`${prefix}--progress-bar__label-text`}>{label}</span>
79+
{StatusIcon && (
80+
<StatusIcon className={`${prefix}--progress-bar__status-icon`} />
81+
)}
82+
</div>
83+
{/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}
5984
<div
6085
className={`${prefix}--progress-bar__track`}
6186
role="progressbar"
87+
aria-invalid={isError}
6288
aria-labelledby={labelId}
6389
aria-describedby={helperText ? helperId : null}
6490
aria-valuemin={!indeterminate ? 0 : null}
6591
aria-valuemax={!indeterminate ? max : null}
6692
aria-valuenow={!indeterminate ? cappedValue : null}>
6793
<div
6894
className={`${prefix}--progress-bar__bar`}
69-
style={{ transform: `scaleX(${percentage})` }}
95+
style={
96+
!isFinished && !isError
97+
? { transform: `scaleX(${percentage})` }
98+
: null
99+
}
70100
/>
71101
</div>
72102
{helperText && (
@@ -109,6 +139,11 @@ ProgressBar.propTypes = {
109139
*/
110140
size: PropTypes.oneOf(['small', 'big']),
111141

142+
/**
143+
* Specify the status.
144+
*/
145+
status: PropTypes.oneOf(['active', 'finished', 'error']),
146+
112147
/**
113148
* Defines the alignment variant of the progress bar.
114149
*/

packages/react/src/components/ProgressBar/next/ProgressBar.stories.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const Example = () => {
5959
<ProgressBar
6060
value={running ? progress : null}
6161
max={size}
62+
status={progress === size ? 'finished' : 'active'}
6263
label="Export data"
6364
helperText={helperText}
6465
/>

0 commit comments

Comments
 (0)