diff --git a/apps/vr-tests-react-components/src/stories/Field.stories.tsx b/apps/vr-tests-react-components/src/stories/Field.stories.tsx
index d84d951888ddd4..2e4b0f0a4a7868 100644
--- a/apps/vr-tests-react-components/src/stories/Field.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Field.stories.tsx
@@ -28,7 +28,7 @@ const AllFields = (
-
+
diff --git a/apps/vr-tests-react-components/src/stories/Progress.stories.tsx b/apps/vr-tests-react-components/src/stories/Progress.stories.tsx
index 05d2c567954f92..dbcb867f9375c8 100644
--- a/apps/vr-tests-react-components/src/stories/Progress.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Progress.stories.tsx
@@ -14,4 +14,7 @@ storiesOf('Progress converged', module)
includeHighContrast: true,
includeRtl: true,
})
- .addStory('Determinate with thickness large', () => );
+ .addStory('Determinate with thickness large', () => )
+ .addStory('Error', () => )
+ .addStory('Warning', () => )
+ .addStory('Success', () => );
diff --git a/change/@fluentui-react-field-6d4517ea-1bd4-42ab-bc43-7779ccaa3de2.json b/change/@fluentui-react-field-6d4517ea-1bd4-42ab-bc43-7779ccaa3de2.json
new file mode 100644
index 00000000000000..9475cef5cad0a8
--- /dev/null
+++ b/change/@fluentui-react-field-6d4517ea-1bd4-42ab-bc43-7779ccaa3de2.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: Add support for validationState to ProgressField",
+ "packageName": "@fluentui/react-field",
+ "email": "behowell@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-react-progress-7997b7b6-eeaf-45b7-aa8d-d0ac737e14f1.json b/change/@fluentui-react-progress-7997b7b6-eeaf-45b7-aa8d-d0ac737e14f1.json
new file mode 100644
index 00000000000000..bc3cc284e07bdd
--- /dev/null
+++ b/change/@fluentui-react-progress-7997b7b6-eeaf-45b7-aa8d-d0ac737e14f1.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: Add validationState to Progress, to make the bar red or green",
+ "packageName": "@fluentui/react-progress",
+ "email": "behowell@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-field/etc/react-field.api.md b/packages/react-components/react-field/etc/react-field.api.md
index 06c25baafdec4a..fdace08fb846f5 100644
--- a/packages/react-components/react-field/etc/react-field.api.md
+++ b/packages/react-components/react-field/etc/react-field.api.md
@@ -53,6 +53,7 @@ export type FieldConfig = {
component: T;
classNames: SlotClassNames>;
labelConnection?: 'htmlFor' | 'aria-labelledby';
+ ariaInvalidOnError?: boolean;
};
// @public
diff --git a/packages/react-components/react-field/src/components/Field/Field.types.ts b/packages/react-components/react-field/src/components/Field/Field.types.ts
index 6ddc8d99e46fa6..f1644d289d9aec 100644
--- a/packages/react-components/react-field/src/components/Field/Field.types.ts
+++ b/packages/react-components/react-field/src/components/Field/Field.types.ts
@@ -117,6 +117,13 @@ export type FieldConfig = {
* @default htmlFor
*/
labelConnection?: 'htmlFor' | 'aria-labelledby';
+
+ /**
+ * Should the aria-invalid and aria-errormessage attributes be set when validationState="error".
+ *
+ * @default true
+ */
+ ariaInvalidOnError?: boolean;
};
/**
diff --git a/packages/react-components/react-field/src/components/Field/useField.tsx b/packages/react-components/react-field/src/components/Field/useField.tsx
index cb5ea6d01f2bb7..acd4da41721cab 100644
--- a/packages/react-components/react-field/src/components/Field/useField.tsx
+++ b/packages/react-components/react-field/src/components/Field/useField.tsx
@@ -67,7 +67,7 @@ export const useField_unstable = (
): FieldState => {
const [fieldProps, controlProps] = getPartitionedFieldProps(props);
const { orientation = 'vertical', validationState } = fieldProps;
- const { labelConnection = 'htmlFor' } = params;
+ const { labelConnection = 'htmlFor', ariaInvalidOnError = true } = params;
const baseId = useId('field-');
@@ -118,7 +118,7 @@ export const useField_unstable = (
control['aria-labelledby'] ??= label.id;
}
- if (validationState === 'error') {
+ if (validationState === 'error' && ariaInvalidOnError) {
control['aria-invalid'] ??= true;
if (validationMessage) {
control['aria-errormessage'] ??= validationMessage.id;
diff --git a/packages/react-components/react-field/src/components/ProgressField/ProgressField.test.tsx b/packages/react-components/react-field/src/components/ProgressField/ProgressField.test.tsx
index 1724b31d2fe815..71d0adff6888f0 100644
--- a/packages/react-components/react-field/src/components/ProgressField/ProgressField.test.tsx
+++ b/packages/react-components/react-field/src/components/ProgressField/ProgressField.test.tsx
@@ -9,7 +9,7 @@ describe('ProgressField', () => {
displayName: 'ProgressField',
});
- // Most functionality is tested by Field.test.tsx, and RadioGroup's tests
+ // Most functionality is tested by Field.test.tsx and Progress.test.tsx
it('uses aria-labelledby for the label', () => {
const result = render();
@@ -21,4 +21,15 @@ describe('ProgressField', () => {
expect(progress.getAttribute('aria-labelledby')).toBe(label.id);
expect(label.htmlFor).toBeFalsy();
});
+
+ it('uses aria-describedby on error, instead of aria-errormessage ', () => {
+ const result = render();
+
+ const progress = result.getByRole('progressbar');
+ const message = result.getByText('Test error') as HTMLLabelElement;
+
+ expect(message.id).toBeTruthy();
+ expect(progress.getAttribute('aria-describedby')).toBe(message.id);
+ expect(progress.getAttribute('aria-invalid')).toBeNull();
+ });
});
diff --git a/packages/react-components/react-field/src/components/ProgressField/ProgressField.tsx b/packages/react-components/react-field/src/components/ProgressField/ProgressField.tsx
index d4a5019d9e253b..799ad6c7452fb0 100644
--- a/packages/react-components/react-field/src/components/ProgressField/ProgressField.tsx
+++ b/packages/react-components/react-field/src/components/ProgressField/ProgressField.tsx
@@ -13,7 +13,9 @@ export const ProgressField: ForwardRefComponent = React.forw
component: Progress,
classNames: progressFieldClassNames,
labelConnection: 'aria-labelledby',
+ ariaInvalidOnError: false,
});
+ state.control.validationState = state.validationState;
useFieldStyles_unstable(state);
return renderField_unstable(state);
});
diff --git a/packages/react-components/react-progress/Spec.md b/packages/react-components/react-progress/Spec.md
index 611709c849e994..a7ae7cd1719472 100644
--- a/packages/react-components/react-progress/Spec.md
+++ b/packages/react-components/react-progress/Spec.md
@@ -44,6 +44,9 @@ function App() {
- The default Progress that animates indefinitely
- Determinate Progress
- The determinate form of the Progress component that incrementally loads from 0% to 100%
+- Error/success
+ - The validationState prop can be set to "error", "warning", or "success" to make the bar red, orange, or green, respectively.
+ - The prop name was chosen to align with the Field prop of the same name, allowing ProgressField to have the same API as other fields.
#### Adding Label and Description with ProgressField
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 51c8b9d15f1c4a..5c1f21cf89253d 100644
--- a/packages/react-components/react-progress/etc/react-progress.api.md
+++ b/packages/react-components/react-progress/etc/react-progress.api.md
@@ -23,6 +23,7 @@ export type ProgressProps = Omit, 'size'> & {
value?: number;
max?: number;
thickness?: 'medium' | 'large';
+ validationState?: 'success' | 'warning' | 'error';
};
// @public (undocumented)
@@ -32,7 +33,7 @@ export type ProgressSlots = {
};
// @public
-export type ProgressState = ComponentState & Required> & Pick;
+export type ProgressState = ComponentState & Required> & Pick;
// @public
export const renderProgress_unstable: (state: ProgressState) => JSX.Element;
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 1b559e78e60083..93727ae9b068f6 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
@@ -38,6 +38,11 @@ export type ProgressProps = Omit, 'size'> & {
* @default 'medium'
*/
thickness?: 'medium' | 'large';
+
+ /**
+ * The status of the progress bar. Changes the color of the bar.
+ */
+ validationState?: 'success' | 'warning' | 'error';
};
/**
@@ -45,4 +50,4 @@ export type ProgressProps = Omit, 'size'> & {
*/
export type ProgressState = ComponentState &
Required> &
- Pick;
+ Pick;
diff --git a/packages/react-components/react-progress/src/components/Progress/useProgress.tsx b/packages/react-components/react-progress/src/components/Progress/useProgress.tsx
index c6e3cc4131db8f..3e7f37bab88411 100644
--- a/packages/react-components/react-progress/src/components/Progress/useProgress.tsx
+++ b/packages/react-components/react-progress/src/components/Progress/useProgress.tsx
@@ -13,7 +13,7 @@ import type { ProgressProps, ProgressState } from './Progress.types';
*/
export const useProgress_unstable = (props: ProgressProps, ref: React.Ref): ProgressState => {
// Props
- const { max = 1.0, shape = 'rounded', thickness = 'medium', value } = props;
+ const { max = 1.0, shape = 'rounded', thickness = 'medium', validationState, value } = props;
const root = getNativeElementProps('div', {
ref,
@@ -33,6 +33,7 @@ export const useProgress_unstable = (props: ProgressProps, ref: React.Ref {
- const { max, shape, thickness, value } = state;
+ const { max, shape, thickness, validationState, value } = state;
const rootStyles = useRootStyles();
const barStyles = useBarStyles();
const { dir } = useFluent();
@@ -130,6 +140,7 @@ export const useProgressStyles_unstable = (state: ProgressState): ProgressState
value === undefined && dir === 'rtl' && barStyles.rtl,
barStyles[thickness],
value !== undefined && value > ZERO_THRESHOLD && barStyles.nonZeroDeterminate,
+ validationState && barStyles[validationState],
state.bar.className,
);
}
diff --git a/packages/react-components/react-progress/src/stories/Progress/ProgressValidationState.stories.tsx b/packages/react-components/react-progress/src/stories/Progress/ProgressValidationState.stories.tsx
new file mode 100644
index 00000000000000..5a1b00cf0b5c5c
--- /dev/null
+++ b/packages/react-components/react-progress/src/stories/Progress/ProgressValidationState.stories.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { makeStyles } from '@fluentui/react-components';
+import { Progress } from '@fluentui/react-progress';
+
+const useStyles = makeStyles({
+ container: {
+ display: 'flex',
+ flexDirection: 'column',
+ rowGap: '20px',
+ },
+});
+
+export const ValidationState = () => {
+ const styles = useStyles();
+ return (
+
+ );
+};
+
+ValidationState.parameters = {
+ docs: {
+ name: 'Validation State',
+ description: {
+ story:
+ 'The `validationState` prop can be used to indicate an `"error"` state (red), `"warning"` state (orange), ' +
+ 'or `"success"` state (green).',
+ },
+ },
+};
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 8ec0cf39a5d19b..bf19fc27b00036 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
@@ -7,6 +7,7 @@ export { Default } from './ProgressDefault.stories';
export { Shape } from './ProgressShape.stories';
export { Thickness } from './ProgressBarThickness.stories';
export { Indeterminate } from './ProgressIndeterminate.stories';
+export { ValidationState } from './ProgressValidationState.stories';
export { Max } from './ProgressMax.stories';
export default {