-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(AvatarStack): add component (#924)
- Loading branch information
Showing
28 changed files
with
463 additions
and
1 deletion.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
@use '../../../styles/mixins'; | ||
@use '../Avatar/variables' as avatar-variables; | ||
@use '../variables'; | ||
|
||
$block: '.#{variables.$ns}avatar-stack'; | ||
|
||
#{$block} { | ||
--_--more-button-size: #{avatar-variables.$default-size}; | ||
--_--more-button-border-width: 1px; | ||
|
||
display: inline-flex; | ||
justify-content: flex-end; | ||
flex-direction: row-reverse; | ||
|
||
margin: 0; | ||
padding: 0; | ||
|
||
&_overlap-size_s { | ||
--_--overlap: var(--g-spacing-1); | ||
} | ||
|
||
&_overlap-size_m { | ||
--_--overlap: var(--g-spacing-2); | ||
} | ||
|
||
&_overlap-size_l { | ||
--_--overlap: var(--g-spacing-3); | ||
} | ||
|
||
&__item { | ||
display: flex; | ||
z-index: 0; | ||
border-radius: 100%; | ||
|
||
&:not(:first-child) { | ||
margin-inline-end: calc(-1 * var(--_--overlap)); | ||
} | ||
} | ||
|
||
&__more-button { | ||
@include mixins.button-reset; | ||
|
||
border-radius: 100%; | ||
|
||
width: var(--_--more-button-size); | ||
height: var(--_--more-button-size); | ||
|
||
&:focus-visible { | ||
outline: var(--g-color-line-focus) solid 2px; | ||
outline-offset: 0; | ||
} | ||
|
||
&_size { | ||
@each $size-name, $size-value in avatar-variables.$sizes { | ||
&_#{$size-name} { | ||
--_--more-button-size: #{$size-value}; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import React from 'react'; | ||
|
||
import {Avatar} from '../Avatar'; | ||
import {block} from '../utils/cn'; | ||
|
||
import {AvatarStackItem} from './AvatarStackItem'; | ||
import {AvatarStackMoreButton} from './AvatarStackMoreButton'; | ||
import i18n from './i18n'; | ||
import type {AvatarStackProps} from './types'; | ||
|
||
import './AvatarStack.scss'; | ||
|
||
const b = block('avatar-stack'); | ||
|
||
const AvatarStackComponent = ({ | ||
max = 3, | ||
overlapSize = 's', | ||
size, | ||
children, | ||
className, | ||
renderMore, | ||
}: AvatarStackProps) => { | ||
const visibleItems: React.ReactElement[] = []; | ||
let moreItems = 0; | ||
|
||
React.Children.forEach(children, (child) => { | ||
if (!React.isValidElement(child)) { | ||
return; | ||
} | ||
|
||
const item = <AvatarStackItem key={visibleItems.length}>{child}</AvatarStackItem>; | ||
|
||
if (visibleItems.length <= max) { | ||
visibleItems.unshift(item); | ||
} else { | ||
moreItems += 1; | ||
} | ||
}); | ||
|
||
const hasMoreButton = moreItems > 0; | ||
/** Avatars + more button, or just avatars, when avatars count is equal to `max` or less */ | ||
const normalOverflow = moreItems >= 1; | ||
|
||
return ( | ||
// Safari remove role=list with some styles, applied to li items, so we need | ||
// to restore role manually | ||
// eslint-disable-next-line jsx-a11y/no-redundant-roles | ||
<ul className={b({'overlap-size': overlapSize}, className)} role={'list'}> | ||
{hasMoreButton ? ( | ||
<AvatarStackItem key="more-button"> | ||
{renderMore ? ( | ||
renderMore({count: moreItems}) | ||
) : ( | ||
<Avatar | ||
text={`+${moreItems}`} | ||
aria-label={i18n('more', {count: moreItems})} | ||
size={size} | ||
/> | ||
)} | ||
</AvatarStackItem> | ||
) : null} | ||
{normalOverflow ? visibleItems.slice(0, max) : visibleItems} | ||
</ul> | ||
); | ||
}; | ||
|
||
AvatarStackComponent.displayName = 'AvatarStack'; | ||
|
||
export const AvatarStack = Object.assign(AvatarStackComponent, {MoreButton: AvatarStackMoreButton}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
|
||
import {block} from '../utils/cn'; | ||
|
||
const b = block('avatar-stack'); | ||
|
||
type Props = React.PropsWithChildren<{}>; | ||
|
||
export const AvatarStackItem = ({children}: Props) => { | ||
return <li className={b('item')}>{children}</li>; | ||
}; | ||
|
||
AvatarStackItem.displayName = 'AvatarStack.Item'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React from 'react'; | ||
|
||
import type {AvatarSize} from '../Avatar'; | ||
import {Avatar, DEFAULT_AVATAR_SIZE} from '../Avatar'; | ||
import {block} from '../utils/cn'; | ||
|
||
import i18n from './i18n'; | ||
|
||
const b = block('avatar-stack'); | ||
|
||
export type AvatarStackMoreButtonProps = Pick< | ||
React.HTMLProps<HTMLButtonElement>, | ||
'className' | 'onClick' | 'aria-label' | ||
> & { | ||
size?: AvatarSize; | ||
count: number; | ||
}; | ||
|
||
export const AvatarStackMoreButton = React.forwardRef< | ||
HTMLButtonElement, | ||
AvatarStackMoreButtonProps | ||
>(({className, size = DEFAULT_AVATAR_SIZE, onClick, count, 'aria-label': ariaLabel}, ref) => { | ||
return ( | ||
<button | ||
ref={ref} | ||
type="button" | ||
className={b('more-button', {size}, className)} | ||
onClick={onClick} | ||
> | ||
<Avatar | ||
text={`+${count}`} | ||
size={size} | ||
aria-label={ariaLabel || i18n('more', {count})} | ||
/> | ||
</button> | ||
); | ||
}); | ||
|
||
AvatarStackMoreButton.displayName = 'AvatarStack.MoreButton'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<!--GITHUB_BLOCK--> | ||
|
||
# AvatarStack | ||
|
||
<!--/GITHUB_BLOCK--> | ||
|
||
```ts | ||
import {AvatarStack} from '@gravity-ui/uikit'; | ||
``` | ||
|
||
Stack of images with overlap over next image and optional control. This is usually users avatars. | ||
|
||
## Usage | ||
|
||
Component is not limit you to what components to render, basic usage is: | ||
|
||
```tsx | ||
<AvatarStack> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login1`} /> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login2`} /> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login3`} /> | ||
</AvatarStack> | ||
``` | ||
|
||
## Properties | ||
|
||
| Name | Description | Type | Default | | ||
| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------: | :-----: | | ||
| max | How much avatars should be visible before more button. If avatars count is only 1 short from `max`, than more button would be replaced with avatar. | `number` | 3 | | ||
| overlapSize | How much each item should overlap next one. `s` recommended for `Avatar`'s of sizes `xs`-`m`, `m` recomended for `l` size avatars and `l` overlap for `xl` avatars | `s`, `m`, `l` | `s` | | ||
| size | Size for control displaying extra avatars. Value same to `Avatar` size. | `AvatarSize` | | | ||
| className | Class name of root DOM node | `string` | | | ||
| children | List of avatars, probably with some extra wrappers | `Object[]` | | | ||
| renderMore | Custom render for control displaying extra avatars | `function(options: {count: number}): ReactElement` | | | ||
|
||
### AvatarStack.MoreButton | ||
|
||
Component for overriding more button | ||
|
||
```tsx | ||
<AvatarStack | ||
renderMore={({count}) => ( | ||
<Tooltip content={'More users'}> | ||
<AvatarStack.MoreButton count={count} /> | ||
</Tooltip> | ||
)} | ||
> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login1`} /> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login2`} /> | ||
<Avatar imgUrl={`https://i.pravatar.cc/150?u=login3`} /> | ||
</AvatarStack> | ||
``` |
Binary file added
BIN
+9.47 KB
....test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+8.37 KB
...al.test.tsx-snapshots/AvatarStack-render-story-MoreButton-dark-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+8.75 KB
...test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.77 KB
...l.test.tsx-snapshots/AvatarStack-render-story-MoreButton-light-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+8.28 KB
...t.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.24 KB
...est.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-dark-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.49 KB
....tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+6.58 KB
...st.tsx-snapshots/AvatarStack-render-story-MoreButtonOmit-light-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.31 KB
....test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+6.64 KB
...al.test.tsx-snapshots/AvatarStack-render-story-SingleItem-dark-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+6.57 KB
...test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+5.92 KB
...l.test.tsx-snapshots/AvatarStack-render-story-SingleItem-light-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 139 additions & 0 deletions
139
src/components/AvatarStack/__stories__/AvatarStack.stories.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
25 changes: 25 additions & 0 deletions
25
src/components/AvatarStack/__tests__/AvatarStack.visual.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React from 'react'; | ||
|
||
import {test} from '~playwright/core'; | ||
|
||
import {AvatarStackStories} from './stories'; | ||
|
||
test.describe('AvatarStack', () => { | ||
test('render story <SingleItem>', async ({mount, expectScreenshot}) => { | ||
await mount(<AvatarStackStories.SingleItem randomAvatar={false} />); | ||
|
||
await expectScreenshot(); | ||
}); | ||
|
||
test('render story <MoreButton>', async ({mount, expectScreenshot}) => { | ||
await mount(<AvatarStackStories.MoreButton randomAvatar={false} />); | ||
|
||
await expectScreenshot(); | ||
}); | ||
|
||
test('render story <MoreButtonOmit>', async ({mount, expectScreenshot}) => { | ||
await mount(<AvatarStackStories.MoreButtonOmit randomAvatar={false} />); | ||
|
||
await expectScreenshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import {composeStories} from '@storybook/react'; | ||
|
||
import * as CSFStories from '../__stories__/AvatarStack.stories'; | ||
|
||
export const AvatarStackStories = composeStories(CSFStories); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"more": ["and {{count}} more", "and {{count}} more", "and {{count}} more"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import {addComponentKeysets} from '../../utils/addComponentKeysets'; | ||
|
||
import en from './en.json'; | ||
import ru from './ru.json'; | ||
|
||
const COMPONENT = 'AvatarStack'; | ||
|
||
export default addComponentKeysets({en, ru}, COMPONENT); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"more": ["И eщё {{count}}", "И eщё {{count}}", "И eщё {{count}}"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export {AvatarStack} from './AvatarStack'; | ||
export type {AvatarStackProps, AvatarStackOverlapSize} from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type React from 'react'; | ||
|
||
import type {AvatarSize} from '../Avatar'; | ||
|
||
export type AvatarStackOverlapSize = 's' | 'm' | 'l'; | ||
|
||
export interface AvatarStackProps { | ||
/** Amount of avatars to be shown before more button. Default 3. */ | ||
max?: number; | ||
/** | ||
* How much each avatar should overlap next one | ||
* | Avatar sizes | Recommended overlap | | ||
* | :----------: | :-----------------: | | ||
* | `xs`-`m` | `s` | | ||
* | `l` | `m` | | ||
* | `xl` | `l` | | ||
*/ | ||
overlapSize?: AvatarStackOverlapSize; | ||
/** | ||
* Size for control displaying count of extra avatars | ||
*/ | ||
size?: AvatarSize; | ||
className?: string; | ||
/** | ||
* Children would be wrapped for "stacking" | ||
* @example | ||
* <AvatarStack> | ||
* <Avatar/> | ||
* <Tooltip content="Some info"><Avatar/></Tooltip> | ||
* </AvatarStack> | ||
*/ | ||
children?: React.ReactNode; | ||
/** | ||
* Custom render for control displaying extra data | ||
* @example | ||
* <AvatarStack renderMore={({count}) => <Button>+{count}</Button>}> | ||
* <Avatar/> | ||
* </AvatarStack> | ||
*/ | ||
renderMore?: (options: {count: number}) => React.ReactElement; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters