Skip to content
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(AvatarStack): add component #924

Merged
merged 86 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
70a9585
feat(ImageStack): add component
ogonkov Aug 21, 2023
6502d96
docs: readme
ogonkov Aug 21, 2023
0f2f4b3
docs: readme
ogonkov Aug 21, 2023
e7422a9
docs: readme
ogonkov Aug 21, 2023
e8073a9
feat: add `overlapSize`
ogonkov Aug 22, 2023
f7e8185
chore: popup placement
ogonkov Aug 22, 2023
c8cd5ae
feat: use custom button for +X
ogonkov Aug 23, 2023
ebffdfe
chore: story
ogonkov Aug 23, 2023
ee91fc6
chore: story popover content
ogonkov Aug 23, 2023
99d94d1
refactor: remove `pk`
ogonkov Aug 23, 2023
2c005ec
chore: rename types
ogonkov Aug 23, 2023
c294237
docs: props
ogonkov Aug 23, 2023
f339518
docs: corrections
ogonkov Aug 23, 2023
085c59d
chore: rename component
ogonkov Aug 28, 2023
afa22f8
chore: rename component
ogonkov Aug 28, 2023
78f42a5
feat: remove `itemClassName`
ogonkov Aug 28, 2023
5a0baaa
feat: remove `itemClassName`
ogonkov Aug 28, 2023
b32adc6
docs: chore
ogonkov Aug 28, 2023
74c094c
chore: add myself as component maintainer
ogonkov Aug 28, 2023
793529e
refactor: replce sass var with css var
ogonkov Jan 29, 2024
2648688
refactor: private var
ogonkov Jan 29, 2024
ecd0e4c
refactor: css var for overlap
ogonkov Jan 29, 2024
1e4e8d9
refactor: move overlap styles to root element
ogonkov Jan 29, 2024
3cdb321
fix: selector
ogonkov Jan 29, 2024
0e4452d
refactor: replace render props with children mapping
ogonkov Jan 29, 2024
5acb3a0
chore: support new api in stories
ogonkov Jan 29, 2024
d038c99
chore: rename story
ogonkov Jan 29, 2024
901f637
chore: move story
ogonkov Jan 29, 2024
fa8e311
refactor: mixin name
ogonkov Jan 29, 2024
b0afeef
docs: reflect api changes
ogonkov Jan 29, 2024
89ec9ae
fix: var
ogonkov May 20, 2024
0288b0b
fix: use new variables for avatar
ogonkov May 20, 2024
a767360
fix: replace mixin with style
ogonkov May 20, 2024
10e2b79
fix: imports after rebase
ogonkov May 20, 2024
e50dbc1
chore: lint
ogonkov May 20, 2024
432d49f
fix: import after rebase
ogonkov May 20, 2024
d0989f1
fix: imports after rebase
ogonkov May 20, 2024
3059a4f
fix: sass var usage
ogonkov May 20, 2024
56ec630
fix: var name
ogonkov May 20, 2024
2fa5bc4
chore: remove console.log
ogonkov May 20, 2024
57cf12e
chore: set `alt`
ogonkov May 20, 2024
6b574e2
feat: add `render` prop
ogonkov May 20, 2024
aaa87ab
chore: type
ogonkov May 20, 2024
1c26cbb
chore: lighter story
ogonkov May 20, 2024
61df637
feat: add `max` prop
ogonkov May 20, 2024
8633f09
feat: render button
ogonkov May 20, 2024
73c4d83
fix: add `aria-label`
ogonkov May 20, 2024
bedcafb
fix: do not set border
ogonkov May 20, 2024
6c994b3
fix: write generated button in `li`
ogonkov May 20, 2024
98ad918
chore: add comments
ogonkov May 20, 2024
d58921b
refactor: rename variables
ogonkov May 20, 2024
0b8ac8b
chore: set useless `alt` for the tests
ogonkov May 21, 2024
122b9ab
chore: select avatar size in storybook
ogonkov May 21, 2024
85a7b6c
chore: make tests grateful
ogonkov May 21, 2024
0727870
chore: use default value in stories
ogonkov May 21, 2024
39c6a72
feat: use default size for more button
ogonkov May 21, 2024
eac1128
chore: change case
ogonkov May 28, 2024
b4a93bc
chore: change case
ogonkov May 28, 2024
73e18ac
feat: return `renderMoreButton` prop
ogonkov May 28, 2024
0b27e4e
docs: prop description
ogonkov May 28, 2024
759a4e4
feat: add `size` prop
ogonkov May 28, 2024
035d318
docs: copy
ogonkov May 28, 2024
9f68aa3
chore: expand description
ogonkov May 28, 2024
166829a
chore: expand description
ogonkov May 28, 2024
496df47
chore: support `size` in stories
ogonkov May 28, 2024
1051aaa
fix: outline display
ogonkov May 28, 2024
2ab85c9
refactor: use Avatar for button content
ogonkov May 28, 2024
3b3a35a
chore: convert stories to csf 3 format
ogonkov May 30, 2024
49dd454
chore: change stories names
ogonkov May 30, 2024
78c4bc5
fix: install only required browsers
ogonkov May 30, 2024
20222b8
chore: add option to disable avatars for tests
ogonkov May 30, 2024
df5f6b6
test: single avatar
ogonkov May 30, 2024
6bee8db
fix: test start
ogonkov May 31, 2024
0149fa9
fix: button label
ogonkov May 31, 2024
b779f40
test: stories
ogonkov May 31, 2024
8b016e2
test: wait for tooltip to appear
ogonkov May 31, 2024
29078f4
fix: add aria-label
ogonkov May 31, 2024
4ddbc36
chore: ignore contrast
ogonkov May 31, 2024
0b2f054
chore: export `AvatarStackMoreButtonProps`
ogonkov Jul 12, 2024
088404d
refactor: `renderMoreButton` -> `renderMore`
ogonkov Jul 12, 2024
3ac9b42
chore: rename tests
ogonkov Jul 12, 2024
72ae9ac
chore: rename tests
ogonkov Jul 12, 2024
71138c5
chore: rename tests
ogonkov Jul 12, 2024
83117ef
test: update screenshots
ogonkov Jul 12, 2024
978c295
fix: set button `type`
ogonkov Jul 17, 2024
8e6b0f1
chore: lint
ogonkov Jul 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/src/components/Alert @IsaevAlexandr
/src/components/ArrowToggle @Marginy605
/src/components/Avatar @DakEnviy
/src/components/AvatarStack @ogonkov
#/src/components/Breadcrumbs
/src/components/Button @amje
/src/components/Card @Lunory
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"lint": "run-p lint:*",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build && npm pkg delete engines",
"playwright:install": "playwright install --with-deps",
"playwright:install": "playwright install chromium webkit --with-deps",
"playwright": "playwright test --config=playwright/playwright.config.ts",
"playwright:update": "npm run playwright -- -u",
"playwright:docker": "./scripts/playwright-docker.sh 'npm run playwright'",
Expand Down
61 changes: 61 additions & 0 deletions src/components/AvatarStack/AvatarStack.scss
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};
}
}
}
}
}
69 changes: 69 additions & 0 deletions src/components/AvatarStack/AvatarStack.tsx
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});
13 changes: 13 additions & 0 deletions src/components/AvatarStack/AvatarStackItem.tsx
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';
39 changes: 39 additions & 0 deletions src/components/AvatarStack/AvatarStackMoreButton.tsx
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
amje marked this conversation as resolved.
Show resolved Hide resolved
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';
52 changes: 52 additions & 0 deletions src/components/AvatarStack/README.md
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>
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 src/components/AvatarStack/__stories__/AvatarStack.stories.tsx
ogonkov marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

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();
});
});
5 changes: 5 additions & 0 deletions src/components/AvatarStack/__tests__/stories.ts
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);
3 changes: 3 additions & 0 deletions src/components/AvatarStack/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"more": ["and {{count}} more", "and {{count}} more", "and {{count}} more"]
amje marked this conversation as resolved.
Show resolved Hide resolved
}
8 changes: 8 additions & 0 deletions src/components/AvatarStack/i18n/index.ts
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);
3 changes: 3 additions & 0 deletions src/components/AvatarStack/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"more": ["И eщё {{count}}", "И eщё {{count}}", "И eщё {{count}}"]
}
2 changes: 2 additions & 0 deletions src/components/AvatarStack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {AvatarStack} from './AvatarStack';
export type {AvatarStackProps, AvatarStackOverlapSize} from './types';
41 changes: 41 additions & 0 deletions src/components/AvatarStack/types.ts
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;
DaffPunks marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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;
}
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './Divider';
export * from './DropdownMenu';
export * from './Hotkey';
export * from './Icon';
export * from './AvatarStack';
amje marked this conversation as resolved.
Show resolved Hide resolved
export * from './Label';
export * from './Link';
export * from './List';
Expand Down
Loading