diff --git a/packages/react-components/react-progress/bundle-size/Progress.fixture.js b/packages/react-components/react-progress/bundle-size/Progress.fixture.js new file mode 100644 index 0000000000000..73a0b613245c9 --- /dev/null +++ b/packages/react-components/react-progress/bundle-size/Progress.fixture.js @@ -0,0 +1,7 @@ +import { Progress } from '@fluentui/react-progress'; + +console.log(Progress); + +export default { + name: 'Progress', +}; diff --git a/packages/react-components/react-progress/etc/react-progress.api.md b/packages/react-components/react-progress/etc/react-progress.api.md index 88fe6d26784e0..e68d89f5ffb96 100644 --- a/packages/react-components/react-progress/etc/react-progress.api.md +++ b/packages/react-components/react-progress/etc/react-progress.api.md @@ -18,15 +18,23 @@ export const Progress: ForwardRefComponent; export const progressClassNames: SlotClassNames; // @public -export type ProgressProps = ComponentProps & {}; +export type ProgressProps = Omit, 'size'> & { + indeterminate?: boolean; + percentComplete?: number; + thickness?: 'medium' | 'large'; +}; // @public (undocumented) export type ProgressSlots = { - root: Slot<'div'>; + root: NonNullable>; + label?: Slot<'span'>; + bar?: NonNullable>; + track?: NonNullable>; + description?: Slot<'span'>; }; // @public -export type ProgressState = ComponentState; +export type ProgressState = ComponentState & Required>; // @public export const renderProgress_unstable: (state: ProgressState) => JSX.Element; diff --git a/packages/react-components/react-progress/package.json b/packages/react-components/react-progress/package.json index 658565b1c98d8..2614f42fd2705 100644 --- a/packages/react-components/react-progress/package.json +++ b/packages/react-components/react-progress/package.json @@ -14,6 +14,7 @@ "license": "MIT", "scripts": { "build": "just-scripts build", + "bundle-size": "bundle-size measure", "clean": "just-scripts clean", "code-style": "just-scripts code-style", "just": "just-scripts", @@ -32,6 +33,7 @@ "@fluentui/scripts": "^1.0.0" }, "dependencies": { + "@fluentui/react-shared-contexts": "^9.0.1", "@fluentui/react-theme": "^9.1.0", "@fluentui/react-utilities": "^9.1.0", "@griffel/react": "^1.3.0", diff --git a/packages/react-components/react-progress/src/components/Progress/Progress.test.tsx b/packages/react-components/react-progress/src/components/Progress/Progress.test.tsx index 02f0aa7d114c9..467578b5ef475 100644 --- a/packages/react-components/react-progress/src/components/Progress/Progress.test.tsx +++ b/packages/react-components/react-progress/src/components/Progress/Progress.test.tsx @@ -7,6 +7,16 @@ describe('Progress', () => { isConformant({ Component: Progress, displayName: 'Progress', + testOptions: { + 'has-static-classnames': [ + { + props: { + label: 'Test Label', + description: 'Test Description', + }, + }, + ], + }, }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests diff --git a/packages/react-components/react-progress/src/components/Progress/Progress.tsx b/packages/react-components/react-progress/src/components/Progress/Progress.tsx index f0214b25fab14..314b815dff094 100644 --- a/packages/react-components/react-progress/src/components/Progress/Progress.tsx +++ b/packages/react-components/react-progress/src/components/Progress/Progress.tsx @@ -6,7 +6,7 @@ import type { ProgressProps } from './Progress.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * Progress component - TODO: add more docs + * A progress bar shows the progression of a task. */ export const Progress: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useProgress_unstable(props, ref); diff --git a/packages/react-components/react-progress/src/components/Progress/Progress.types.ts b/packages/react-components/react-progress/src/components/Progress/Progress.types.ts index c109d5e10d383..9c20d7f6b7890 100644 --- a/packages/react-components/react-progress/src/components/Progress/Progress.types.ts +++ b/packages/react-components/react-progress/src/components/Progress/Progress.types.ts @@ -1,17 +1,55 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type ProgressSlots = { - root: Slot<'div'>; + /** + * The root of the Progress + * The root slot receives the `className` and `style` specified directly on the ``. + */ + root: NonNullable>; + /** + * The title of the Progress. + * The label slot receives the styling related to the title associated with the Progress. + */ + label?: Slot<'span'>; + /** + * The animated slot of the Progress + * The bar slot receives the styling related to the loading bar associated with the Progress + */ + bar?: NonNullable>; + /** + * The track slot of the Progress + * The track slot receives the styling related to the loading bar track associated with the Progress + */ + track?: NonNullable>; + /** + * The description slot of the Progress + * The description slot receives the styling related to the description associated with the Progress + */ + description?: Slot<'span'>; }; /** * Progress Props */ -export type ProgressProps = ComponentProps & {}; +export type ProgressProps = Omit, 'size'> & { + /** + * Prop to set whether the Progress is determinate or indeterminate + * @default false + */ + indeterminate?: boolean; + /** + * Percentage of the operation's completeness, numerically between 0 and 100. + */ + percentComplete?: number; + /** + * The thickness of the Progress bar + * @default 'medium' + */ + thickness?: 'medium' | 'large'; +}; /** * State used in rendering Progress */ -export type ProgressState = ComponentState; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ProgressProps -// & Required> +export type ProgressState = ComponentState & + Required>; diff --git a/packages/react-components/react-progress/src/components/Progress/__snapshots__/Progress.test.tsx.snap b/packages/react-components/react-progress/src/components/Progress/__snapshots__/Progress.test.tsx.snap index 0d923fa8eeeaa..de2329fcb86df 100644 --- a/packages/react-components/react-progress/src/components/Progress/__snapshots__/Progress.test.tsx.snap +++ b/packages/react-components/react-progress/src/components/Progress/__snapshots__/Progress.test.tsx.snap @@ -4,8 +4,18 @@ exports[`Progress renders a default state 1`] = `
- Default Progress +
+
`; diff --git a/packages/react-components/react-progress/src/components/Progress/renderProgress.tsx b/packages/react-components/react-progress/src/components/Progress/renderProgress.tsx index 1e5071c1e7b62..3816b3b4d9ea1 100644 --- a/packages/react-components/react-progress/src/components/Progress/renderProgress.tsx +++ b/packages/react-components/react-progress/src/components/Progress/renderProgress.tsx @@ -7,7 +7,12 @@ import type { ProgressState, ProgressSlots } from './Progress.types'; */ export const renderProgress_unstable = (state: ProgressState) => { const { slots, slotProps } = getSlots(state); - - // TODO Add additional slots in the appropriate place - return ; + return ( + + {slots.label && } + {slots.track && } + {slots.bar && } + {slots.description && } + + ); }; diff --git a/packages/react-components/react-progress/src/components/Progress/useProgress.ts b/packages/react-components/react-progress/src/components/Progress/useProgress.ts deleted file mode 100644 index 8f8a8f207ffaf..0000000000000 --- a/packages/react-components/react-progress/src/components/Progress/useProgress.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; -import type { ProgressProps, ProgressState } from './Progress.types'; - -/** - * Create the state required to render Progress. - * - * The returned state can be modified with hooks such as useProgressStyles_unstable, - * before being passed to renderProgress_unstable. - * - * @param props - props from this instance of Progress - * @param ref - reference to root HTMLElement of Progress - */ -export const useProgress_unstable = (props: ProgressProps, ref: React.Ref): ProgressState => { - return { - // TODO add appropriate props/defaults - components: { - // TODO add each slot's element type or component - root: 'div', - }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), - root: getNativeElementProps('div', { - ref, - ...props, - }), - }; -}; diff --git a/packages/react-components/react-progress/src/components/Progress/useProgress.tsx b/packages/react-components/react-progress/src/components/Progress/useProgress.tsx new file mode 100644 index 0000000000000..1e9c0eea257e6 --- /dev/null +++ b/packages/react-components/react-progress/src/components/Progress/useProgress.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { getNativeElementProps, resolveShorthand, useId } from '@fluentui/react-utilities'; +import type { ProgressProps, ProgressState } from './Progress.types'; + +/** + * Create the state required to render Progress. + * + * The returned state can be modified with hooks such as useProgressStyles_unstable, + * before being passed to renderProgress_unstable. + * + * @param props - props from this instance of Progress + * @param ref - reference to root HTMLElement of Progress + */ +export const useProgress_unstable = (props: ProgressProps, ref: React.Ref): ProgressState => { + // Props + const { thickness = 'medium', indeterminate = false, percentComplete = 0 } = props; + const baseId = useId('progress-'); + + const root = getNativeElementProps('div', { ref, role: 'progressbar', ...props }); + + const label = resolveShorthand(props.label, { + defaultProps: { + id: baseId + '__label', + }, + }); + + const description = resolveShorthand(props.description, { + defaultProps: { + id: baseId + '__description', + }, + }); + + const bar = resolveShorthand(props.bar, { + required: true, + defaultProps: { + 'aria-valuemin': indeterminate ? undefined : 0, + 'aria-valuemax': indeterminate ? undefined : 100, + 'aria-valuenow': indeterminate ? undefined : Math.floor(percentComplete), + }, + }); + + const track = resolveShorthand(props.track, { + required: true, + }); + + if (label && !root['aria-label'] && !root['aria-labelledby']) { + root['aria-labelledby'] = label.id; + } + + const state: ProgressState = { + indeterminate, + percentComplete, + thickness, + components: { + root: 'div', + bar: 'div', + track: 'div', + label: 'span', + description: 'span', + }, + root, + bar, + track, + label, + description, + }; + + return state; +}; diff --git a/packages/react-components/react-progress/src/components/Progress/useProgressStyles.ts b/packages/react-components/react-progress/src/components/Progress/useProgressStyles.ts index 9ac4217a103f5..949fdd8f9ceb9 100644 --- a/packages/react-components/react-progress/src/components/Progress/useProgressStyles.ts +++ b/packages/react-components/react-progress/src/components/Progress/useProgressStyles.ts @@ -1,33 +1,191 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; -import type { ProgressSlots, ProgressState } from './Progress.types'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { tokens, typographyStyles } from '@fluentui/react-theme'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import type { ProgressState, ProgressSlots } from './Progress.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; export const progressClassNames: SlotClassNames = { root: 'fui-Progress', - // TODO: add class names for all slots on ProgressSlots. - // Should be of the form `: 'fui-Progress__` + bar: 'fui-Progress__bar', + track: 'fui-Progress__track', + label: 'fui-Progress__label', + description: 'fui-Progress__description', +}; + +// If the percentComplete is near 0, don't animate it. +// This prevents animations on reset to 0 scenarios. +const ZERO_THRESHOLD = 0.01; + +// Internal CSS vars +export const progressCssVars = { + percentageCssVar: '--fui-Progress--percentage', +}; + +const barThicknessValues = { + medium: '2px', + large: '4px', +}; + +const indeterminateProgress = { + '0%': { + left: '0%', + }, + '100%': { + left: '100%', + }, }; /** * Styles for the root slot */ -const useStyles = makeStyles({ +const useRootStyles = makeStyles({ root: { - // TODO Add default styles for the root element + display: 'grid', + rowGap: '8px', + ...shorthands.overflow('hidden'), + }, +}); + +/** + * Styles for the title + */ +const useLabelStyles = makeStyles({ + base: { + gridRowStart: '1', + ...typographyStyles.body1, + color: tokens.colorNeutralForeground1, + }, +}); + +/** + * Styles for the description + */ +const useDescriptionStyles = makeStyles({ + base: { + gridRowStart: '3', + ...typographyStyles.caption1, + color: tokens.colorNeutralForeground2, + }, +}); + +/** + * Styles for the progress bar + */ +const useBarStyles = makeStyles({ + base: { + gridColumnStart: '1', + gridRowStart: '2', + backgroundColor: tokens.colorCompoundBrandBackground, + + '@media screen and (forced-colors: active)': { + backgroundColor: 'Highlight', + }, + }, + medium: { + height: barThicknessValues.medium, + }, + large: { + height: barThicknessValues.large, + }, + determinate: { + width: `var(${progressCssVars.percentageCssVar})`, + }, + nonZeroDeterminate: { + transitionProperty: 'width', + transitionDuration: '0.3s', + transitionTimingFunction: 'ease', + }, + indeterminate: { + maxWidth: '33%', + position: 'relative', + backgroundImage: `linear-gradient( + to right, + ${tokens.colorNeutralBackground6} 0%, + ${tokens.colorCompoundBrandBackground} 50%, + ${tokens.colorNeutralBackground6} 100% + )`, + animationName: indeterminateProgress, + animationDuration: '3s', + animationIterationCount: 'infinite', }, - // TODO add additional classes for different states and/or slots + rtl: { + animationDirection: 'reverse', + }, +}); + +const useTrackStyles = makeStyles({ + base: { + gridRowStart: '2', + gridColumnStart: '1', + backgroundColor: tokens.colorNeutralBackground6, + + '@media screen and (forced-colors: active)': { + ...shorthands.borderBottom('1px', 'solid', 'CanvasText'), + }, + }, + medium: { + height: barThicknessValues.medium, + }, + large: { + height: barThicknessValues.large, + }, }); /** * Apply styling to the Progress slots based on the state */ export const useProgressStyles_unstable = (state: ProgressState): ProgressState => { - const styles = useStyles(); - state.root.className = mergeClasses(progressClassNames.root, styles.root, state.root.className); + const { indeterminate, thickness, percentComplete } = state; + const rootStyles = useRootStyles(); + const barStyles = useBarStyles(); + const trackStyles = useTrackStyles(); + const labelStyles = useLabelStyles(); + const descriptionStyles = useDescriptionStyles(); + const { dir } = useFluent(); + + state.root.className = mergeClasses(progressClassNames.root, rootStyles.root, state.root.className); + + if (state.bar) { + state.bar.className = mergeClasses( + progressClassNames.bar, + barStyles.base, + indeterminate && barStyles.indeterminate, + indeterminate && dir === 'rtl' && barStyles.rtl, + barStyles[thickness], + !indeterminate && barStyles.determinate, + !indeterminate && percentComplete > ZERO_THRESHOLD && barStyles.nonZeroDeterminate, + state.bar.className, + ); + } + + if (state.track) { + state.track.className = mergeClasses( + progressClassNames.track, + trackStyles.base, + trackStyles[thickness], + state.track.className, + ); + } + + if (state.label) { + state.label.className = mergeClasses(progressClassNames.label, labelStyles.base, state.label.className); + } + + if (state.description) { + state.description.className = mergeClasses( + progressClassNames.description, + descriptionStyles.base, + state.description.className, + ); + } - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + if (state.bar && !indeterminate) { + state.bar.style = { + [progressCssVars.percentageCssVar]: Math.min(100, Math.max(0, percentComplete)) + '%', + ...state.bar.style, + }; + } return state; }; diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressAppearance.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/ProgressAppearance.stories.tsx new file mode 100644 index 0000000000000..87156421e399e --- /dev/null +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressAppearance.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { makeStyles, shorthands } from '@fluentui/react-components'; +import { Progress } from '@fluentui/react-progress'; + +const useStyles = makeStyles({ + container: { + ...shorthands.padding('20px'), + }, +}); + +export const Appearance = () => { + const styles = useStyles(); + + return ( +
+ + + + + + + +
+ ); +}; + +Appearance.parameters = { + docs: { + description: { + story: + `Progress can be shown in a few different ways.\n` + + `It can be shown as just the bar, with the bar, label and description, with just the bar and label, and with + just the bar and description`, + }, + }, +}; diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressBarThickness.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/ProgressBarThickness.stories.tsx new file mode 100644 index 0000000000000..44f1417e65d08 --- /dev/null +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressBarThickness.stories.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { makeStyles, shorthands } from '@fluentui/react-components'; +import { Progress } from '@fluentui/react-progress'; + +const useStyles = makeStyles({ + container: { + ...shorthands.padding('20px', '0px'), + }, +}); + +export const Thickness = () => { + const styles = useStyles(); + + return ( +
+ + + +
+ ); +}; + +Thickness.parameters = { + docs: { + description: { + story: `Progress can be one of two sizes.\n` + `It can be shown as the medium or large`, + }, + }, +}; diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressBestPractices.md b/packages/react-components/react-progress/src/stories/Progress/ProgressBestPractices.md index 08ff8ddeeb5f8..54b3b1504fe39 100644 --- a/packages/react-components/react-progress/src/stories/Progress/ProgressBestPractices.md +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressBestPractices.md @@ -2,4 +2,13 @@ ### Do +- Use an `indeterminate` Progress when the total units to completion is unknown +- Display operation description +- Show text above and/or below the bar +- Combine steps of a single operation into one bar + ### Don't + +- Use only a single word description +- Show text to the right or left of the bar +- Cause progress to "rewind" to show new steps diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressDefault.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/ProgressDefault.stories.tsx index c153616597a17..7ce82ea9e0175 100644 --- a/packages/react-components/react-progress/src/stories/Progress/ProgressDefault.stories.tsx +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressDefault.stories.tsx @@ -1,4 +1,14 @@ import * as React from 'react'; import { Progress, ProgressProps } from '@fluentui/react-progress'; -export const Default = (props: Partial) => ; +export const Default = (props: Partial) => { + return ; +}; + +Default.parameters = { + docs: { + description: { + story: `Default determinate Progress bar`, + }, + }, +}; diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressDescription.md b/packages/react-components/react-progress/src/stories/Progress/ProgressDescription.md index e69de29bb2d1d..45d926fdee736 100644 --- a/packages/react-components/react-progress/src/stories/Progress/ProgressDescription.md +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressDescription.md @@ -0,0 +1 @@ +A Progress provides a visual representation of content being loaded or processed. diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressIndeterminate.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/ProgressIndeterminate.stories.tsx new file mode 100644 index 0000000000000..5ed4f128c41d5 --- /dev/null +++ b/packages/react-components/react-progress/src/stories/Progress/ProgressIndeterminate.stories.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Progress } from '@fluentui/react-progress'; + +export const Indeterminate = () => { + return ; +}; + +Indeterminate.parameters = { + docs: { + description: { + story: `Progress can also come in an indeterminate form. + The indeterminate form is useful for showing a buffer or loading state.`, + }, + }, +}; diff --git a/packages/react-components/react-progress/src/stories/Progress/index.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/index.stories.tsx index 94317e3a5e09a..34db5475de66b 100644 --- a/packages/react-components/react-progress/src/stories/Progress/index.stories.tsx +++ b/packages/react-components/react-progress/src/stories/Progress/index.stories.tsx @@ -4,9 +4,12 @@ import descriptionMd from './ProgressDescription.md'; import bestPracticesMd from './ProgressBestPractices.md'; export { Default } from './ProgressDefault.stories'; +export { Appearance } from './ProgressAppearance.stories'; +export { Thickness } from './ProgressBarThickness.stories'; +export { Indeterminate } from './ProgressIndeterminate.stories'; export default { - title: 'Components/Progress', + title: 'Preview Components/Progress', component: Progress, parameters: { docs: {