Skip to content

Commit

Permalink
feat(UserAvatar): implementation of name,size props (carbon-design-sy…
Browse files Browse the repository at this point in the history
…stem#4312)

* feat(useravatar):implementation of name,size props

* fix(useravatar): changed story name

* fix(useravatar): review changes suggested

* fix(useravatar): resolve test-c4p

* fix(useravatar): review changes suggested

* fix(useravatar): review changes suggested

* fix(useravatar): review changes suggested

* fix(useravatar): review changes suggested

* fix(useravatar): review changes suggested

* fix(useravatar): name changes suggested

---------

Co-authored-by: elysia <[email protected]>
  • Loading branch information
2 people authored and paul-balchin-ibm committed Feb 26, 2024
1 parent ef93720 commit f407692
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2562,7 +2562,7 @@ p.c4p--about-modal__copyright-text:first-child {
height: 2.5rem;
}
.c4p--side-panel .c4p--side-panel__slug-and-close {
position: fixed;
position: absolute;
z-index: 10; /* must be higher than title container border bottom */
top: 0;
right: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
@use '../../global/styles/mixins';
@use '@carbon/colors' as *;
@use '@carbon/styles/scss/theme' as *;
@use '@carbon/styles/scss/spacing' as *;
@use '@carbon/styles/scss/type';

// Other Carbon settings if needed
// TODO: @use '@carbon/styles/scss/grid';
Expand All @@ -23,6 +25,20 @@

$block-class: #{$pkg-prefix}--user-avatar;

$sizes: (
xl: $spacing-10,
lg: $spacing-09,
md: $spacing-07,
sm: $spacing-06,
);

@mixin size($size) {
$_size: map-get($sizes, $size);

width: $_size;
height: $_size;
}

.#{$block-class} {
position: relative;
display: flex;
Expand All @@ -32,10 +48,15 @@ $block-class: #{$pkg-prefix}--user-avatar;
justify-content: center;
border: 0.5px solid transparent;
border-radius: 100%;
color: $text-inverse;
outline: none;
outline-offset: 3px;
}

.#{$block-class} svg {
color: $icon-inverse;
}

.#{$block-class}__tooltip {
&:focus-within .#{$block-class} {
// stylelint-disable-next-line carbon/theme-token-use
Expand All @@ -55,3 +76,22 @@ $block-class: #{$pkg-prefix}--user-avatar;
.#{$block-class}--dark-cyan {
@include setBgColor($cyan-60);
}
.#{$block-class}--xl {
@include size('xl');
@include type.type-style('heading-04');
}

.#{$block-class}--lg {
@include size('lg');
@include type.type-style('heading-03');
}

.#{$block-class}--md {
@include size('md');
@include type.type-style('body-compact-01');
}

.#{$block-class}--sm {
@include size('sm');
@include type.type-style('label-01');
}
87 changes: 53 additions & 34 deletions packages/ibm-products/src/components/UserAvatar/UserAvatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import cx from 'classnames';
import { getDevtoolsProps } from '../../global/js/utils/devtools';
import { pkg /*, carbon */ } from '../../settings';

import { User, Group } from '@carbon/react/icons';

import { Tooltip, usePrefix } from '@carbon/react';
import { TooltipTrigger } from '../TooltipTrigger';
import { User } from '@carbon/react/icons';
// Carbon and package components we use.
/* TODO: @import(s) of carbon components and other package components. */

Expand Down Expand Up @@ -47,9 +46,8 @@ const componentName = 'UserAvatar';
*/

const defaults = {
renderIcon: () => <User size={32} />,
size: 'md',
tooltipAlignment: 'bottom',
tooltipText: 'Thomas J. Watson',
};

export let UserAvatar = React.forwardRef(
Expand All @@ -58,37 +56,50 @@ export let UserAvatar = React.forwardRef(
// The component props, in alphabetical order (for consistency).
backgroundColor,
className,
name,
/* TODO: add other props for UserAvatar, with default values if needed */
renderIcon = defaults.renderIcon,
tooltipText = defaults.tooltipText,
renderIcon: RenderIcon,
size = defaults.size,
tooltipText,
tooltipAlignment = defaults.tooltipAlignment,
// Collect any other property values passed in.
...rest
},
ref
) => {
const carbonPrefix = usePrefix();
const icons = {
user: {
md: <User size={32} />,
},
group: {
md: <Group size={32} />,
},
const iconSize = {
sm: 16,
md: 20,
lg: 24,
xl: 32,
};
const getItem = (renderIcon) => {
if (renderIcon === User) {
return icons.user['md'];
} else if (renderIcon === Group) {
return icons.group['md'];
} else {
return renderIcon;
const formatInitials = () => {
const parts = name.split(' ');
const firstChar = parts[0].charAt(0).toUpperCase();
const secondChar = parts[0].charAt(1).toUpperCase();
if (parts.length === 1) {
return firstChar + secondChar;
}
const lastChar = parts[parts.length - 1].charAt(0).toUpperCase();
const initials = [firstChar];
if (lastChar) {
initials.push(lastChar);
}
return ''.concat(...initials);
};
const getItem = () => {
const iconProps = { size: iconSize[size] };
if (RenderIcon) {
return <RenderIcon {...iconProps} />;
}
if (name) {
return formatInitials();
}
return <User {...iconProps} />;
};

const SetItem = getItem(renderIcon);

const renderUserAvatar = () => (
const Avatar = () => (
<div
{
// Pass through any other property values as HTML attributes.
Expand All @@ -98,6 +109,7 @@ export let UserAvatar = React.forwardRef(
blockClass, // Apply the block class to the main HTML element
className, // Apply any supplied class names to the main HTML element.
`${blockClass}--${backgroundColor}`,
`${blockClass}--${size}`,
// example: `${blockClass}__template-string-class-${kind}-n-${size}`,
{
// switched classes dependant on props or state
Expand All @@ -108,24 +120,24 @@ export let UserAvatar = React.forwardRef(
role="img"
{...getDevtoolsProps(componentName)}
>
<SetItem />
{getItem()}
</div>
);

return (
SetItem &&
(tooltipText ? (
if (tooltipText) {
return (
<Tooltip
align={tooltipAlignment}
label={tooltipText}
className={`${blockClass}__tooltip ${carbonPrefix}--icon-tooltip`}
>
<TooltipTrigger>{renderUserAvatar()}</TooltipTrigger>
<TooltipTrigger>
<Avatar />
</TooltipTrigger>
</Tooltip>
) : (
renderUserAvatar()
))
);
);
}
return <Avatar />;
}
);

Expand All @@ -149,11 +161,18 @@ UserAvatar.propTypes = {
* Provide an optional class to be applied to the containing node.
*/
className: PropTypes.string,

/**
* When passing the name prop, either send the initials to be used or the user's full name. The first two capital letters of the user's name will be used as the name.
*/
name: PropTypes.string,
/**
* Provide a custom icon to use if you need to use an icon other than the default one
*/
renderIcon: PropTypes.func,
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Set the size of the avatar circle
*/
size: PropTypes.oneOf(['xl', 'lg', 'md', 'sm']),
/**
* Specify how the trigger should align with the tooltip
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { UserAvatar } from '.';
{/* TODO: One example per designed use case. */}

<Canvas>
<Story id={getStoryId(UserAvatar.displayName, 'user-avatar')} />
<Story id={getStoryId(UserAvatar.displayName, 'default')} />
</Canvas>

## Code sample
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { UserAvatar } from '.';
import mdx from './UserAvatar.mdx';
import styles from './_storybook-styles.scss';
import { Add, Group, User } from '@carbon/react/icons';

const defaultArgs = {
backgroundColor: 'light-cyan',
Expand All @@ -39,6 +40,19 @@ export default {
},
options: ['light-cyan', 'dark-cyan'],
},
renderIcon: {
control: {
type: 'select',
},
options: ['No icon', 'User', 'Group', 'Add'],
mapping: { 'No icon': '', User: User, Group: Group, Add: Add },
},
size: {
control: {
type: 'radio',
},
options: ['xl', 'lg', 'md', 'sm'],
},
tooltipAlignment: {
control: {
type: 'select',
Expand All @@ -55,6 +69,10 @@ export default {
],
},
},
args: {
size: 'md',
tooltipAlignment: 'bottom',
},
parameters: {
styles,
docs: {
Expand All @@ -80,10 +98,13 @@ const Template = (args) => {
* TODO: Declare one or more stories, generally one per design scenario.
* NB no need for a 'Playground' because all stories have all controls anyway.
*/
export const userAvatar = prepareStory(Template, {
export const Default = prepareStory(Template, {
storyName: 'Default',
args: {
...defaultArgs,
// TODO: Component args - https://storybook.js.org/docs/react/writing-stories/args#UserAvatar-args
name: 'thomas j. watson',
tooltipText: 'Thomas J. Watson',
renderIcon: 'No icon',
},
});
38 changes: 35 additions & 3 deletions packages/ibm-products/src/components/UserAvatar/UserAvatar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro

import { pkg } from '../../settings';
import { pkg, carbon } from '../../settings';
import uuidv4 from '../../global/js/utils/uuidv4';

import { UserAvatar } from '.';
import { User } from '@carbon/react/icons';

const blockClass = `${pkg.prefix}--user-avatar`;
const componentName = UserAvatar.displayName;
Expand All @@ -33,13 +34,13 @@ describe(componentName, () => {
});

it('should return an icon for the avatar image', async () => {
const { container } = renderComponent();
const { container } = renderComponent({ renderIcon: User });
const renderedSVG = container.getElementsByTagName('svg');
expect(renderedSVG).toBeTruthy();
});

it('has no accessibility violations', async () => {
const { container } = renderComponent();
const { container } = renderComponent({ renderIcon: User });
expect(container).toBeAccessible(componentName);
expect(container).toHaveNoAxeViolations();
});
Expand Down Expand Up @@ -70,4 +71,35 @@ describe(componentName, () => {
componentName
);
});

it('should return appropriately size circle based on size prop', async () => {
renderComponent({ size: 'md' });
const element = screen.getByRole('img');
const hasSizeClass = element.className.includes('md');
expect(hasSizeClass).toBeTruthy();
});

it('should render the initials when passed the name prop', async () => {
renderComponent({ name: 'Display name' });
expect(screen.getByText(/DN/));
});

it('should render the initials when simply passing two names to the name prop', async () => {
renderComponent({ name: 'DN' });
expect(screen.getByText(/DN/));
});

it('should render a tooltip if the tooltipText is supplied', async () => {
renderComponent({ tooltipText: 'Display name' });
const element = screen.getByRole('img');
const tooltipElement = element.closest(`span.${carbon.prefix}--tooltip`);
expect(tooltipElement).toBeTruthy();
});

it('should not render a tooltip if the tooltipText is not supplied', async () => {
renderComponent({ tooltipText: '' });
const element = screen.getByRole('img');
const tooltipElement = element.closest(`span.${carbon.prefix}--tooltip`);
expect(tooltipElement).not.toBeTruthy();
});
});

0 comments on commit f407692

Please sign in to comment.