diff --git a/.changeset/eleven-chefs-poke.md b/.changeset/eleven-chefs-poke.md new file mode 100644 index 00000000000..c4311d3b2f2 --- /dev/null +++ b/.changeset/eleven-chefs-poke.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +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. diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-forced-colors-dark-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-forced-colors-dark-linux.png index e9509ca5779..06967705847 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-forced-colors-dark-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-forced-colors-dark-linux.png differ diff --git a/packages/react/src/AvatarStack/AvatarStack.module.css b/packages/react/src/AvatarStack/AvatarStack.module.css index da7f3d3eec5..c960b963d84 100644 --- a/packages/react/src/AvatarStack/AvatarStack.module.css +++ b/packages/react/src/AvatarStack/AvatarStack.module.css @@ -1,8 +1,6 @@ /* stylelint-disable selector-max-specificity */ .AvatarStack { --avatar-border-width: 1px; - --overlap-size: calc(var(--avatar-stack-size) * 0.55); - --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85); --mask-size: calc(100% + (var(--avatar-border-width) * 2)); --mask-start: -1; --opacity-step: 15%; @@ -13,6 +11,16 @@ height: var(--avatar-stack-size); isolation: isolate; + &:where([data-variant='cascade']) { + --overlap-size: calc(var(--avatar-stack-size) * 0.55); + --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85); + } + + &:where([data-variant='stack']) { + --overlap-size: calc(var(--avatar-stack-size) * 0.55); + --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.55); + } + &:where([data-responsive]) { @media screen and (--viewportRange-narrow) { --avatar-stack-size: var(--stackSize-narrow); @@ -27,13 +35,23 @@ } } - &:where([data-avatar-count='1']) { + &:where([data-avatar-count='1'][data-shape='circle']) { .AvatarItem { /* stylelint-disable-next-line primer/box-shadow */ box-shadow: 0 0 0 var(--avatar-border-width) var(--avatar-borderColor); } } + &:where([data-avatar-count='1'][data-shape='square']) .AvatarItem { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 1px 0 rgba(0, 0, 0, 1); + } + + &:where([data-avatar-count='1'][data-shape='square'][data-align-right]) .AvatarItem { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: -1px 0 rgba(0, 0, 0, 1); + } + &:where([data-avatar-count='2']) { /* MIN-WIDTH CALC FORMULA EXPLAINED: @@ -43,7 +61,7 @@ min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size))); } - &:where([data-avatar-count='3']) { + &:where([data-avatar-count='3'][data-variant='cascade']) { /* MIN-WIDTH CALC FORMULA EXPLAINED: avatar size ➡️ var(--avatar-stack-size) @@ -56,7 +74,16 @@ ); } - &:where([data-avatar-count='3+']) { + &:where([data-avatar-count='3'][data-variant='stack']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd avatar & 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 2 + */ + min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2); + } + + &:where([data-avatar-count='3+'][data-variant='cascade']) { /* MIN-WIDTH CALC FORMULA EXPLAINED: avatar size ➡️ var(--avatar-stack-size) @@ -69,6 +96,17 @@ ); } + &:where([data-avatar-count='3+'][data-variant='stack']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd to 4th avatars ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 3 + */ + min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 3); + + --overlap-size: var(--overlap-size-avatar-three-plus); + } + &:where([data-align-right]) { --mask-start: 1; @@ -100,11 +138,21 @@ mask-position 0.2s ease-in-out, mask-size 0.2s ease-in-out; - &:is(img) { + .AvatarStack:where([data-shape='circle']) &:is(img) { /* stylelint-disable-next-line primer/box-shadow */ box-shadow: 0 0 0 var(--avatar-border-width) transparent; } + .AvatarStack:where([data-shape='square']) &:is(img) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 1px 0 rgba(255, 255, 255, 1); + } + + .AvatarStack:where([data-shape='square'][data-align-right]) &:is(img) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: -1px 0 rgba(255, 255, 255, 1); + } + &:first-child { margin-inline-start: 0; } @@ -112,8 +160,6 @@ &:nth-child(n + 2) { /* stylelint-disable-next-line primer/spacing */ margin-inline-start: calc(var(--overlap-size) * -1); - /* stylelint-disable-next-line declaration-property-value-no-unknown */ - 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); mask-repeat: no-repeat, no-repeat; mask-size: var(--mask-size) var(--mask-size), @@ -135,23 +181,58 @@ padding: 0.1px; } - &:nth-child(n + 3) { + /* Circular mask */ + .AvatarStack:where([data-shape='circle']) &:nth-child(n + 2) { + /* stylelint-disable-next-line declaration-property-value-no-unknown */ + 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); + } + + /* Square mask */ + .AvatarStack:where([data-shape='square']) &:nth-child(n + 2) { + /* stylelint-disable-next-line declaration-property-value-no-unknown */ + 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); + } + + /* Cascade variant override for nth-child(n + 3) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 3) { --overlap-size: var(--overlap-size-avatar-three-plus); /* stylelint-disable-next-line alpha-value-notation */ opacity: calc(100% - 2 * var(--opacity-step)); } - &:nth-child(n + 4) { + /* Cascade variant override for nth-child(n + 4) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 4) { /* stylelint-disable-next-line alpha-value-notation */ opacity: calc(100% - 3 * var(--opacity-step)); } - &:nth-child(n + 5) { + /* Cascade variant override for nth-child(n + 5) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 5) { /* stylelint-disable-next-line alpha-value-notation */ opacity: calc(100% - 4 * var(--opacity-step)); } + .AvatarStack:where([data-shape='square']) &:nth-child(1) { + z-index: 5; + } + + .AvatarStack:where([data-shape='square']) &:nth-child(2) { + z-index: 4; + } + + .AvatarStack:where([data-shape='square']) &:nth-child(3) { + z-index: 3; + } + + .AvatarStack:where([data-shape='square']) &:nth-child(4) { + z-index: 2; + } + + .AvatarStack:where([data-shape='square']) &:nth-child(5) { + z-index: 1; + } + &:nth-child(n + 6) { visibility: hidden; opacity: 0; diff --git a/packages/react/src/AvatarStack/AvatarStack.tsx b/packages/react/src/AvatarStack/AvatarStack.tsx index dadb3a9b6b7..7c1288f5399 100644 --- a/packages/react/src/AvatarStack/AvatarStack.tsx +++ b/packages/react/src/AvatarStack/AvatarStack.tsx @@ -10,11 +10,12 @@ import classes from './AvatarStack.module.css' import {hasInteractiveNodes} from '../internal/utils/hasInteractiveNodes' import {BoxWithFallback} from '../internal/components/BoxWithFallback' -const transformChildren = (children: React.ReactNode) => { +const transformChildren = (children: React.ReactNode, shape: AvatarStackProps['shape']) => { return React.Children.map(children, child => { if (!React.isValidElement(child)) return child return React.cloneElement(child, { ...child.props, + square: shape === 'square' ? true : undefined, className: clsx(child.props.className, 'pc-AvatarItem', classes.AvatarItem), }) }) @@ -23,6 +24,8 @@ const transformChildren = (children: React.ReactNode) => { export type AvatarStackProps = { alignRight?: boolean disableExpand?: boolean + variant?: 'cascade' | 'stack' + shape?: 'circle' | 'square' size?: number | ResponsiveValue className?: string children: React.ReactNode @@ -57,7 +60,17 @@ const AvatarStackBody = ({ ) } -const AvatarStack = ({children, alignRight, disableExpand, size, className, style, sx: sxProp}: AvatarStackProps) => { +const AvatarStack = ({ + children, + variant = 'cascade', + shape = 'circle', + alignRight, + disableExpand, + size, + className, + style, + sx: sxProp, +}: AvatarStackProps) => { const [hasInteractiveChildren, setHasInteractiveChildren] = useState(false) const stackContainer = useRef(null) @@ -149,11 +162,15 @@ const AvatarStack = ({children, alignRight, disableExpand, size, className, styl return ( 3 ? '3+' : count} data-align-right={alignRight ? '' : undefined} data-responsive={!size || isResponsiveValue(size) ? '' : undefined} className={clsx( { + 'pc-AvatarStack--variant': variant, + 'pc-AvatarStack--shape': shape, 'pc-AvatarStack--two': count === 2, 'pc-AvatarStack--three': count === 3, 'pc-AvatarStack--three-plus': count > 3, @@ -171,7 +188,7 @@ const AvatarStack = ({children, alignRight, disableExpand, size, className, styl stackContainer={stackContainer} > {' '} - {transformChildren(children)} + {transformChildren(children, shape)} ) diff --git a/packages/react/src/AvatarStack/__snapshots__/AvatarStack.test.tsx.snap b/packages/react/src/AvatarStack/__snapshots__/AvatarStack.test.tsx.snap index f6e033e3abc..04f86ce225d 100644 --- a/packages/react/src/AvatarStack/__snapshots__/AvatarStack.test.tsx.snap +++ b/packages/react/src/AvatarStack/__snapshots__/AvatarStack.test.tsx.snap @@ -2,10 +2,12 @@ exports[`AvatarStack > respects alignRight props 1`] = `
-`; - -exports[`Link passes href down to link element 1`] = ` - -`; - -exports[`Link respects hoverColor prop 1`] = ` - -`; - -exports[`Link respects the "sx" prop when "muted" prop is also passed 1`] = ` -.c0 { - color: var(--fgColor-onEmphasis,var(--color-fg-on-emphasis,#ffffff)); -} - - -`; - -exports[`Link respects the "muted" prop 1`] = ` - -`; diff --git a/packages/react/src/Popover/__snapshots__/Popover.test.tsx.snap b/packages/react/src/Popover/__snapshots__/Popover.test.tsx.snap deleted file mode 100644 index 968d125dc09..00000000000 --- a/packages/react/src/Popover/__snapshots__/Popover.test.tsx.snap +++ /dev/null @@ -1,205 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Popover renders correctly for a caret position of bottom 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of bottom-left 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of bottom-right 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of left 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of left-bottom 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of left-top 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of right 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of right-bottom 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of right-top 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of top 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of top-left 1`] = ` -
-
- Hello! -
-
-`; - -exports[`Popover renders correctly for a caret position of top-right 1`] = ` -
-
- Hello! -
-
-`;