Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const AllFields = (
<CheckboxField label="Checkbox" {...props} />
<ComboboxField label="Combo box field" {...props} />
<InputField label="Input field" {...props} />
<ProgressField label="Progress field" {...props} />
<ProgressField label="Progress field" value={0.5} {...props} />
<RadioGroupField label="Radio group field" {...props}>
<Radio label="Option one" />
<Radio label="Option two" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ storiesOf('Progress converged', module)
includeHighContrast: true,
includeRtl: true,
})
.addStory('Determinate with thickness large', () => <Progress value={0.5} thickness="large" />);
.addStory('Determinate with thickness large', () => <Progress value={0.5} thickness="large" />)
.addStory('Error', () => <Progress value={0.5} validationState="error" />)
.addStory('Warning', () => <Progress value={0.5} validationState="warning" />)
.addStory('Success', () => <Progress value={0.5} validationState="success" />);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Add support for validationState to ProgressField",
"packageName": "@fluentui/react-field",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Add validationState to Progress, to make the bar red or green",
"packageName": "@fluentui/react-progress",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type FieldConfig<T extends FieldComponent> = {
component: T;
classNames: SlotClassNames<FieldSlots<T>>;
labelConnection?: 'htmlFor' | 'aria-labelledby';
ariaInvalidOnError?: boolean;
};

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ export type FieldConfig<T extends FieldComponent> = {
* @default htmlFor
*/
labelConnection?: 'htmlFor' | 'aria-labelledby';

/**
* Should the aria-invalid and aria-errormessage attributes be set when validationState="error".
*
* @default true
*/
ariaInvalidOnError?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const useField_unstable = <T extends FieldComponent>(
): FieldState<T> => {
const [fieldProps, controlProps] = getPartitionedFieldProps(props);
const { orientation = 'vertical', validationState } = fieldProps;
const { labelConnection = 'htmlFor' } = params;
const { labelConnection = 'htmlFor', ariaInvalidOnError = true } = params;

const baseId = useId('field-');

Expand Down Expand Up @@ -118,7 +118,7 @@ export const useField_unstable = <T extends FieldComponent>(
control['aria-labelledby'] ??= label.id;
}

if (validationState === 'error') {
if (validationState === 'error' && ariaInvalidOnError) {
control['aria-invalid'] ??= true;
if (validationMessage) {
control['aria-errormessage'] ??= validationMessage.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ProgressField label="Test label" />);
Expand All @@ -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(<ProgressField label="Test label" validationState="error" validationMessage="Test error" />);

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const ProgressField: ForwardRefComponent<ProgressFieldProps> = React.forw
component: Progress,
classNames: progressFieldClassNames,
labelConnection: 'aria-labelledby',
ariaInvalidOnError: false,
});
state.control.validationState = state.validationState;
useFieldStyles_unstable(state);
return renderField_unstable(state);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/react-components/react-progress/Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type ProgressProps = Omit<ComponentProps<ProgressSlots>, 'size'> & {
value?: number;
max?: number;
thickness?: 'medium' | 'large';
validationState?: 'success' | 'warning' | 'error';
};

// @public (undocumented)
Expand All @@ -32,7 +33,7 @@ export type ProgressSlots = {
};

// @public
export type ProgressState = ComponentState<ProgressSlots> & Required<Pick<ProgressProps, 'max' | 'shape' | 'thickness'>> & Pick<ProgressProps, 'value'>;
export type ProgressState = ComponentState<ProgressSlots> & Required<Pick<ProgressProps, 'max' | 'shape' | 'thickness'>> & Pick<ProgressProps, 'value' | 'validationState'>;

// @public
export const renderProgress_unstable: (state: ProgressState) => JSX.Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ export type ProgressProps = Omit<ComponentProps<ProgressSlots>, 'size'> & {
* @default 'medium'
*/
thickness?: 'medium' | 'large';

/**
* The status of the progress bar. Changes the color of the bar.
*/
validationState?: 'success' | 'warning' | 'error';
};

/**
* State used in rendering Progress
*/
export type ProgressState = ComponentState<ProgressSlots> &
Required<Pick<ProgressProps, 'max' | 'shape' | 'thickness'>> &
Pick<ProgressProps, 'value'>;
Pick<ProgressProps, 'value' | 'validationState'>;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ProgressProps, ProgressState } from './Progress.types';
*/
export const useProgress_unstable = (props: ProgressProps, ref: React.Ref<HTMLElement>): 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,
Expand All @@ -33,6 +33,7 @@ export const useProgress_unstable = (props: ProgressProps, ref: React.Ref<HTMLEl
shape,
thickness,
value,
validationState,
components: {
root: 'div',
bar: 'div',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const useBarStyles = makeStyles({
backgroundImage: `linear-gradient(
to right,
${tokens.colorNeutralBackground6} 0%,
${tokens.colorCompoundBrandBackground} 50%,
${tokens.colorTransparentBackground} 50%,
${tokens.colorNeutralBackground6} 100%
)`,
animationName: indeterminateProgress,
Expand All @@ -103,13 +103,23 @@ const useBarStyles = makeStyles({
rtl: {
animationName: indeterminateProgressRTL,
},

error: {
backgroundColor: tokens.colorPaletteRedForeground1,
},
warning: {
backgroundColor: tokens.colorPaletteDarkOrangeForeground1,
},
success: {
backgroundColor: tokens.colorPaletteGreenForeground1,
},
});

/**
* Apply styling to the Progress slots based on the state
*/
export const useProgressStyles_unstable = (state: ProgressState): ProgressState => {
const { max, shape, thickness, value } = state;
const { max, shape, thickness, validationState, value } = state;
const rootStyles = useRootStyles();
const barStyles = useBarStyles();
const { dir } = useFluent();
Expand All @@ -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,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
<Progress value={0.75} validationState="error" />
<Progress value={0.95} validationState="warning" />
<Progress value={1} validationState="success" />
</div>
);
};

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).',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down