From d38b71f16a237313418877062590c9500c764d1d Mon Sep 17 00:00:00 2001 From: EnricoGianoglio Date: Fri, 29 May 2026 17:13:25 +0200 Subject: [PATCH 1/3] add optional icon slot from CAP visual refresh --- .../library/etc/react-label.api.md | 1 + .../src/components/Label/Label.types.ts | 5 ++ .../src/components/Label/renderLabel.tsx | 1 + .../library/src/components/Label/useLabel.tsx | 5 +- .../components/Label/useLabelStyles.styles.ts | 59 +++++++++++++++++++ .../stories/src/Label/LabelIcon.stories.tsx | 20 +++++++ .../stories/src/Label/index.stories.tsx | 1 + 7 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/react-components/react-label/stories/src/Label/LabelIcon.stories.tsx 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.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 ( + + ); +}; + +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', From 340aba60ac91cabb46161094f259acec6581c1f2 Mon Sep 17 00:00:00 2001 From: EnricoGianoglio Date: Fri, 29 May 2026 17:18:24 +0200 Subject: [PATCH 2/3] add change file --- ...i-react-label-0c145c1c-dd96-48f9-8d7f-3b351f94548d.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-label-0c145c1c-dd96-48f9-8d7f-3b351f94548d.json 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" +} From b717d1819d04f4c4a9f54171a17dbf8b971d37a8 Mon Sep 17 00:00:00 2001 From: EnricoGianoglio Date: Wed, 10 Jun 2026 17:10:06 +0200 Subject: [PATCH 3/3] fix label tests --- .../react-label/library/src/components/Label/Label.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' }, }, ], },