diff --git a/docs/data/base/components/progress/IndeterminateProgress.js b/docs/data/base/components/progress/IndeterminateProgress.js new file mode 100644 index 0000000000..3e504194c3 --- /dev/null +++ b/docs/data/base/components/progress/IndeterminateProgress.js @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { styled, keyframes, css } from '@mui/system'; +import * as BaseProgress from '@base_ui/react/Progress'; + +export default function IndeterminateProgress() { + return ( +
+ + + Uploading files + + + + + +
+ ); +} + +const Progress = styled(BaseProgress.Root)` + display: flex; + flex-flow: column nowrap; + gap: 1rem; +`; + +const ProgressTrack = styled(BaseProgress.Track)( + ({ theme }) => ` + position: relative; + width: 100%; + height: 4px; + border-radius: 9999px; + background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]}; + display: flex; + overflow: hidden; + `, +); + +const indeterminateLoading = keyframes` + from { + transform: translateX(-100%); + } + + to { + transform: translateX(20rem); + } +`; + +const ProgressIndicator = styled(BaseProgress.Indicator)( + ({ theme }) => css` + background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500}; + border-radius: inherit; + + &[data-state='indeterminate'] { + width: 25%; + animation: ${indeterminateLoading} 1.5s infinite ease-in-out; + will-change: transform; + } + `, +); + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/IndeterminateProgress.tsx b/docs/data/base/components/progress/IndeterminateProgress.tsx new file mode 100644 index 0000000000..3e504194c3 --- /dev/null +++ b/docs/data/base/components/progress/IndeterminateProgress.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { styled, keyframes, css } from '@mui/system'; +import * as BaseProgress from '@base_ui/react/Progress'; + +export default function IndeterminateProgress() { + return ( +
+ + + Uploading files + + + + + +
+ ); +} + +const Progress = styled(BaseProgress.Root)` + display: flex; + flex-flow: column nowrap; + gap: 1rem; +`; + +const ProgressTrack = styled(BaseProgress.Track)( + ({ theme }) => ` + position: relative; + width: 100%; + height: 4px; + border-radius: 9999px; + background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]}; + display: flex; + overflow: hidden; + `, +); + +const indeterminateLoading = keyframes` + from { + transform: translateX(-100%); + } + + to { + transform: translateX(20rem); + } +`; + +const ProgressIndicator = styled(BaseProgress.Indicator)( + ({ theme }) => css` + background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500}; + border-radius: inherit; + + &[data-state='indeterminate'] { + width: 25%; + animation: ${indeterminateLoading} 1.5s infinite ease-in-out; + will-change: transform; + } + `, +); + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/IndeterminateProgress.tsx.preview b/docs/data/base/components/progress/IndeterminateProgress.tsx.preview new file mode 100644 index 0000000000..e45ea5cb8b --- /dev/null +++ b/docs/data/base/components/progress/IndeterminateProgress.tsx.preview @@ -0,0 +1,8 @@ + + + Uploading files + + + + + \ No newline at end of file diff --git a/docs/data/base/components/progress/RtlProgress.js b/docs/data/base/components/progress/RtlProgress.js new file mode 100644 index 0000000000..149c0dd84c --- /dev/null +++ b/docs/data/base/components/progress/RtlProgress.js @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { styled } from '@mui/system'; +import * as BaseProgress from '@base_ui/react/Progress'; + +export default function RtlProgress() { + return ( +
+ + + Uploading files (RTL) + + + + + +
+ ); +} + +const Progress = styled(BaseProgress.Root)` + display: flex; + flex-flow: column nowrap; + gap: 1rem; +`; + +const ProgressTrack = styled(BaseProgress.Track)( + ({ theme }) => ` + position: relative; + width: 100%; + height: 4px; + border-radius: 9999px; + background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]}; + display: flex; + overflow: hidden; + `, +); + +const ProgressIndicator = styled(BaseProgress.Indicator)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500}; + border-radius: inherit; + `, +); + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/RtlProgress.tsx b/docs/data/base/components/progress/RtlProgress.tsx new file mode 100644 index 0000000000..149c0dd84c --- /dev/null +++ b/docs/data/base/components/progress/RtlProgress.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { styled } from '@mui/system'; +import * as BaseProgress from '@base_ui/react/Progress'; + +export default function RtlProgress() { + return ( +
+ + + Uploading files (RTL) + + + + + +
+ ); +} + +const Progress = styled(BaseProgress.Root)` + display: flex; + flex-flow: column nowrap; + gap: 1rem; +`; + +const ProgressTrack = styled(BaseProgress.Track)( + ({ theme }) => ` + position: relative; + width: 100%; + height: 4px; + border-radius: 9999px; + background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]}; + display: flex; + overflow: hidden; + `, +); + +const ProgressIndicator = styled(BaseProgress.Indicator)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500}; + border-radius: inherit; + `, +); + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/RtlProgress.tsx.preview b/docs/data/base/components/progress/RtlProgress.tsx.preview new file mode 100644 index 0000000000..daa832be54 --- /dev/null +++ b/docs/data/base/components/progress/RtlProgress.tsx.preview @@ -0,0 +1,8 @@ + + + Uploading files (RTL) + + + + + \ No newline at end of file diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.js b/docs/data/base/components/progress/UnstyledProgressIntroduction.js new file mode 100644 index 0000000000..d39f50a80f --- /dev/null +++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.js @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { useTheme } from '@mui/system'; +import * as Progress from '@base_ui/react/Progress'; + +export default function UnstyledProgressIntroduction() { + return ( +
+ + + Uploading files + + + + + + +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +export function Styles() { + const isDarkMode = useIsDarkMode(); + return ( + + ); +} + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx new file mode 100644 index 0000000000..d39f50a80f --- /dev/null +++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { useTheme } from '@mui/system'; +import * as Progress from '@base_ui/react/Progress'; + +export default function UnstyledProgressIntroduction() { + return ( +
+ + + Uploading files + + + + + + +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +export function Styles() { + const isDarkMode = useIsDarkMode(); + return ( + + ); +} + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview new file mode 100644 index 0000000000..b710e80892 --- /dev/null +++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview @@ -0,0 +1,9 @@ + + + Uploading files + + + + + + \ No newline at end of file diff --git a/docs/data/base/components/progress/progress.md b/docs/data/base/components/progress/progress.md new file mode 100644 index 0000000000..43a85431ea --- /dev/null +++ b/docs/data/base/components/progress/progress.md @@ -0,0 +1,137 @@ +--- +productId: base-ui +title: React Progress components +components: ProgressRoot, ProgressTrack, ProgressIndicator +hooks: useProgressRoot, useProgressIndicator +githubLabel: 'component: progress' +waiAria: https://www.w3.org/TR/wai-aria-1.2/#progressbar +packageName: '@base_ui/react' +--- + +# Progress + +

The Progress component displays the status of a task or operation over time.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +{{"demo": "UnstyledProgressIntroduction.js", "defaultCodeOpen": false, "bg": "gradient"}} + +## Installation + +Base UI components are all available as a single package. + + + +```bash npm +npm install @base_ui/react +``` + +```bash yarn +yarn add @base_ui/react +``` + +```bash pnpm +pnpm add @base_ui/react +``` + + + +Once you have the package installed, import the component. + +```jsx +import * as Progress from '@base_ui/react/Progress'; +``` + +### Anatomy + +Progress + +- `` is a top-level component that wraps the other components. +- `` renders the rail that represents the total length or duration of progress. +- `` renders the filled portion of the track. + +```tsx + + + + + +``` + +## Value + +### Determinate + +The `value` prop represents the percentage value of the Progress component. The default minimum and maximum values are `0` and `100`, and can be changed with the `min` and `max` props. When progress is determinate the `data-state` attribute is initially `'loading'`, changing to `'complete'` when `value` equals `max`. + +```tsx +function App() { + const [progressValue] = React.useState(25); + return ( + + + + + + ); +} +``` + +### Indeterminate + +Set `value` to `null` to configure an indeterminate progress bar. The `data-state` attribute will be set to `indeterminate`. + +```tsx + + + + + +``` + +{{"demo": "IndeterminateProgress.js"}} + +## RTL + +Set the `direction` prop to `'rtl'` to change the direction that the `Indicator` fills towards for right-to-left languages: + +```jsx +{/* Subcomponents */} +``` + +{{"demo": "RtlProgress.js"}} + +## Overriding default components + +Use the `render` prop to override the rendered element for all subcomponents: + +```jsx +} /> +// or + } /> +``` + +## Accessibility + +The Progress component implements the [ARIA progressbar specification](https://www.w3.org/TR/wai-aria-1.2/#progressbar). + +When using Progress, ensure that it has a human-readable text label by using either the `aria-label`, `aria-labelledby`, or `getAriaLabel` prop: + +```tsx + + Loading progress + + + + + +// or + + + + + + +``` diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts index 0ce753a28b..63c7f332e9 100644 --- a/docs/data/base/pages.ts +++ b/docs/data/base/pages.ts @@ -49,7 +49,7 @@ const pages: readonly MuiPage[] = [ children: [ { pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' }, { pathname: '/base-ui/react-dialog', title: 'Dialog' }, - // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' }, + { pathname: '/base-ui/react-progress', title: 'Progress' }, ], }, // { diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index d9d16fe2e6..a90cdc701d 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -169,6 +169,18 @@ module.exports = [ { pathname: '/base-ui/react-popper/components-api/#popper', title: 'Popper' }, { pathname: '/base-ui/react-popup/components-api/#popup', title: 'Popup' }, { pathname: '/base-ui/react-portal/components-api/#portal', title: 'Portal' }, + { + pathname: '/base-ui/react-progress/components-api/#progress-indicator', + title: 'ProgressIndicator', + }, + { + pathname: '/base-ui/react-progress/components-api/#progress-root', + title: 'ProgressRoot', + }, + { + pathname: '/base-ui/react-progress/components-api/#progress-track', + title: 'ProgressTrack', + }, { pathname: '/base-ui/react-select/components-api/#select', title: 'Select' }, { pathname: '/base-ui/react-slider/components-api/#slider-control', @@ -298,6 +310,14 @@ module.exports = [ pathname: '/base-ui/react-select/hooks-api/#use-option-context-stabilizer', title: 'useOptionContextStabilizer', }, + { + pathname: '/base-ui/react-progress/hooks-api/#use-progress-indicator', + title: 'useProgressIndicator', + }, + { + pathname: '/base-ui/react-progress/hooks-api/#use-progress-root', + title: 'useProgressRoot', + }, { pathname: '/base-ui/react-select/hooks-api/#use-select', title: 'useSelect' }, { pathname: '/base-ui/react-slider/hooks-api/#use-slider-control', diff --git a/docs/pages/base-ui/api/progress-indicator.json b/docs/pages/base-ui/api/progress-indicator.json new file mode 100644 index 0000000000..aa7e0347eb --- /dev/null +++ b/docs/pages/base-ui/api/progress-indicator.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "ProgressIndicator", + "imports": [ + "import * as Progress from '@base_ui/react/Progress';\nconst ProgressIndicator = Progress.Indicator;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "ProgressIndicator", + "forwardsRefTo": "HTMLSpanElement", + "filename": "/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/progress-root.json b/docs/pages/base-ui/api/progress-root.json new file mode 100644 index 0000000000..3f19432036 --- /dev/null +++ b/docs/pages/base-ui/api/progress-root.json @@ -0,0 +1,43 @@ +{ + "props": { + "aria-label": { "type": { "name": "string" } }, + "aria-labelledby": { "type": { "name": "string" } }, + "aria-valuetext": { "type": { "name": "string" } }, + "className": { "type": { "name": "union", "description": "func
| string" } }, + "direction": { + "type": { "name": "enum", "description": "'ltr'
| 'rtl'" }, + "default": "'ltr'" + }, + "getAriaLabel": { + "type": { "name": "func" }, + "signature": { + "type": "function(value: number | null) => string", + "describedArgs": ["value"] + } + }, + "getAriaValueText": { + "type": { "name": "func" }, + "signature": { + "type": "function(value: number | null) => string", + "describedArgs": ["value"] + } + }, + "max": { "type": { "name": "number" }, "default": "100" }, + "min": { "type": { "name": "number" }, "default": "0" }, + "render": { "type": { "name": "union", "description": "element
| func" } }, + "value": { "type": { "name": "number" }, "default": "null" } + }, + "name": "ProgressRoot", + "imports": [ + "import * as Progress from '@base_ui/react/Progress';\nconst ProgressRoot = Progress.Root;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "ProgressRoot", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Progress/Root/ProgressRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/progress-track.json b/docs/pages/base-ui/api/progress-track.json new file mode 100644 index 0000000000..d3c67ca359 --- /dev/null +++ b/docs/pages/base-ui/api/progress-track.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "ProgressTrack", + "imports": [ + "import * as Progress from '@base_ui/react/Progress';\nconst ProgressTrack = Progress.Track;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "ProgressTrack", + "forwardsRefTo": "HTMLSpanElement", + "filename": "/packages/mui-base/src/Progress/Track/ProgressTrack.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-progress-indicator.json b/docs/pages/base-ui/api/use-progress-indicator.json new file mode 100644 index 0000000000..7d06812894 --- /dev/null +++ b/docs/pages/base-ui/api/use-progress-indicator.json @@ -0,0 +1,27 @@ +{ + "parameters": { + "value": { + "type": { "name": "number | null", "description": "number | null" }, + "required": true + }, + "direction": { + "type": { "name": "ProgressDirection", "description": "ProgressDirection" }, + "default": "'ltr'" + }, + "max": { "type": { "name": "number", "description": "number" }, "default": "100" }, + "min": { "type": { "name": "number", "description": "number" }, "default": "0" } + }, + "returnValue": { + "getRootProps": { + "type": { + "name": "(externalProps?: React.ComponentPropsWithRef<'span'>) => React.ComponentPropsWithRef<'span'>", + "description": "(externalProps?: React.ComponentPropsWithRef<'span'>) => React.ComponentPropsWithRef<'span'>" + }, + "required": true + } + }, + "name": "useProgressIndicator", + "filename": "/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts", + "imports": ["import { useProgressIndicator } from '@base_ui/react/Progress';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-progress-root.json b/docs/pages/base-ui/api/use-progress-root.json new file mode 100644 index 0000000000..bd60ccd8f7 --- /dev/null +++ b/docs/pages/base-ui/api/use-progress-root.json @@ -0,0 +1,57 @@ +{ + "parameters": { + "value": { + "type": { "name": "number | null", "description": "number | null" }, + "default": "null", + "required": true + }, + "aria-label": { "type": { "name": "string", "description": "string" } }, + "aria-labelledby": { "type": { "name": "string", "description": "string" } }, + "aria-valuetext": { "type": { "name": "string", "description": "string" } }, + "direction": { + "type": { "name": "ProgressDirection", "description": "ProgressDirection" }, + "default": "'ltr'" + }, + "getAriaLabel": { + "type": { + "name": "(index: number | null) => string", + "description": "(index: number | null) => string" + } + }, + "getAriaValueText": { + "type": { + "name": "(value: number | null) => string", + "description": "(value: number | null) => string" + } + }, + "max": { "type": { "name": "number", "description": "number" }, "default": "100" }, + "min": { "type": { "name": "number", "description": "number" }, "default": "0" } + }, + "returnValue": { + "direction": { + "type": { "name": "ProgressDirection", "description": "ProgressDirection" }, + "required": true + }, + "getRootProps": { + "type": { + "name": "(externalProps?: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>", + "description": "(externalProps?: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>" + }, + "required": true + }, + "max": { "type": { "name": "number", "description": "number" }, "required": true }, + "min": { "type": { "name": "number", "description": "number" }, "required": true }, + "state": { + "type": { "name": "ProgressStatus", "description": "ProgressStatus" }, + "required": true + }, + "value": { + "type": { "name": "number | null", "description": "number | null" }, + "required": true + } + }, + "name": "useProgressRoot", + "filename": "/packages/mui-base/src/Progress/Root/useProgressRoot.ts", + "imports": ["import { useProgressRoot } from '@base_ui/react/Progress';"], + "demos": "" +} diff --git a/docs/pages/base-ui/react-progress/[docsTab]/index.js b/docs/pages/base-ui/react-progress/[docsTab]/index.js new file mode 100644 index 0000000000..e96594e077 --- /dev/null +++ b/docs/pages/base-ui/react-progress/[docsTab]/index.js @@ -0,0 +1,86 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/progress/progress.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import ProgressIndicatorApiJsonPageContent from '../../api/progress-indicator.json'; +import ProgressRootApiJsonPageContent from '../../api/progress-root.json'; +import ProgressTrackApiJsonPageContent from '../../api/progress-track.json'; +import useProgressIndicatorApiJsonPageContent from '../../api/use-progress-indicator.json'; +import useProgressRootApiJsonPageContent from '../../api/use-progress-root.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const ProgressIndicatorApiReq = require.context( + 'docs-base/translations/api-docs/progress-indicator', + false, + /\.\/progress-indicator.*.json$/, + ); + const ProgressIndicatorApiDescriptions = mapApiPageTranslations(ProgressIndicatorApiReq); + + const ProgressRootApiReq = require.context( + 'docs-base/translations/api-docs/progress-root', + false, + /\.\/progress-root.*.json$/, + ); + const ProgressRootApiDescriptions = mapApiPageTranslations(ProgressRootApiReq); + + const ProgressTrackApiReq = require.context( + 'docs-base/translations/api-docs/progress-track', + false, + /\.\/progress-track.*.json$/, + ); + const ProgressTrackApiDescriptions = mapApiPageTranslations(ProgressTrackApiReq); + + const useProgressIndicatorApiReq = require.context( + 'docs-base/translations/api-docs/use-progress-indicator', + false, + /\.\/use-progress-indicator.*.json$/, + ); + const useProgressIndicatorApiDescriptions = mapApiPageTranslations(useProgressIndicatorApiReq); + + const useProgressRootApiReq = require.context( + 'docs-base/translations/api-docs/use-progress-root', + false, + /\.\/use-progress-root.*.json$/, + ); + const useProgressRootApiDescriptions = mapApiPageTranslations(useProgressRootApiReq); + + return { + props: { + componentsApiDescriptions: { + ProgressIndicator: ProgressIndicatorApiDescriptions, + ProgressRoot: ProgressRootApiDescriptions, + ProgressTrack: ProgressTrackApiDescriptions, + }, + componentsApiPageContents: { + ProgressIndicator: ProgressIndicatorApiJsonPageContent, + ProgressRoot: ProgressRootApiJsonPageContent, + ProgressTrack: ProgressTrackApiJsonPageContent, + }, + hooksApiDescriptions: { + useProgressIndicator: useProgressIndicatorApiDescriptions, + useProgressRoot: useProgressRootApiDescriptions, + }, + hooksApiPageContents: { + useProgressIndicator: useProgressIndicatorApiJsonPageContent, + useProgressRoot: useProgressRootApiJsonPageContent, + }, + }, + }; +}; diff --git a/docs/pages/base-ui/react-progress/index.js b/docs/pages/base-ui/react-progress/index.js new file mode 100644 index 0000000000..ae0ef5d8da --- /dev/null +++ b/docs/pages/base-ui/react-progress/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/progress/progress.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/pages/experiments/progress.tsx b/docs/pages/experiments/progress.tsx new file mode 100644 index 0000000000..f98f217bb4 --- /dev/null +++ b/docs/pages/experiments/progress.tsx @@ -0,0 +1,241 @@ +import * as React from 'react'; +import { useTheme } from '@mui/system'; +import * as Progress from '@base_ui/react/Progress'; + +const VAL1 = 33; + +const CUSTOM_BUFFER_VAL = 77; + +export default function ProgressDemos() { + return ( +
+ + + + + + + + + Indeterminate Progress + + + + + + + + + Progress (RTL) + + + + + + + + + Indeterminate (RTL) + + + + + + +

Customizations

+ + `${value}% complete, ${CUSTOM_BUFFER_VAL}% buffered`} + max={Math.min(100, CUSTOM_BUFFER_VAL)} + > + + Custom Buffer Component + + + + + + + + `${value}% complete, ${CUSTOM_BUFFER_VAL}% buffered`} + max={Math.min(100, CUSTOM_BUFFER_VAL)} + direction="rtl" + > + + Custom Buffer Component (RTL) + + + + + + + +
+ ); +} + +function MyBuffer(props: any) { + const { value, style, ...rest } = props; + const percentageValue = valueToPercent(value, 0, 100); + return ( + + ); +} + +function valueToPercent(value: number | undefined, min: number, max: number) { + if (value === undefined) { + return value; + } + + return ((value - min) * 100) / (max - min); +} + +const grey = { + 50: '#F3F6F9', + 100: '#E5EAF2', + 200: '#DAE2ED', + 300: '#C7D0DD', + 400: '#B0B8C4', + 500: '#9DA8B7', + 600: '#6B7A90', + 700: '#434D5B', + 800: '#303740', + 900: '#1C2025', +}; + +const BLUE400 = '#3399FF'; +const BLUE500 = '#007FFF'; + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +export function Styles() { + const isDarkMode = useIsDarkMode(); + return ( + + ); +} diff --git a/docs/translations/api-docs/progress-indicator/progress-indicator.json b/docs/translations/api-docs/progress-indicator/progress-indicator.json new file mode 100644 index 0000000000..4bc12cf1e0 --- /dev/null +++ b/docs/translations/api-docs/progress-indicator/progress-indicator.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/progress-root/progress-root.json b/docs/translations/api-docs/progress-root/progress-root.json new file mode 100644 index 0000000000..cdac80157b --- /dev/null +++ b/docs/translations/api-docs/progress-root/progress-root.json @@ -0,0 +1,31 @@ +{ + "componentDescription": "", + "propDescriptions": { + "aria-label": { "description": "The label for the Indicator component." }, + "aria-labelledby": { + "description": "An id or space-separated list of ids of elements that label the Indicator component." + }, + "aria-valuetext": { + "description": "A string value that provides a human-readable text alternative for the current value of the progress indicator." + }, + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "direction": { "description": "The direction that progress bars fill in" }, + "getAriaLabel": { + "description": "Accepts a function which returns a string value that provides an accessible name for the Indicator component", + "typeDescriptions": { "value": "The component's value" } + }, + "getAriaValueText": { + "description": "Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator.", + "typeDescriptions": { "value": "The component's value to format" } + }, + "max": { "description": "The maximum value" }, + "min": { "description": "The minimum value" }, + "render": { "description": "A function to customize rendering of the component." }, + "value": { + "description": "The current value. The component is indeterminate when value is null." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/progress-track/progress-track.json b/docs/translations/api-docs/progress-track/progress-track.json new file mode 100644 index 0000000000..4bc12cf1e0 --- /dev/null +++ b/docs/translations/api-docs/progress-track/progress-track.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json b/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json new file mode 100644 index 0000000000..3c787fafc5 --- /dev/null +++ b/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json @@ -0,0 +1,12 @@ +{ + "hookDescription": "", + "parametersDescriptions": { + "direction": { "description": "The direction that progress bars fill in" }, + "max": { "description": "The maximum value" }, + "min": { "description": "The minimum value" }, + "value": { + "description": "The current value. The component is indeterminate when value is null." + } + }, + "returnValueDescriptions": {} +} diff --git a/docs/translations/api-docs/use-progress-root/use-progress-root.json b/docs/translations/api-docs/use-progress-root/use-progress-root.json new file mode 100644 index 0000000000..a9fdd3927f --- /dev/null +++ b/docs/translations/api-docs/use-progress-root/use-progress-root.json @@ -0,0 +1,30 @@ +{ + "hookDescription": "", + "parametersDescriptions": { + "aria-label": { "description": "The label for the Indicator component." }, + "aria-labelledby": { + "description": "An id or space-separated list of ids of elements that label the Indicator component." + }, + "aria-valuetext": { + "description": "A string value that provides a human-readable text alternative for the current value of the progress indicator." + }, + "direction": { "description": "The direction that progress bars fill in" }, + "getAriaLabel": { + "description": "Accepts a function which returns a string value that provides an accessible name for the Indicator component" + }, + "getAriaValueText": { + "description": "Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator." + }, + "max": { "description": "The maximum value" }, + "min": { "description": "The minimum value" }, + "value": { + "description": "The current value. The component is indeterminate when value is null." + } + }, + "returnValueDescriptions": { + "direction": { "description": "The direction that progress bars fill in" }, + "max": { "description": "The maximum value" }, + "min": { "description": "The minimum value" }, + "value": { "description": "Value of the component" } + } +} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 7be5cd967b..c7b63b0824 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -230,6 +230,7 @@ "feedback": "Feedback", "/base-ui/react-alert-dialog": "Alert Dialog", "/base-ui/react-dialog": "Dialog", + "/base-ui/react-progress": "Progress", "navigation": "Navigation", "/base-ui/react-tabs": "Tabs", "/base-ui/guides": "How-to guides", diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx new file mode 100644 index 0000000000..3fe97be71f --- /dev/null +++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Progress from '@base_ui/react/Progress'; +import { ProgressContext } from '@base_ui/react/Progress'; +import { describeConformance } from '../../../test/describeConformance'; +import type { ProgressContextValue } from '../Root/ProgressRoot.types'; + +const contextValue: ProgressContextValue = { + direction: 'ltr', + max: 100, + min: 0, + value: 30, + state: 'loading', + ownerState: { + direction: 'ltr', + max: 100, + min: 0, + }, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'span', + render: (node) => { + const { container, ...other } = render( + {node}, + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLSpanElement, + })); + + describe('internal styles', () => { + it('determinate', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { getByTestId } = render( + + + + + , + ); + + const indicator = getByTestId('indicator'); + + expect(indicator).toHaveComputedStyle({ + left: '0px', + width: '33%', + }); + }); + + it('indeterminate', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { getByTestId } = render( + + + + + , + ); + + const indicator = getByTestId('indicator'); + + expect(indicator).toHaveComputedStyle({}); + }); + }); +}); diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx new file mode 100644 index 0000000000..180072c34c --- /dev/null +++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx @@ -0,0 +1,57 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useProgressIndicator } from './useProgressIndicator'; +import { useProgressContext } from '../Root/ProgressContext'; +import { progressStyleHookMapping } from '../Root/styleHooks'; +import { ProgressIndicatorProps } from './ProgressIndicator.types'; + +const ProgressIndicator = React.forwardRef(function ProgressIndicator( + props: ProgressIndicatorProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { direction, max, min, value, ownerState } = useProgressContext(); + + const { getRootProps } = useProgressIndicator({ + direction, + max, + min, + value, + }); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'span', + ownerState, + className, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: progressStyleHookMapping, + }); + + return renderElement(); +}); + +ProgressIndicator.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { ProgressIndicator }; diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts new file mode 100644 index 0000000000..dcdf95ae69 --- /dev/null +++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts @@ -0,0 +1,33 @@ +import { BaseUIComponentProps } from '../../utils/types'; +import { ProgressDirection, ProgressRootOwnerState } from '../Root/ProgressRoot.types'; + +export interface ProgressIndicatorProps + extends BaseUIComponentProps<'span', ProgressRootOwnerState> {} + +export interface UseProgressIndicatorParameters { + /** + * The direction that progress bars fill in + * @default 'ltr' + */ + direction?: ProgressDirection; + /** + * The maximum value + * @default 100 + */ + max?: number; + /** + * The minimum value + * @default 0 + */ + min?: number; + /** + * The current value. The component is indeterminate when value is `null`. + */ + value: number | null; +} + +export interface UseProgressIndicatorReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'span'>, + ) => React.ComponentPropsWithRef<'span'>; +} diff --git a/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts b/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts new file mode 100644 index 0000000000..883501567a --- /dev/null +++ b/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts @@ -0,0 +1,57 @@ +'use client'; +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { + UseProgressIndicatorParameters, + UseProgressIndicatorReturnValue, +} from './ProgressIndicator.types'; + +function valueToPercent(value: number, min: number, max: number) { + return ((value - min) * 100) / (max - min); +} +/** + * + * Demos: + * + * - [Progress](https://mui.com/base-ui/react-progress/#hooks) + * + * API: + * + * - [useProgressIndicator API](https://mui.com/base-ui/react-progress/hooks-api/#use-progress-indicator) + */ +function useProgressIndicator( + parameters: UseProgressIndicatorParameters, +): UseProgressIndicatorReturnValue { + const { direction, max = 100, min = 0, value } = parameters; + + const isRtl = direction === 'rtl'; + + const percentageValue = + Number.isFinite(value) && value !== null ? valueToPercent(value, min, max) : null; + + const getStyles = React.useCallback(() => { + if (!percentageValue) { + return {}; + } + + return { + [isRtl ? 'right' : 'left']: 0, + height: 'inherit', + width: `${percentageValue}%`, + }; + }, [isRtl, percentageValue]); + + const getRootProps: UseProgressIndicatorReturnValue['getRootProps'] = React.useCallback( + (externalProps = {}) => + mergeReactProps<'span'>(externalProps, { + style: getStyles(), + }), + [getStyles], + ); + + return { + getRootProps, + }; +} + +export { useProgressIndicator }; diff --git a/packages/mui-base/src/Progress/Root/ProgressContext.tsx b/packages/mui-base/src/Progress/Root/ProgressContext.tsx new file mode 100644 index 0000000000..cfdddc11a1 --- /dev/null +++ b/packages/mui-base/src/Progress/Root/ProgressContext.tsx @@ -0,0 +1,25 @@ +'use client'; +import * as React from 'react'; +import { ProgressContextValue } from './ProgressRoot.types'; + +export interface ProgressProviderProps { + value: ProgressContextValue; + children: React.ReactNode; +} + +/** + * @ignore - internal component. + */ +export const ProgressContext = React.createContext(undefined); + +if (process.env.NODE_ENV !== 'production') { + ProgressContext.displayName = 'ProgressContext'; +} + +export function useProgressContext() { + const context = React.useContext(ProgressContext); + if (context === undefined) { + throw new Error('useProgressContext must be used inside a Progress component'); + } + return context; +} diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx b/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx new file mode 100644 index 0000000000..928b063f71 --- /dev/null +++ b/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx @@ -0,0 +1,64 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Progress from '@base_ui/react/Progress'; +import { describeConformance } from '../../../test/describeConformance'; +import type { ProgressRootProps } from './ProgressRoot.types'; + +function TestProgress(props: ProgressRootProps) { + return ( + + + + + + ); +} + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + })); + + it('renders a progressbar', () => { + const { getByRole } = render( + + + + + , + ); + + expect(getByRole('progressbar')).to.have.attribute('aria-valuenow', '30'); + }); + + describe('ARIA attributes', () => { + it('sets the correct aria attributes', () => { + const { getByRole } = render( + + + + + , + ); + + const progressbar = getByRole('progressbar'); + + expect(progressbar).to.have.attribute('aria-valuenow', '30'); + expect(progressbar).to.have.attribute('aria-valuemin', '0'); + expect(progressbar).to.have.attribute('aria-valuemax', '100'); + expect(progressbar).to.have.attribute('aria-valuetext', '30%'); + }); + + it('should update aria-valuenow when value changes', () => { + const { getByRole, setProps } = render(); + const progressbar = getByRole('progressbar'); + setProps({ value: 77 }); + expect(progressbar).to.have.attribute('aria-valuenow', '77'); + }); + }); +}); diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.tsx b/packages/mui-base/src/Progress/Root/ProgressRoot.tsx new file mode 100644 index 0000000000..6e3787d3ba --- /dev/null +++ b/packages/mui-base/src/Progress/Root/ProgressRoot.tsx @@ -0,0 +1,141 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useProgressRoot } from './useProgressRoot'; +import { ProgressContext } from './ProgressContext'; +import { progressStyleHookMapping } from './styleHooks'; +import { + ProgressContextValue, + ProgressRootOwnerState, + ProgressRootProps, +} from './ProgressRoot.types'; + +const ProgressRoot = React.forwardRef(function ProgressRoot( + props: ProgressRootProps, + forwardedRef: React.ForwardedRef, +) { + const { + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuetext': ariaValuetext, + direction = 'ltr', + getAriaLabel, + getAriaValueText, + max = 100, + min = 0, + value, + render, + className, + ...otherProps + } = props; + + const { getRootProps, ...progress } = useProgressRoot({ + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuetext': ariaValuetext, + direction, + getAriaLabel, + getAriaValueText, + max, + min, + value, + }); + + const ownerState: ProgressRootOwnerState = React.useMemo( + () => ({ + direction, + max, + min, + state: progress.state, + }), + [direction, max, min, progress.state], + ); + + const contextValue: ProgressContextValue = React.useMemo( + () => ({ + ...progress, + ownerState, + }), + [progress, ownerState], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + ownerState, + className, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: progressStyleHookMapping, + }); + + return ( + {renderElement()} + ); +}); + +ProgressRoot.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * The label for the Indicator component. + */ + 'aria-label': PropTypes.string, + /** + * An id or space-separated list of ids of elements that label the Indicator component. + */ + 'aria-labelledby': PropTypes.string, + /** + * A string value that provides a human-readable text alternative for the current value of the progress indicator. + */ + 'aria-valuetext': PropTypes.string, + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * The direction that progress bars fill in + * @default 'ltr' + */ + direction: PropTypes.oneOf(['ltr', 'rtl']), + /** + * Accepts a function which returns a string value that provides an accessible name for the Indicator component + * @param {number | null} value The component's value + * @returns {string} + */ + getAriaLabel: PropTypes.func, + /** + * Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator. + * @param {number | null} value The component's value to format + * @returns {string} + */ + getAriaValueText: PropTypes.func, + /** + * The maximum value + * @default 100 + */ + max: PropTypes.number, + /** + * The minimum value + * @default 0 + */ + min: PropTypes.number, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * The current value. The component is indeterminate when value is `null`. + * @default null + */ + value: PropTypes.number, +} as any; + +export { ProgressRoot }; diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts b/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts new file mode 100644 index 0000000000..5dec7d0f1a --- /dev/null +++ b/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts @@ -0,0 +1,89 @@ +import { BaseUIComponentProps } from '../../utils/types'; + +export type ProgressContextValue = Omit & { + ownerState: ProgressRootOwnerState; +}; + +export type ProgressRootOwnerState = { + direction: ProgressDirection; + max: number; + min: number; +}; + +export interface ProgressRootProps + extends UseProgressRootParameters, + BaseUIComponentProps<'div', ProgressRootOwnerState> {} + +export type ProgressDirection = 'ltr' | 'rtl'; + +export type ProgressStatus = 'indeterminate' | 'loading' | 'complete'; + +export interface UseProgressRootParameters { + /** + * The label for the Indicator component. + */ + 'aria-label'?: string; + /** + * An id or space-separated list of ids of elements that label the Indicator component. + */ + 'aria-labelledby'?: string; + /** + * A string value that provides a human-readable text alternative for the current value of the progress indicator. + */ + 'aria-valuetext'?: string; + /** + * The direction that progress bars fill in + * @default 'ltr' + */ + direction?: ProgressDirection; + /** + * Accepts a function which returns a string value that provides an accessible name for the Indicator component + * @param {number | null} value The component's value + * @returns {string} + */ + getAriaLabel?: (index: number | null) => string; + /** + * Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator. + * @param {number | null} value The component's value to format + * @returns {string} + */ + getAriaValueText?: (value: number | null) => string; + /** + * The maximum value + * @default 100 + */ + max?: number; + /** + * The minimum value + * @default 0 + */ + min?: number; + /** + * The current value. The component is indeterminate when value is `null`. + * @default null + */ + value: number | null; +} + +export interface UseProgressRootReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'div'>, + ) => React.ComponentPropsWithRef<'div'>; + /** + * The direction that progress bars fill in + */ + direction: ProgressDirection; + /** + * The maximum value + */ + max: number; + /** + * The minimum value + */ + min: number; + /** + * Value of the component + */ + value: number | null; + state: ProgressStatus; +} diff --git a/packages/mui-base/src/Progress/Root/styleHooks.ts b/packages/mui-base/src/Progress/Root/styleHooks.ts new file mode 100644 index 0000000000..808c29c561 --- /dev/null +++ b/packages/mui-base/src/Progress/Root/styleHooks.ts @@ -0,0 +1,8 @@ +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import type { ProgressRootOwnerState } from './ProgressRoot.types'; + +export const progressStyleHookMapping: CustomStyleHookMapping = { + direction: () => null, + max: () => null, + min: () => null, +}; diff --git a/packages/mui-base/src/Progress/Root/useProgressRoot.ts b/packages/mui-base/src/Progress/Root/useProgressRoot.ts new file mode 100644 index 0000000000..c8d612ecab --- /dev/null +++ b/packages/mui-base/src/Progress/Root/useProgressRoot.ts @@ -0,0 +1,81 @@ +'use client'; +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { + ProgressStatus, + UseProgressRootParameters, + UseProgressRootReturnValue, +} from './ProgressRoot.types'; + +function getDefaultAriaValueText(value: number | null) { + if (value === null) { + return 'indeterminate progress'; + } + + return `${value}%`; +} /** + * + * Demos: + * + * - [Progress](https://mui.com/base-ui/react-progress/#hooks) + * + * API: + * + * - [useProgressRoot API](https://mui.com/base-ui/react-progress/hooks-api/#use-progress-root) + */ +function useProgressRoot(parameters: UseProgressRootParameters): UseProgressRootReturnValue { + const { + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuetext': ariaValuetext, + direction = 'ltr', + getAriaLabel, + getAriaValueText, + max = 100, + min = 0, + value, + } = parameters; + + let state: ProgressStatus = 'indeterminate'; + if (Number.isFinite(value)) { + state = value === max ? 'complete' : 'loading'; + } + + const getRootProps: UseProgressRootReturnValue['getRootProps'] = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + 'aria-label': getAriaLabel ? getAriaLabel(value) : ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-valuemax': max, + 'aria-valuemin': min, + 'aria-valuenow': value ?? undefined, + 'aria-valuetext': getAriaValueText + ? getAriaValueText(value) + : ariaValuetext ?? getDefaultAriaValueText(value), + dir: direction, + role: 'progressbar', + }), + [ + ariaLabel, + ariaLabelledby, + ariaValuetext, + direction, + getAriaLabel, + getAriaValueText, + max, + min, + value, + ], + ); + + return { + getRootProps, + direction, + max, + min, + value, + state, + }; +} + +export { useProgressRoot }; diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx b/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx new file mode 100644 index 0000000000..0fb5ec9415 --- /dev/null +++ b/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import * as Progress from '@base_ui/react/Progress'; +import { ProgressContext } from '@base_ui/react/Progress'; +import { describeConformance } from '../../../test/describeConformance'; +import type { ProgressContextValue } from '../Root/ProgressRoot.types'; + +const contextValue: ProgressContextValue = { + direction: 'ltr', + max: 100, + min: 0, + value: 30, + state: 'loading', + ownerState: { + direction: 'ltr', + max: 100, + min: 0, + }, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'span', + render: (node) => { + const { container, ...other } = render( + {node}, + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLSpanElement, + })); +}); diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.tsx b/packages/mui-base/src/Progress/Track/ProgressTrack.tsx new file mode 100644 index 0000000000..c18036eae8 --- /dev/null +++ b/packages/mui-base/src/Progress/Track/ProgressTrack.tsx @@ -0,0 +1,48 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useProgressContext } from '../Root/ProgressContext'; +import { progressStyleHookMapping } from '../Root/styleHooks'; +import { ProgressTrackProps } from './ProgressTrack.types'; + +const ProgressTrack = React.forwardRef(function ProgressTrack( + props: ProgressTrackProps, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { ownerState } = useProgressContext(); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'span', + ownerState, + className, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: progressStyleHookMapping, + }); + + return renderElement(); +}); + +ProgressTrack.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { ProgressTrack }; diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts b/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts new file mode 100644 index 0000000000..3700c4dfbe --- /dev/null +++ b/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts @@ -0,0 +1,4 @@ +import { BaseUIComponentProps } from '../../utils/types'; +import { ProgressRootOwnerState } from '../Root/ProgressRoot.types'; + +export interface ProgressTrackProps extends BaseUIComponentProps<'span', ProgressRootOwnerState> {} diff --git a/packages/mui-base/src/Progress/index.barrel.ts b/packages/mui-base/src/Progress/index.barrel.ts new file mode 100644 index 0000000000..a22c330c34 --- /dev/null +++ b/packages/mui-base/src/Progress/index.barrel.ts @@ -0,0 +1,15 @@ +export { ProgressRoot } from './Root/ProgressRoot'; +export type * from './Root/ProgressRoot.types'; +export { useProgressRoot } from './Root/useProgressRoot'; +export * from './Root/ProgressContext'; + +export { ProgressTrack } from './Track/ProgressTrack'; +export type { ProgressTrackProps } from './Track/ProgressTrack.types'; + +export { ProgressIndicator } from './Indicator/ProgressIndicator'; +export type { + ProgressIndicatorProps, + UseProgressIndicatorParameters, + UseProgressIndicatorReturnValue, +} from './Indicator/ProgressIndicator.types'; +export { useProgressIndicator } from './Indicator/useProgressIndicator'; diff --git a/packages/mui-base/src/Progress/index.ts b/packages/mui-base/src/Progress/index.ts new file mode 100644 index 0000000000..b0997635a6 --- /dev/null +++ b/packages/mui-base/src/Progress/index.ts @@ -0,0 +1,21 @@ +export { ProgressRoot as Root } from './Root/ProgressRoot'; +export { + ProgressRootOwnerState as ProgressOwnerState, + ProgressRootProps as RootProps, + UseProgressRootParameters, + UseProgressRootReturnValue, + ProgressContextValue, +} from './Root/ProgressRoot.types'; +export { useProgressRoot } from './Root/useProgressRoot'; +export * from './Root/ProgressContext'; + +export { ProgressTrack as Track } from './Track/ProgressTrack'; +export type { ProgressTrackProps as TrackProps } from './Track/ProgressTrack.types'; + +export { ProgressIndicator as Indicator } from './Indicator/ProgressIndicator'; +export type { + ProgressIndicatorProps as IndicatorProps, + UseProgressIndicatorParameters, + UseProgressIndicatorReturnValue, +} from './Indicator/ProgressIndicator.types'; +export { useProgressIndicator } from './Indicator/useProgressIndicator'; diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts index 9a78cfba56..7f29893f01 100644 --- a/packages/mui-base/src/index.ts +++ b/packages/mui-base/src/index.ts @@ -3,6 +3,7 @@ export * from './Checkbox/index.barrel'; export * from './Dialog/index.barrel'; export * from './NumberField/index.barrel'; export * from './Popover/index.barrel'; +export * from './Progress/index.barrel'; export * from './Slider/index.barrel'; export * from './Switch/index.barrel'; export * from './Tabs/index.barrel';