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 4f1875277be0f6..7cd96d235082f0 100644
--- a/apps/vr-tests-react-components/src/stories/Field.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Field.stories.tsx
@@ -109,6 +109,44 @@ storiesOf('Field', module)
))
+ .addStory('infoButton', () => (
+
+
+
+ ))
+ .addStory('infoButton+required', () => (
+
+
+
+ ))
+ .addStory('infoButton+longLabel', () => (
+
+
+
+ ))
+ .addStory('infoButton+size:small', () => (
+
+
+
+ ))
+ .addStory('infoButton+size:large', () => (
+
+
+
+ ))
+ .addStory('infoButton+noLabel', () => (
+
+
+
+ ))
+ .addStory('infoButton+horizontal', () => (
+
+
+
+ ))
.addStory('Checkbox:error', () => (
diff --git a/change/@fluentui-react-field-88b192d9-9525-4c3a-89ca-3761b2b9f1fe.json b/change/@fluentui-react-field-88b192d9-9525-4c3a-89ca-3761b2b9f1fe.json
new file mode 100644
index 00000000000000..ade3cff3219802
--- /dev/null
+++ b/change/@fluentui-react-field-88b192d9-9525-4c3a-89ca-3761b2b9f1fe.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: Add infoButton slot to Field",
+ "packageName": "@fluentui/react-field",
+ "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 3aef86de247097..6a3259f88806cd 100644
--- a/packages/react-components/react-field/etc/react-field.api.md
+++ b/packages/react-components/react-field/etc/react-field.api.md
@@ -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';
@@ -39,13 +40,15 @@ export type FieldProps = Omit, 'children'> & {
export type FieldSlots = {
root: NonNullable>;
label?: Slot;
+ infoButton?: Slot;
+ labelWrapper?: Slot<'div'>;
validationMessage?: Slot<'div'>;
validationMessageIcon?: Slot<'span'>;
hint?: Slot<'div'>;
};
// @public
-export type FieldState = ComponentState> & Required>;
+export type FieldState = ComponentState> & Required>;
// @internal @deprecated (undocumented)
export const getDeprecatedFieldClassNames: (controlRootClassName: string) => {
diff --git a/packages/react-components/react-field/package.json b/packages/react-components/react-field/package.json
index ba48d7703b9f8e..74ad9d04fd3e00 100644
--- a/packages/react-components/react-field/package.json
+++ b/packages/react-components/react-field/package.json
@@ -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",
diff --git a/packages/react-components/react-field/src/components/Field/Field.test.tsx b/packages/react-components/react-field/src/components/Field/Field.test.tsx
index 175093bb25f35d..9c840f6ebde9c7 100644
--- a/packages/react-components/react-field/src/components/Field/Field.test.tsx
+++ b/packages/react-components/react-field/src/components/Field/Field.test.tsx
@@ -13,8 +13,8 @@ describe('Field', () => {
props: {
label: 'Test label',
hint: 'Test hint',
+ infoButton: { content: 'Test info button' },
validationMessage: 'Test validation message',
- validationState: 'error',
},
},
],
@@ -189,4 +189,18 @@ describe('Field', () => {
'aria-required': true,
});
});
+
+ it('sets infoButton aria-labelledby to the label and the button', () => {
+ const result = render(
+
+
+ ,
+ );
+
+ 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}`);
+ });
});
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 1f355046bf53fc..9aceb8cca7082c 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
@@ -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';
@@ -21,6 +22,21 @@ export type FieldSlots = {
*/
label?: Slot;
+ /**
+ * An InfoButton associated with the field.
+ *
+ * @example
+ * ```
+ *
+ * ```
+ */
+ infoButton?: Slot;
+
+ /**
+ * 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`.
@@ -100,4 +116,4 @@ export type FieldProps = Omit, 'children'> & {
* State used in rendering Field
*/
export type FieldState = ComponentState> &
- Required>;
+ Required>;
diff --git a/packages/react-components/react-field/src/components/Field/renderField.tsx b/packages/react-components/react-field/src/components/Field/renderField.tsx
index 309226f59c71b9..084d7da5dbd196 100644
--- a/packages/react-components/react-field/src/components/Field/renderField.tsx
+++ b/packages/react-components/react-field/src/components/Field/renderField.tsx
@@ -10,7 +10,14 @@ export const renderField_unstable = (state: FieldState) => {
return (
- {slots.label && }
+ {slots.labelWrapper ? (
+
+ {slots.label && }
+ {slots.infoButton && }
+
+ ) : (
+ slots.label &&
+ )}
{slotProps.root.children}
{slots.validationMessage && (
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 172b4bfd7f703d..77fc44b3352747 100644
--- a/packages/react-components/react-field/src/components/Field/useField.tsx
+++ b/packages/react-components/react-field/src/components/Field/useField.tsx
@@ -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';
@@ -27,7 +28,7 @@ export const useField_unstable = (props: FieldProps, ref: React.Ref = {
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`,
@@ -37,7 +39,7 @@ const useRootStyles = makeStyles({
},
});
-const useLabelStyles = makeStyles({
+const useLabelWrapperStyles = makeStyles({
base: {
paddingTop: tokens.spacingVerticalXXS,
paddingBottom: tokens.spacingVerticalXXS,
@@ -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,
@@ -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) {
diff --git a/packages/react-components/react-field/src/util/makeDeprecatedField.tsx b/packages/react-components/react-field/src/util/makeDeprecatedField.tsx
index 14c870e335de26..49d59eae507cef 100644
--- a/packages/react-components/react-field/src/util/makeDeprecatedField.tsx
+++ b/packages/react-components/react-field/src/util/makeDeprecatedField.tsx
@@ -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.
@@ -101,6 +101,10 @@ export function makeDeprecatedField(
* @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`,
});
diff --git a/packages/react-components/react-field/stories/Field/FieldWithInfoButton.stories.tsx b/packages/react-components/react-field/stories/Field/FieldWithInfoButton.stories.tsx
new file mode 100644
index 00000000000000..0688163198367c
--- /dev/null
+++ b/packages/react-components/react-field/stories/Field/FieldWithInfoButton.stories.tsx
@@ -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 = () => (
+
+
+
+);
+
+WithInfoButton.storyName = 'With InfoButton';
+WithInfoButton.parameters = {
+ docs: {
+ description: {
+ story: 'The `infoButton` slot allows the addition of an `` after the label.',
+ },
+ },
+};
diff --git a/packages/react-components/react-field/stories/Field/index.stories.tsx b/packages/react-components/react-field/stories/Field/index.stories.tsx
index dda5953c7d5fea..d0f3e1572f0a7d 100644
--- a/packages/react-components/react-field/stories/Field/index.stories.tsx
+++ b/packages/react-components/react-field/stories/Field/index.stories.tsx
@@ -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';