Skip to content

Commit

Permalink
Add an indeterminate linear progress bar (#3429)
Browse files Browse the repository at this point in the history
Co-authored-by: origami-z <[email protected]>
Co-authored-by: Josh Wooding <[email protected]>
Co-authored-by: navkaur76 <[email protected]>
  • Loading branch information
4 people authored Jul 8, 2024
1 parent da92421 commit 8b43adf
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 219 deletions.
9 changes: 9 additions & 0 deletions .changeset/four-bugs-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@salt-ds/core": minor
---

Updated `LinearProgress` to display a moving line to represent an unspecified wait time, when `value` is `undefined`.

`<LinearProgress />`

_Note_: `value` and `bufferValue` are no longer default to `0`. Previously above code would render a 0% progress bar, which was not a good reflection of intent. You can still achieve it by passing in `value={0}`.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { composeStories } from "@storybook/react";
import * as linearProgressStories from "@stories/progress/linear-progress.stories";

const composedStories = composeStories(linearProgressStories);
const { Default } = composedStories;
const { Default, Indeterminate } = composedStories;
describe("GIVEN a LinearProgress", () => {
it("SHOULD render progress bar with correct value with correct value and percentage", () => {
cy.mount(<Default value={50} />);
Expand All @@ -18,4 +18,18 @@ describe("GIVEN a LinearProgress", () => {
cy.findByRole("progressbar").contains("75 %");
cy.findByRole("progressbar").should("not.contain.text", "0"); // test regression #3202
});

it("SHOULD render progress bar with bufferValue provided", () => {
cy.mount(<Default bufferValue={50} />);
cy.findByRole("progressbar")
.get(".saltLinearProgress-buffer")
.should("exist");
});

it("SHOULD render indeterminate progress bar", () => {
cy.mount(<Indeterminate />);
cy.findByRole("progressbar").should("have.attr", "aria-valuemax", "100");
cy.findByRole("progressbar").should("have.attr", "aria-valuemin", "0");
cy.findByRole("progressbar").should("not.have.attr", "aria-valuenow");
});
});
23 changes: 23 additions & 0 deletions packages/core/src/progress/LinearProgress/LinearProgress.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,26 @@
white-space: nowrap;
padding-left: var(--salt-spacing-100);
}

.saltLinearProgress-indeterminate.saltLinearProgress-bar {
position: absolute;
left: 0px;
bottom: 0px;
top: 0px;
transform-origin: left center;
width: 66%;
animation: 1.8s ease-in-out infinite salt-indeterminate-progress-bar;
}

@keyframes salt-indeterminate-progress-bar {
0% {
transform: translateX(-100%);
}
60% {
/* 155% is slightly more than moving the bar off screen (with width of 66%) */
transform: translateX(155%);
}
100% {
transform: translateX(200%);
}
}
43 changes: 27 additions & 16 deletions packages/core/src/progress/LinearProgress/LinearProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from "react";
import { ComponentPropsWithoutRef, forwardRef } from "react";
import { clsx } from "clsx";
import { makePrefixer } from "../../utils";
import { Text } from "../../text";
Expand All @@ -12,7 +12,8 @@ const withBaseName = makePrefixer("saltLinearProgress");
export interface LinearProgressProps extends ComponentPropsWithoutRef<"div"> {
/**
* The value of the buffer indicator.
* Value between 0 and max.
* Value between `min` and `max`.
* When no `value` and `bufferValue` is passed in, show as indeterminate state.
*/
bufferValue?: number;
/**
Expand All @@ -31,7 +32,8 @@ export interface LinearProgressProps extends ComponentPropsWithoutRef<"div"> {
min?: number;
/**
* The value of the progress indicator.
* Value between 0 and max.
* Value between `min` and `max`.
* When no `value` and `bufferValue` is passed in, show as indeterminate state.
*/
value?: number;
}
Expand All @@ -43,8 +45,8 @@ export const LinearProgress = forwardRef<HTMLDivElement, LinearProgressProps>(
hideLabel = false,
max = 100,
min = 0,
value = 0,
bufferValue = 0,
value,
bufferValue,
...rest
},
ref
Expand All @@ -56,13 +58,17 @@ export const LinearProgress = forwardRef<HTMLDivElement, LinearProgressProps>(
window: targetWindow,
});

const progress = ((value - min) / (max - min)) * 100;
const buffer = ((bufferValue - min) / (max - min)) * 100;
const barStyle: CSSProperties = {};
const bufferStyle: CSSProperties = {};

barStyle.width = `${progress}%`;
bufferStyle.width = `${buffer}%`;
const isIndeterminate = value === undefined && bufferValue === undefined;
const progress =
value === undefined ? 0 : ((value - min) / (max - min)) * 100;
const buffer =
bufferValue === undefined ? 0 : ((bufferValue - min) / (max - min)) * 100;
const barStyle = {
width: isIndeterminate ? undefined : `${progress}%`,
};
const bufferStyle = {
width: `${buffer}%`,
};

return (
<div
Expand All @@ -71,19 +77,24 @@ export const LinearProgress = forwardRef<HTMLDivElement, LinearProgressProps>(
role="progressbar"
aria-valuemax={max}
aria-valuemin={min}
aria-valuenow={Math.round(value)}
aria-valuenow={value === undefined ? undefined : Math.round(value)}
{...rest}
>
<div className={withBaseName("barContainer")}>
<div className={withBaseName("bar")} style={barStyle} />
{bufferValue > 0 ? (
<div
className={clsx(withBaseName("bar"), {
[withBaseName("indeterminate")]: isIndeterminate,
})}
style={barStyle}
/>
{bufferValue && bufferValue > 0 ? (
<div className={withBaseName("buffer")} style={bufferStyle} />
) : null}
<div className={withBaseName("track")} />
</div>
{!hideLabel && (
<Text styleAs="h2" className={withBaseName("progressLabel")}>
{`${Math.round(progress)} %`}
{isIndeterminate ? `— %` : `${Math.round(progress)} %`}
</Text>
)}
</div>
Expand Down
8 changes: 5 additions & 3 deletions packages/core/stories/progress/linear-progress.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Meta, StoryFn } from "@storybook/react";
import {
Button,
FlowLayout,
StackLayout,
CircularProgress,
FlowLayout,
LinearProgress,
StackLayout,
} from "@salt-ds/core";
import { Meta, StoryFn } from "@storybook/react";
import { useProgressingValue } from "./useProgressingValue";

import "./progress.stories.css";
Expand Down Expand Up @@ -91,3 +91,5 @@ export const ProgressingValue: StoryFn<typeof LinearProgress> = () => (
export const ProgressingBufferValue: StoryFn<typeof LinearProgress> = () => (
<ProgressBufferWithControls ProgressComponent={LinearProgress} />
);

export const Indeterminate = Default.bind({});
3 changes: 3 additions & 0 deletions packages/core/stories/progress/progress.qa.stories.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.noAnimation.saltLinearProgress .saltLinearProgress-indeterminate.saltLinearProgress-bar {
animation: none;
}
8 changes: 8 additions & 0 deletions packages/core/stories/progress/progress.qa.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Meta, StoryFn } from "@storybook/react";
import { CircularProgress, LinearProgress } from "@salt-ds/core";
import { QAContainer, QAContainerProps } from "docs/components";

import "./progress.qa.stories.css";

export default {
title: "Core/Progress/Progress QA",
component: CircularProgress,
Expand Down Expand Up @@ -35,6 +37,12 @@ export const ExamplesGrid: StoryFn<QAContainerProps> = (props) => {
hideLabel
/>
<CircularProgress aria-label="Download" value={38} hideLabel />
<LinearProgress
aria-label="Download"
// Chromatic doesn't work https://www.chromatic.com/docs/animations/#css-animations
className="noAnimation"
style={{ padding: "50px" }}
/>
</QAContainer>
);
};
Expand Down
31 changes: 11 additions & 20 deletions site/docs/components/progress/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,38 @@ Use linear or circular depending on the context, layout and functionality of an

</LivePreview>

<LivePreview componentName="progress" exampleName="HiddenLabel" displayName="Hide label">
<LivePreview componentName="progress" exampleName="LinearIndeterminate" displayName="Indeterminate linear progress">

## Hide label
## Indeterminate linear progress

Use the `hideLabel` prop to display the progress without a label.
Use an indeterminate linear progress to indicate ongoing progress until a determinate value can be specified.

</LivePreview>

<LivePreview componentName="progress" exampleName="WithMaxVal" displayName="With maximum value">
<LivePreview componentName="progress" exampleName="WithMinVal" displayName="Min and max values">

## With maximum value
## Min and max values

Specify the maximum value of the progress indicator. The percentage progress will be calculated using this value. The default value is 100.
Specify the `min` and `max` values of the progress indicator. The default range is 0 - 100.

</LivePreview>
<LivePreview componentName="progress" exampleName="WithBuffer" displayName="With buffer">

<LivePreview componentName="progress" exampleName="WithMinVal" displayName="With minimum value">

## With minimum value
## With buffer

Specify the minimum value of the progress indicator. The percentage progress will be calculated using this value. The default value is 0.
Specify a buffer value to indicate loading state. The buffer is a pending value so will not affect the progress label.

</LivePreview>

<LivePreview componentName="progress" exampleName="WithProgVal" displayName="With progressing value">

## With progressing value

Dynamically represent a progressing value in the progress indicator.

</LivePreview>
<LivePreview componentName="progress" exampleName="WithBuffer" displayName="With buffer">

## With buffer

Specify a buffer value to indicate loading state. The buffer does not have a label and will not affect the progress label.

</LivePreview>
<LivePreview componentName="progress" exampleName="WithProgBufferVal" displayName="With progressing buffer value">
<LivePreview componentName="progress" exampleName="WithProgBufferVal" displayName="With progressing buffer">

## With progressing buffer value
## With progressing buffer

Dynamically represent a progressing buffer value in the progress indicator.

Expand Down
39 changes: 10 additions & 29 deletions site/src/examples/progress/HiddenLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,20 @@
import { ReactElement, useState } from "react";
import {
FlexItem,
FlexLayout,
FlowLayout,
RadioButton,
RadioButtonGroup,
CircularProgress,
FlexItem,
LinearProgress,
StackLayout,
} from "@salt-ds/core";
import { ReactElement } from "react";

export const HiddenLabel = (): ReactElement => {
const [selectedType, setSelectedType] = useState("circular");

return (
<FlexLayout direction="column" style={{ height: "100%" }}>
<FlowLayout justify="center" gap={1}>
<RadioButtonGroup
direction="horizontal"
value={selectedType}
aria-label="Progress type control"
onChange={(e) => setSelectedType(e.target.value)}
>
<RadioButton label="Circular" value="circular" />
<RadioButton label="Linear" value="linear" />
</RadioButtonGroup>
</FlowLayout>

<FlexItem style={{ margin: "auto" }}>
{selectedType === "circular" && (
<CircularProgress aria-label="Download" value={38} hideLabel />
)}
{selectedType === "linear" && (
<LinearProgress aria-label="Download" value={38} hideLabel />
)}
<StackLayout align="center">
<FlexItem>
<CircularProgress aria-label="Download" value={38} hideLabel />
</FlexItem>
<FlexItem>
<LinearProgress aria-label="Download" value={38} hideLabel />
</FlexItem>
</FlexLayout>
</StackLayout>
);
};
28 changes: 28 additions & 0 deletions site/src/examples/progress/LinearIndeterminate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
Button,
LinearProgress,
Text,
Toast,
ToastContent,
} from "@salt-ds/core";
import { CloseIcon } from "@salt-ds/icons";
import { ReactElement } from "react";

export const LinearIndeterminate = (): ReactElement => {
return (
<Toast status="info">
<ToastContent>
<div>
<Text>
<strong>File uploading</strong>
</Text>
<Text>File upload to shared drive in progress.</Text>
<LinearProgress aria-label="Download" />
</div>
</ToastContent>
<Button variant="secondary" aria-label="Dismiss">
<CloseIcon aria-hidden />
</Button>
</Toast>
);
};
38 changes: 10 additions & 28 deletions site/src/examples/progress/WithBuffer.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,23 @@
import { ReactElement, useState } from "react";
import {
CircularProgress,
FlexItem,
FlexLayout,
FlowLayout,
H3,
RadioButton,
RadioButtonGroup,
CircularProgress,
LinearProgress,
StackLayout,
} from "@salt-ds/core";
import { ReactElement } from "react";

export const WithBuffer = (): ReactElement => {
const [selectedType, setSelectedType] = useState("circular");

return (
<FlexLayout direction="column" align="center" style={{ height: "100%" }}>
<StackLayout align="center">
<H3> value = 38, buffer value = 60</H3>
<FlowLayout justify="center" gap={1}>
<RadioButtonGroup
direction="horizontal"
value={selectedType}
aria-label="Progress type control"
onChange={(e) => setSelectedType(e.target.value)}
>
<RadioButton label="Circular" value="circular" />
<RadioButton label="Linear" value="linear" />
</RadioButtonGroup>
</FlowLayout>

<FlexItem style={{ margin: "auto" }}>
{selectedType === "circular" && (
<CircularProgress aria-label="Download" value={38} bufferValue={60} />
)}
{selectedType === "linear" && (
<LinearProgress aria-label="Download" value={38} bufferValue={60} />
)}
<FlexItem>
<CircularProgress aria-label="Download" value={38} bufferValue={60} />
</FlexItem>
<FlexItem>
<LinearProgress aria-label="Download" value={38} bufferValue={60} />
</FlexItem>
</FlexLayout>
</StackLayout>
);
};
Loading

0 comments on commit 8b43adf

Please sign in to comment.