|
| 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