Skip to content
Closed
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
38 changes: 38 additions & 0 deletions apps/vr-tests-react-components/src/stories/Field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,44 @@ storiesOf('Field', module)
<Checkbox label="Checkbox in a horizontal field" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵 fluentuiv9 Open the Visual Regressions report to inspect the 7 screenshots

✅ There was 7 screenshots added, 0 screenshots removed, 1862 screenshots unchanged, 0 screenshots with different dimensions and 0 screenshots with visible difference.

unknown 7 screenshots
Image Name Diff(in Pixels) Image Type
Field.infoButton+horizontal.default.chromium.png 0 Added
Field.infoButton+longLabel.default.chromium.png 0 Added
Field.infoButton+noLabel.default.chromium.png 0 Added
Field.infoButton+required.default.chromium.png 0 Added
Field.infoButton+size-large.default.chromium.png 0 Added
Field.infoButton+size-small.default.chromium.png 0 Added
Field.infoButton.default.chromium.png 0 Added

</Field>
))
.addStory('infoButton', () => (
<Field label="With info" infoButton={{ content: 'Example' }}>
<Input />
</Field>
))
.addStory('infoButton+required', () => (
<Field label="Required with info" required infoButton={{ content: 'Example' }}>
<Input />
</Field>
))
.addStory('infoButton+longLabel', () => (
<Field
label="With info button and a very long label that should wrap and the info button to appear on the last line"
infoButton={{ content: 'Example' }}
>
<Input />
</Field>
))
.addStory('infoButton+size:small', () => (
<Field label="Small with info" infoButton={{ content: 'Example' }} size="small">
<Input size="small" />
</Field>
))
.addStory('infoButton+size:large', () => (
<Field label="Large with info" infoButton={{ content: 'Example' }} size="large">
<Input size="large" />
</Field>
))
.addStory('infoButton+noLabel', () => (
<Field infoButton={{ content: 'Example' }}>
<Input />
</Field>
))
.addStory('infoButton+horizontal', () => (
<Field orientation="horizontal" label="With info" infoButton={{ content: 'Example' }}>
<Input />
</Field>
))
.addStory('Checkbox:error', () => (
<Field validationMessage="Error message">
<Checkbox label="Checkbox in a Field with an error" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Add infoButton slot to Field",
"packageName": "@fluentui/react-field",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { ForwardRefComponent } from '@fluentui/react-utilities';
import { InfoButton } from '@fluentui/react-infobutton';
import { Label } from '@fluentui/react-label';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
Expand Down Expand Up @@ -39,13 +40,15 @@ export type FieldProps = Omit<ComponentProps<FieldSlots>, 'children'> & {
export type FieldSlots = {
root: NonNullable<Slot<'div'>>;
label?: Slot<typeof Label>;
infoButton?: Slot<typeof InfoButton>;
labelWrapper?: Slot<'div'>;
validationMessage?: Slot<'div'>;
validationMessageIcon?: Slot<'span'>;
hint?: Slot<'div'>;
};

// @public
export type FieldState = ComponentState<Required<FieldSlots>> & Required<Pick<FieldProps, 'orientation' | 'validationState'>>;
export type FieldState = ComponentState<Required<FieldSlots>> & Required<Pick<FieldProps, 'orientation' | 'validationState' | 'size'>>;

// @internal @deprecated (undocumented)
export const getDeprecatedFieldClassNames: (controlRootClassName: string) => {
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-field/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@fluentui/react-context-selector": "^9.1.10",
"@fluentui/react-icons": "^2.0.175",
"@fluentui/react-infobutton": "9.0.0-beta.17",
"@fluentui/react-label": "^9.0.22",
"@fluentui/react-theme": "^9.1.5",
"@fluentui/react-utilities": "^9.6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ describe('Field', () => {
props: {
label: 'Test label',
hint: 'Test hint',
infoButton: { content: 'Test info button' },
validationMessage: 'Test validation message',
validationState: 'error',
},
},
],
Expand Down Expand Up @@ -189,4 +189,18 @@ describe('Field', () => {
'aria-required': true,
});
});

it('sets infoButton aria-labelledby to the label and the button', () => {
const result = render(
<Field label="Test label" infoButton={{ content: 'test' }}>
<input />
</Field>,
);

const label = result.getByText('Test label');
const infoButton = result.getByRole('button');

expect(label.id).toBeTruthy();
expect(infoButton.getAttribute('aria-labelledby')).toBe(`${label.id} ${infoButton.id}`);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { InfoButton } from '@fluentui/react-infobutton';
import { Label } from '@fluentui/react-label';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

Expand All @@ -21,6 +22,21 @@ export type FieldSlots = {
*/
label?: Slot<typeof Label>;

/**
* An InfoButton associated with the field.
*
* @example
* ```
* <Field infoButton={{ content="..." }} />
* ```
*/
infoButton?: Slot<typeof InfoButton>;

/**
* Wrapper around the label and infoButton. By default, this is only rendered when there is an infoButton.
*/
labelWrapper?: Slot<'div'>;

/**
* A message about the validation state. By default, this is an error message, but it can be a success, warning,
* or custom message by setting `validationState`.
Expand Down Expand Up @@ -100,4 +116,4 @@ export type FieldProps = Omit<ComponentProps<FieldSlots>, 'children'> & {
* State used in rendering Field
*/
export type FieldState = ComponentState<Required<FieldSlots>> &
Required<Pick<FieldProps, 'orientation' | 'validationState'>>;
Required<Pick<FieldProps, 'orientation' | 'validationState' | 'size'>>;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ export const renderField_unstable = (state: FieldState) => {

return (
<slots.root {...slotProps.root}>
{slots.label && <slots.label {...slotProps.label} />}
{slots.labelWrapper ? (
<slots.labelWrapper {...slotProps.labelWrapper}>
{slots.label && <slots.label {...slotProps.label} />}
{slots.infoButton && <slots.infoButton {...slotProps.infoButton} />}
</slots.labelWrapper>
) : (
slots.label && <slots.label {...slotProps.label} />
)}
{slotProps.root.children}
{slots.validationMessage && (
<slots.validationMessage {...slotProps.validationMessage}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';

import { CheckmarkCircle12Filled, ErrorCircle12Filled, Warning12Filled } from '@fluentui/react-icons';
import { InfoButton } from '@fluentui/react-infobutton';
import { Label } from '@fluentui/react-label';
import { getNativeElementProps, resolveShorthand, useId } from '@fluentui/react-utilities';
import type { FieldChildProps, FieldProps, FieldState } from './Field.types';
Expand All @@ -27,7 +28,7 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
orientation = 'vertical',
required,
validationState = props.validationMessage ? 'error' : 'none',
size,
size = 'medium',
} = props;

const baseId = useId('field-');
Expand All @@ -43,6 +44,21 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
},
});

const infoButton = resolveShorthand(props.infoButton, {
defaultProps: {
id: baseId + '__infoButton',
size,
},
});

if (label && infoButton && !infoButton['aria-labelledby']) {
infoButton['aria-labelledby'] = label.id + ' ' + infoButton.id;
}

const labelWrapper = resolveShorthand(props.labelWrapper, {
required: !!infoButton,
});

const validationMessage = resolveShorthand(props.validationMessage, {
defaultProps: {
id: baseId + '__validationMessage',
Expand Down Expand Up @@ -102,15 +118,20 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref<HTMLDivEleme
return {
orientation,
validationState,
size,
components: {
root: 'div',
label: Label,
infoButton: InfoButton,
labelWrapper: 'div',
validationMessage: 'div',
validationMessageIcon: 'span',
hint: 'div',
},
root,
label,
infoButton,
labelWrapper,
validationMessageIcon,
validationMessage,
hint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { FieldSlots, FieldState } from './Field.types';
export const fieldClassNames: SlotClassNames<FieldSlots> = {
root: `fui-Field`,
label: `fui-Field__label`,
infoButton: `fui-Field__infoButton`,
labelWrapper: `fui-Field__labelWrapper`,
validationMessage: `fui-Field__validationMessage`,
validationMessageIcon: `fui-Field__validationMessageIcon`,
hint: `fui-Field__hint`,
Expand Down Expand Up @@ -37,7 +39,7 @@ const useRootStyles = makeStyles({
},
});

const useLabelStyles = makeStyles({
const useLabelWrapperStyles = makeStyles({
base: {
paddingTop: tokens.spacingVerticalXXS,
paddingBottom: tokens.spacingVerticalXXS,
Expand All @@ -63,6 +65,25 @@ const useLabelStyles = makeStyles({
},
});

const useLabelStyles = makeStyles({
base: {
verticalAlign: 'top',
},
});

const useInfoButtonStyles = makeStyles({
base: {
verticalAlign: 'top',
marginTop: `calc(0px - ${tokens.spacingVerticalXXS})`,
marginBottom: `calc(0px - ${tokens.spacingVerticalXXS})`,
},

large: {
marginTop: '-1px',
marginBottom: '-1px',
},
});

const useSecondaryTextBaseClassName = makeResetStyles({
marginTop: tokens.spacingVerticalXXS,
color: tokens.colorNeutralForeground3,
Expand Down Expand Up @@ -120,19 +141,45 @@ export const useFieldStyles_unstable = (state: FieldState) => {
state.root.className,
);

const labelWrapperStyles = useLabelWrapperStyles();

// Class name applied to the either the labelWrapper if it is present, or the label itself otherwise.
const labelContainerClassName = mergeClasses(
labelWrapperStyles.base,
horizontal && labelWrapperStyles.horizontal,
!horizontal && labelWrapperStyles.vertical,
state.size === 'large' && labelWrapperStyles.large,
!horizontal && state.size === 'large' && labelWrapperStyles.verticalLarge,
);

if (state.labelWrapper) {
state.labelWrapper.className = mergeClasses(
fieldClassNames.labelWrapper,
labelContainerClassName,
state.labelWrapper.className,
);
}

const labelStyles = useLabelStyles();
if (state.label) {
state.label.className = mergeClasses(
fieldClassNames.label,
labelStyles.base,
horizontal && labelStyles.horizontal,
!horizontal && labelStyles.vertical,
state.label.size === 'large' && labelStyles.large,
!horizontal && state.label.size === 'large' && labelStyles.verticalLarge,
!state.labelWrapper && labelContainerClassName,
state.label.className,
);
}

const infoButtonStyles = useInfoButtonStyles();
if (state.infoButton) {
state.infoButton.className = mergeClasses(
fieldClassNames.infoButton,
infoButtonStyles.base,
state.size === 'large' && infoButtonStyles.large,
state.infoButton.className,
);
}

const validationMessageIconBaseClassName = useValidationMessageIconBaseClassName();
const validationMessageIconStyles = useValidationMessageIconStyles();
if (state.validationMessageIcon) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { ForwardRefComponent } from '@fluentui/react-utilities';
import type { FieldProps } from '../Field';
import { Field, fieldClassNames } from '../Field';
import { Field } from '../Field';

/**
* @deprecated Only for use to make deprecated [Control]Field shim components.
Expand Down Expand Up @@ -101,6 +101,10 @@ export function makeDeprecatedField<ControlProps>(
* @internal
*/
export const getDeprecatedFieldClassNames = (controlRootClassName: string) => ({
...fieldClassNames,
control: controlRootClassName,
root: `fui-Field`,
label: `fui-Field__label`,
validationMessage: `fui-Field__validationMessage`,
validationMessageIcon: `fui-Field__validationMessageIcon`,
hint: `fui-Field__hint`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

import { Input } from '@fluentui/react-components';
import { Field } from '@fluentui/react-components/unstable';

export const WithInfoButton = () => (
<Field label="Example" required infoButton={{ content: 'This is example content for an InfoButton.' }}>
<Input />
</Field>
);

WithInfoButton.storyName = 'With InfoButton';
WithInfoButton.parameters = {
docs: {
description: {
story: 'The `infoButton` slot allows the addition of an `<InfoButton />` after the label.',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { Hint } from './FieldHint.stories';
export { Horizontal } from './FieldHorizontal.stories';
export { Required } from './FieldRequired.stories';
export { Size } from './FieldSize.stories';
export { WithInfoButton } from './FieldWithInfoButton.stories';
export { ComponentExamples } from './FieldComponentExamples.stories';
export { RenderFunction } from './FieldRenderFunction.stories';

Expand Down