-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: New Component Avatar Group [HOMER-2118] #2518
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
size-limit report 📦
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't see the new component in the preview: https://forma-36-git-feat-avatargrouphomer-2118.colorfuldemo.com/components/avatar
Is this expected?
### Variants | ||
|
||
The group can be rendered in two variations | ||
|
||
- **spaced** - default variant | ||
- **stacked** | ||
|
||
```jsx file=./examples/AvatarGroupVariantExample.tsx | ||
|
||
``` | ||
|
||
### Sizes | ||
|
||
The group can be rendered in two different sizes | ||
|
||
- **Small** - 24px | ||
- **Medium** - 32px, default size | ||
|
||
```jsx file=./examples/AvatarGroupSizeExample.tsx | ||
|
||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: based on the example of the Avatar
shouldn't this headings be under ## Examples
?
It also doesn't have the import
section
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we have specified structure for readme files, https://github.com/contentful/forma-36/blob/7e46b96fd42f9a760987802acd7ef0b1811e9894/scripts/plop-templates/package/README.mdx.hbs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs are broken: https://forma-36-git-feat-avatargrouphomer-2118.colorfuldemo.com/components/avatar
At least one of the errors is because the new component has not been added to the SandpackRenderer and LiveEditor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than what I've commented, looks good!
Also make sure to add AvatarGroup import here and pass it to the liveProviderScope
SandpackRenderer though should work fine when the new version gets published.
@@ -97,3 +97,39 @@ When the avatar image is loading, a loading skeleton will be shown automatically | |||
## Accessibility | |||
|
|||
Make sure to pass a fitting `alt` property, especially if the avatar is used by itself without the user's name next to it. | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's follow the same structure we have in Button
for example, which also has a ButtonGroup
. And by the same structure I mean separate folder avatar/src/AvatarGroup
where you can have implementation, styles, README, etc.
### Variants | ||
|
||
The group can be rendered in two variations | ||
|
||
- **spaced** - default variant | ||
- **stacked** | ||
|
||
```jsx file=./examples/AvatarGroupVariantExample.tsx | ||
|
||
``` | ||
|
||
### Sizes | ||
|
||
The group can be rendered in two different sizes | ||
|
||
- **Small** - 24px | ||
- **Medium** - 32px, default size | ||
|
||
```jsx file=./examples/AvatarGroupSizeExample.tsx | ||
|
||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we have specified structure for readme files, https://github.com/contentful/forma-36/blob/7e46b96fd42f9a760987802acd7ef0b1811e9894/scripts/plop-templates/package/README.mdx.hbs
|
||
import { cx } from 'emotion'; | ||
|
||
export type Variant = 'stacked' | 'spaced'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we export it I'd give it a more specific name
export type Variant = 'stacked' | 'spaced'; | |
export type AvatarGroupVariant = 'stacked' | 'spaced'; |
|
||
export interface AvatarGroupProps extends CommonProps { | ||
spacing?: SpacingTokens; | ||
className?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
className
is already assigned from CommonProps
className?: string; |
const renderAvatars = ( | ||
children: AvatarGroupProps['children'], | ||
size: AvatarGroupProps['size'], | ||
variant: AvatarGroupProps['variant'], | ||
) => { | ||
const styles = getAvatarGroupStyles(size); | ||
|
||
if (React.Children.count(children) > 3) { | ||
return ( | ||
<> | ||
{React.Children.map(children, (child, index) => { | ||
if (index < 2) { | ||
return React.cloneElement(child as React.ReactElement, { | ||
key: `avatar-rendered-${index}`, | ||
size: size, | ||
className: cx((child as React.ReactElement).props.className, { | ||
[styles.avatarStacked]: variant === 'stacked', | ||
[styles.avatarSpaced]: variant === 'spaced', | ||
}), | ||
}); | ||
} | ||
})} | ||
<Menu placement="bottom-end"> | ||
<Menu.Trigger> | ||
<button | ||
type="button" | ||
className={cx( | ||
{ | ||
[styles.avatarStacked]: variant === 'stacked', | ||
[styles.avatarSpaced]: variant === 'spaced', | ||
}, | ||
styles.moreAvatarsBtn, | ||
)} | ||
> | ||
{React.Children.count(children) - 2} | ||
</button> | ||
</Menu.Trigger> | ||
<Menu.List> | ||
{React.Children.toArray(children) | ||
.slice(2) | ||
.map((child, index) => { | ||
return ( | ||
<Menu.Item | ||
className={styles.moreAvatarsItem} | ||
key={`avatar-${index}`} | ||
> | ||
{React.cloneElement(child as React.ReactElement, { | ||
key: `avatar-menuitem-${index}`, | ||
size: 'tiny', | ||
})} | ||
{(child as React.ReactElement).props.alt} | ||
</Menu.Item> | ||
); | ||
})} | ||
</Menu.List> | ||
</Menu> | ||
</> | ||
); | ||
} | ||
return React.Children.map(children, (child, index) => { | ||
return React.cloneElement(child as React.ReactElement, { | ||
key: `avatar-rendered-${index}`, | ||
size: size, | ||
className: cx((child as React.ReactElement).props.className, { | ||
[styles.avatarStacked]: variant === 'stacked', | ||
[styles.avatarSpaced]: variant === 'spaced', | ||
}), | ||
}); | ||
}); | ||
}; | ||
|
||
function _AvatarGroup( | ||
{ | ||
children, | ||
className, | ||
size = 'medium', | ||
variant = 'spaced', | ||
testId = 'cf-ui-avatar-group', | ||
}: AvatarGroupProps, | ||
forwardedRef: React.Ref<HTMLDivElement>, | ||
) { | ||
return ( | ||
<Flex | ||
flexDirection="row" | ||
className={cx(className)} | ||
data-test-id={testId} | ||
ref={forwardedRef} | ||
> | ||
{renderAvatars(children, size, variant)} | ||
</Flex> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simplify it a bit. Sorry for nitpicking, just wanted to share the idea.
const renderAvatars = ( | |
children: AvatarGroupProps['children'], | |
size: AvatarGroupProps['size'], | |
variant: AvatarGroupProps['variant'], | |
) => { | |
const styles = getAvatarGroupStyles(size); | |
if (React.Children.count(children) > 3) { | |
return ( | |
<> | |
{React.Children.map(children, (child, index) => { | |
if (index < 2) { | |
return React.cloneElement(child as React.ReactElement, { | |
key: `avatar-rendered-${index}`, | |
size: size, | |
className: cx((child as React.ReactElement).props.className, { | |
[styles.avatarStacked]: variant === 'stacked', | |
[styles.avatarSpaced]: variant === 'spaced', | |
}), | |
}); | |
} | |
})} | |
<Menu placement="bottom-end"> | |
<Menu.Trigger> | |
<button | |
type="button" | |
className={cx( | |
{ | |
[styles.avatarStacked]: variant === 'stacked', | |
[styles.avatarSpaced]: variant === 'spaced', | |
}, | |
styles.moreAvatarsBtn, | |
)} | |
> | |
{React.Children.count(children) - 2} | |
</button> | |
</Menu.Trigger> | |
<Menu.List> | |
{React.Children.toArray(children) | |
.slice(2) | |
.map((child, index) => { | |
return ( | |
<Menu.Item | |
className={styles.moreAvatarsItem} | |
key={`avatar-${index}`} | |
> | |
{React.cloneElement(child as React.ReactElement, { | |
key: `avatar-menuitem-${index}`, | |
size: 'tiny', | |
})} | |
{(child as React.ReactElement).props.alt} | |
</Menu.Item> | |
); | |
})} | |
</Menu.List> | |
</Menu> | |
</> | |
); | |
} | |
return React.Children.map(children, (child, index) => { | |
return React.cloneElement(child as React.ReactElement, { | |
key: `avatar-rendered-${index}`, | |
size: size, | |
className: cx((child as React.ReactElement).props.className, { | |
[styles.avatarStacked]: variant === 'stacked', | |
[styles.avatarSpaced]: variant === 'spaced', | |
}), | |
}); | |
}); | |
}; | |
function _AvatarGroup( | |
{ | |
children, | |
className, | |
size = 'medium', | |
variant = 'spaced', | |
testId = 'cf-ui-avatar-group', | |
}: AvatarGroupProps, | |
forwardedRef: React.Ref<HTMLDivElement>, | |
) { | |
return ( | |
<Flex | |
flexDirection="row" | |
className={cx(className)} | |
data-test-id={testId} | |
ref={forwardedRef} | |
> | |
{renderAvatars(children, size, variant)} | |
</Flex> | |
); | |
} | |
function _AvatarGroup( | |
{ | |
children, | |
className, | |
size = 'medium', | |
variant = 'spaced', | |
testId = 'cf-ui-avatar-group', | |
}: AvatarGroupProps, | |
forwardedRef: React.Ref<HTMLDivElement>, | |
) { | |
const styles = getAvatarGroupStyles(size); | |
const childrenArray = React.Children.toArray(children); | |
const childrenToRenderCount = childrenArray.length > 3 ? 2 : 3; | |
const childrenToRender = childrenArray.slice(0, childrenToRenderCount); | |
const childrenInMenu = childrenArray.slice(childrenToRenderCount); | |
return ( | |
<Flex | |
flexDirection="row" | |
className={cx(className)} | |
data-test-id={testId} | |
ref={forwardedRef} | |
> | |
{childrenToRender.map((child, index) => { | |
return React.cloneElement(child as React.ReactElement, { | |
key: `avatar-rendered-${index}`, | |
size: size, | |
className: cx((child as React.ReactElement).props.className, { | |
[styles.avatarStacked]: variant === 'stacked', | |
[styles.avatarSpaced]: variant === 'spaced', | |
}), | |
}); | |
})} | |
{childrenInMenu.length > 0 && ( | |
<Menu placement="bottom-end"> | |
<Menu.Trigger> | |
<button | |
type="button" | |
className={cx( | |
{ | |
[styles.avatarStacked]: variant === 'stacked', | |
[styles.avatarSpaced]: variant === 'spaced', | |
}, | |
styles.moreAvatarsBtn, | |
)} | |
> | |
{childrenInMenu.length} | |
</button> | |
</Menu.Trigger> | |
<Menu.List> | |
{childrenInMenu.map((child, index) => { | |
return ( | |
<Menu.Item | |
className={styles.moreAvatarsItem} | |
key={`avatar-${index}`} | |
> | |
{React.cloneElement(child as React.ReactElement, { | |
key: `avatar-menuitem-${index}`, | |
size: 'tiny', | |
})} | |
{(child as React.ReactElement).props.alt} | |
</Menu.Item> | |
); | |
})} | |
</Menu.List> | |
</Menu> | |
)} | |
</Flex> | |
); | |
} |
import { cx } from 'emotion'; | ||
|
||
export interface AvatarGroupProps extends CommonProps { | ||
spacing?: SpacingTokens; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the spacing
prop being used anywhere, should it be?
…forma-36 into feat/Avatar_Group_HOMER-2118
Can you please bump the package.json version? You can do it with |
@denkristoffer Done |
Purpose of PR
Introduces a new component for grouping Avatars. It comes in two sizes and two different designs (stacked and spaced). It will show a maximum of 3 Avatar children, the rest of the children get rendered as a Menu.
The Avatar components require an alt-text which will be displayed as the user name.