Skip to content

Commit b436be7

Browse files
francineluccajonrohan
authored andcommitted
TEST: Revert "chore(AvatarStack): Remove the CSS modules feature flag from the AvatarStack component" (#5879)
1 parent 4731891 commit b436be7

File tree

4 files changed

+395
-52
lines changed

4 files changed

+395
-52
lines changed

.changeset/famous-emus-chew.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/react/src/AvatarStack/AvatarStack.tsx

Lines changed: 225 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,188 @@
11
import {clsx} from 'clsx'
22
import React, {useEffect, useRef, useState} from 'react'
3-
import type {SxProp} from '../sx'
3+
import styled from 'styled-components'
4+
import {get} from '../constants'
5+
import Box from '../Box'
6+
import type {BetterCssProperties, BetterSystemStyleObject, SxProp} from '../sx'
7+
import sx, {merge} from '../sx'
48
import type {AvatarProps} from '../Avatar/Avatar'
59
import {DEFAULT_AVATAR_SIZE} from '../Avatar/Avatar'
610
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
711
import {isResponsiveValue} from '../hooks/useResponsiveValue'
12+
import {getBreakpointDeclarations} from '../utils/getBreakpointDeclarations'
813
import {defaultSxProp} from '../utils/defaultSxProp'
914
import type {WidthOnlyViewportRangeKeys} from '../utils/types/ViewportRangeKeys'
1015
import classes from './AvatarStack.module.css'
16+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
17+
import {useFeatureFlag} from '../FeatureFlags'
1118
import {hasInteractiveNodes} from '../internal/utils/hasInteractiveNodes'
12-
import {toggleSxComponent} from '../internal/utils/toggleSxComponent'
19+
import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles'
1320

14-
const transformChildren = (children: React.ReactNode) => {
21+
type StyledAvatarStackWrapperProps = {
22+
count?: number
23+
} & SxProp
24+
25+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_ga'
26+
27+
const AvatarStackWrapper = toggleStyledComponent(
28+
CSS_MODULES_FEATURE_FLAG,
29+
'span',
30+
styled.span<StyledAvatarStackWrapperProps>`
31+
--avatar-border-width: 1px;
32+
--overlap-size: calc(var(--avatar-stack-size) * 0.55);
33+
--overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85);
34+
--mask-size: calc(100% + (var(--avatar-border-width) * 2));
35+
--mask-start: -1;
36+
--opacity-step: 15%;
37+
38+
display: flex;
39+
position: relative;
40+
height: var(--avatar-stack-size);
41+
min-width: var(--avatar-stack-size);
42+
isolation: isolate;
43+
44+
.pc-AvatarStackBody {
45+
display: flex;
46+
position: absolute;
47+
48+
${getGlobalFocusStyles('1px')}
49+
}
50+
51+
.pc-AvatarItem {
52+
--avatar-size: var(--avatar-stack-size);
53+
flex-shrink: 0;
54+
height: var(--avatar-stack-size);
55+
width: var(--avatar-stack-size);
56+
position: relative;
57+
overflow: hidden;
58+
display: flex;
59+
transition:
60+
margin 0.2s ease-in-out,
61+
opacity 0.2s ease-in-out,
62+
mask-position 0.2s ease-in-out,
63+
mask-size 0.2s ease-in-out;
64+
65+
&:is(img) {
66+
box-shadow: 0 0 0 var(--avatar-border-width)
67+
${props => (props.count === 1 ? get('colors.avatar.border') : 'transparent')};
68+
}
69+
70+
&:first-child {
71+
margin-inline-start: 0;
72+
}
73+
74+
&:nth-child(n + 2) {
75+
margin-inline-start: calc(var(--overlap-size) * -1);
76+
mask-image: radial-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgba(0, 0, 0, 0) 71%),
77+
linear-gradient(rgb(0, 0, 0) 0 0);
78+
mask-repeat: no-repeat, no-repeat;
79+
mask-size:
80+
var(--mask-size) var(--mask-size),
81+
auto;
82+
mask-composite: exclude;
83+
// HORIZONTAL POSITION CALC FORMULA EXPLAINED:
84+
// width of the visible part of the avatar ➡️ var(--avatar-stack-size) - var(--overlap-size)
85+
// multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start)
86+
// subtract the avatar border width ➡️ var(--avatar-border-width)
87+
mask-position:
88+
calc((var(--avatar-stack-size) - var(--overlap-size)) * var(--mask-start) - var(--avatar-border-width)) center,
89+
0 0;
90+
// HACK: This padding fixes a weird rendering bug where a tiiiiny outline is visible at the edges of the element
91+
padding: 0.1px;
92+
}
93+
94+
&:nth-child(n + 3) {
95+
--overlap-size: var(--overlap-size-avatar-three-plus);
96+
opacity: calc(100% - 2 * var(--opacity-step));
97+
}
98+
99+
&:nth-child(n + 4) {
100+
opacity: calc(100% - 3 * var(--opacity-step));
101+
}
102+
103+
&:nth-child(n + 5) {
104+
opacity: calc(100% - 4 * var(--opacity-step));
105+
}
106+
107+
&:nth-child(n + 6) {
108+
opacity: 0;
109+
visibility: hidden;
110+
}
111+
}
112+
113+
&.pc-AvatarStack--two {
114+
// MIN-WIDTH CALC FORMULA EXPLAINED:
115+
// avatar size ➡️ var(--avatar-stack-size)
116+
// plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size)
117+
min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)));
118+
}
119+
120+
&.pc-AvatarStack--three {
121+
// MIN-WIDTH CALC FORMULA EXPLAINED:
122+
// avatar size ➡️ var(--avatar-stack-size)
123+
// plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size)
124+
// plus the visible part of the 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)
125+
min-width: calc(
126+
var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) +
127+
(var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus))
128+
);
129+
}
130+
131+
&.pc-AvatarStack--three-plus {
132+
// MIN-WIDTH CALC FORMULA EXPLAINED:
133+
// avatar size ➡️ var(--avatar-stack-size)
134+
// plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size)
135+
// plus the visible part of the 3rd AND 4th avatar ➡️ (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2
136+
min-width: calc(
137+
var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) +
138+
(var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2
139+
);
140+
}
141+
142+
&.pc-AvatarStack--right {
143+
--mask-start: 1;
144+
direction: rtl;
145+
}
146+
147+
.pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover,
148+
.pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within {
149+
width: auto;
150+
151+
.pc-AvatarItem {
152+
// reset size of the mask to prevent unintentially clipping due to the additional size created by the border width
153+
--mask-size: 100%;
154+
margin-inline-start: ${get('space.1')};
155+
opacity: 1;
156+
visibility: visible;
157+
// HORIZONTAL POSITION CALC FORMULA EXPLAINED:
158+
// width of the full avatar ➡️ var(--avatar-stack-size)
159+
// multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start)
160+
mask-position:
161+
calc(var(--avatar-stack-size) * var(--mask-start)) center,
162+
0 0;
163+
164+
${getGlobalFocusStyles('1px')}
165+
166+
&:first-child {
167+
margin-inline-start: 0;
168+
}
169+
}
170+
}
171+
172+
.pc-AvatarStack--disableExpand {
173+
position: relative;
174+
}
175+
176+
${sx};
177+
`,
178+
)
179+
180+
const transformChildren = (children: React.ReactNode, enabled: boolean) => {
15181
return React.Children.map(children, child => {
16182
if (!React.isValidElement(child)) return child
17183
return React.cloneElement(child, {
18184
...child.props,
19-
className: clsx(child.props.className, 'pc-AvatarItem', classes.AvatarItem),
185+
className: clsx(child.props.className, 'pc-AvatarItem', {[classes.AvatarItem]: enabled}),
20186
})
21187
})
22188
}
@@ -43,16 +209,28 @@ const AvatarStackBody = ({
43209
const bodyClassNames = clsx('pc-AvatarStackBody', {
44210
'pc-AvatarStack--disableExpand': disableExpand,
45211
})
212+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
46213

214+
if (enabled) {
215+
return (
216+
<div
217+
data-disable-expand={disableExpand ? '' : undefined}
218+
className={clsx(bodyClassNames, classes.AvatarStackBody)}
219+
tabIndex={!hasInteractiveChildren && !disableExpand ? 0 : undefined}
220+
ref={stackContainer}
221+
>
222+
{children}
223+
</div>
224+
)
225+
}
47226
return (
48-
<div
49-
data-disable-expand={disableExpand ? '' : undefined}
50-
className={clsx(bodyClassNames, classes.AvatarStackBody)}
227+
<Box
228+
className={bodyClassNames}
51229
tabIndex={!hasInteractiveChildren && !disableExpand ? 0 : undefined}
52230
ref={stackContainer}
53231
>
54232
{children}
55-
</div>
233+
</Box>
56234
)
57235
}
58236

@@ -65,6 +243,7 @@ const AvatarStack = ({
65243
style,
66244
sx: sxProp = defaultSxProp,
67245
}: AvatarStackProps) => {
246+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
68247
const [hasInteractiveChildren, setHasInteractiveChildren] = useState<boolean | undefined>(false)
69248
const stackContainer = useRef<HTMLDivElement>(null)
70249

@@ -142,46 +321,66 @@ const AvatarStack = ({
142321
const getResponsiveAvatarSizeStyles = () => {
143322
// if there is no size set on the AvatarStack, use the `size` props of the Avatar children to set the `--avatar-stack-size` CSS variable
144323
if (!size) {
145-
return {
146-
'--stackSize-narrow': `${childSizes.narrow}px`,
147-
'--stackSize-regular': `${childSizes.regular}px`,
148-
'--stackSize-wide': `${childSizes.wide}px`,
324+
if (enabled) {
325+
return {
326+
'--stackSize-narrow': `${childSizes.narrow}px`,
327+
'--stackSize-regular': `${childSizes.regular}px`,
328+
'--stackSize-wide': `${childSizes.wide}px`,
329+
}
149330
}
331+
332+
return getBreakpointDeclarations(
333+
childSizes,
334+
'--avatar-stack-size' as keyof React.CSSProperties,
335+
value => `${value}px`,
336+
)
150337
}
151338

152339
// if the `size` prop is set and responsive, set the `--avatar-stack-size` CSS variable for each viewport
153340
if (isResponsiveValue(size)) {
154-
return {
155-
'--stackSize-narrow': `${size.narrow || DEFAULT_AVATAR_SIZE}px`,
156-
'--stackSize-regular': `${size.regular || DEFAULT_AVATAR_SIZE}px`,
157-
'--stackSize-wide': `${size.wide || DEFAULT_AVATAR_SIZE}px`,
341+
if (enabled) {
342+
return {
343+
'--stackSize-narrow': `${size.narrow || DEFAULT_AVATAR_SIZE}px`,
344+
'--stackSize-regular': `${size.regular || DEFAULT_AVATAR_SIZE}px`,
345+
'--stackSize-wide': `${size.wide || DEFAULT_AVATAR_SIZE}px`,
346+
}
158347
}
348+
349+
return getBreakpointDeclarations(
350+
size,
351+
'--avatar-stack-size' as keyof React.CSSProperties,
352+
value => `${value || DEFAULT_AVATAR_SIZE}px`,
353+
)
159354
}
160355

161356
// if the `size` prop is set and not responsive, it is a number, so we can just set the `--avatar-stack-size` CSS variable to that number
162357
return {'--avatar-stack-size': `${size}px`} as React.CSSProperties
163358
}
164359

165-
const BaseComponentWrapper = toggleSxComponent('div')
360+
const avatarStackSx = merge<BetterCssProperties | BetterSystemStyleObject>(
361+
!enabled && getResponsiveAvatarSizeStyles(),
362+
sxProp as SxProp,
363+
)
166364

167365
return (
168-
<BaseComponentWrapper
169-
data-avatar-count={count > 3 ? '3+' : count}
170-
data-align-right={alignRight ? '' : undefined}
171-
data-responsive={!size || isResponsiveValue(size) ? '' : undefined}
172-
className={clsx(wrapperClassNames, classes.AvatarStack)}
173-
style={{...getResponsiveAvatarSizeStyles(), ...style}}
174-
sx={sxProp}
366+
<AvatarStackWrapper
367+
count={enabled ? undefined : count}
368+
data-avatar-count={enabled ? (count > 3 ? '3+' : count) : undefined}
369+
data-align-right={enabled && alignRight ? '' : undefined}
370+
data-responsive={enabled && (!size || isResponsiveValue(size)) ? '' : undefined}
371+
className={clsx(wrapperClassNames, {[classes.AvatarStack]: enabled})}
372+
style={enabled ? {...getResponsiveAvatarSizeStyles(), style} : style}
373+
sx={avatarStackSx}
175374
>
176375
<AvatarStackBody
177376
disableExpand={disableExpand}
178377
hasInteractiveChildren={hasInteractiveChildren}
179378
stackContainer={stackContainer}
180379
>
181380
{' '}
182-
{transformChildren(children)}
381+
{transformChildren(children, enabled)}
183382
</AvatarStackBody>
184-
</BaseComponentWrapper>
383+
</AvatarStackWrapper>
185384
)
186385
}
187386

0 commit comments

Comments
 (0)