diff --git a/src/components/UserAvatar/README.md b/src/components/UserAvatar/README.md
index 8f19f308b1..086f2e550d 100644
--- a/src/components/UserAvatar/README.md
+++ b/src/components/UserAvatar/README.md
@@ -8,5 +8,7 @@ Component for displaying user avatar.
| :-------- | :--------------- | :------- | :------ | :--------------------------------------------------------- |
| imgUrl | `string` | | | Link to image |
| size | `UserAvatarSize` | | 'm' | Component size. Possible values: `xs`, `s`, `m`, `l`, `xl` |
+| srcSet | `string` | | | `srcSet` attribute of the image |
+| sizes | `string` | | | `sizes` attribute of the image |
| title | `string` | | | Tooltip text on hover |
| className | `string` | | | Class name |
diff --git a/src/components/UserAvatar/UserAvatar.scss b/src/components/UserAvatar/UserAvatar.scss
index 639118e167..7e18f9be27 100644
--- a/src/components/UserAvatar/UserAvatar.scss
+++ b/src/components/UserAvatar/UserAvatar.scss
@@ -3,13 +3,16 @@
$block: '.#{variables.$ns}user-avatar';
#{$block} {
- box-sizing: border-box;
display: inline-block;
+ overflow: hidden;
+
+ box-sizing: border-box;
+
+ border-radius: 50%;
background-color: var(--yc-color-base-background);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
- border-radius: 50%;
&_size {
&_xs {
@@ -37,4 +40,8 @@ $block: '.#{variables.$ns}user-avatar';
height: 50px;
}
}
+
+ &__figure {
+ object-fit: cover;
+ }
}
diff --git a/src/components/UserAvatar/UserAvatar.tsx b/src/components/UserAvatar/UserAvatar.tsx
index 28da8c28ff..994f66f125 100644
--- a/src/components/UserAvatar/UserAvatar.tsx
+++ b/src/components/UserAvatar/UserAvatar.tsx
@@ -1,12 +1,14 @@
import React from 'react';
import {block} from '../utils/cn';
+import {SIZES} from './constants';
+import type {UserAvatarSize} from './types';
import './UserAvatar.scss';
-export type UserAvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl';
-
export interface UserAvatarProps {
imgUrl?: string;
size?: UserAvatarSize;
+ srcSet?: string;
+ sizes?: string;
title?: string;
className?: string;
/** @deprecated Use appropriate component, like `` instead */
@@ -15,15 +17,26 @@ export interface UserAvatarProps {
const b = block('user-avatar');
-export function UserAvatar({imgUrl, size = 'm', title, className, onClick}: UserAvatarProps) {
+export function UserAvatar({
+ imgUrl,
+ size = 'm',
+ srcSet,
+ sizes,
+ title,
+ className,
+ onClick,
+}: UserAvatarProps) {
return (
-
+
+
+
);
}
diff --git a/src/components/UserAvatar/__stories__/UserAvatar.stories.tsx b/src/components/UserAvatar/__stories__/UserAvatar.stories.tsx
index 4aedec997e..29fa9e422c 100644
--- a/src/components/UserAvatar/__stories__/UserAvatar.stories.tsx
+++ b/src/components/UserAvatar/__stories__/UserAvatar.stories.tsx
@@ -1,5 +1,8 @@
-import React from 'react';
-import {Meta, Story} from '@storybook/react';
+import {useArgs} from '@storybook/client-api';
+import React, {useEffect} from 'react';
+import {ComponentStory, Meta, Story} from '@storybook/react';
+import {faker} from '@faker-js/faker/locale/en';
+import {getAvatarSrcSet} from './getAvatarSrcSet';
import {UserAvatar, UserAvatarProps} from '../UserAvatar';
@@ -26,3 +29,28 @@ export const Size: Story = (args) => (
);
Size.args = {imgUrl};
+
+const randomAvatars = faker.helpers
+ .uniqueArray(() => faker.datatype.number({min: 20, max: 500}), 30)
+ .reduce(
+ (sizes, num) => ({
+ ...sizes,
+ [num]: faker.image.cats(num, Math.round((num / 640) * 480)),
+ }),
+ {},
+ );
+export const WithSrcSet: ComponentStory = (args) => {
+ const [, setArgs] = useArgs();
+
+ useEffect(() => {
+ if (args.size) {
+ setArgs({srcSet: getAvatarSrcSet(args.size, randomAvatars)});
+ }
+ }, [args.size]);
+
+ return ;
+};
+WithSrcSet.args = {
+ imgUrl: faker.image.cats(),
+ size: 'xl',
+};
diff --git a/src/components/UserAvatar/__stories__/getAvatarSrcSet.ts b/src/components/UserAvatar/__stories__/getAvatarSrcSet.ts
new file mode 100644
index 0000000000..ab785043f8
--- /dev/null
+++ b/src/components/UserAvatar/__stories__/getAvatarSrcSet.ts
@@ -0,0 +1,24 @@
+import {getClosestNumber} from './getClosestNumber';
+import {getSrcSet} from './getSrcSet';
+import {SrcSetType} from './types';
+import {SIZES} from '../constants';
+import type {UserAvatarSize} from '../types';
+
+export function getAvatarSrcSet(
+ size: UserAvatarSize,
+ sizes: Record,
+ {multipliers = [1, 2, 3, 4]} = {},
+) {
+ const availableSizes = Object.keys(sizes)
+ .map((size) => Number(size))
+ .sort((a, b) => a - b);
+ const baseSize = SIZES[size];
+ const srcSet = multipliers.map((multiplier) => {
+ const targetSize = multiplier * baseSize;
+ const appropriateSize = getClosestNumber(targetSize, availableSizes);
+
+ return [sizes[appropriateSize], `${multiplier}x`] as const;
+ });
+
+ return getSrcSet(srcSet as SrcSetType);
+}
diff --git a/src/components/UserAvatar/__stories__/getClosestNumber.ts b/src/components/UserAvatar/__stories__/getClosestNumber.ts
new file mode 100644
index 0000000000..2736a67b57
--- /dev/null
+++ b/src/components/UserAvatar/__stories__/getClosestNumber.ts
@@ -0,0 +1,26 @@
+export function getClosestNumber(targetNumber: number, availableNumbers: number[]) {
+ const stack = [...availableNumbers];
+ let previousNumber: number | undefined;
+
+ while (stack.length) {
+ const currentNumber = stack.pop() as number;
+ const isBigger = targetNumber > currentNumber;
+ const isSame = targetNumber === currentNumber;
+
+ if (isSame) {
+ return currentNumber;
+ }
+
+ if (isBigger) {
+ if (previousNumber) {
+ return previousNumber;
+ }
+
+ return currentNumber;
+ }
+
+ previousNumber = currentNumber;
+ }
+
+ return previousNumber as number;
+}
diff --git a/src/components/UserAvatar/__stories__/getSrcSet.ts b/src/components/UserAvatar/__stories__/getSrcSet.ts
new file mode 100644
index 0000000000..268302c885
--- /dev/null
+++ b/src/components/UserAvatar/__stories__/getSrcSet.ts
@@ -0,0 +1,19 @@
+import type {SrcSetType} from './types';
+
+export function getSrcSet(srcSet: SrcSetType) {
+ let srcSetString = '';
+
+ for (const item of srcSet) {
+ let part = '';
+
+ if (typeof item === 'string') {
+ part = item;
+ } else {
+ part = item.join(' ');
+ }
+
+ srcSetString = `${srcSetString}${srcSetString === '' ? '' : ', '}${part}`;
+ }
+
+ return srcSetString;
+}
diff --git a/src/components/UserAvatar/__stories__/types.ts b/src/components/UserAvatar/__stories__/types.ts
new file mode 100644
index 0000000000..150cd57797
--- /dev/null
+++ b/src/components/UserAvatar/__stories__/types.ts
@@ -0,0 +1,11 @@
+type SrcSetUrlItem = string;
+
+type SrcSetWidthDescriptor = T extends `${infer _A}w` ? T : never;
+type SrcSetWidthItem = [string, SrcSetWidthDescriptor];
+type SrcSetWidthType = SrcSetWidthItem | SrcSetUrlItem;
+
+type SrcSetDensityDescriptor = T extends `${infer _A}x` ? T : never;
+type SrcSetDensityItem = [string, SrcSetDensityDescriptor];
+type SrcSetDensityType = SrcSetDensityItem | SrcSetUrlItem;
+
+export type SrcSetType = SrcSetWidthType[] | SrcSetDensityType[];
diff --git a/src/components/UserAvatar/constants.ts b/src/components/UserAvatar/constants.ts
new file mode 100644
index 0000000000..56fb5830e9
--- /dev/null
+++ b/src/components/UserAvatar/constants.ts
@@ -0,0 +1,9 @@
+import type {UserAvatarSize} from './types';
+
+export const SIZES: Record = {
+ xs: 24,
+ s: 28,
+ m: 32,
+ l: 42,
+ xl: 50,
+};
diff --git a/src/components/UserAvatar/index.ts b/src/components/UserAvatar/index.ts
index d1b24f3b30..955e86c014 100644
--- a/src/components/UserAvatar/index.ts
+++ b/src/components/UserAvatar/index.ts
@@ -1 +1,3 @@
export * from './UserAvatar';
+export type {UserAvatarSize} from './types';
+export {SIZES as AVATAR_SIZES} from './constants';
diff --git a/src/components/UserAvatar/types.ts b/src/components/UserAvatar/types.ts
new file mode 100644
index 0000000000..89f3a3d430
--- /dev/null
+++ b/src/components/UserAvatar/types.ts
@@ -0,0 +1 @@
+export type UserAvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl';