Skip to content

Commit

Permalink
Merge pull request #20361 from akeneo/dsm-update-2023-12-14
Browse files Browse the repository at this point in the history
DSM Update
  • Loading branch information
tseho authored Dec 14, 2023
2 parents 8a0e54c + 5b63854 commit 89cbc95
Show file tree
Hide file tree
Showing 20 changed files with 581 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`renders the expected elements 1`] = `
<div>
<div>
<span
class="sc-dlnjPT jLeOJM"
class="sc-jSFkmK jjMYxm"
>
Success
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
launch: {
dumpio: true,
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
},
server: {
command: 'yarn http-server storybook-static -p 6006',
Expand Down
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {Meta, Story, ArgsTable, Canvas} from '@storybook/addon-docs';
import {Avatar} from './Avatar.tsx';
import {Avatars} from './Avatars.tsx';

<Meta
title="Components/Avatar"
component={Avatar}
argTypes={{
firstName: {control: {type: 'text'}},
lastName: {control: {type: 'text'}},
username: {control: {type: 'text'}},
avatarUrl: {control: {type: 'text'}},
size: {control: {type: 'select', options: ['default', 'big']}},
}}
args={{
firstName: 'John',
lastName: 'Doe',
username: 'admin',
avatarUrl: undefined,
size: 'default',
}}
/>

# Avatar

## Usage

This component is used to display users avatars. If no avatar is available, first letters of the first and last name are
displayed with a dedicated color.

## Playground

<Canvas>
<Story name="Standard">
{args => {
return <Avatar firstName={'John'} lastName={'Doe'} username={'admin'} {...args} />;
}}
</Story>
</Canvas>

<ArgsTable story="Standard" />

## Variation on background colors

The background color is based from the username.

<Canvas>
<Story name="Background colors">
{args => {
return (
<>
<Avatar {...args} firstName={'Albert'} lastName={'Doe'} username={'a'} />
<Avatar {...args} firstName={'Bertrand'} lastName={'Doe'} username={'b'} />
<Avatar {...args} firstName={'Chris'} lastName={'Doe'} username={'c'} />
<Avatar {...args} firstName={'Danny'} lastName={'Doe'} username={'d'} />
<Avatar {...args} firstName={'Elon'} lastName={'Doe'} username={'e'} />
<Avatar {...args} firstName={'Fred'} lastName={'Doe'} username={'f'} />
<Avatar {...args} firstName={'Gus'} lastName={'Doe'} username={'g'} />
<Avatar {...args} firstName={'Helen'} lastName={'Doe'} username={'h'} />
<Avatar {...args} firstName={'Isabel'} lastName={'Doe'} username={'i'} />
<Avatar {...args} firstName={'John'} lastName={'Doe'} username={'j'} />
<Avatar {...args} firstName={'Kurt'} lastName={'Doe'} username={'k'} />
<Avatar {...args} firstName={'Leonard'} lastName={'Doe'} username={'l'} />
</>
);
}}
</Story>
</Canvas>

## Variation with image

<Canvas>
<Story name="With image">
{args => {
return (
<>
<Avatar {...args} avatarUrl={'https://picsum.photos/seed/akeneo/32/32'} />
</>
);
}}
</Story>
</Canvas>

## Variation on List

You can use a dedicated component to display avatar list. After a defined maximum, other avatars are not displayed.

<Canvas>
<Story name="Avatar list">
{args => {
return (
<>
<Avatars
max={5}
title="Helen Doe&#10;Isabel Doe&#10;John Doe&#10;Kurt Doe&#10;Leonard Doe"
>
<Avatar
{...args}
firstName={'Albert'}
lastName={'Doe'}
username={'a'}
avatarUrl={'https://picsum.photos/seed/akeneo/32/32'}
/>
<Avatar {...args} firstName={'Bertrand'} lastName={'Doe'} username={'b'} />
<Avatar
{...args}
firstName={'Chris'}
lastName={'Doe'}
username={'c'}
avatarUrl={'https://picsum.photos/seed/bkeneo/32/32'}
/>
<Avatar {...args} firstName={'Danny'} lastName={'Doe'} username={'d'} />
<Avatar
{...args}
firstName={'Elon'}
lastName={'Doe'}
username={'e'}
avatarUrl={'https://picsum.photos/seed/ckeneo/32/32'}
/>
<Avatar {...args} firstName={'Fred'} lastName={'Doe'} username={'f'} />
<Avatar {...args} firstName={'Gus'} lastName={'Doe'} username={'g'} />
<Avatar {...args} firstName={'Helen'} lastName={'Doe'} username={'h'} />
<Avatar {...args} firstName={'Isabel'} lastName={'Doe'} username={'i'} />
<Avatar {...args} firstName={'John'} lastName={'Doe'} username={'j'} />
<Avatar {...args} firstName={'Kurt'} lastName={'Doe'} username={'k'} />
<Avatar {...args} firstName={'Leonard'} lastName={'Doe'} username={'l'} />
</Avatars>
</>
);
}}
</Story>
</Canvas>

## Variation on Size

<Canvas>
<Story name="Avatar size">
{args => {
return (
<>
<Avatar {...args} size={'default'} />
<Avatar {...args} size={'big'} />
</>
);
}}
</Story>
</Canvas>
102 changes: 102 additions & 0 deletions front-packages/akeneo-design-system/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {useMemo} from 'react';
import styled, {css} from 'styled-components';
import {useTheme} from '../../hooks';
import {Override} from '../../shared';
import {AkeneoThemedProps, getColor} from '../../theme';

const AvatarContainer = styled.span<AvatarProps & AkeneoThemedProps>`
${({size}) =>
size === 'default'
? css`
height: 32px;
width: 32px;
line-height: 32px;
font-size: 15px;
border-radius: 32px;
`
: css`
height: 140px;
width: 140px;
line-height: 140px;
font-size: 66px;
border-radius: 140px;
`}
display: inline-block;
color: ${getColor('white')};
text-align: center;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
text-transform: uppercase;
cursor: ${({onClick}) => (onClick ? 'pointer' : 'default')};
`;

type AvatarProps = Override<
React.HTMLAttributes<HTMLSpanElement>,
{
/**
* Username to use as fallback if the avatar is not provided and the Firstname and Lastname are empty.
*/
username: string;

/**
* Firstname to use as fallback with the Lastname if the avatar is not provided.
*/
firstName: string;

/**
* Lastname to use as fallback with the Firstname if the avatar is not provided.
*/
lastName: string;

/**
* Url of the avatar image.
*/
avatarUrl?: string;

/**
* Size of the avatar.
*/
size?: 'default' | 'big';
}
>;

const Avatar = ({username, firstName, lastName, avatarUrl, size = 'default', ...rest}: AvatarProps) => {
const theme = useTheme();

const fallback = (
firstName.trim().charAt(0) + lastName.trim().charAt(0) || username.substring(0, 2)
).toLocaleUpperCase();
const title = `${firstName} ${lastName}`.trim() || username;

const backgroundColor = useMemo(() => {
const colorId = username.split('').reduce<number>((s, l) => s + l.charCodeAt(0), 0);
const colors = [
theme.colorAlternative.green120,
theme.colorAlternative.darkCyan120,
theme.colorAlternative.forestGreen120,
theme.colorAlternative.oliveGreen120,
theme.colorAlternative.blue120,
theme.colorAlternative.darkBlue120,
theme.colorAlternative.hotPink120,
theme.colorAlternative.red120,
theme.colorAlternative.coralRed120,
theme.colorAlternative.yellow120,
theme.colorAlternative.orange120,
theme.colorAlternative.chocolate120,
];

return colors[colorId % colors.length];
}, [theme, username]);

const style = avatarUrl ? {backgroundImage: `url(${avatarUrl})`} : {backgroundColor};

return (
<AvatarContainer size={size} {...rest} style={style} title={title}>
{avatarUrl ? '' : fallback}
</AvatarContainer>
);
};

export {Avatar};
export type {AvatarProps};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import {render, screen} from '../../storybook/test-util';
import {Avatar} from './Avatar';

test('renders', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toBeInTheDocument();
});

test('avatar image', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" avatarUrl="path/to/image" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toHaveStyle('background-image: url(path/to/image)');
});

test('deterministic fallback color', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toHaveStyle('background-color: rgb(68, 31, 0)');
});

test('fallback to firstname + lastname', () => {
render(<Avatar username="" firstName="John" lastName="Doe" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toHaveTextContent('JD');
});

test('fallback to firstname only', () => {
render(<Avatar username="" firstName="John" lastName="" />);

const avatar = screen.getByTitle('John');
expect(avatar).toHaveTextContent('J');
});

test('fallback to lastname only', () => {
render(<Avatar username="" firstName="" lastName="Doe" />);

const avatar = screen.getByTitle('Doe');
expect(avatar).toHaveTextContent('D');
});

test('fallback to username', () => {
render(<Avatar username="john" firstName="" lastName="" />);

const avatar = screen.getByTitle('john');
expect(avatar).toHaveTextContent('JO');
});

test('initial are converted to uppercase', () => {
render(<Avatar username="" firstName="john" lastName="doe" />);

const avatar = screen.getByTitle('john doe');
expect(avatar).toHaveTextContent('JD');
});

test('size default', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toHaveStyle('width: 32px');
});

test('size big', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" size="big" />);

const avatar = screen.getByTitle('John Doe');
expect(avatar).toHaveStyle('width: 140px');
});

test('supports ...rest props', () => {
render(<Avatar username="john" firstName="John" lastName="Doe" data-testid="my_value" />);

expect(screen.getByTestId('my_value')).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, {Children} from 'react';
import styled from 'styled-components';
import {Override} from '../../shared';
import {AkeneoThemedProps, getColor} from '../../theme';

const AvatarListContainer = styled.div<AvatarsProps & AkeneoThemedProps>`
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
& > * {
margin-right: -4px;
position: relative;
}
`;

const RemainingAvatar = styled.span`
height: 32px;
width: 32px;
display: inline-block;
border: 1px solid ${getColor('grey', 10)};
line-height: 32px;
text-align: center;
font-size: 15px;
border-radius: 32px;
background-color: ${getColor('white')};
`;

type AvatarsProps = Override<
React.HTMLAttributes<HTMLDivElement>,
{
max: number;
}
>;

const Avatars = ({max, children, ...rest}: AvatarsProps) => {
const childrenArray = Children.toArray(children);
const displayedChildren = childrenArray.slice(0, max);
const remainingChildrenCount = childrenArray.length - max;
const reverseChildren = displayedChildren.reverse();

return (
<AvatarListContainer {...rest}>
{remainingChildrenCount > 0 && <RemainingAvatar>+{remainingChildrenCount}</RemainingAvatar>}
{reverseChildren}
</AvatarListContainer>
);
};

export {Avatars};
export type {AvatarsProps};
Loading

0 comments on commit 89cbc95

Please sign in to comment.