Skip to content

Commit

Permalink
feat(progress-bar): add support for statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
janhassel committed Mar 24, 2022
1 parent b580542 commit 02edafb
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 15 deletions.
59 changes: 55 additions & 4 deletions packages/components/src/components/progress-bar/_progress-bar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,23 @@
.#{$prefix}--progress-bar__label {
@include type-style('body-short-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;
}
Expand All @@ -40,7 +49,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);
Expand Down Expand Up @@ -70,10 +80,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 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%;
Expand All @@ -91,11 +138,15 @@
}

.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__label {
flex-shrink: 0;
margin-right: $spacing-05;
margin-bottom: 0;
}

.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__track {
flex-basis: 0;
flex-grow: 1;
}

.#{$prefix}--progress-bar--inline .#{$prefix}--progress-bar__helper-text {
@include hidden;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8736,6 +8736,16 @@ Map {
],
"type": "oneOf",
},
"status": Object {
"args": Array [
Array [
"active",
"finished",
"error",
],
],
"type": "oneOf",
},
"type": Object {
"args": Array [
Array [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const sizes = {
'Big (big) - default': 'big',
};

const statuses = {
'Active (active) - default': 'active',
'Finished (finished)': 'finished',
'Error (error)': 'error',
};

const types = {
'Default (default)': 'default',
'Inline (inline)': 'inline',
Expand All @@ -33,6 +39,7 @@ const props = () => ({
label: text('Label text (label)', 'Progress bar label'),
max: number('Maximum value (max)', 100),
size: select('Size (size)', sizes, 'big'),
status: select('Status (status)', statuses, 'active'),
type: select('Type (type)', types, 'default'),
value: number('Current value (value)', 75),
});
Expand Down Expand Up @@ -89,6 +96,7 @@ export const Example = () => {
<ProgressBar
value={running ? progress : null}
max={size}
status={progress === size ? 'finished' : 'active'}
label="Export data"
helperText={helperText}
/>
Expand Down
42 changes: 39 additions & 3 deletions packages/react/src/components/ProgressBar/ProgressBar-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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);
});

Expand All @@ -42,9 +44,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(<ProgressBar {...props} hideLabel />);
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);
Expand Down Expand Up @@ -91,6 +95,38 @@ describe('ProgressBar', () => {
.classList.contains(className)
).toBe(true);
});

it('supports finished status', () => {
wrapper.rerender(<ProgressBar {...props} status="finished" />);

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(<ProgressBar {...props} status="error" />);

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', () => {
Expand Down
45 changes: 40 additions & 5 deletions packages/react/src/components/ProgressBar/ProgressBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { CheckmarkFilled16, ErrorFilled16 } from '@carbon/icons-react';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';

Expand All @@ -18,14 +19,19 @@ function ProgressBar({
label,
max = 100,
size = 'big',
status = 'active',
type = 'default',
value,
}) {
const labelId = useId('progress-bar');
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) {
Expand All @@ -34,6 +40,11 @@ function ProgressBar({
if (cappedValue < 0) {
cappedValue = 0;
}
if (isError) {
cappedValue = 0;
} else if (isFinished) {
cappedValue = max;
}

const percentage = cappedValue / max;

Expand All @@ -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
);
Expand All @@ -51,22 +64,39 @@ function ProgressBar({
[`${prefix}--visually-hidden`]: hideLabel,
});

let StatusIcon = null;

if (isError) {
StatusIcon = ErrorFilled16;
} else if (isFinished) {
StatusIcon = CheckmarkFilled16;
}

return (
<div className={wrapperClasses}>
<span className={labelClasses} id={labelId}>
{label}
</span>
<div className={labelClasses} id={labelId}>
<span className={`${prefix}--progress-bar__label-text`}>{label}</span>
{StatusIcon && (
<StatusIcon className={`${prefix}--progress-bar__status-icon`} />
)}
</div>
{/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}
<div
className={`${prefix}--progress-bar__track`}
role="progressbar"
aria-invalid={isError}
aria-labelledby={labelId}
aria-describedby={helperText ? helperId : null}
aria-valuemin={!indeterminate ? 0 : null}
aria-valuemax={!indeterminate ? max : null}
aria-valuenow={!indeterminate ? cappedValue : null}>
<div
className={`${prefix}--progress-bar__bar`}
style={{ transform: `scaleX(${percentage})` }}
style={
!isFinished && !isError
? { transform: `scaleX(${percentage})` }
: null
}
/>
</div>
{helperText && (
Expand Down Expand Up @@ -109,6 +139,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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const Example = () => {
<ProgressBar
value={running ? progress : null}
max={size}
status={progress === size ? 'finished' : 'active'}
label="Export data"
helperText={helperText}
/>
Expand Down
Loading

0 comments on commit 02edafb

Please sign in to comment.