diff --git a/.changeset/four-bugs-reply.md b/.changeset/four-bugs-reply.md new file mode 100644 index 00000000000..04de5c81367 --- /dev/null +++ b/.changeset/four-bugs-reply.md @@ -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`. + +`` + +_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}`. diff --git a/packages/core/src/__tests__/__e2e__/progress/LinearProgress.cy.tsx b/packages/core/src/__tests__/__e2e__/progress/LinearProgress.cy.tsx index 876114eca6d..0521dece381 100644 --- a/packages/core/src/__tests__/__e2e__/progress/LinearProgress.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/progress/LinearProgress.cy.tsx @@ -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(); @@ -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(); + cy.findByRole("progressbar") + .get(".saltLinearProgress-buffer") + .should("exist"); + }); + + it("SHOULD render indeterminate progress bar", () => { + cy.mount(); + 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"); + }); }); diff --git a/packages/core/src/progress/LinearProgress/LinearProgress.css b/packages/core/src/progress/LinearProgress/LinearProgress.css index 721e46567cb..6f05c3eb796 100644 --- a/packages/core/src/progress/LinearProgress/LinearProgress.css +++ b/packages/core/src/progress/LinearProgress/LinearProgress.css @@ -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%); + } +} diff --git a/packages/core/src/progress/LinearProgress/LinearProgress.tsx b/packages/core/src/progress/LinearProgress/LinearProgress.tsx index cf47703973d..a8bf70caa97 100644 --- a/packages/core/src/progress/LinearProgress/LinearProgress.tsx +++ b/packages/core/src/progress/LinearProgress/LinearProgress.tsx @@ -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"; @@ -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; /** @@ -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; } @@ -43,8 +45,8 @@ export const LinearProgress = forwardRef( hideLabel = false, max = 100, min = 0, - value = 0, - bufferValue = 0, + value, + bufferValue, ...rest }, ref @@ -56,13 +58,17 @@ export const LinearProgress = forwardRef( 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 (
( role="progressbar" aria-valuemax={max} aria-valuemin={min} - aria-valuenow={Math.round(value)} + aria-valuenow={value === undefined ? undefined : Math.round(value)} {...rest} >
-
- {bufferValue > 0 ? ( +
+ {bufferValue && bufferValue > 0 ? (
) : null}
{!hideLabel && ( - {`${Math.round(progress)} %`} + {isIndeterminate ? `— %` : `${Math.round(progress)} %`} )}
diff --git a/packages/core/stories/progress/linear-progress.stories.tsx b/packages/core/stories/progress/linear-progress.stories.tsx index 08c7d0b92e3..c0fc6d7b9e1 100644 --- a/packages/core/stories/progress/linear-progress.stories.tsx +++ b/packages/core/stories/progress/linear-progress.stories.tsx @@ -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"; @@ -91,3 +91,5 @@ export const ProgressingValue: StoryFn = () => ( export const ProgressingBufferValue: StoryFn = () => ( ); + +export const Indeterminate = Default.bind({}); diff --git a/packages/core/stories/progress/progress.qa.stories.css b/packages/core/stories/progress/progress.qa.stories.css new file mode 100644 index 00000000000..b4c59b32812 --- /dev/null +++ b/packages/core/stories/progress/progress.qa.stories.css @@ -0,0 +1,3 @@ +.noAnimation.saltLinearProgress .saltLinearProgress-indeterminate.saltLinearProgress-bar { + animation: none; +} diff --git a/packages/core/stories/progress/progress.qa.stories.tsx b/packages/core/stories/progress/progress.qa.stories.tsx index 78b8e8b5847..4ec25d6e970 100644 --- a/packages/core/stories/progress/progress.qa.stories.tsx +++ b/packages/core/stories/progress/progress.qa.stories.tsx @@ -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, @@ -35,6 +37,12 @@ export const ExamplesGrid: StoryFn = (props) => { hideLabel /> + ); }; diff --git a/site/docs/components/progress/examples.mdx b/site/docs/components/progress/examples.mdx index 926feaa691d..6fe4bf10a7e 100644 --- a/site/docs/components/progress/examples.mdx +++ b/site/docs/components/progress/examples.mdx @@ -26,30 +26,28 @@ Use linear or circular depending on the context, layout and functionality of an - + -## 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. - + -## 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. + - - -## 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. - ## With progressing value @@ -57,16 +55,9 @@ Specify the minimum value of the progress indicator. The percentage progress wil Dynamically represent a progressing value in the progress indicator. - - -## With buffer - -Specify a buffer value to indicate loading state. The buffer does not have a label and will not affect the progress label. - - - + -## With progressing buffer value +## With progressing buffer Dynamically represent a progressing buffer value in the progress indicator. diff --git a/site/src/examples/progress/HiddenLabel.tsx b/site/src/examples/progress/HiddenLabel.tsx index 55e110aaf65..0882432ab80 100644 --- a/site/src/examples/progress/HiddenLabel.tsx +++ b/site/src/examples/progress/HiddenLabel.tsx @@ -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 ( - - - setSelectedType(e.target.value)} - > - - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + + - + ); }; diff --git a/site/src/examples/progress/LinearIndeterminate.tsx b/site/src/examples/progress/LinearIndeterminate.tsx new file mode 100644 index 00000000000..70c216676f4 --- /dev/null +++ b/site/src/examples/progress/LinearIndeterminate.tsx @@ -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 ( + + +
+ + File uploading + + File upload to shared drive in progress. + +
+
+ +
+ ); +}; diff --git a/site/src/examples/progress/WithBuffer.tsx b/site/src/examples/progress/WithBuffer.tsx index b61136e6310..357fc4a229f 100644 --- a/site/src/examples/progress/WithBuffer.tsx +++ b/site/src/examples/progress/WithBuffer.tsx @@ -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 ( - +

value = 38, buffer value = 60

- - setSelectedType(e.target.value)} - > - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + -
+ ); }; diff --git a/site/src/examples/progress/WithMaxVal.tsx b/site/src/examples/progress/WithMaxVal.tsx index 9f3efad98eb..50efdee791f 100644 --- a/site/src/examples/progress/WithMaxVal.tsx +++ b/site/src/examples/progress/WithMaxVal.tsx @@ -1,41 +1,22 @@ -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 WithMaxVal = (): ReactElement => { - const [selectedType, setSelectedType] = useState("circular"); - return ( - +

max = 500, value = 250

- - setSelectedType(e.target.value)} - > - - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + -
+ ); }; diff --git a/site/src/examples/progress/WithMinVal.tsx b/site/src/examples/progress/WithMinVal.tsx index 05e0ffeba58..fead1d96880 100644 --- a/site/src/examples/progress/WithMinVal.tsx +++ b/site/src/examples/progress/WithMinVal.tsx @@ -1,54 +1,36 @@ -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 WithMinVal = (): ReactElement => { - const [selectedType, setSelectedType] = useState("circular"); - const max = 40; const min = 20; const value = 30; return ( - +

{`max = ${max}, min = ${min}, value = ${value}`}

- - setSelectedType(e.target.value)} - > - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + -
+ ); }; diff --git a/site/src/examples/progress/WithProgBufferVal.tsx b/site/src/examples/progress/WithProgBufferVal.tsx index ff74fbe9b65..9c7eba18b41 100644 --- a/site/src/examples/progress/WithProgBufferVal.tsx +++ b/site/src/examples/progress/WithProgBufferVal.tsx @@ -1,14 +1,12 @@ -import { ReactElement, useState, useEffect, useCallback } from "react"; import { Button, + CircularProgress, FlexItem, - FlexLayout, FlowLayout, - RadioButton, - RadioButtonGroup, - CircularProgress, LinearProgress, + StackLayout, } from "@salt-ds/core"; +import { ReactElement, useCallback, useEffect, useState } from "react"; function useProgressingValue(updateInterval = 100) { const [bufferValue, setBufferValue] = useState(0); @@ -45,7 +43,7 @@ function useProgressingValue(updateInterval = 100) { handleStop(); } }, - [bufferValue] + [bufferValue, handleStop] ); return { @@ -60,10 +58,9 @@ function useProgressingValue(updateInterval = 100) { export const WithProgBufferVal = (): ReactElement => { const { handleReset, handleStart, handleStop, isProgressing, bufferValue } = useProgressingValue(); - const [selectedType, setSelectedType] = useState("circular"); return ( - + - - setSelectedType(e.target.value)} - > - - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + - + ); }; diff --git a/site/src/examples/progress/WithProgVal.tsx b/site/src/examples/progress/WithProgVal.tsx index 3233fbc5759..caba3202074 100644 --- a/site/src/examples/progress/WithProgVal.tsx +++ b/site/src/examples/progress/WithProgVal.tsx @@ -1,14 +1,12 @@ -import { ReactElement, useState, useEffect, useCallback } from "react"; import { Button, + CircularProgress, FlexItem, - FlexLayout, FlowLayout, - RadioButton, - RadioButtonGroup, - CircularProgress, LinearProgress, + StackLayout, } from "@salt-ds/core"; +import { ReactElement, useCallback, useEffect, useState } from "react"; function useProgressingValue(updateInterval = 100) { const [value, setValue] = useState(0); @@ -60,10 +58,9 @@ function useProgressingValue(updateInterval = 100) { export const WithProgVal = (): ReactElement => { const { handleReset, handleStart, handleStop, isProgressing, value } = useProgressingValue(); - const [selectedType, setSelectedType] = useState("circular"); return ( - + - - setSelectedType(e.target.value)} - > - - - - - - - {selectedType === "circular" && ( - - )} - {selectedType === "linear" && ( - - )} + + + + + - + ); }; diff --git a/site/src/examples/progress/index.ts b/site/src/examples/progress/index.ts index 2363afa574f..9c2222195ee 100644 --- a/site/src/examples/progress/index.ts +++ b/site/src/examples/progress/index.ts @@ -1,5 +1,6 @@ export * from "./Circular"; export * from "./Linear"; +export * from "./LinearIndeterminate"; export * from "./HiddenLabel"; export * from "./WithBuffer"; export * from "./WithMaxVal";