Skip to content

Commit

Permalink
feat(UserAvatar): add srcset support (#641)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogonkov authored Apr 28, 2023
1 parent fd29127 commit fe05925
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/components/UserAvatar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
11 changes: 9 additions & 2 deletions src/components/UserAvatar/UserAvatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -37,4 +40,8 @@ $block: '.#{variables.$ns}user-avatar';
height: 50px;
}
}

&__figure {
object-fit: cover;
}
}
35 changes: 24 additions & 11 deletions src/components/UserAvatar/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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 `<Button/>` instead */
Expand All @@ -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 (
<div
title={title}
className={b({size}, className)}
style={{
backgroundImage: `url(${imgUrl})`,
}}
onClick={onClick}
/>
<div title={title} className={b({size}, className)} onClick={onClick}>
<img
className={b('figure')}
width={SIZES[size]}
height={SIZES[size]}
src={imgUrl}
srcSet={srcSet}
sizes={sizes}
alt={''}
/>
</div>
);
}
32 changes: 30 additions & 2 deletions src/components/UserAvatar/__stories__/UserAvatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -26,3 +29,28 @@ export const Size: Story<UserAvatarProps> = (args) => (
</div>
);
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<typeof UserAvatar> = (args) => {
const [, setArgs] = useArgs();

useEffect(() => {
if (args.size) {
setArgs({srcSet: getAvatarSrcSet(args.size, randomAvatars)});
}
}, [args.size]);

return <UserAvatar {...args} />;
};
WithSrcSet.args = {
imgUrl: faker.image.cats(),
size: 'xl',
};
24 changes: 24 additions & 0 deletions src/components/UserAvatar/__stories__/getAvatarSrcSet.ts
Original file line number Diff line number Diff line change
@@ -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<number, string>,
{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<typeof multipliers>);
}
26 changes: 26 additions & 0 deletions src/components/UserAvatar/__stories__/getClosestNumber.ts
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/components/UserAvatar/__stories__/getSrcSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type {SrcSetType} from './types';

export function getSrcSet<T>(srcSet: SrcSetType<T>) {
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;
}
11 changes: 11 additions & 0 deletions src/components/UserAvatar/__stories__/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type SrcSetUrlItem = string;

type SrcSetWidthDescriptor<T> = T extends `${infer _A}w` ? T : never;
type SrcSetWidthItem<T> = [string, SrcSetWidthDescriptor<T>];
type SrcSetWidthType<T> = SrcSetWidthItem<T> | SrcSetUrlItem;

type SrcSetDensityDescriptor<T> = T extends `${infer _A}x` ? T : never;
type SrcSetDensityItem<T> = [string, SrcSetDensityDescriptor<T>];
type SrcSetDensityType<T> = SrcSetDensityItem<T> | SrcSetUrlItem;

export type SrcSetType<T> = SrcSetWidthType<T>[] | SrcSetDensityType<T>[];
9 changes: 9 additions & 0 deletions src/components/UserAvatar/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {UserAvatarSize} from './types';

export const SIZES: Record<UserAvatarSize, number> = {
xs: 24,
s: 28,
m: 32,
l: 42,
xl: 50,
};
2 changes: 2 additions & 0 deletions src/components/UserAvatar/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './UserAvatar';
export type {UserAvatarSize} from './types';
export {SIZES as AVATAR_SIZES} from './constants';
1 change: 1 addition & 0 deletions src/components/UserAvatar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type UserAvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl';

0 comments on commit fe05925

Please sign in to comment.