11import { clsx } from 'clsx'
22import React , { useEffect , useRef , useState } from 'react'
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'
3+ import type { SxProp } from '../sx'
84import type { AvatarProps } from '../Avatar/Avatar'
95import { DEFAULT_AVATAR_SIZE } from '../Avatar/Avatar'
106import type { ResponsiveValue } from '../hooks/useResponsiveValue'
117import { isResponsiveValue } from '../hooks/useResponsiveValue'
12- import { getBreakpointDeclarations } from '../utils/getBreakpointDeclarations'
138import { defaultSxProp } from '../utils/defaultSxProp'
149import type { WidthOnlyViewportRangeKeys } from '../utils/types/ViewportRangeKeys'
1510import classes from './AvatarStack.module.css'
16- import { toggleStyledComponent } from '../internal/utils/toggleStyledComponent'
17- import { useFeatureFlag } from '../FeatureFlags'
1811import { hasInteractiveNodes } from '../internal/utils/hasInteractiveNodes'
19- import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles '
12+ import { toggleSxComponent } from '../internal/utils/toggleSxComponent '
2013
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 ) => {
14+ const transformChildren = ( children : React . ReactNode ) => {
18115 return React . Children . map ( children , child => {
18216 if ( ! React . isValidElement ( child ) ) return child
18317 return React . cloneElement ( child , {
18418 ...child . props ,
185- className : clsx ( child . props . className , 'pc-AvatarItem' , { [ classes . AvatarItem ] : enabled } ) ,
19+ className : clsx ( child . props . className , 'pc-AvatarItem' , classes . AvatarItem ) ,
18620 } )
18721 } )
18822}
@@ -209,28 +43,16 @@ const AvatarStackBody = ({
20943 const bodyClassNames = clsx ( 'pc-AvatarStackBody' , {
21044 'pc-AvatarStack--disableExpand' : disableExpand ,
21145 } )
212- const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
21346
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- }
22647 return (
227- < Box
228- className = { bodyClassNames }
48+ < div
49+ data-disable-expand = { disableExpand ? '' : undefined }
50+ className = { clsx ( bodyClassNames , classes . AvatarStackBody ) }
22951 tabIndex = { ! hasInteractiveChildren && ! disableExpand ? 0 : undefined }
23052 ref = { stackContainer }
23153 >
23254 { children }
233- </ Box >
55+ </ div >
23456 )
23557}
23658
@@ -243,7 +65,6 @@ const AvatarStack = ({
24365 style,
24466 sx : sxProp = defaultSxProp ,
24567} : AvatarStackProps ) => {
246- const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
24768 const [ hasInteractiveChildren , setHasInteractiveChildren ] = useState < boolean | undefined > ( false )
24869 const stackContainer = useRef < HTMLDivElement > ( null )
24970
@@ -321,66 +142,46 @@ const AvatarStack = ({
321142 const getResponsiveAvatarSizeStyles = ( ) => {
322143 // 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
323144 if ( ! size ) {
324- if ( enabled ) {
325- return {
326- '--stackSize-narrow' : `${ childSizes . narrow } px` ,
327- '--stackSize-regular' : `${ childSizes . regular } px` ,
328- '--stackSize-wide' : `${ childSizes . wide } px` ,
329- }
145+ return {
146+ '--stackSize-narrow' : `${ childSizes . narrow } px` ,
147+ '--stackSize-regular' : `${ childSizes . regular } px` ,
148+ '--stackSize-wide' : `${ childSizes . wide } px` ,
330149 }
331-
332- return getBreakpointDeclarations (
333- childSizes ,
334- '--avatar-stack-size' as keyof React . CSSProperties ,
335- value => `${ value } px` ,
336- )
337150 }
338151
339152 // if the `size` prop is set and responsive, set the `--avatar-stack-size` CSS variable for each viewport
340153 if ( isResponsiveValue ( size ) ) {
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- }
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` ,
347158 }
348-
349- return getBreakpointDeclarations (
350- size ,
351- '--avatar-stack-size' as keyof React . CSSProperties ,
352- value => `${ value || DEFAULT_AVATAR_SIZE } px` ,
353- )
354159 }
355160
356161 // 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
357162 return { '--avatar-stack-size' : `${ size } px` } as React . CSSProperties
358163 }
359164
360- const avatarStackSx = merge < BetterCssProperties | BetterSystemStyleObject > (
361- ! enabled && getResponsiveAvatarSizeStyles ( ) ,
362- sxProp as SxProp ,
363- )
165+ const BaseComponentWrapper = toggleSxComponent ( 'div' )
364166
365167 return (
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 }
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 }
374175 >
375176 < AvatarStackBody
376177 disableExpand = { disableExpand }
377178 hasInteractiveChildren = { hasInteractiveChildren }
378179 stackContainer = { stackContainer }
379180 >
380181 { ' ' }
381- { transformChildren ( children , enabled ) }
182+ { transformChildren ( children ) }
382183 </ AvatarStackBody >
383- </ AvatarStackWrapper >
184+ </ BaseComponentWrapper >
384185 )
385186}
386187
0 commit comments