diff --git a/docs/data/material/components/progress/CircularCustomScale.js b/docs/data/material/components/progress/CircularCustomScale.js new file mode 100644 index 00000000000000..4c1634a3fb50de --- /dev/null +++ b/docs/data/material/components/progress/CircularCustomScale.js @@ -0,0 +1,26 @@ +import * as React from 'react'; +import CircularProgress from '@mui/material/CircularProgress'; + +export default function CircularCustomScale() { + const [progress, setProgress] = React.useState(10); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 20 ? 10 : prevProgress + 2)); + }, 800); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + ); +} diff --git a/docs/data/material/components/progress/CircularCustomScale.tsx b/docs/data/material/components/progress/CircularCustomScale.tsx new file mode 100644 index 00000000000000..4c1634a3fb50de --- /dev/null +++ b/docs/data/material/components/progress/CircularCustomScale.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import CircularProgress from '@mui/material/CircularProgress'; + +export default function CircularCustomScale() { + const [progress, setProgress] = React.useState(10); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 20 ? 10 : prevProgress + 2)); + }, 800); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + ); +} diff --git a/docs/data/material/components/progress/CircularCustomScale.tsx.preview b/docs/data/material/components/progress/CircularCustomScale.tsx.preview new file mode 100644 index 00000000000000..a7c766ecf33d4c --- /dev/null +++ b/docs/data/material/components/progress/CircularCustomScale.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/material/components/progress/CircularWithValueLabel.js b/docs/data/material/components/progress/CircularWithValueLabel.js index a5e42a9386ede1..9a1d3aca6e6130 100644 --- a/docs/data/material/components/progress/CircularWithValueLabel.js +++ b/docs/data/material/components/progress/CircularWithValueLabel.js @@ -39,7 +39,7 @@ function CircularProgressWithLabel(props) { CircularProgressWithLabel.propTypes = { /** * The value of the progress indicator for the determinate variant. - * Value between 0 and 100. + * Value between `min` and `max`. * @default 0 */ value: PropTypes.number.isRequired, diff --git a/docs/data/material/components/progress/LinearQuery.js b/docs/data/material/components/progress/LinearQuery.js new file mode 100644 index 00000000000000..4785ca8d14b3cd --- /dev/null +++ b/docs/data/material/components/progress/LinearQuery.js @@ -0,0 +1,10 @@ +import Box from '@mui/material/Box'; +import LinearProgress from '@mui/material/LinearProgress'; + +export default function LinearQuery() { + return ( + + + + ); +} diff --git a/docs/data/material/components/progress/LinearQuery.tsx b/docs/data/material/components/progress/LinearQuery.tsx new file mode 100644 index 00000000000000..4785ca8d14b3cd --- /dev/null +++ b/docs/data/material/components/progress/LinearQuery.tsx @@ -0,0 +1,10 @@ +import Box from '@mui/material/Box'; +import LinearProgress from '@mui/material/LinearProgress'; + +export default function LinearQuery() { + return ( + + + + ); +} diff --git a/docs/data/material/components/progress/LinearQuery.tsx.preview b/docs/data/material/components/progress/LinearQuery.tsx.preview new file mode 100644 index 00000000000000..b14a4d9082d17d --- /dev/null +++ b/docs/data/material/components/progress/LinearQuery.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/material/components/progress/LinearWithAriaValueText.js b/docs/data/material/components/progress/LinearWithAriaValueText.js new file mode 100644 index 00000000000000..3aa59fda652f93 --- /dev/null +++ b/docs/data/material/components/progress/LinearWithAriaValueText.js @@ -0,0 +1,77 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import LinearProgress from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +function LinearProgressWithLabelAndValue({ max, min, value, ...rest }) { + const progressText = `Elevator at floor ${value} out of ${max}.`; + const progressId = React.useId(); + return ( +
+ + Elevator status + + + + + + + + {progressText} + + + +
+ ); +} + +LinearProgressWithLabelAndValue.propTypes = { + /** + * The maximum value for the progress indicator for the determinate and buffer variants. + * @default 100 + */ + max: PropTypes.number.isRequired, + /** + * The minimum value for the progress indicator for the determinate and buffer variants. + * @default 0 + */ + min: PropTypes.number.isRequired, + /** + * The value of the progress indicator for the determinate and buffer variants. + * Value between `min` and `max`. + */ + value: PropTypes.number.isRequired, +}; + +export default function LinearWithAriaValueText() { + const [progress, setProgress] = React.useState(1); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 10 ? 1 : prevProgress + 1)); + }, 800); + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + + ); +} diff --git a/docs/data/material/components/progress/LinearWithAriaValueText.tsx b/docs/data/material/components/progress/LinearWithAriaValueText.tsx new file mode 100644 index 00000000000000..0ec123cad46176 --- /dev/null +++ b/docs/data/material/components/progress/LinearWithAriaValueText.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +type LinearProgressWithLabelAndValueProps = LinearProgressProps & { + min: number; + max: number; + value: number; +}; + +function LinearProgressWithLabelAndValue({ + max, + min, + value, + ...rest +}: LinearProgressWithLabelAndValueProps) { + const progressText = `Elevator at floor ${value} out of ${max}.`; + const progressId = React.useId(); + return ( +
+ + Elevator status + + + + + + + + {progressText} + + + +
+ ); +} + +export default function LinearWithAriaValueText() { + const [progress, setProgress] = React.useState(1); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 10 ? 1 : prevProgress + 1)); + }, 800); + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + + ); +} diff --git a/docs/data/material/components/progress/LinearWithAriaValueText.tsx.preview b/docs/data/material/components/progress/LinearWithAriaValueText.tsx.preview new file mode 100644 index 00000000000000..1595531620a5ba --- /dev/null +++ b/docs/data/material/components/progress/LinearWithAriaValueText.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/material/components/progress/LinearWithValueLabel.js b/docs/data/material/components/progress/LinearWithValueLabel.js index 85251478f227df..4bb45a6ad23769 100644 --- a/docs/data/material/components/progress/LinearWithValueLabel.js +++ b/docs/data/material/components/progress/LinearWithValueLabel.js @@ -4,29 +4,40 @@ import LinearProgress from '@mui/material/LinearProgress'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -function LinearProgressWithLabel(props) { +function LinearProgressWithLabelAndValue(props) { + const progressId = React.useId(); return ( - - - +
+ + Uploading photos… + + + + + + + + {`${Math.round(props.value)}%`} + + - - - {`${Math.round(props.value)}%`} - - - +
); } -LinearProgressWithLabel.propTypes = { +LinearProgressWithLabelAndValue.propTypes = { /** * The value of the progress indicator for the determinate and buffer variants. - * Value between 0 and 100. + * Value between `min` and `max`. */ value: PropTypes.number.isRequired, }; @@ -45,7 +56,7 @@ export default function LinearWithValueLabel() { return ( - + ); } diff --git a/docs/data/material/components/progress/LinearWithValueLabel.tsx b/docs/data/material/components/progress/LinearWithValueLabel.tsx index 49d05a435fdf26..a35eafeeb34512 100644 --- a/docs/data/material/components/progress/LinearWithValueLabel.tsx +++ b/docs/data/material/components/progress/LinearWithValueLabel.tsx @@ -3,23 +3,35 @@ import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgres import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) { +function LinearProgressWithLabelAndValue( + props: LinearProgressProps & { value: number }, +) { + const progressId = React.useId(); return ( - - - +
+ + Uploading photos… + + + + + + + + {`${Math.round(props.value)}%`} + + - - {`${Math.round(props.value)}%`} - - +
); } @@ -37,7 +49,7 @@ export default function LinearWithValueLabel() { return ( - + ); } diff --git a/docs/data/material/components/progress/LinearWithValueLabel.tsx.preview b/docs/data/material/components/progress/LinearWithValueLabel.tsx.preview index 106f18a93820c2..62528e5a310292 100644 --- a/docs/data/material/components/progress/LinearWithValueLabel.tsx.preview +++ b/docs/data/material/components/progress/LinearWithValueLabel.tsx.preview @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/data/material/components/progress/progress.md b/docs/data/material/components/progress/progress.md index b60007b63f5fa1..38f1f5c2545a15 100644 --- a/docs/data/material/components/progress/progress.md +++ b/docs/data/material/components/progress/progress.md @@ -38,6 +38,12 @@ The animations of the components rely on CSS as much as possible to work even be {{"demo": "CircularDeterminate.js"}} +### Circular custom scale + +By default, progress values are expected in the 0–100 range. You can customize this range by using the `min` and `max` props. + +{{"demo": "CircularCustomScale.js"}} + ### Circular track {{"demo": "CircularEnableTrack.js"}} @@ -56,6 +62,10 @@ The animations of the components rely on CSS as much as possible to work even be {{"demo": "LinearIndeterminate.js"}} +### Linear query + +{{"demo": "LinearQuery.js"}} + ### Linear color {{"demo": "LinearColor.js"}} @@ -68,38 +78,15 @@ The animations of the components rely on CSS as much as possible to work even be {{"demo": "LinearBuffer.js"}} -### Linear with label +### Linear with label and value label {{"demo": "LinearWithValueLabel.js"}} -## Non-standard ranges - -The progress components accept a value in the range 0 - 100. This simplifies things for screen-reader users, where these are the default min / max values. Sometimes, however, you might be working with a data source where the values fall outside this range. Here's how you can easily transform a value in any range to a scale of 0 - 100: - -```jsx -// MIN = Minimum expected value -// MAX = Maximum expected value -// Function to normalise the values (MIN / MAX could be integrated) -const normalise = (value) => ((value - MIN) * 100) / (MAX - MIN); - -// Example component that utilizes the `normalise` function at the point of render. -function Progress(props) { - return ( - - - - - ); -} -``` +### Linear with custom value text + +By default, the progress value is read by assistive technology as percentages. Use `aria-valuetext` when the progress value does not involve percentages. + +{{"demo": "LinearWithAriaValueText.js"}} ## Customization diff --git a/docs/pages/material-ui/api/circular-progress.json b/docs/pages/material-ui/api/circular-progress.json index f63f73040a471b..fc7517c646aa5e 100644 --- a/docs/pages/material-ui/api/circular-progress.json +++ b/docs/pages/material-ui/api/circular-progress.json @@ -10,6 +10,8 @@ }, "disableShrink": { "type": { "name": "custom", "description": "bool" }, "default": "false" }, "enableTrackSlot": { "type": { "name": "bool" }, "default": "false" }, + "max": { "type": { "name": "number" }, "default": "100" }, + "min": { "type": { "name": "number" }, "default": "0" }, "size": { "type": { "name": "union", "description": "number
| string" }, "default": "40" diff --git a/docs/pages/material-ui/api/linear-progress.json b/docs/pages/material-ui/api/linear-progress.json index 24bad2d39cb85c..7d8e28f3c14f49 100644 --- a/docs/pages/material-ui/api/linear-progress.json +++ b/docs/pages/material-ui/api/linear-progress.json @@ -8,6 +8,8 @@ }, "default": "'primary'" }, + "max": { "type": { "name": "number" }, "default": "100" }, + "min": { "type": { "name": "number" }, "default": "0" }, "sx": { "type": { "name": "union", diff --git a/docs/translations/api-docs/circular-progress/circular-progress.json b/docs/translations/api-docs/circular-progress/circular-progress.json index d8c41375452659..57dbd12f9bc486 100644 --- a/docs/translations/api-docs/circular-progress/circular-progress.json +++ b/docs/translations/api-docs/circular-progress/circular-progress.json @@ -11,6 +11,12 @@ "enableTrackSlot": { "description": "If true, a track circle slot is mounted to show a subtle background for the progress. The size and thickness apply to the track slot to be consistent with the progress circle." }, + "max": { + "description": "The maximum value for the progress indicator for the determinate variant." + }, + "min": { + "description": "The minimum value for the progress indicator for the determinate variant." + }, "size": { "description": "The size of the component. If using a number, the pixel unit is assumed. If using a string, you need to provide the CSS unit, for example '3rem'." }, @@ -19,7 +25,7 @@ }, "thickness": { "description": "The thickness of the circle." }, "value": { - "description": "The value of the progress indicator for the determinate variant. Value between 0 and 100." + "description": "The value of the progress indicator for the determinate variant. Value between min and max." }, "variant": { "description": "The variant to use. Use indeterminate when there is no progress value." diff --git a/docs/translations/api-docs/linear-progress/linear-progress.json b/docs/translations/api-docs/linear-progress/linear-progress.json index 58187bd226c6a9..2e778c18335e1d 100644 --- a/docs/translations/api-docs/linear-progress/linear-progress.json +++ b/docs/translations/api-docs/linear-progress/linear-progress.json @@ -5,13 +5,21 @@ "color": { "description": "The color of the component. It supports both default and custom theme colors, which can be added as shown in the palette customization guide." }, + "max": { + "description": "The maximum value for the progress indicator for the determinate and buffer variants." + }, + "min": { + "description": "The minimum value for the progress indicator for the determinate and buffer variants." + }, "sx": { "description": "The system prop that allows defining system overrides as well as additional CSS styles." }, "value": { - "description": "The value of the progress indicator for the determinate and buffer variants. Value between 0 and 100." + "description": "The value of the progress indicator for the determinate and buffer variants. Value between min and max." + }, + "valueBuffer": { + "description": "The value for the buffer variant. Value between min and max." }, - "valueBuffer": { "description": "The value for the buffer variant. Value between 0 and 100." }, "variant": { "description": "The variant to use. Use indeterminate or query when there is no progress value." } diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.d.ts b/packages/mui-material/src/CircularProgress/CircularProgress.d.ts index ccfaacc659102b..5b0bd16bb2f0db 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.d.ts +++ b/packages/mui-material/src/CircularProgress/CircularProgress.d.ts @@ -40,6 +40,16 @@ export interface CircularProgressProps extends StandardProps< * @default false */ enableTrackSlot?: boolean | undefined; + /** + * The maximum value for the progress indicator for the determinate variant. + * @default 100 + */ + max?: number | undefined; + /** + * The minimum value for the progress indicator for the determinate variant. + * @default 0 + */ + min?: number | undefined; /** * The size of the component. * If using a number, the pixel unit is assumed. @@ -58,7 +68,7 @@ export interface CircularProgressProps extends StandardProps< thickness?: number | undefined; /** * The value of the progress indicator for the determinate variant. - * Value between 0 and 100. + * Value between `min` and `max`. * @default 0 */ value?: number | undefined; diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.js b/packages/mui-material/src/CircularProgress/CircularProgress.js index 0686fade4245c8..953a45df7e6d71 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.js @@ -155,7 +155,7 @@ const CircularProgressCircle = styled('circle', { props: ({ ownerState }) => ownerState.variant === 'indeterminate' && !ownerState.disableShrink, style: dashAnimation || { - // At runtime for Pigment CSS, `bufferAnimation` will be null and the generated keyframe will be used. + // At runtime for Pigment CSS, `dashAnimation` will be null and the generated keyframe will be used. animation: `${circularDashKeyframe} 1.4s ease-in-out infinite`, }, }, @@ -187,6 +187,8 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref color = 'primary', disableShrink = false, enableTrackSlot = false, + min: minProp, + max: maxProp, size = 40, style, thickness = 3.6, @@ -195,6 +197,17 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref ...other } = props; + if (process.env.NODE_ENV !== 'production') { + if (variant === 'indeterminate' && (minProp !== undefined || maxProp !== undefined)) { + console.warn( + `MUI: You have provided the \`min\` or \`max\` props with a 'indeterminate' variant. These props will have no effect.`, + ); + } + } + + const min = minProp ?? 0; + const max = maxProp ?? 100; + const ownerState = { ...props, color, @@ -214,10 +227,24 @@ const CircularProgress = React.forwardRef(function CircularProgress(inProps, ref if (variant === 'determinate') { const circumference = 2 * Math.PI * ((SIZE - thickness) / 2); + + if (process.env.NODE_ENV !== 'production') { + if (value < min || value > max || min >= max) { + console.error( + `MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ); + } + } + + const range = max - min; circleStyle.strokeDasharray = circumference.toFixed(3); - rootProps['aria-valuenow'] = Math.round(value); - circleStyle.strokeDashoffset = `${(((100 - value) / 100) * circumference).toFixed(3)}px`; + circleStyle.strokeDashoffset = + range > 0 ? `${(((max - value) / range) * circumference).toFixed(3)}px` : '0px'; rootStyle.transform = 'rotate(-90deg)'; + + rootProps['aria-valuenow'] = Math.round(value); + rootProps['aria-valuemin'] = min; + rootProps['aria-valuemax'] = max; } return ( @@ -306,6 +333,16 @@ CircularProgress.propTypes /* remove-proptypes */ = { * @default false */ enableTrackSlot: PropTypes.bool, + /** + * The maximum value for the progress indicator for the determinate variant. + * @default 100 + */ + max: PropTypes.number, + /** + * The minimum value for the progress indicator for the determinate variant. + * @default 0 + */ + min: PropTypes.number, /** * The size of the component. * If using a number, the pixel unit is assumed. @@ -332,7 +369,7 @@ CircularProgress.propTypes /* remove-proptypes */ = { thickness: PropTypes.number, /** * The value of the progress indicator for the determinate variant. - * Value between 0 and 100. + * Value between `min` and `max`. * @default 0 */ value: PropTypes.number, diff --git a/packages/mui-material/src/CircularProgress/CircularProgress.test.js b/packages/mui-material/src/CircularProgress/CircularProgress.test.js index 706d5e2d104202..0cad1c6017a708 100644 --- a/packages/mui-material/src/CircularProgress/CircularProgress.test.js +++ b/packages/mui-material/src/CircularProgress/CircularProgress.test.js @@ -1,5 +1,9 @@ import { expect } from 'chai'; -import { createRenderer } from '@mui/internal-test-utils'; +import { + createRenderer, + strictModeDoubleLoggingSuppressed, + screen, +} from '@mui/internal-test-utils'; import CircularProgress, { circularProgressClasses as classes, } from '@mui/material/CircularProgress'; @@ -170,4 +174,80 @@ describe('', () => { expect(trackEl.style.strokeDashoffset).to.equal(''); }); }); + + describe('prop: min & max', () => { + it('should be able to use custom min and max values', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar).to.have.attribute('aria-valuenow', '5'); + expect(progressbar).to.have.attribute('aria-valuemin', '0'); + expect(progressbar).to.have.attribute('aria-valuemax', '10'); + }); + + it('min and max values should be used to calculate the circumference of the circle', () => { + const { container } = render( + , + ); + const [circle] = container.querySelectorAll('svg circle'); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar).to.have.nested.property('style.transform', 'rotate(-90deg)'); + expect(circle.style.strokeDasharray).to.match(/126\.920?(px)?/gm); + expect(circle.style.strokeDashoffset).to.match(/95\.190?(px)?/gm); + }); + + it('should fallback to 0px strokeDashoffset if max is less than min', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { container } = render( + , + ); + const [circle] = container.querySelectorAll('svg circle'); + expect(circle.style.strokeDashoffset).to.equal('0px'); + + errorSpy.mockRestore(); + }); + + it('should error if min, max, and value props are invalid', () => { + expect(() => { + render(); + }).toErrorDev([ + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', + !strictModeDoubleLoggingSuppressed && + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', + ]); + expect(() => { + render(); + }).toErrorDev([ + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=15.', + !strictModeDoubleLoggingSuppressed && + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=15.', + ]); + expect(() => { + render(); + }).toErrorDev([ + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=5.', + !strictModeDoubleLoggingSuppressed && + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=5.', + ]); + expect(() => { + render(); + }).toErrorDev([ + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=25.', + !strictModeDoubleLoggingSuppressed && + 'MUI: The min, max, and value props in CircularProgress should be numbers where min < max and min <= value <= max. Received min=10, max=20, value=25.', + ]); + }); + + it('should warn if min and max props are provided with an indeterminate variant', () => { + expect(() => { + render(); + }).toWarnDev([ + "MUI: You have provided the `min` or `max` props with a 'indeterminate' variant. These props will have no effect.", + !strictModeDoubleLoggingSuppressed && + "MUI: You have provided the `min` or `max` props with a 'indeterminate' variant. These props will have no effect.", + ]); + }); + }); }); diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.d.ts b/packages/mui-material/src/LinearProgress/LinearProgress.d.ts index b6f062fd51d47b..8592d522225b98 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.d.ts +++ b/packages/mui-material/src/LinearProgress/LinearProgress.d.ts @@ -28,18 +28,28 @@ export interface LinearProgressProps extends StandardProps< LinearProgressPropsColorOverrides > | undefined; + /** + * The maximum value for the progress indicator for the determinate and buffer variants. + * @default 100 + */ + max?: number | undefined; + /** + * The minimum value for the progress indicator for the determinate and buffer variants. + * @default 0 + */ + min?: number | undefined; /** * The system prop that allows defining system overrides as well as additional CSS styles. */ sx?: SxProps | undefined; /** * The value of the progress indicator for the determinate and buffer variants. - * Value between 0 and 100. + * Value between `min` and `max`. */ value?: number | undefined; /** * The value for the buffer variant. - * Value between 0 and 100. + * Value between `min` and `max`. */ valueBuffer?: number | undefined; /** diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.js b/packages/mui-material/src/LinearProgress/LinearProgress.js index 620156e49a4e62..1f3c071e33665c 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.js @@ -357,6 +357,8 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { const { className, color = 'primary', + max: maxProp, + min: minProp, value, valueBuffer, variant = 'indeterminate', @@ -368,6 +370,20 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { variant, }; + if (process.env.NODE_ENV !== 'production') { + if ( + ['indeterminate', 'query'].includes(variant) && + (minProp !== undefined || maxProp !== undefined) + ) { + console.warn( + `MUI: You have provided the \`min\` or \`max\` props with a 'indeterminate' or 'query' variant. These props will have no effect.`, + ); + } + } + + const min = minProp ?? 0; + const max = maxProp ?? 100; + const classes = useUtilityClasses(ownerState); const isRtl = useRtl(); @@ -376,28 +392,47 @@ const LinearProgress = React.forwardRef(function LinearProgress(inProps, ref) { if (variant === 'determinate' || variant === 'buffer') { if (value !== undefined) { - rootProps['aria-valuenow'] = Math.round(value); - rootProps['aria-valuemin'] = 0; - rootProps['aria-valuemax'] = 100; - let transform = value - 100; + if (process.env.NODE_ENV !== 'production') { + if (value < min || value > max || min >= max) { + console.error( + `MUI: The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=${min}, max=${max}, value=${value}.`, + ); + } + } + + const range = max - min; + let transform = ((value - min) / range) * 100 - 100; if (isRtl) { transform = -transform; } - inlineStyles.bar1.transform = `translateX(${transform}%)`; + inlineStyles.bar1.transform = range > 0 ? `translateX(${transform}%)` : undefined; + + rootProps['aria-valuenow'] = Math.round(value); + rootProps['aria-valuemin'] = min; + rootProps['aria-valuemax'] = max; } else if (process.env.NODE_ENV !== 'production') { console.error( 'MUI: You need to provide a value prop ' + - 'when using the determinate or buffer variant of LinearProgress .', + 'when using the determinate or buffer variant of LinearProgress.', ); } } if (variant === 'buffer') { if (valueBuffer !== undefined) { - let transform = (valueBuffer || 0) - 100; + if (process.env.NODE_ENV !== 'production') { + if (valueBuffer < min || valueBuffer > max || valueBuffer < value || min >= max) { + console.error( + `MUI: The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=${min}, max=${max}, value=${value}, valueBuffer=${valueBuffer}.`, + ); + } + } + + const range = max - min; + let transform = ((valueBuffer - min) / range) * 100 - 100; if (isRtl) { transform = -transform; } - inlineStyles.bar2.transform = `translateX(${transform}%)`; + inlineStyles.bar2.transform = range > 0 ? `translateX(${transform}%)` : undefined; } else if (process.env.NODE_ENV !== 'production') { console.error( 'MUI: You need to provide a valueBuffer prop ' + @@ -457,6 +492,16 @@ LinearProgress.propTypes /* remove-proptypes */ = { PropTypes.oneOf(['inherit', 'primary', 'secondary']), PropTypes.string, ]), + /** + * The maximum value for the progress indicator for the determinate and buffer variants. + * @default 100 + */ + max: PropTypes.number, + /** + * The minimum value for the progress indicator for the determinate and buffer variants. + * @default 0 + */ + min: PropTypes.number, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -467,12 +512,12 @@ LinearProgress.propTypes /* remove-proptypes */ = { ]), /** * The value of the progress indicator for the determinate and buffer variants. - * Value between 0 and 100. + * Value between `min` and `max`. */ value: PropTypes.number, /** * The value for the buffer variant. - * Value between 0 and 100. + * Value between `min` and `max`. */ valueBuffer: PropTypes.number, /** diff --git a/packages/mui-material/src/LinearProgress/LinearProgress.test.js b/packages/mui-material/src/LinearProgress/LinearProgress.test.js index aebcd614c9f69c..f153820010dd7b 100644 --- a/packages/mui-material/src/LinearProgress/LinearProgress.test.js +++ b/packages/mui-material/src/LinearProgress/LinearProgress.test.js @@ -4,6 +4,7 @@ import { screen, strictModeDoubleLoggingSuppressed, } from '@mui/internal-test-utils'; +import RtlProvider from '@mui/system/RtlProvider'; import LinearProgress, { linearProgressClasses as classes } from '@mui/material/LinearProgress'; import describeConformance from '../../test/describeConformance'; @@ -83,6 +84,17 @@ describe('', () => { expect(progressbar.children[0]).to.have.nested.property('style.transform', 'translateX(-23%)'); }); + it('should set opposite width of bar1 on determinate variant in RTL', () => { + render( + + , + , + ); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar.children[0]).to.have.nested.property('style.transform', 'translateX(23%)'); + }); + it('should render with buffer classes for the primary color by default', () => { render(); const progressbar = screen.getByRole('progressbar'); @@ -132,6 +144,7 @@ describe('', () => { it('should render with query classes', () => { render(); + const progressbar = screen.getByRole('progressbar'); expect(progressbar).to.have.class(classes.query); @@ -169,4 +182,141 @@ describe('', () => { ]); }); }); + + describe('prop: min & max', () => { + it('should be able to use custom min and max values', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar).to.have.attribute('aria-valuenow', '5'); + expect(progressbar).to.have.attribute('aria-valuemin', '0'); + expect(progressbar).to.have.attribute('aria-valuemax', '10'); + }); + + it('min and max values should be used to calculate the width of the bar', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar.children[0]).to.have.nested.property( + 'style.transform', + 'translateX(-75%)', + ); + }); + + it('min and max values should be used to calculate the width of the buffer bar', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar.querySelector(`.${classes.bar1}`)).to.have.nested.property( + 'style.transform', + 'translateX(-75%)', + ); + expect(progressbar.querySelector(`.${classes.bar2}`)).to.have.nested.property( + 'style.transform', + 'translateX(-25%)', + ); + }); + + it('should not add transform style to the progress bar when min is equal or larger than max', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar.children[0].style.transform).to.equal(''); + + errorSpy.mockRestore(); + }); + + it('should not add transform style to the buffer bar when min is equal or larger than max', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + const progressbar = screen.getByRole('progressbar'); + + expect(progressbar.children[1].style.transform).to.equal(''); + expect(progressbar.children[2].style.transform).to.equal(''); + + errorSpy.mockRestore(); + }); + + it('should warn if the value is out of range', () => { + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=-1.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=-1.', + ]); + + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=11.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=0, max=10, value=11.', + ]); + }); + + it('should error if the valueBuffer is out of range or less than the value prop', () => { + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=4.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=4.', + ]); + + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=11.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=11.', + ]); + + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=-1.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, value, and valueBuffer props in LinearProgress should be numbers where min < max and min <= value <= valueBuffer <= max. Received min=0, max=10, value=5, valueBuffer=-1.', + ]); + }); + + it('should error if min is equal or greater than max', () => { + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=0, value=5.', + ]); + expect(() => { + render(); + }).toErrorDev([ + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=5.', + !strictModeDoubleLoggingSuppressed && + 'The min, max, and value props in LinearProgress should be numbers where min < max and min <= value <= max. Received min=10, max=10, value=5.', + ]); + }); + + it('should warn if variant is indeterminate or query and min or max props are provided', () => { + expect(() => { + render(); + }).toWarnDev([ + "MUI: You have provided the `min` or `max` props with a 'indeterminate' or 'query' variant. These props will have no effect.", + !strictModeDoubleLoggingSuppressed && + "MUI: You have provided the `min` or `max` props with a 'indeterminate' or 'query' variant. These props will have no effect.", + ]); + + expect(() => { + render(); + }).toWarnDev([ + "MUI: You have provided the `min` or `max` props with a 'indeterminate' or 'query' variant. These props will have no effect.", + !strictModeDoubleLoggingSuppressed && + "MUI: You have provided the `min` or `max` props with a 'indeterminate' or 'query' variant. These props will have no effect.", + ]); + }); + }); });