Skip to content

Commit 7e2289e

Browse files
Update Badge to use semantic tokens (#34765)
Co-authored-by: Mitch-At-Work <mifraser@microsoft.com>
1 parent eb445e1 commit 7e2289e

File tree

16 files changed

+1189
-83
lines changed

16 files changed

+1189
-83
lines changed

packages/react-components/semantic-style-hooks-preview/library/etc/semantic-style-hooks-preview.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { AccordionHeaderState } from '@fluentui/react-accordion';
88
import { AvatarState } from '@fluentui/react-avatar';
9+
import { BadgeState } from '@fluentui/react-badge';
910
import { ButtonState } from '@fluentui/react-button';
1011
import { CheckboxState } from '@fluentui/react-checkbox';
1112
import { CompoundButtonState } from '@fluentui/react-button';
@@ -37,6 +38,7 @@ import { MessageBarBodyState } from '@fluentui/react-message-bar';
3738
import { MessageBarState } from '@fluentui/react-message-bar';
3839
import { MessageBarTitleState } from '@fluentui/react-message-bar';
3940
import { PersonaState } from '@fluentui/react-persona';
41+
import { PresenceBadgeState } from '@fluentui/react-badge';
4042
import { ProgressBarState } from '@fluentui/react-progress';
4143
import { RadioState } from '@fluentui/react-radio';
4244
import { RatingDisplayState } from '@fluentui/react-rating';
@@ -63,6 +65,9 @@ export const useSemanticAccordionHeaderStyles: (_state: unknown) => AccordionHea
6365
// @public (undocumented)
6466
export const useSemanticAvatarStyles: (_state: unknown) => AvatarState;
6567

68+
// @public
69+
export const useSemanticBadgeStyles: (_state: unknown) => BadgeState;
70+
6671
// @public (undocumented)
6772
export const useSemanticButtonStyles: (_state: unknown) => ButtonState;
6873

@@ -156,6 +161,9 @@ export const useSemanticOverlayDrawerSurfaceStyles: (_state: unknown) => DialogS
156161
// @public
157162
export const useSemanticPersonaStyles: (_state: unknown) => PersonaState;
158163

164+
// @public
165+
export const useSemanticPresenceBadgeStyles: (_state: unknown) => PresenceBadgeState;
166+
159167
// @public
160168
export const useSemanticProgressBarStyles: (_state: unknown) => ProgressBarState;
161169

packages/react-components/semantic-style-hooks-preview/library/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"dependencies": {
2727
"@fluentui/react-accordion": "^9.6.8",
2828
"@fluentui/react-avatar": "^9.7.6",
29+
"@fluentui/react-badge": "^9.2.54",
2930
"@fluentui/react-button": "^9.4.6",
3031
"@fluentui/react-checkbox": "^9.3.6",
3132
"@fluentui/react-dialog": "^9.12.8",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { useSemanticBadgeStyles } from './useSemanticBadgeStyles.styles';
2+
export { useSemanticPresenceBadgeStyles } from './useSemanticPresenceBadgeStyles.styles';
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import { shorthands, makeResetStyles, makeStyles, mergeClasses } from '@griffel/react';
2+
import { tokens } from '@fluentui/react-theme';
3+
import { badgeClassNames, type BadgeState } from '@fluentui/react-badge';
4+
import * as semanticTokens from '@fluentui/semantic-tokens';
5+
import { getSlotClassNameProp_unstable } from '@fluentui/react-utilities';
6+
7+
// The text content of the badge has additional horizontal padding, but there is no `text` slot to add that padding to.
8+
// Instead, add extra padding to the root, and a negative margin on the icon to "remove" the extra padding on the icon.
9+
10+
const useRootClassName = makeResetStyles({
11+
display: 'inline-flex',
12+
boxSizing: 'border-box',
13+
alignItems: 'center',
14+
justifyContent: 'center',
15+
position: 'relative',
16+
fontFamily: semanticTokens.textStyleDefaultHeaderFontFamily,
17+
fontWeight: semanticTokens.textStyleDefaultHeaderWeight,
18+
fontSize: semanticTokens.textRampLegalFontSize,
19+
lineHeight: semanticTokens.textRampLegalLineHeight,
20+
height: '20px',
21+
minWidth: '20px',
22+
padding: `0 calc(${semanticTokens.ctrlBadgePadding} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
23+
borderRadius: semanticTokens.cornerCircular,
24+
// Use a transparent stroke (rather than no border) so the border is visible in high contrast
25+
borderColor: semanticTokens._ctrlBadgeNullColor,
26+
27+
'::after': {
28+
content: '""',
29+
position: 'absolute',
30+
top: 0,
31+
left: 0,
32+
bottom: 0,
33+
right: 0,
34+
borderStyle: 'solid',
35+
borderColor: 'inherit',
36+
borderWidth: semanticTokens.strokeWidthDefault,
37+
borderRadius: 'inherit',
38+
},
39+
});
40+
41+
const useRootStyles = makeStyles({
42+
fontSmallToTiny: {
43+
fontFamily: semanticTokens.textStyleDefaultRegularFontFamily,
44+
fontWeight: semanticTokens._ctrlBadgeTextStyleSemiBoldWeight,
45+
fontSize: semanticTokens.textRampSmLegalFontSize,
46+
lineHeight: semanticTokens.textRampSmLegalLineHeight,
47+
},
48+
49+
// size
50+
51+
tiny: {
52+
width: '6px',
53+
height: '6px',
54+
fontSize: '4px',
55+
lineHeight: '4px',
56+
minWidth: 'unset',
57+
padding: 'unset',
58+
},
59+
'extra-small': {
60+
width: '10px',
61+
height: '10px',
62+
fontSize: '6px',
63+
lineHeight: '6px',
64+
minWidth: 'unset',
65+
padding: 'unset',
66+
},
67+
small: {
68+
minWidth: '16px',
69+
height: '16px',
70+
padding: `0 calc(${semanticTokens.ctrlBadgeSmPadding} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
71+
},
72+
medium: {
73+
// Set by useRootClassName
74+
},
75+
large: {
76+
fontSize: semanticTokens.textRampLgLegalFontSize,
77+
lineHeight: semanticTokens.textRampLgLegalLineHeight,
78+
minWidth: '24px',
79+
height: '24px',
80+
padding: `0 calc(${semanticTokens.ctrlBadgeLgPadding} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
81+
},
82+
'extra-large': {
83+
fontSize: semanticTokens.textRampLgLegalFontSize,
84+
lineHeight: semanticTokens.textRampLgLegalLineHeight,
85+
minWidth: '32px',
86+
height: '32px',
87+
padding: `0 calc(${semanticTokens._ctrlBadgeXLPadding} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
88+
},
89+
90+
// shape
91+
92+
square: { borderRadius: semanticTokens.cornerZero },
93+
rounded: { borderRadius: semanticTokens.ctrlBadgeCorner },
94+
roundedSmallToTiny: { borderRadius: semanticTokens._ctrlBadgeSmallTinyCorner },
95+
circular: {
96+
// Set by useRootClassName
97+
},
98+
// hide the border when appearance is "ghost"
99+
100+
borderGhost: {
101+
// The border is applied in an ::after pseudo-element because it should not affect layout.
102+
// The padding and size of the badge should be the same regardless of whether or not it has a border.
103+
'::after': {
104+
display: 'none',
105+
},
106+
},
107+
108+
// appearance: filled
109+
110+
filled: {
111+
// Set by useRootClassName
112+
},
113+
'filled-brand': {
114+
backgroundColor: semanticTokens.statusBrandBackground,
115+
color: semanticTokens.statusBrandForeground,
116+
},
117+
'filled-danger': {
118+
backgroundColor: semanticTokens.statusDangerBackground,
119+
color: semanticTokens.statusDangerForeground,
120+
},
121+
'filled-important': {
122+
backgroundColor: semanticTokens.statusImportantBackground,
123+
color: semanticTokens.statusImportantForeground,
124+
},
125+
'filled-informative': {
126+
backgroundColor: semanticTokens.statusInformativeBackground,
127+
color: semanticTokens.statusInformativeForeground,
128+
},
129+
'filled-severe': {
130+
backgroundColor: tokens.colorPaletteDarkOrangeBackground3, // Missing semantic token equivalent
131+
color: tokens.colorNeutralForegroundOnBrand,
132+
},
133+
'filled-subtle': {
134+
backgroundColor: tokens.colorNeutralBackground1, // Missing semantic token equivalent
135+
color: tokens.colorNeutralForeground1,
136+
},
137+
'filled-success': {
138+
backgroundColor: semanticTokens.statusSuccessBackground,
139+
color: semanticTokens.statusSuccessForeground,
140+
},
141+
'filled-warning': {
142+
backgroundColor: semanticTokens._ctrlBadgeStatusWarningBackground,
143+
color: semanticTokens.statusWarningForeground,
144+
},
145+
146+
// appearance: ghost
147+
148+
ghost: {
149+
// No shared colors between ghost appearances
150+
},
151+
'ghost-brand': {
152+
color: semanticTokens.statusBrandTintForeground,
153+
},
154+
'ghost-danger': {
155+
color: semanticTokens.statusDangerTintForeground,
156+
},
157+
'ghost-important': {
158+
color: semanticTokens.statusImportantTintForeground,
159+
},
160+
'ghost-informative': {
161+
color: semanticTokens.statusInformativeTintForeground,
162+
},
163+
'ghost-severe': {
164+
color: tokens.colorPaletteDarkOrangeForeground3, // Missing semantic token equivalent
165+
},
166+
'ghost-subtle': {
167+
color: tokens.colorNeutralForegroundStaticInverted, // Missing semantic token equivalents
168+
},
169+
'ghost-success': {
170+
color: semanticTokens._ctrlBadgeStatusSuccessTintForeground3,
171+
},
172+
'ghost-warning': {
173+
color: semanticTokens._ctrlBadgeStatusWarningTintForeground2,
174+
},
175+
176+
// appearance: outline
177+
178+
outline: {
179+
...shorthands.borderColor('currentColor'),
180+
},
181+
'outline-brand': {
182+
color: semanticTokens.statusBrandTintForeground,
183+
},
184+
'outline-danger': {
185+
color: semanticTokens.statusDangerTintForeground,
186+
...shorthands.borderColor(semanticTokens.statusDangerStroke),
187+
},
188+
'outline-important': {
189+
color: semanticTokens.statusImportantTintForeground,
190+
...shorthands.borderColor(semanticTokens.statusImportantStroke),
191+
},
192+
'outline-informative': {
193+
color: semanticTokens.statusInformativeTintForeground,
194+
...shorthands.borderColor(semanticTokens.statusInformativeStroke),
195+
},
196+
'outline-severe': {
197+
color: tokens.colorPaletteDarkOrangeForeground3, // Missing semantic token equivalent
198+
},
199+
'outline-subtle': {
200+
color: tokens.colorNeutralForegroundStaticInverted, // Missing semantic token equivalent
201+
},
202+
'outline-success': {
203+
color: semanticTokens._ctrlBadgeStatusSuccessTintForeground3,
204+
...shorthands.borderColor(semanticTokens.statusSuccessStroke),
205+
},
206+
'outline-warning': {
207+
color: semanticTokens._ctrlBadgeStatusWarningTintForeground2,
208+
},
209+
210+
// appearance: tint
211+
212+
tint: {
213+
// No shared colors between tint appearances
214+
},
215+
'tint-brand': {
216+
backgroundColor: semanticTokens.statusBrandTintBackground,
217+
color: semanticTokens._ctrlBadgeStatusBrandTintForeground,
218+
...shorthands.borderColor(semanticTokens.statusBrandTintStroke),
219+
},
220+
'tint-danger': {
221+
backgroundColor: semanticTokens._ctrlBadgeStatusDangerTintBackground,
222+
color: semanticTokens._ctrlBadgeStatusDangerTintForeground,
223+
...shorthands.borderColor(semanticTokens._ctrlBadgeStatusDangerTintStroke),
224+
},
225+
'tint-important': {
226+
backgroundColor: semanticTokens._ctrlBadgeStatusImportantTintBackground,
227+
color: semanticTokens._ctrlBadgeStatusImportantTintForeground,
228+
...shorthands.borderColor(semanticTokens.statusImportantTintStroke),
229+
},
230+
'tint-informative': {
231+
backgroundColor: semanticTokens.statusInformativeTintBackground,
232+
color: semanticTokens.statusInformativeTintForeground,
233+
...shorthands.borderColor(semanticTokens._ctrlBadgeStatusInformativeTintStroke),
234+
},
235+
'tint-severe': {
236+
//come back to this
237+
backgroundColor: tokens.colorPaletteDarkOrangeBackground1,
238+
color: tokens.colorPaletteDarkOrangeForeground1,
239+
...shorthands.borderColor(tokens.colorPaletteDarkOrangeBorder1),
240+
},
241+
'tint-subtle': {
242+
//come back to this
243+
backgroundColor: tokens.colorNeutralBackground1,
244+
color: tokens.colorNeutralForeground3,
245+
...shorthands.borderColor(tokens.colorNeutralStroke2),
246+
},
247+
'tint-success': {
248+
backgroundColor: semanticTokens._ctrlBadgeStatusSuccessTintBackground,
249+
color: semanticTokens._ctrlBadgeStatusSuccessTintForeground,
250+
...shorthands.borderColor(semanticTokens._ctrlBadgeStatusSuccessTintStroke),
251+
},
252+
'tint-warning': {
253+
backgroundColor: semanticTokens._ctrlBadgeStatusWarningTintBackground,
254+
color: semanticTokens._ctrlBadgeStatusWarningTintForeground,
255+
...shorthands.borderColor(semanticTokens._ctrlBadgeStatusWarningTintStroke),
256+
},
257+
});
258+
259+
const useIconRootClassName = makeResetStyles({
260+
display: 'flex',
261+
lineHeight: '1',
262+
margin: `0 calc(-1 * ${semanticTokens._ctrlBadgePaddingTextSide})`, // Remove text padding added to root
263+
fontSize: '12px',
264+
});
265+
266+
const useIconStyles = makeStyles({
267+
beforeText: {
268+
marginRight: `calc(${semanticTokens._ctrlBadgePaddingRightSide} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
269+
},
270+
afterText: {
271+
marginLeft: `calc(${semanticTokens._ctrlBadgePaddingLeftSide} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
272+
},
273+
274+
beforeTextXL: {
275+
marginRight: `calc(${semanticTokens._ctrlBadgePaddingRightSideXL} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
276+
},
277+
afterTextXL: {
278+
marginLeft: `calc(${semanticTokens._ctrlBadgePaddingLeftSideXL} + ${semanticTokens._ctrlBadgePaddingTextSide})`,
279+
},
280+
281+
// size
282+
283+
tiny: {
284+
fontSize: '6px',
285+
},
286+
'extra-small': {
287+
fontSize: '10px',
288+
},
289+
small: {
290+
fontSize: '12px',
291+
},
292+
medium: {
293+
// Set by useIconRootClassName
294+
},
295+
large: {
296+
fontSize: '16px',
297+
},
298+
'extra-large': {
299+
fontSize: '20px',
300+
},
301+
});
302+
303+
/**
304+
* Applies style classnames to slots
305+
*/
306+
export const useSemanticBadgeStyles = (_state: unknown): BadgeState => {
307+
'use no memo';
308+
309+
const state = _state as BadgeState;
310+
const rootClassName = useRootClassName();
311+
const rootStyles = useRootStyles();
312+
313+
const smallToTiny = state.size === 'small' || state.size === 'extra-small' || state.size === 'tiny';
314+
315+
state.root.className = mergeClasses(
316+
state.root.className,
317+
badgeClassNames.root,
318+
rootClassName,
319+
smallToTiny && rootStyles.fontSmallToTiny,
320+
rootStyles[state.size],
321+
rootStyles[state.shape],
322+
state.shape === 'rounded' && smallToTiny && rootStyles.roundedSmallToTiny,
323+
state.appearance === 'ghost' && rootStyles.borderGhost,
324+
rootStyles[state.appearance],
325+
rootStyles[`${state.appearance}-${state.color}` as const],
326+
getSlotClassNameProp_unstable(state.root),
327+
);
328+
329+
const iconRootClassName = useIconRootClassName();
330+
const iconStyles = useIconStyles();
331+
if (state.icon) {
332+
let iconPositionClass;
333+
if (state.root.children) {
334+
if (state.size === 'extra-large') {
335+
iconPositionClass = state.iconPosition === 'after' ? iconStyles.afterTextXL : iconStyles.beforeTextXL;
336+
} else {
337+
iconPositionClass = state.iconPosition === 'after' ? iconStyles.afterText : iconStyles.beforeText;
338+
}
339+
}
340+
341+
state.icon.className = mergeClasses(
342+
state.icon.className,
343+
badgeClassNames.icon,
344+
iconRootClassName,
345+
iconPositionClass,
346+
iconStyles[state.size],
347+
getSlotClassNameProp_unstable(state.icon),
348+
);
349+
}
350+
351+
return state;
352+
};

0 commit comments

Comments
 (0)