From 3ccfcf951f2d5a74afddc70da4e794d88357f0d3 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 13 Apr 2023 16:42:37 -0700 Subject: [PATCH 1/5] fix: Make border around badge transparent, not a solid color --- .../src/stories/Avatar.stories.tsx | 12 +- ...-e8c58b22-266b-4609-9169-1a26cdef500c.json | 7 + .../src/components/Avatar/useAvatarStyles.ts | 279 +++++++++++++----- 3 files changed, 231 insertions(+), 67 deletions(-) create mode 100644 change/@fluentui-react-avatar-e8c58b22-266b-4609-9169-1a26cdef500c.json diff --git a/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx b/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx index 282b0674b386ed..b2a51142d1aec6 100644 --- a/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx +++ b/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx @@ -2,6 +2,7 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; import { Steps, StoryWright } from 'storywright'; import { Avatar, AvatarProps } from '@fluentui/react-avatar'; +import { tokens } from '@fluentui/react-theme'; import { PeopleRegular, PersonCallRegular } from '@fluentui/react-icons'; const imageRoot = 'http://fabricweb.azureedge.net/fabric-website/assets/images/avatar'; @@ -230,11 +231,20 @@ storiesOf('Avatar Converged', module) .addStory('size+active+ring-shadow', () => ( ))*/ + .addStory( + 'badgeMask', + () => ( +
+ +
+ ), + { includeRtl: true }, + ) .addStory('customSize+image', () => ) .addStory('customSize+name+badge', () => ( )) - .addStory('customSize+icon+active', () => ) + .addStory('customSize+icon+active', () => ) .addStory('color', () => , { includeHighContrast: true, includeDarkMode: true, 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 00000000000000..75d39b8085001d --- /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/useAvatarStyles.ts b/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts index 8165d7b7c2091b..e9ef1f8bbc1c4f 100644 --- a/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts +++ b/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts @@ -11,6 +11,14 @@ export const avatarClassNames: SlotClassNames = { badge: 'fui-Avatar__badge', }; +// CSS variables used internally in Avatar's styles +const vars = { + badgeRadius: '--fui-Avatar-badgeRadius', + badgeGap: '--fui-Avatar-badgeGap', + badgeAlign: '--fui-Avatar-badgeAlign', + ringWidth: '--fui-Avatar-ringWidth', +}; + const useRootClassName = makeResetStyles({ display: 'inline-block', flexShrink: 0, @@ -55,6 +63,26 @@ const useIconInitialsClassName = makeResetStyles({ borderRadius: 'inherit', }); +/** + * Helper to create a maskImage that punches out a circle larger than the badge by `badgeGap`. + * This creates a transparent gap between the badge and Avatar. + */ +const badgeCutout = (margin?: string) => { + const center = margin ? `calc(var(${vars.badgeRadius}) + ${margin})` : `var(${vars.badgeRadius})`; + return { + maskImage: + `radial-gradient(circle at bottom ${center} var(${vars.badgeAlign}) ${center}, ` + + // radial-gradient does not have anti-aliasing, so the transparent and opaque circles are offset by +/- 0.25px + // to "fade" from transparent to opaque over a half-pixel and ease the transition. + `transparent calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) - 0.25px), ` + + `white calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) + 0.25px))`, + + // Griffel won't auto-flip the "right" alignment to "left" in RTL if it is inline in the maskImage, + // so split it out into a css variable that will auto-flip. + [vars.badgeAlign]: 'right', + }; +}; + const useStyles = makeStyles({ textCaption2Strong: { fontSize: tokens.fontSizeBase100 }, textCaption1Strong: { fontSize: tokens.fontSizeBase200 }, @@ -85,14 +113,16 @@ const useStyles = makeStyles({ transitionDuration: '0.01ms', }, - '::before': { - content: '""', + // ::before is the ring, and ::after is the shadow. + // The individual ring/shadow clases set content: "" to display it when appropriate. + '::before,::after': { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, - + zIndex: -1, + ...shorthands.margin(`calc(-2 * var(${vars.ringWidth}, 0px))`), ...shorthands.borderRadius('inherit'), transitionProperty: 'margin, opacity', transitionDuration: `${tokens.durationUltraSlow}, ${tokens.durationSlower}`, @@ -105,40 +135,51 @@ const useStyles = makeStyles({ }, ring: { + // Show the ::before pseudo-element, which is the ring '::before': { + content: '""', ...shorthands.borderStyle('solid'), + ...shorthands.borderWidth(`var(${vars.ringWidth})`), }, }, + ringBadgeCutout: { + '::before': badgeCutout(/*margin =*/ `2 * var(${vars.ringWidth})`), + }, ringThick: { - '::before': { - ...shorthands.margin(`calc(-2 * ${tokens.strokeWidthThick})`), - ...shorthands.borderWidth(tokens.strokeWidthThick), - }, + [vars.ringWidth]: tokens.strokeWidthThick, }, ringThicker: { - '::before': { - ...shorthands.margin(`calc(-2 * ${tokens.strokeWidthThicker})`), - ...shorthands.borderWidth(tokens.strokeWidthThicker), - }, + [vars.ringWidth]: tokens.strokeWidthThicker, }, ringThickest: { - '::before': { - ...shorthands.margin(`calc(-2 * ${tokens.strokeWidthThickest})`), - ...shorthands.borderWidth(tokens.strokeWidthThickest), - }, + [vars.ringWidth]: tokens.strokeWidthThickest, }, - shadow4: { '::before': { boxShadow: tokens.shadow4 } }, - shadow8: { '::before': { boxShadow: tokens.shadow8 } }, - shadow16: { '::before': { boxShadow: tokens.shadow16 } }, - shadow28: { '::before': { boxShadow: tokens.shadow28 } }, + shadow: { + // Show the ::after pseudo-element, which is the shadow + '::after': { + content: '""', + }, + }, + shadow4: { + '::after': { boxShadow: tokens.shadow4 }, + }, + shadow8: { + '::after': { boxShadow: tokens.shadow8 }, + }, + shadow16: { + '::after': { boxShadow: tokens.shadow16 }, + }, + shadow28: { + '::after': { boxShadow: tokens.shadow28 }, + }, inactive: { opacity: '0.8', transform: 'scale(0.875)', transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - '::before': { + '::before,::after': { ...shorthands.margin(0), opacity: 0, transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, @@ -149,12 +190,9 @@ 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}`, - }, + badgeCutout: badgeCutout(), icon12: { fontSize: '12px' }, icon16: { fontSize: '16px' }, @@ -180,168 +218,260 @@ export const useSizeStyles = makeStyles({ 96: { width: '96px', height: '96px' }, 120: { width: '120px', height: '120px' }, 128: { width: '128px', height: '128px' }, + + // Badge size: applied when there is a badge + tiny: { + [vars.badgeRadius]: '3px', + [vars.badgeGap]: tokens.strokeWidthThin, + }, + 'extra-small': { + [vars.badgeRadius]: '5px', + [vars.badgeGap]: tokens.strokeWidthThin, + }, + small: { + [vars.badgeRadius]: '6px', + [vars.badgeGap]: tokens.strokeWidthThin, + }, + medium: { + [vars.badgeRadius]: '8px', + [vars.badgeGap]: tokens.strokeWidthThin, + }, + large: { + [vars.badgeRadius]: '10px', + [vars.badgeGap]: tokens.strokeWidthThick, + }, + 'extra-large': { + [vars.badgeRadius]: '14px', + [vars.badgeGap]: tokens.strokeWidthThick, + }, }); const useColorStyles = makeStyles({ neutral: { color: tokens.colorNeutralForeground3, backgroundColor: tokens.colorNeutralBackground6, - // The ::before element is the ring when active - '::before': { color: tokens.colorBrandStroke1 }, }, brand: { color: tokens.colorNeutralForegroundStaticInverted, backgroundColor: tokens.colorBrandBackgroundStatic, - '::before': { color: tokens.colorBrandStroke1 }, }, 'dark-red': { color: tokens.colorPaletteDarkRedForeground2, backgroundColor: tokens.colorPaletteDarkRedBackground2, - '::before': { color: tokens.colorPaletteDarkRedBorderActive }, }, cranberry: { color: tokens.colorPaletteCranberryForeground2, backgroundColor: tokens.colorPaletteCranberryBackground2, - '::before': { color: tokens.colorPaletteCranberryBorderActive }, }, red: { color: tokens.colorPaletteRedForeground2, backgroundColor: tokens.colorPaletteRedBackground2, - '::before': { color: tokens.colorPaletteRedBorderActive }, }, pumpkin: { color: tokens.colorPalettePumpkinForeground2, backgroundColor: tokens.colorPalettePumpkinBackground2, - '::before': { color: tokens.colorPalettePumpkinBorderActive }, }, peach: { color: tokens.colorPalettePeachForeground2, backgroundColor: tokens.colorPalettePeachBackground2, - '::before': { color: tokens.colorPalettePeachBorderActive }, }, marigold: { color: tokens.colorPaletteMarigoldForeground2, backgroundColor: tokens.colorPaletteMarigoldBackground2, - '::before': { color: tokens.colorPaletteMarigoldBorderActive }, }, gold: { color: tokens.colorPaletteGoldForeground2, backgroundColor: tokens.colorPaletteGoldBackground2, - '::before': { color: tokens.colorPaletteGoldBorderActive }, }, brass: { color: tokens.colorPaletteBrassForeground2, backgroundColor: tokens.colorPaletteBrassBackground2, - '::before': { color: tokens.colorPaletteBrassBorderActive }, }, brown: { color: tokens.colorPaletteBrownForeground2, backgroundColor: tokens.colorPaletteBrownBackground2, - '::before': { color: tokens.colorPaletteBrownBorderActive }, }, forest: { color: tokens.colorPaletteForestForeground2, backgroundColor: tokens.colorPaletteForestBackground2, - '::before': { color: tokens.colorPaletteForestBorderActive }, }, seafoam: { color: tokens.colorPaletteSeafoamForeground2, backgroundColor: tokens.colorPaletteSeafoamBackground2, - '::before': { color: tokens.colorPaletteSeafoamBorderActive }, }, 'dark-green': { color: tokens.colorPaletteDarkGreenForeground2, backgroundColor: tokens.colorPaletteDarkGreenBackground2, - '::before': { color: tokens.colorPaletteDarkGreenBorderActive }, }, 'light-teal': { color: tokens.colorPaletteLightTealForeground2, backgroundColor: tokens.colorPaletteLightTealBackground2, - '::before': { color: tokens.colorPaletteLightTealBorderActive }, }, teal: { color: tokens.colorPaletteTealForeground2, backgroundColor: tokens.colorPaletteTealBackground2, - '::before': { color: tokens.colorPaletteTealBorderActive }, }, steel: { color: tokens.colorPaletteSteelForeground2, backgroundColor: tokens.colorPaletteSteelBackground2, - '::before': { color: tokens.colorPaletteSteelBorderActive }, }, blue: { color: tokens.colorPaletteBlueForeground2, backgroundColor: tokens.colorPaletteBlueBackground2, - '::before': { color: tokens.colorPaletteBlueBorderActive }, }, 'royal-blue': { color: tokens.colorPaletteRoyalBlueForeground2, backgroundColor: tokens.colorPaletteRoyalBlueBackground2, - '::before': { color: tokens.colorPaletteRoyalBlueBorderActive }, }, cornflower: { color: tokens.colorPaletteCornflowerForeground2, backgroundColor: tokens.colorPaletteCornflowerBackground2, - '::before': { color: tokens.colorPaletteCornflowerBorderActive }, }, navy: { color: tokens.colorPaletteNavyForeground2, backgroundColor: tokens.colorPaletteNavyBackground2, - '::before': { color: tokens.colorPaletteNavyBorderActive }, }, lavender: { color: tokens.colorPaletteLavenderForeground2, backgroundColor: tokens.colorPaletteLavenderBackground2, - '::before': { color: tokens.colorPaletteLavenderBorderActive }, }, purple: { color: tokens.colorPalettePurpleForeground2, backgroundColor: tokens.colorPalettePurpleBackground2, - '::before': { color: tokens.colorPalettePurpleBorderActive }, }, grape: { color: tokens.colorPaletteGrapeForeground2, backgroundColor: tokens.colorPaletteGrapeBackground2, - '::before': { color: tokens.colorPaletteGrapeBorderActive }, }, lilac: { color: tokens.colorPaletteLilacForeground2, backgroundColor: tokens.colorPaletteLilacBackground2, - '::before': { color: tokens.colorPaletteLilacBorderActive }, }, pink: { color: tokens.colorPalettePinkForeground2, backgroundColor: tokens.colorPalettePinkBackground2, - '::before': { color: tokens.colorPalettePinkBorderActive }, }, magenta: { color: tokens.colorPaletteMagentaForeground2, backgroundColor: tokens.colorPaletteMagentaBackground2, - '::before': { color: tokens.colorPaletteMagentaBorderActive }, }, plum: { color: tokens.colorPalettePlumForeground2, backgroundColor: tokens.colorPalettePlumBackground2, - '::before': { color: tokens.colorPalettePlumBorderActive }, }, beige: { color: tokens.colorPaletteBeigeForeground2, backgroundColor: tokens.colorPaletteBeigeBackground2, - '::before': { color: tokens.colorPaletteBeigeBorderActive }, }, mink: { color: tokens.colorPaletteMinkForeground2, backgroundColor: tokens.colorPaletteMinkBackground2, - '::before': { color: tokens.colorPaletteMinkBorderActive }, }, platinum: { color: tokens.colorPalettePlatinumForeground2, backgroundColor: tokens.colorPalettePlatinumBackground2, - '::before': { color: tokens.colorPalettePlatinumBorderActive }, }, anchor: { color: tokens.colorPaletteAnchorForeground2, backgroundColor: tokens.colorPaletteAnchorBackground2, + }, +}); + +const useRingColorStyles = makeStyles({ + neutral: { + '::before': { color: tokens.colorBrandStroke1 }, + }, + brand: { + '::before': { color: tokens.colorBrandStroke1 }, + }, + 'dark-red': { + '::before': { color: tokens.colorPaletteDarkRedBorderActive }, + }, + cranberry: { + '::before': { color: tokens.colorPaletteCranberryBorderActive }, + }, + red: { + '::before': { color: tokens.colorPaletteRedBorderActive }, + }, + pumpkin: { + '::before': { color: tokens.colorPalettePumpkinBorderActive }, + }, + peach: { + '::before': { color: tokens.colorPalettePeachBorderActive }, + }, + marigold: { + '::before': { color: tokens.colorPaletteMarigoldBorderActive }, + }, + gold: { + '::before': { color: tokens.colorPaletteGoldBorderActive }, + }, + brass: { + '::before': { color: tokens.colorPaletteBrassBorderActive }, + }, + brown: { + '::before': { color: tokens.colorPaletteBrownBorderActive }, + }, + forest: { + '::before': { color: tokens.colorPaletteForestBorderActive }, + }, + seafoam: { + '::before': { color: tokens.colorPaletteSeafoamBorderActive }, + }, + 'dark-green': { + '::before': { color: tokens.colorPaletteDarkGreenBorderActive }, + }, + 'light-teal': { + '::before': { color: tokens.colorPaletteLightTealBorderActive }, + }, + teal: { + '::before': { color: tokens.colorPaletteTealBorderActive }, + }, + steel: { + '::before': { color: tokens.colorPaletteSteelBorderActive }, + }, + blue: { + '::before': { color: tokens.colorPaletteBlueBorderActive }, + }, + 'royal-blue': { + '::before': { color: tokens.colorPaletteRoyalBlueBorderActive }, + }, + cornflower: { + '::before': { color: tokens.colorPaletteCornflowerBorderActive }, + }, + navy: { + '::before': { color: tokens.colorPaletteNavyBorderActive }, + }, + lavender: { + '::before': { color: tokens.colorPaletteLavenderBorderActive }, + }, + purple: { + '::before': { color: tokens.colorPalettePurpleBorderActive }, + }, + grape: { + '::before': { color: tokens.colorPaletteGrapeBorderActive }, + }, + lilac: { + '::before': { color: tokens.colorPaletteLilacBorderActive }, + }, + pink: { + '::before': { color: tokens.colorPalettePinkBorderActive }, + }, + magenta: { + '::before': { color: tokens.colorPaletteMagentaBorderActive }, + }, + plum: { + '::before': { color: tokens.colorPalettePlumBorderActive }, + }, + beige: { + '::before': { color: tokens.colorPaletteBeigeBorderActive }, + }, + mink: { + '::before': { color: tokens.colorPaletteMinkBorderActive }, + }, + platinum: { + '::before': { color: tokens.colorPalettePlatinumBorderActive }, + }, + anchor: { '::before': { color: tokens.colorPaletteAnchorBorderActive }, }, }); @@ -355,8 +485,13 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { const styles = useStyles(); const sizeStyles = useSizeStyles(); const colorStyles = useColorStyles(); + const ringColorStyles = useRingColorStyles(); - const rootClasses = [rootClassName, size !== 32 && sizeStyles[size], colorStyles[color]]; + const rootClasses = [ + rootClassName, + size !== 32 && sizeStyles[size], + state.badge && sizeStyles[state.badge.size || 'medium'], + ]; if (size <= 24) { rootClasses.push(styles.textCaption2Strong); @@ -388,7 +523,10 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { rootClasses.push(styles.activeOrInactive); if (activeAppearance === 'ring' || activeAppearance === 'ring-shadow') { - rootClasses.push(styles.ring); + rootClasses.push(styles.ring, ringColorStyles[color]); + if (state.badge) { + rootClasses.push(styles.ringBadgeCutout); + } if (size <= 48) { rootClasses.push(styles.ringThick); @@ -400,6 +538,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { } if (activeAppearance === 'shadow' || activeAppearance === 'ring-shadow') { + rootClasses.push(styles.shadow); if (size <= 28) { rootClasses.push(styles.shadow4); } else if (size <= 48) { @@ -420,20 +559,26 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { 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) { - state.image.className = mergeClasses(avatarClassNames.image, imageClassName, state.image.className); + state.image.className = mergeClasses( + avatarClassNames.image, + imageClassName, + state.badge && styles.badgeCutout, + state.image.className, + ); } if (state.initials) { - state.initials.className = mergeClasses(avatarClassNames.initials, iconInitialsClassName, state.initials.className); + state.initials.className = mergeClasses( + avatarClassNames.initials, + iconInitialsClassName, + colorStyles[color], + state.badge && styles.badgeCutout, + state.initials.className, + ); } if (state.icon) { @@ -458,6 +603,8 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { avatarClassNames.icon, iconInitialsClassName, iconSizeClass, + colorStyles[color], + state.badge && styles.badgeCutout, state.icon.className, ); } From c7305686b6387d9bb681076a85c91d940e63fc11 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Thu, 13 Apr 2023 17:01:17 -0700 Subject: [PATCH 2/5] Use a checkerboard background for the badgeMask VR test --- .../src/stories/Avatar.stories.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx b/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx index b2a51142d1aec6..47648745354f01 100644 --- a/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx +++ b/apps/vr-tests-react-components/src/stories/Avatar.stories.tsx @@ -234,8 +234,16 @@ storiesOf('Avatar Converged', module) .addStory( 'badgeMask', () => ( -
- +
+
), { includeRtl: true }, From eb998138217cc8982f2282069d490bd6acfaaf52 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Fri, 14 Apr 2023 01:59:40 -0700 Subject: [PATCH 3/5] chore: Move more styles into makeResetStyles, reduce bundle size --- .../src/components/Avatar/useAvatarStyles.ts | 134 ++++++++---------- 1 file changed, 59 insertions(+), 75 deletions(-) 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 e9ef1f8bbc1c4f..89e51c27ba051a 100644 --- a/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts +++ b/packages/react-components/react-avatar/src/components/Avatar/useAvatarStyles.ts @@ -16,6 +16,7 @@ const vars = { badgeRadius: '--fui-Avatar-badgeRadius', badgeGap: '--fui-Avatar-badgeGap', badgeAlign: '--fui-Avatar-badgeAlign', + badgeMask: '--fui-Avatar-badgeMask', ringWidth: '--fui-Avatar-ringWidth', }; @@ -30,6 +31,30 @@ const useRootClassName = makeResetStyles({ fontSize: tokens.fontSizeBase300, width: '32px', height: '32px', + + // ::before is the ring, and ::after is the shadow. + // These are not displayed by default; the ring and shadow clases set content: "" to display them when appropriate. + '::before,::after': { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + zIndex: -1, + margin: `calc(-2 * var(${vars.ringWidth}, 0px))`, + borderRadius: 'inherit', + transitionProperty: 'margin, opacity', + transitionTimingFunction: `${tokens.curveEasyEaseMax}, ${tokens.curveLinear}`, + transitionDuration: `${tokens.durationUltraSlow}, ${tokens.durationSlower}`, + '@media screen and (prefers-reduced-motion: reduce)': { + transitionDuration: '0.01ms', + }, + }, + '::before': { + borderStyle: 'solid', + borderWidth: `var(${vars.ringWidth})`, + maskImage: `var(${vars.badgeMask})`, + }, }); const useImageClassName = makeResetStyles({ @@ -42,6 +67,7 @@ const useImageClassName = makeResetStyles({ borderRadius: 'inherit', objectFit: 'cover', verticalAlign: 'top', + maskImage: `var(${vars.badgeMask})`, }); const useIconInitialsClassName = makeResetStyles({ @@ -61,28 +87,9 @@ const useIconInitialsClassName = makeResetStyles({ textAlign: 'center', userSelect: 'none', borderRadius: 'inherit', + maskImage: `var(${vars.badgeMask})`, }); -/** - * Helper to create a maskImage that punches out a circle larger than the badge by `badgeGap`. - * This creates a transparent gap between the badge and Avatar. - */ -const badgeCutout = (margin?: string) => { - const center = margin ? `calc(var(${vars.badgeRadius}) + ${margin})` : `var(${vars.badgeRadius})`; - return { - maskImage: - `radial-gradient(circle at bottom ${center} var(${vars.badgeAlign}) ${center}, ` + - // radial-gradient does not have anti-aliasing, so the transparent and opaque circles are offset by +/- 0.25px - // to "fade" from transparent to opaque over a half-pixel and ease the transition. - `transparent calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) - 0.25px), ` + - `white calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) + 0.25px))`, - - // Griffel won't auto-flip the "right" alignment to "left" in RTL if it is inline in the maskImage, - // so split it out into a css variable that will auto-flip. - [vars.badgeAlign]: 'right', - }; -}; - const useStyles = makeStyles({ textCaption2Strong: { fontSize: tokens.fontSizeBase100 }, textCaption1Strong: { fontSize: tokens.fontSizeBase200 }, @@ -112,54 +119,37 @@ const useStyles = makeStyles({ '@media screen and (prefers-reduced-motion: reduce)': { transitionDuration: '0.01ms', }, + }, + + inactive: { + opacity: '0.8', + transform: 'scale(0.875)', + transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - // ::before is the ring, and ::after is the shadow. - // The individual ring/shadow clases set content: "" to display it when appropriate. '::before,::after': { - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - right: 0, - zIndex: -1, - ...shorthands.margin(`calc(-2 * var(${vars.ringWidth}, 0px))`), - ...shorthands.borderRadius('inherit'), - transitionProperty: 'margin, opacity', - transitionDuration: `${tokens.durationUltraSlow}, ${tokens.durationSlower}`, - transitionTimingFunction: `${tokens.curveEasyEaseMax}, ${tokens.curveLinear}`, - - '@media screen and (prefers-reduced-motion: reduce)': { - transitionDuration: '0.01ms', - }, + ...shorthands.margin(0), + opacity: 0, + transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, }, }, ring: { // Show the ::before pseudo-element, which is the ring - '::before': { - content: '""', - ...shorthands.borderStyle('solid'), - ...shorthands.borderWidth(`var(${vars.ringWidth})`), - }, - }, - ringBadgeCutout: { - '::before': badgeCutout(/*margin =*/ `2 * var(${vars.ringWidth})`), + '::before': { content: '""' }, }, ringThick: { - [vars.ringWidth]: tokens.strokeWidthThick, + '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThick }, }, ringThicker: { - [vars.ringWidth]: tokens.strokeWidthThicker, + '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThicker }, }, ringThickest: { - [vars.ringWidth]: tokens.strokeWidthThickest, + '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThickest }, }, shadow: { // Show the ::after pseudo-element, which is the shadow - '::after': { - content: '""', - }, + '::after': { content: '""' }, }, shadow4: { '::after': { boxShadow: tokens.shadow4 }, @@ -174,25 +164,28 @@ const useStyles = makeStyles({ '::after': { boxShadow: tokens.shadow28 }, }, - inactive: { - opacity: '0.8', - transform: 'scale(0.875)', - transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - - '::before,::after': { - ...shorthands.margin(0), - opacity: 0, - transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - }, - }, - + // Applied to the badge slot badge: { position: 'absolute', bottom: 0, right: 0, }, - badgeCutout: badgeCutout(), + // Applied to the root slot + rootWithBadge: { + [vars.badgeMask]: + `radial-gradient(circle at ` + + `bottom calc(var(${vars.badgeRadius}) + 2 * var(${vars.ringWidth}, 0px)) ` + + `var(${vars.badgeAlign}) calc(var(${vars.badgeRadius}) + 2 * var(${vars.ringWidth}, 0px)), ` + + // radial-gradient does not have anti-aliasing, so the transparent and opaque circles are offset by +/- 0.25px + // to "fade" from transparent to opaque over a half-pixel and ease the transition. + `transparent calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) - 0.25px), ` + + `white calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) + 0.25px))`, + + // Griffel won't auto-flip the "right" alignment to "left" in RTL if it is inline in the maskImage, + // so split it out into a css variable that will auto-flip. + [vars.badgeAlign]: 'right', + }, icon12: { fontSize: '12px' }, icon16: { fontSize: '16px' }, @@ -491,6 +484,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { rootClassName, size !== 32 && sizeStyles[size], state.badge && sizeStyles[state.badge.size || 'medium'], + state.badge && styles.rootWithBadge, ]; if (size <= 24) { @@ -524,9 +518,6 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { if (activeAppearance === 'ring' || activeAppearance === 'ring-shadow') { rootClasses.push(styles.ring, ringColorStyles[color]); - if (state.badge) { - rootClasses.push(styles.ringBadgeCutout); - } if (size <= 48) { rootClasses.push(styles.ringThick); @@ -563,12 +554,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { } if (state.image) { - state.image.className = mergeClasses( - avatarClassNames.image, - imageClassName, - state.badge && styles.badgeCutout, - state.image.className, - ); + state.image.className = mergeClasses(avatarClassNames.image, imageClassName, state.image.className); } if (state.initials) { @@ -576,7 +562,6 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { avatarClassNames.initials, iconInitialsClassName, colorStyles[color], - state.badge && styles.badgeCutout, state.initials.className, ); } @@ -604,7 +589,6 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { iconInitialsClassName, iconSizeClass, colorStyles[color], - state.badge && styles.badgeCutout, state.icon.className, ); } From 20d532e4857ea7bff070102fd330de818f423be9 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Fri, 14 Apr 2023 02:00:52 -0700 Subject: [PATCH 4/5] Change file --- ...-react-avatar-0275892a-086c-48a0-bced-da21c133fd82.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-avatar-0275892a-086c-48a0-bced-da21c133fd82.json diff --git a/change/@fluentui-react-avatar-0275892a-086c-48a0-bced-da21c133fd82.json b/change/@fluentui-react-avatar-0275892a-086c-48a0-bced-da21c133fd82.json new file mode 100644 index 00000000000000..56913cbdedf8fd --- /dev/null +++ b/change/@fluentui-react-avatar-0275892a-086c-48a0-bced-da21c133fd82.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: Reduce bundle size by refactoring styles", + "packageName": "@fluentui/react-avatar", + "email": "behowell@microsoft.com", + "dependentChangeType": "patch" +} From 27fe5deb900c4127e2f688e0a80a55c1ee684cb8 Mon Sep 17 00:00:00 2001 From: Ben Howell Date: Fri, 14 Apr 2023 15:16:52 -0700 Subject: [PATCH 5/5] Fix up styles --- .../src/components/Avatar/useAvatarStyles.ts | 92 +++++++++++-------- 1 file changed, 54 insertions(+), 38 deletions(-) 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 89e51c27ba051a..8d76450576f0c2 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,7 @@ -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 { AvatarSlots, AvatarState } from './Avatar.types'; export const avatarClassNames: SlotClassNames = { root: 'fui-Avatar', @@ -16,7 +16,6 @@ const vars = { badgeRadius: '--fui-Avatar-badgeRadius', badgeGap: '--fui-Avatar-badgeGap', badgeAlign: '--fui-Avatar-badgeAlign', - badgeMask: '--fui-Avatar-badgeMask', ringWidth: '--fui-Avatar-ringWidth', }; @@ -53,7 +52,6 @@ const useRootClassName = makeResetStyles({ '::before': { borderStyle: 'solid', borderWidth: `var(${vars.ringWidth})`, - maskImage: `var(${vars.badgeMask})`, }, }); @@ -67,7 +65,6 @@ const useImageClassName = makeResetStyles({ borderRadius: 'inherit', objectFit: 'cover', verticalAlign: 'top', - maskImage: `var(${vars.badgeMask})`, }); const useIconInitialsClassName = makeResetStyles({ @@ -87,9 +84,29 @@ const useIconInitialsClassName = makeResetStyles({ textAlign: 'center', userSelect: 'none', borderRadius: 'inherit', - maskImage: `var(${vars.badgeMask})`, }); +/** + * Helper to create a maskImage that punches out a circle larger than the badge by `badgeGap`. + * This creates a transparent gap between the badge and Avatar. + */ +const badgeCutout = (margin?: string) => { + const center = margin ? `calc(var(${vars.badgeRadius}) + ${margin})` : `var(${vars.badgeRadius})`; + + // radial-gradient does not have anti-aliasing, so the transparent and opaque circles are offset by +/- 0.25px + // to "fade" from transparent to opaque over a half-pixel and ease the transition. + return { + maskImage: + `radial-gradient(circle at bottom ${center} var(${vars.badgeAlign}) ${center}, ` + + `transparent calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) - 0.25px), ` + + `white calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) + 0.25px))`, + + // Griffel won't auto-flip the "right" alignment to "left" in RTL if it is inline in the maskImage, + // so split it out into a css variable that will auto-flip. + [vars.badgeAlign]: 'right', + }; +}; + const useStyles = makeStyles({ textCaption2Strong: { fontSize: tokens.fontSizeBase100 }, textCaption1Strong: { fontSize: tokens.fontSizeBase200 }, @@ -121,30 +138,21 @@ const useStyles = makeStyles({ }, }, - inactive: { - opacity: '0.8', - transform: 'scale(0.875)', - transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - - '::before,::after': { - ...shorthands.margin(0), - opacity: 0, - transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, - }, - }, - ring: { // Show the ::before pseudo-element, which is the ring '::before': { content: '""' }, }, + ringBadgeCutout: { + '::before': badgeCutout(/*margin =*/ `2 * var(${vars.ringWidth})`), + }, ringThick: { - '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThick }, + [vars.ringWidth]: tokens.strokeWidthThick, }, ringThicker: { - '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThicker }, + [vars.ringWidth]: tokens.strokeWidthThicker, }, ringThickest: { - '::before,::after': { [vars.ringWidth]: tokens.strokeWidthThickest }, + [vars.ringWidth]: tokens.strokeWidthThickest, }, shadow: { @@ -164,6 +172,18 @@ const useStyles = makeStyles({ '::after': { boxShadow: tokens.shadow28 }, }, + inactive: { + opacity: '0.8', + transform: 'scale(0.875)', + transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, + + '::before,::after': { + ...shorthands.margin(0), + opacity: 0, + transitionTimingFunction: `${tokens.curveDecelerateMin}, ${tokens.curveLinear}`, + }, + }, + // Applied to the badge slot badge: { position: 'absolute', @@ -171,21 +191,8 @@ const useStyles = makeStyles({ right: 0, }, - // Applied to the root slot - rootWithBadge: { - [vars.badgeMask]: - `radial-gradient(circle at ` + - `bottom calc(var(${vars.badgeRadius}) + 2 * var(${vars.ringWidth}, 0px)) ` + - `var(${vars.badgeAlign}) calc(var(${vars.badgeRadius}) + 2 * var(${vars.ringWidth}, 0px)), ` + - // radial-gradient does not have anti-aliasing, so the transparent and opaque circles are offset by +/- 0.25px - // to "fade" from transparent to opaque over a half-pixel and ease the transition. - `transparent calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) - 0.25px), ` + - `white calc(var(${vars.badgeRadius}) + var(${vars.badgeGap}) + 0.25px))`, - - // Griffel won't auto-flip the "right" alignment to "left" in RTL if it is inline in the maskImage, - // so split it out into a css variable that will auto-flip. - [vars.badgeAlign]: 'right', - }, + // Applied to the image, initials, or icon slot when there is a badge + badgeCutout: badgeCutout(), icon12: { fontSize: '12px' }, icon16: { fontSize: '16px' }, @@ -484,7 +491,6 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { rootClassName, size !== 32 && sizeStyles[size], state.badge && sizeStyles[state.badge.size || 'medium'], - state.badge && styles.rootWithBadge, ]; if (size <= 24) { @@ -518,6 +524,9 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { if (activeAppearance === 'ring' || activeAppearance === 'ring-shadow') { rootClasses.push(styles.ring, ringColorStyles[color]); + if (state.badge) { + rootClasses.push(styles.ringBadgeCutout); + } if (size <= 48) { rootClasses.push(styles.ringThick); @@ -554,7 +563,12 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { } if (state.image) { - state.image.className = mergeClasses(avatarClassNames.image, imageClassName, state.image.className); + state.image.className = mergeClasses( + avatarClassNames.image, + imageClassName, + state.badge && styles.badgeCutout, + state.image.className, + ); } if (state.initials) { @@ -562,6 +576,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { avatarClassNames.initials, iconInitialsClassName, colorStyles[color], + state.badge && styles.badgeCutout, state.initials.className, ); } @@ -589,6 +604,7 @@ export const useAvatarStyles_unstable = (state: AvatarState): AvatarState => { iconInitialsClassName, iconSizeClass, colorStyles[color], + state.badge && styles.badgeCutout, state.icon.className, ); }