Skip to content

Commit

Permalink
feat: update avatar and user components
Browse files Browse the repository at this point in the history
  • Loading branch information
DakEnviy committed Dec 22, 2023
1 parent e9c092d commit 8bc2a07
Show file tree
Hide file tree
Showing 52 changed files with 1,211 additions and 358 deletions.
5 changes: 3 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/src/components/ActionTooltip @amje
/src/components/Alert @IsaevAlexandr
/src/components/ArrowToggle @Marginy605
/src/components/Avatar @DakEnviy @DaffPunks
#/src/components/Breadcrumbs
/src/components/Button @amje
/src/components/Card @Lunory
Expand Down Expand Up @@ -30,8 +31,8 @@
/src/components/Radio @zamkovskaya
/src/components/RadioButton @zamkovskaya
/src/components/RadioGroup @zamkovskaya
/src/components/User @DaffPunks
/src/components/UserAvatar @DaffPunks
/src/components/User @DakEnviy @DaffPunks
/src/components/UserWrapper @DakEnviy @DaffPunks
/src/components/Select @korvin89
/src/components/Sheet @mournfulCoroner
/src/components/Skeleton @SeqviriouM
Expand Down
157 changes: 157 additions & 0 deletions src/components/Avatar/Avatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
@use '../variables';

$block: '.#{variables.$ns-new}avatar';

#{$block} {
overflow: hidden;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background-color: var(--g-color-base-background);

&__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

&__icon > svg {
display: block;
}

&__text {
font-weight: 500;
}

&_withBorder,
&_view_outlined {
position: relative;

&::before,
&::after {
content: '';
z-index: 1;
position: absolute;
inset: 0;
border-radius: 50%;
}

&::before {
border: 3px solid var(--g-color-base-background);
}

&::after {
border: 2px solid currentColor;
}
}

&_size {
&_3xs {
width: 17px;
height: 17px;
}

&_2xs {
width: 20px;
height: 20px;
}

&_xs {
width: 24px;
height: 24px;
}

&_s {
width: 28px;
height: 28px;
}

&_m {
width: 32px;
height: 32px;
}

&_l {
width: 42px;
height: 42px;
}

&_xl {
width: 50px;
height: 50px;
}

&_3xs,
&_2xs,
&_xs,
&_s {
#{$block}__text {
font-size: var(--g-text-caption-1-font-size);
line-height: var(--g-text-caption-1-line-height);
}
}

&_m,
&_l {
#{$block}__text {
font-size: var(--g-text-body-1-font-size);
line-height: var(--g-text-body-1-line-height);
}
}

&_xl {
#{$block}__text {
font-size: var(--g-text-body-2-font-size);
line-height: var(--g-text-body-2-line-height);
}
}
}

&_theme {
&_normal {
&#{$block}_view_filled {
background-color: var(--g-color-base-misc-light);
color: unset;

#{$block}__icon,
#{$block}__text {
color: var(--g-color-text-misc);
}
}

&#{$block}_view_outlined {
background-color: var(--g-color-base-background);
color: var(--g-color-text-misc);

#{$block}__icon,
#{$block}__text {
color: var(--g-color-text-misc);
}
}
}

&_brand {
&#{$block}_view_filled {
background-color: var(--g-color-base-brand);
color: unset;

#{$block}__icon,
#{$block}__text {
color: var(--g-color-text-brand-contrast);
}
}

&#{$block}_view_outlined {
background-color: var(--g-color-base-background);
color: var(--g-color-text-brand);

#{$block}__icon,
#{$block}__text {
color: var(--g-color-text-brand);
}
}
}
}
}
87 changes: 87 additions & 0 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';

import {blockNew} from '../utils/cn';

import {AvatarIcon} from './AvatarIcon';
import {AvatarImage} from './AvatarImage';
import {AvatarText} from './AvatarText';
import type {AvatarProps} from './types/main';

import './Avatar.scss';

const b = blockNew('avatar');

export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>((props, ref) => {
const {
size = 'm',
theme = 'normal',
view = 'filled',
backgroundColor,
borderColor,
title,
ariaAttributes,
className,
qa,
} = props;

const style = React.useMemo(
() => ({backgroundColor, color: borderColor}),
[backgroundColor, borderColor],
);

const renderContent = () => {
if ('imgUrl' in props && props.imgUrl) {
return (
<AvatarImage
className={b('image')}
imgUrl={props.imgUrl}
fallbackImgUrl={props.fallbackImgUrl}
sizes={props.sizes}
srcSet={props.srcSet}
alt={props.alt || title}
loading={props.loading}
size={size}
/>
);
}

if ('icon' in props && props.icon) {
return (
<AvatarIcon
className={b('icon')}
icon={props.icon}
color={props.color}
size={size}
/>
);
}

if ('text' in props && props.text) {
return (
<AvatarText
className={b('text')}
text={props.text}
color={props.color}
size={size}
/>
);
}

return null;
};

return (
<div
className={b({size, theme, view, withBorder: Boolean(borderColor)}, className)}
title={title}
style={style}
data-qa={qa}
ref={ref}
{...ariaAttributes}
>
{renderContent()}
</div>
);
});

Avatar.displayName = 'Avatar';
26 changes: 26 additions & 0 deletions src/components/Avatar/AvatarIcon/AvatarIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

import {Icon} from '../../Icon';
import type {AvatarSize} from '../types/common';

import type {AvatarIconProps} from './types';

const avatarSizeToIconSize: Record<AvatarSize, number> = {
'3xs': 11,
'2xs': 14,
xs: 14,
s: 16,
m: 16,
l: 20,
xl: 24,
};

export const AvatarIcon = ({icon, color, size, className}: AvatarIconProps) => {
const style = React.useMemo(() => ({color}), [color]);

return (
<div style={style} className={className}>
<Icon data={icon} size={avatarSizeToIconSize[size]} />
</div>
);
};
2 changes: 2 additions & 0 deletions src/components/Avatar/AvatarIcon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type {AvatarIconProps} from './types';
export {AvatarIcon} from './AvatarIcon';
7 changes: 7 additions & 0 deletions src/components/Avatar/AvatarIcon/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {IconData} from '../../Icon';
import type {AvatarCommonProps} from '../types/common';

export interface AvatarIconProps extends AvatarCommonProps {
icon: IconData;
color?: string;
}
41 changes: 41 additions & 0 deletions src/components/Avatar/AvatarImage/AvatarImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

import {AVATAR_SIZES} from '../constants';

import type {AvatarImageProps} from './types';

export const AvatarImage = ({
imgUrl,
fallbackImgUrl,
sizes,
srcSet,
alt,
loading,
size,
className,
}: AvatarImageProps) => {
const [isErrored, setIsErrored] = React.useState(false);

const handleError = React.useCallback(() => {
setIsErrored(true);
}, []);

// Reset error if `imgUrl` was changed to check it again
React.useEffect(() => {
setIsErrored(false);
}, [imgUrl]);

return (
<img
className={className}
loading={loading}
width={AVATAR_SIZES[size]}
height={AVATAR_SIZES[size]}
src={fallbackImgUrl && isErrored ? fallbackImgUrl : imgUrl}
sizes={sizes}
srcSet={srcSet}
alt={alt}
onError={handleError}
/>
);
};
2 changes: 2 additions & 0 deletions src/components/Avatar/AvatarImage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type {AvatarImageProps} from './types';
export {AvatarImage} from './AvatarImage';
10 changes: 10 additions & 0 deletions src/components/Avatar/AvatarImage/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {AvatarCommonProps} from '../types/common';

export interface AvatarImageProps extends AvatarCommonProps {
imgUrl: string;
fallbackImgUrl?: string;
sizes?: string;
srcSet?: string;
alt?: string;
loading?: 'eager' | 'lazy';
}
15 changes: 15 additions & 0 deletions src/components/Avatar/AvatarText/AvatarText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

import type {AvatarTextProps} from './types';
import {getAvatarDisplayText} from './utils';

export const AvatarText = ({text, color, size, className}: AvatarTextProps) => {
const style = React.useMemo(() => ({color}), [color]);
const displayText = React.useMemo(() => getAvatarDisplayText(text, size), [size, text]);

return (
<div style={style} className={className}>
{displayText}
</div>
);
};
3 changes: 3 additions & 0 deletions src/components/Avatar/AvatarText/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type {AvatarTextProps} from './types';
export {getAvatarDisplayText} from './utils';
export {AvatarText} from './AvatarText';
6 changes: 6 additions & 0 deletions src/components/Avatar/AvatarText/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type {AvatarCommonProps} from '../types/common';

export interface AvatarTextProps extends AvatarCommonProps {
text: string;
color?: string;
}
13 changes: 13 additions & 0 deletions src/components/Avatar/AvatarText/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {AvatarSize} from '../types/common';

export const getAvatarDisplayText = (text: string, size: AvatarSize) => {
if (size === '3xs') {
return text[0] || '';
}

const words = text.split(' ');
const result =
words.length > 1 ? [words[0][0], words[1][0]].filter(Boolean).join('') : text.slice(0, 2);

return result.toUpperCase();
};
Loading

0 comments on commit 8bc2a07

Please sign in to comment.