Skip to content

Commit 024b82a

Browse files
chanakyavjoshblack
andauthored
Add variant & shape props to AvatarStack component (#6420)
Co-authored-by: chanakyav <[email protected]> Co-authored-by: Josh Black <[email protected]>
1 parent 77f6927 commit 024b82a

File tree

5 files changed

+120
-15
lines changed

5 files changed

+120
-15
lines changed

.changeset/eleven-chefs-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Adds `variant` and `shape` props to `AvatarStack` component. The `variant` prop will allow the component to render in a cascade view (by default) or a new stacked view which will evenly space the avatars and remove opacity. The `shape` prop will allow the avatars to be rendered as circles (by default) or squares.
-21 Bytes
Loading

packages/react/src/AvatarStack/AvatarStack.module.css

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/* stylelint-disable selector-max-specificity */
22
.AvatarStack {
33
--avatar-border-width: 1px;
4-
--overlap-size: calc(var(--avatar-stack-size) * 0.55);
5-
--overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85);
64
--mask-size: calc(100% + (var(--avatar-border-width) * 2));
75
--mask-start: -1;
86
--opacity-step: 15%;
@@ -13,6 +11,16 @@
1311
height: var(--avatar-stack-size);
1412
isolation: isolate;
1513

14+
&:where([data-variant='cascade']) {
15+
--overlap-size: calc(var(--avatar-stack-size) * 0.55);
16+
--overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85);
17+
}
18+
19+
&:where([data-variant='stack']) {
20+
--overlap-size: calc(var(--avatar-stack-size) * 0.55);
21+
--overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.55);
22+
}
23+
1624
&:where([data-responsive]) {
1725
@media screen and (--viewportRange-narrow) {
1826
--avatar-stack-size: var(--stackSize-narrow);
@@ -27,13 +35,23 @@
2735
}
2836
}
2937

30-
&:where([data-avatar-count='1']) {
38+
&:where([data-avatar-count='1'][data-shape='circle']) {
3139
.AvatarItem {
3240
/* stylelint-disable-next-line primer/box-shadow */
3341
box-shadow: 0 0 0 var(--avatar-border-width) var(--avatar-borderColor);
3442
}
3543
}
3644

45+
&:where([data-avatar-count='1'][data-shape='square']) .AvatarItem {
46+
/* stylelint-disable-next-line primer/box-shadow */
47+
box-shadow: 1px 0 rgba(0, 0, 0, 1);
48+
}
49+
50+
&:where([data-avatar-count='1'][data-shape='square'][data-align-right]) .AvatarItem {
51+
/* stylelint-disable-next-line primer/box-shadow */
52+
box-shadow: -1px 0 rgba(0, 0, 0, 1);
53+
}
54+
3755
&:where([data-avatar-count='2']) {
3856
/*
3957
MIN-WIDTH CALC FORMULA EXPLAINED:
@@ -43,7 +61,7 @@
4361
min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)));
4462
}
4563

46-
&:where([data-avatar-count='3']) {
64+
&:where([data-avatar-count='3'][data-variant='cascade']) {
4765
/*
4866
MIN-WIDTH CALC FORMULA EXPLAINED:
4967
avatar size ➡️ var(--avatar-stack-size)
@@ -56,7 +74,16 @@
5674
);
5775
}
5876

59-
&:where([data-avatar-count='3+']) {
77+
&:where([data-avatar-count='3'][data-variant='stack']) {
78+
/*
79+
MIN-WIDTH CALC FORMULA EXPLAINED:
80+
avatar size ➡️ var(--avatar-stack-size)
81+
plus the visible part of the 2nd avatar & 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 2
82+
*/
83+
min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2);
84+
}
85+
86+
&:where([data-avatar-count='3+'][data-variant='cascade']) {
6087
/*
6188
MIN-WIDTH CALC FORMULA EXPLAINED:
6289
avatar size ➡️ var(--avatar-stack-size)
@@ -69,6 +96,17 @@
6996
);
7097
}
7198

99+
&:where([data-avatar-count='3+'][data-variant='stack']) {
100+
/*
101+
MIN-WIDTH CALC FORMULA EXPLAINED:
102+
avatar size ➡️ var(--avatar-stack-size)
103+
plus the visible part of the 2nd to 4th avatars ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 3
104+
*/
105+
min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 3);
106+
107+
--overlap-size: var(--overlap-size-avatar-three-plus);
108+
}
109+
72110
&:where([data-align-right]) {
73111
--mask-start: 1;
74112

@@ -100,20 +138,28 @@
100138
mask-position 0.2s ease-in-out,
101139
mask-size 0.2s ease-in-out;
102140

103-
&:is(img) {
141+
.AvatarStack:where([data-shape='circle']) &:is(img) {
104142
/* stylelint-disable-next-line primer/box-shadow */
105143
box-shadow: 0 0 0 var(--avatar-border-width) transparent;
106144
}
107145

146+
.AvatarStack:where([data-shape='square']) &:is(img) {
147+
/* stylelint-disable-next-line primer/box-shadow */
148+
box-shadow: 1px 0 rgba(255, 255, 255, 1);
149+
}
150+
151+
.AvatarStack:where([data-shape='square'][data-align-right]) &:is(img) {
152+
/* stylelint-disable-next-line primer/box-shadow */
153+
box-shadow: -1px 0 rgba(255, 255, 255, 1);
154+
}
155+
108156
&:first-child {
109157
margin-inline-start: 0;
110158
}
111159

112160
&:nth-child(n + 2) {
113161
/* stylelint-disable-next-line primer/spacing */
114162
margin-inline-start: calc(var(--overlap-size) * -1);
115-
/* stylelint-disable-next-line declaration-property-value-no-unknown */
116-
mask-image: radial-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgba(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0);
117163
mask-repeat: no-repeat, no-repeat;
118164
mask-size:
119165
var(--mask-size) var(--mask-size),
@@ -135,23 +181,58 @@
135181
padding: 0.1px;
136182
}
137183

138-
&:nth-child(n + 3) {
184+
/* Circular mask */
185+
.AvatarStack:where([data-shape='circle']) &:nth-child(n + 2) {
186+
/* stylelint-disable-next-line declaration-property-value-no-unknown */
187+
mask-image: radial-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgba(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0);
188+
}
189+
190+
/* Square mask */
191+
.AvatarStack:where([data-shape='square']) &:nth-child(n + 2) {
192+
/* stylelint-disable-next-line declaration-property-value-no-unknown */
193+
mask-image: linear-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgba(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0);
194+
}
195+
196+
/* Cascade variant override for nth-child(n + 3) */
197+
.AvatarStack:where([data-variant='cascade']) &:nth-child(n + 3) {
139198
--overlap-size: var(--overlap-size-avatar-three-plus);
140199

141200
/* stylelint-disable-next-line alpha-value-notation */
142201
opacity: calc(100% - 2 * var(--opacity-step));
143202
}
144203

145-
&:nth-child(n + 4) {
204+
/* Cascade variant override for nth-child(n + 4) */
205+
.AvatarStack:where([data-variant='cascade']) &:nth-child(n + 4) {
146206
/* stylelint-disable-next-line alpha-value-notation */
147207
opacity: calc(100% - 3 * var(--opacity-step));
148208
}
149209

150-
&:nth-child(n + 5) {
210+
/* Cascade variant override for nth-child(n + 5) */
211+
.AvatarStack:where([data-variant='cascade']) &:nth-child(n + 5) {
151212
/* stylelint-disable-next-line alpha-value-notation */
152213
opacity: calc(100% - 4 * var(--opacity-step));
153214
}
154215

216+
.AvatarStack:where([data-shape='square']) &:nth-child(1) {
217+
z-index: 5;
218+
}
219+
220+
.AvatarStack:where([data-shape='square']) &:nth-child(2) {
221+
z-index: 4;
222+
}
223+
224+
.AvatarStack:where([data-shape='square']) &:nth-child(3) {
225+
z-index: 3;
226+
}
227+
228+
.AvatarStack:where([data-shape='square']) &:nth-child(4) {
229+
z-index: 2;
230+
}
231+
232+
.AvatarStack:where([data-shape='square']) &:nth-child(5) {
233+
z-index: 1;
234+
}
235+
155236
&:nth-child(n + 6) {
156237
visibility: hidden;
157238
opacity: 0;

packages/react/src/AvatarStack/AvatarStack.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import classes from './AvatarStack.module.css'
1010
import {hasInteractiveNodes} from '../internal/utils/hasInteractiveNodes'
1111
import {BoxWithFallback} from '../internal/components/BoxWithFallback'
1212

13-
const transformChildren = (children: React.ReactNode) => {
13+
const transformChildren = (children: React.ReactNode, shape: AvatarStackProps['shape']) => {
1414
return React.Children.map(children, child => {
1515
if (!React.isValidElement(child)) return child
1616
return React.cloneElement(child, {
1717
...child.props,
18+
square: shape === 'square' ? true : undefined,
1819
className: clsx(child.props.className, 'pc-AvatarItem', classes.AvatarItem),
1920
})
2021
})
@@ -23,6 +24,8 @@ const transformChildren = (children: React.ReactNode) => {
2324
export type AvatarStackProps = {
2425
alignRight?: boolean
2526
disableExpand?: boolean
27+
variant?: 'cascade' | 'stack'
28+
shape?: 'circle' | 'square'
2629
size?: number | ResponsiveValue<number>
2730
className?: string
2831
children: React.ReactNode
@@ -57,7 +60,17 @@ const AvatarStackBody = ({
5760
)
5861
}
5962

60-
const AvatarStack = ({children, alignRight, disableExpand, size, className, style, sx: sxProp}: AvatarStackProps) => {
63+
const AvatarStack = ({
64+
children,
65+
variant = 'cascade',
66+
shape = 'circle',
67+
alignRight,
68+
disableExpand,
69+
size,
70+
className,
71+
style,
72+
sx: sxProp,
73+
}: AvatarStackProps) => {
6174
const [hasInteractiveChildren, setHasInteractiveChildren] = useState<boolean | undefined>(false)
6275
const stackContainer = useRef<HTMLDivElement>(null)
6376

@@ -149,11 +162,15 @@ const AvatarStack = ({children, alignRight, disableExpand, size, className, styl
149162
return (
150163
<BoxWithFallback
151164
as="span"
165+
data-variant={variant}
166+
data-shape={shape}
152167
data-avatar-count={count > 3 ? '3+' : count}
153168
data-align-right={alignRight ? '' : undefined}
154169
data-responsive={!size || isResponsiveValue(size) ? '' : undefined}
155170
className={clsx(
156171
{
172+
'pc-AvatarStack--variant': variant,
173+
'pc-AvatarStack--shape': shape,
157174
'pc-AvatarStack--two': count === 2,
158175
'pc-AvatarStack--three': count === 3,
159176
'pc-AvatarStack--three-plus': count > 3,
@@ -171,7 +188,7 @@ const AvatarStack = ({children, alignRight, disableExpand, size, className, styl
171188
stackContainer={stackContainer}
172189
>
173190
{' '}
174-
{transformChildren(children)}
191+
{transformChildren(children, shape)}
175192
</AvatarStackBody>
176193
</BoxWithFallback>
177194
)

packages/react/src/AvatarStack/__snapshots__/AvatarStack.test.tsx.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
exports[`AvatarStack > respects alignRight props 1`] = `
44
<span
5-
class="pc-AvatarStack--three-plus pc-AvatarStack--right prc-AvatarStack-AvatarStack-LcJ4R"
5+
class="pc-AvatarStack--variant pc-AvatarStack--shape pc-AvatarStack--three-plus pc-AvatarStack--right prc-AvatarStack-AvatarStack-LcJ4R"
66
data-align-right=""
77
data-avatar-count="3+"
88
data-responsive=""
9+
data-shape="circle"
10+
data-variant="cascade"
911
style="--stackSize-narrow: 20px; --stackSize-regular: 20px; --stackSize-wide: 20px;"
1012
>
1113
<div

0 commit comments

Comments
 (0)