diff --git a/change/@fluentui-react-label-0c145c1c-dd96-48f9-8d7f-3b351f94548d.json b/change/@fluentui-react-label-0c145c1c-dd96-48f9-8d7f-3b351f94548d.json
new file mode 100644
index 0000000000000..0d82ed6f489f4
--- /dev/null
+++ b/change/@fluentui-react-label-0c145c1c-dd96-48f9-8d7f-3b351f94548d.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: add optional `icon` slot to Label, rendered before the label content",
+ "packageName": "@fluentui/react-label",
+ "email": "egianoglio@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-label/library/docs/MIGRATION.md b/packages/react-components/react-label/library/docs/MIGRATION.md
index 886512703de8e..d27a439d04d49 100644
--- a/packages/react-components/react-label/library/docs/MIGRATION.md
+++ b/packages/react-components/react-label/library/docs/MIGRATION.md
@@ -31,7 +31,7 @@ The v9 API does not support many of the features offered in v0. Some could poten
| | `design` | |
| `disabled` | | `disabled` |
| | `fluid` | |
-| | `icon` | |
+| | `icon` | `icon` |
| | `iconPosition` | |
| | `image` | |
| | `imagePosition` | |
diff --git a/packages/react-components/react-label/library/docs/Spec.md b/packages/react-components/react-label/library/docs/Spec.md
index 9dd5891cdf896..dd6d5c0cd1722 100644
--- a/packages/react-components/react-label/library/docs/Spec.md
+++ b/packages/react-components/react-label/library/docs/Spec.md
@@ -68,11 +68,14 @@ The Label component should be simple as shown below. It will just need the text
+
+}>Label
```
## Variants
- A Label can be rendered with an asterisk or custom text when is set as `required`.
+- A Label can render an optional `icon` slot before its content.
## API
@@ -83,13 +86,16 @@ See API at [Label.types.ts](./src/components/Label/Label.types.ts).
### Public
```tsx
-
+}>
+ I'm a Label
+
```
### DOM
```tsx
@@ -99,6 +105,7 @@ See API at [Label.types.ts](./src/components/Label/Label.types.ts).
```tsx
+ {state.icon && }
{state.children}
diff --git a/packages/react-components/react-label/library/etc/react-label.api.md b/packages/react-components/react-label/library/etc/react-label.api.md
index bfbd8e8de459b..e81ba1c37531f 100644
--- a/packages/react-components/react-label/library/etc/react-label.api.md
+++ b/packages/react-components/react-label/library/etc/react-label.api.md
@@ -36,6 +36,7 @@ export type LabelProps = Omit, 'required'> & {
export type LabelSlots = {
root: Slot<'label'>;
required?: Slot<'span'>;
+ icon?: Slot<'span'>;
};
// @public
diff --git a/packages/react-components/react-label/library/src/components/Label/Label.test.tsx b/packages/react-components/react-label/library/src/components/Label/Label.test.tsx
index 4045e6a103fc6..59d7139d4558d 100644
--- a/packages/react-components/react-label/library/src/components/Label/Label.test.tsx
+++ b/packages/react-components/react-label/library/src/components/Label/Label.test.tsx
@@ -11,7 +11,7 @@ describe('Label', () => {
testOptions: {
'has-static-classnames': [
{
- props: { required: 'Required Test' },
+ props: { required: 'Required Test', icon: 'Icon Test' },
},
],
},
diff --git a/packages/react-components/react-label/library/src/components/Label/Label.types.ts b/packages/react-components/react-label/library/src/components/Label/Label.types.ts
index aa61b6dc445f4..0cbd9e2efe40e 100644
--- a/packages/react-components/react-label/library/src/components/Label/Label.types.ts
+++ b/packages/react-components/react-label/library/src/components/Label/Label.types.ts
@@ -33,6 +33,11 @@ export type LabelProps = Omit, 'required'> & {
export type LabelSlots = {
root: Slot<'label'>;
required?: Slot<'span'>;
+
+ /**
+ * Optional icon rendered alongside the label text, before the label content.
+ */
+ icon?: Slot<'span'>;
};
/**
diff --git a/packages/react-components/react-label/library/src/components/Label/renderLabel.tsx b/packages/react-components/react-label/library/src/components/Label/renderLabel.tsx
index 71681672d352a..3cf87b1749b43 100644
--- a/packages/react-components/react-label/library/src/components/Label/renderLabel.tsx
+++ b/packages/react-components/react-label/library/src/components/Label/renderLabel.tsx
@@ -13,6 +13,7 @@ export const renderLabel_unstable = (state: LabelBaseState): JSXElement => {
return (
+ {state.icon && }
{state.root.children}
{state.required && }
diff --git a/packages/react-components/react-label/library/src/components/Label/useLabel.tsx b/packages/react-components/react-label/library/src/components/Label/useLabel.tsx
index f12948ca5802e..3bc1f2e79d6ea 100644
--- a/packages/react-components/react-label/library/src/components/Label/useLabel.tsx
+++ b/packages/react-components/react-label/library/src/components/Label/useLabel.tsx
@@ -31,14 +31,15 @@ export const useLabel_unstable = (props: LabelProps, ref: React.Ref
* @param ref - reference to root HTMLElement of Label
*/
export const useLabelBase_unstable = (props: LabelBaseProps, ref: React.Ref): LabelBaseState => {
- const { disabled = false, required = false, ...rest } = props;
+ const { disabled = false, required = false, icon, ...rest } = props;
return {
disabled,
required: slot.optional(required === true ? '*' : required || undefined, {
defaultProps: { 'aria-hidden': 'true' },
elementType: 'span',
}),
- components: { root: 'label', required: 'span' },
+ icon: slot.optional(icon, { elementType: 'span' }),
+ components: { root: 'label', required: 'span', icon: 'span' },
root: slot.always(
getIntrinsicElementProps('label', {
ref: ref as React.Ref,
diff --git a/packages/react-components/react-label/library/src/components/Label/useLabelStyles.styles.ts b/packages/react-components/react-label/library/src/components/Label/useLabelStyles.styles.ts
index 7b9587e5900af..5f0462b0e07eb 100644
--- a/packages/react-components/react-label/library/src/components/Label/useLabelStyles.styles.ts
+++ b/packages/react-components/react-label/library/src/components/Label/useLabelStyles.styles.ts
@@ -8,6 +8,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities';
export const labelClassNames: SlotClassNames = {
root: 'fui-Label',
required: 'fui-Label__required',
+ icon: 'fui-Label__icon',
};
/**
@@ -31,6 +32,11 @@ const useStyles = makeStyles({
paddingLeft: tokens.spacingHorizontalXS,
},
+ withIcon: {
+ display: 'inline-flex',
+ alignItems: 'center',
+ },
+
small: {
fontSize: tokens.fontSizeBase200,
lineHeight: tokens.lineHeightBase200,
@@ -52,11 +58,52 @@ const useStyles = makeStyles({
},
});
+/**
+ * Styles for the icon slot
+ */
+const useIconStyles = makeStyles({
+ base: {
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: tokens.borderRadiusMedium,
+ backgroundColor: tokens.colorNeutralBackground3,
+ color: tokens.colorNeutralForeground3,
+ marginRight: tokens.spacingHorizontalXS,
+ },
+
+ small: {
+ fontSize: tokens.fontSizeBase200,
+ height: tokens.fontSizeBase500,
+ width: tokens.fontSizeBase500,
+ },
+
+ smallSemibold: {
+ height: tokens.fontSizeBase400,
+ width: tokens.fontSizeBase400,
+ },
+
+ medium: {
+ fontSize: tokens.fontSizeBase400,
+ height: tokens.fontSizeBase500,
+ width: tokens.fontSizeBase500,
+ },
+
+ large: {
+ fontSize: tokens.fontSizeBase500,
+ height: tokens.fontSizeBase600,
+ width: tokens.fontSizeBase600,
+ borderRadius: tokens.borderRadiusLarge,
+ marginRight: tokens.spacingHorizontalSNudge,
+ },
+});
+
/**
* Apply styling to the Label slots based on the state
*/
export const useLabelStyles_unstable = (state: LabelState): LabelState => {
const styles = useStyles();
+ const iconStyles = useIconStyles();
// eslint-disable-next-line react-hooks/immutability
state.root.className = mergeClasses(
labelClassNames.root,
@@ -64,6 +111,7 @@ export const useLabelStyles_unstable = (state: LabelState): LabelState => {
state.disabled && styles.disabled,
styles[state.size],
state.weight === 'semibold' && styles.semibold,
+ state.icon && styles.withIcon,
state.root.className,
);
@@ -77,5 +125,16 @@ export const useLabelStyles_unstable = (state: LabelState): LabelState => {
);
}
+ if (state.icon) {
+ // eslint-disable-next-line react-hooks/immutability
+ state.icon.className = mergeClasses(
+ labelClassNames.icon,
+ iconStyles.base,
+ iconStyles[state.size],
+ state.size === 'small' && state.weight === 'semibold' && iconStyles.smallSemibold,
+ state.icon.className,
+ );
+ }
+
return state;
};
diff --git a/packages/react-components/react-label/stories/src/Label/LabelIcon.stories.tsx b/packages/react-components/react-label/stories/src/Label/LabelIcon.stories.tsx
new file mode 100644
index 0000000000000..31846a2b38502
--- /dev/null
+++ b/packages/react-components/react-label/stories/src/Label/LabelIcon.stories.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import type { JSXElement } from '@fluentui/react-components';
+import { Label } from '@fluentui/react-components';
+import { InfoRegular } from '@fluentui/react-icons';
+
+export const Icon = (): JSXElement => {
+ return (
+ } required>
+ Label with icon
+
+ );
+};
+
+Icon.parameters = {
+ docs: {
+ description: {
+ story: 'A Label can render an optional `icon` slot before its content.',
+ },
+ },
+};
diff --git a/packages/react-components/react-label/stories/src/Label/index.stories.tsx b/packages/react-components/react-label/stories/src/Label/index.stories.tsx
index 82eeaf2418374..4058ece381697 100644
--- a/packages/react-components/react-label/stories/src/Label/index.stories.tsx
+++ b/packages/react-components/react-label/stories/src/Label/index.stories.tsx
@@ -8,6 +8,7 @@ export { Size } from './LabelSize.stories';
export { Weight } from './LabelWeight.stories';
export { Disabled } from './LabelDisabled.stories';
export { Required } from './LabelRequired.stories';
+export { Icon } from './LabelIcon.stories';
const meta = {
title: 'Components/Label',