diff --git a/change/@fluentui-react-avatar-e8c58b22-266b-4609-9169-1a26cdef500c.json b/change/@fluentui-react-avatar-e8c58b22-266b-4609-9169-1a26cdef500c.json new file mode 100644 index 0000000000000..75d39b8085001 --- /dev/null +++ b/change/@fluentui-react-avatar-e8c58b22-266b-4609-9169-1a26cdef500c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: Make border around badge transparent, not a solid color", + "packageName": "@fluentui/react-avatar", + "email": "behowell@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-avatar/src/components/Avatar/useAvatar.tsx b/packages/react-components/react-avatar/src/components/Avatar/useAvatar.tsx index 1b165c7b50f95..5eb29eec45aad 100644 --- a/packages/react-components/react-avatar/src/components/Avatar/useAvatar.tsx +++ b/packages/react-components/react-avatar/src/components/Avatar/useAvatar.tsx @@ -88,7 +88,7 @@ export const useAvatar_unstable = (props: AvatarProps, ref: React.Ref { +export const getBadgeSize = (size: AvatarState['size']) => { if (size >= 96) { - return 'extra-large'; + return { name: 'extra-large', value: 28 } as const; } else if (size >= 64) { - return 'large'; + return { name: 'large', value: 20 } as const; } else if (size >= 56) { - return 'medium'; + return { name: 'medium', value: 16 } as const; } else if (size >= 40) { - return 'small'; + return { name: 'small', value: 12 } as const; } else if (size >= 28) { - return 'extra-small'; + return { name: 'extra-small', value: 10 } as const; } else { - return 'tiny'; + return { name: 'tiny', value: 6 } as const; } }; diff --git a/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts b/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts index ea50cd030be5c..fb17f8da1faf2 100644 --- a/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts +++ b/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts @@ -1,7 +1,8 @@ -import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react'; import { tokens } from '@fluentui/react-theme'; -import type { AvatarSlots, AvatarState } from './Avatar.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import type { AvatarSize, AvatarSlots, AvatarState } from './Avatar.types'; +import { getBadgeSize } from './useAvatar'; export const avatarClassNames: SlotClassNames = { root: 'fui-Avatar', @@ -23,6 +24,24 @@ const animations = { nullEasing: tokens.curveLinear, }; +const badgeBorderClip = (size: AvatarSize) => { + const radius = getBadgeSize(size).value / 2; + const width = size >= 64 ? /*tokens.strokeWidthThick =*/ 2 : /*tokens.strokeWidthThin =*/ 1; + + // This path will clip out ONLY the outline around the badge. + // It is a large square with a donut punched out that surrounds the badge. + return ( + 'path("' + + // A box that has a {size} margin around the avatar. The margin is to avoid clipping the ring and/or shadow. + `M -${size},-${size} H ${2 * size} V ${2 * size} H -${size} V -${size} ` + + // A circle that is the size of the badge. + `M ${size},${size - radius} a ${radius} ${radius} 0 1 0 0,0.1 ` + + // A circle that is bigger than the badge by the width of the line. + `m ${width},0 a ${radius + width} ${radius + width} 0 1 0 0,0.1 ` + + 'Z")' + ); +}; + const useRootClassName = makeResetStyles({ display: 'inline-block', flexShrink: 0, @@ -178,11 +197,6 @@ const useStyles = makeStyles({ position: 'absolute', bottom: 0, right: 0, - boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorNeutralBackground1}`, - }, - - badgeLarge: { - boxShadow: `0 0 0 ${tokens.strokeWidthThick} ${tokens.colorNeutralBackground1}`, }, icon12: { fontSize: '12px' }, @@ -194,6 +208,23 @@ const useStyles = makeStyles({ icon48: { fontSize: '48px' }, }); +export const useBadgeBorderClipStyles = makeStyles({ + 16: { clipPath: badgeBorderClip(16) }, + 20: { clipPath: badgeBorderClip(20) }, + 24: { clipPath: badgeBorderClip(24) }, + 28: { clipPath: badgeBorderClip(28) }, + 32: { clipPath: badgeBorderClip(32) }, + 36: { clipPath: badgeBorderClip(36) }, + 40: { clipPath: badgeBorderClip(40) }, + 48: { clipPath: badgeBorderClip(48) }, + 56: { clipPath: badgeBorderClip(56) }, + 64: { clipPath: badgeBorderClip(64) }, + 72: { clipPath: badgeBorderClip(72) }, + 96: { clipPath: badgeBorderClip(96) }, + 120: { clipPath: badgeBorderClip(120) }, + 128: { clipPath: badgeBorderClip(128) }, +}); + export const useSizeStyles = makeStyles({ 16: { width: '16px', height: '16px' }, 20: { width: '20px', height: '20px' }, @@ -383,6 +414,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { const iconInitialsClassName = useIconInitialsClassName(); const styles = useStyles(); const sizeStyles = useSizeStyles(); + const badgeBorderClipStyles = useBadgeBorderClipStyles(); const colorStyles = useColorStyles(); const rootClasses = [rootClassName, size !== 32 && sizeStyles[size], colorStyles[color]]; @@ -446,15 +478,14 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { } } + if (state.badge) { + rootClasses.push(badgeBorderClipStyles[size]); + } + state.root.className = mergeClasses(avatarClassNames.root, ...rootClasses, state.root.className); if (state.badge) { - state.badge.className = mergeClasses( - avatarClassNames.badge, - styles.badge, - size >= 64 && styles.badgeLarge, - state.badge.className, - ); + state.badge.className = mergeClasses(avatarClassNames.badge, styles.badge, state.badge.className); } if (state.image) {