From f407692c105c5d7228df27d68c050f74b7f2583c Mon Sep 17 00:00:00 2001 From: Anamika T S <47971732+anamikaanu96@users.noreply.github.com> Date: Mon, 19 Feb 2024 19:50:52 +0530 Subject: [PATCH] feat(UserAvatar): implementation of name,size props (#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 --- .../__snapshots__/styles.test.js.snap | 2 +- .../components/UserAvatar/_user-avatar.scss | 40 +++++++++ .../src/components/UserAvatar/UserAvatar.js | 87 +++++++++++-------- .../src/components/UserAvatar/UserAvatar.mdx | 2 +- .../UserAvatar/UserAvatar.stories.js | 23 ++++- .../components/UserAvatar/UserAvatar.test.js | 38 +++++++- 6 files changed, 152 insertions(+), 40 deletions(-) diff --git a/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap b/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap index 75c44e2bb2..e01a9c6e3e 100644 --- a/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap +++ b/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap @@ -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; diff --git a/packages/ibm-products-styles/src/components/UserAvatar/_user-avatar.scss b/packages/ibm-products-styles/src/components/UserAvatar/_user-avatar.scss index fd2d90ec34..41e9784ec2 100644 --- a/packages/ibm-products-styles/src/components/UserAvatar/_user-avatar.scss +++ b/packages/ibm-products-styles/src/components/UserAvatar/_user-avatar.scss @@ -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'; @@ -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; @@ -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 @@ -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'); +} diff --git a/packages/ibm-products/src/components/UserAvatar/UserAvatar.js b/packages/ibm-products/src/components/UserAvatar/UserAvatar.js index f1255b12f7..f2a17c0324 100644 --- a/packages/ibm-products/src/components/UserAvatar/UserAvatar.js +++ b/packages/ibm-products/src/components/UserAvatar/UserAvatar.js @@ -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. */ @@ -47,9 +46,8 @@ const componentName = 'UserAvatar'; */ const defaults = { - renderIcon: () => , + size: 'md', tooltipAlignment: 'bottom', - tooltipText: 'Thomas J. Watson', }; export let UserAvatar = React.forwardRef( @@ -58,9 +56,11 @@ 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 @@ -68,27 +68,38 @@ export let UserAvatar = React.forwardRef( ref ) => { const carbonPrefix = usePrefix(); - const icons = { - user: { - md: , - }, - group: { - md: , - }, + 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 ; + } + if (name) { + return formatInitials(); + } + return ; }; - const SetItem = getItem(renderIcon); - - const renderUserAvatar = () => ( + const Avatar = () => (
- + {getItem()}
); - return ( - SetItem && - (tooltipText ? ( + if (tooltipText) { + return ( - {renderUserAvatar()} + + + - ) : ( - renderUserAvatar() - )) - ); + ); + } + return ; } ); @@ -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 */ diff --git a/packages/ibm-products/src/components/UserAvatar/UserAvatar.mdx b/packages/ibm-products/src/components/UserAvatar/UserAvatar.mdx index c9218a512c..cefe32effa 100644 --- a/packages/ibm-products/src/components/UserAvatar/UserAvatar.mdx +++ b/packages/ibm-products/src/components/UserAvatar/UserAvatar.mdx @@ -22,7 +22,7 @@ import { UserAvatar } from '.'; {/* TODO: One example per designed use case. */} - + ## Code sample diff --git a/packages/ibm-products/src/components/UserAvatar/UserAvatar.stories.js b/packages/ibm-products/src/components/UserAvatar/UserAvatar.stories.js index 9d7fe5d697..4264c70dcf 100644 --- a/packages/ibm-products/src/components/UserAvatar/UserAvatar.stories.js +++ b/packages/ibm-products/src/components/UserAvatar/UserAvatar.stories.js @@ -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', @@ -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', @@ -55,6 +69,10 @@ export default { ], }, }, + args: { + size: 'md', + tooltipAlignment: 'bottom', + }, parameters: { styles, docs: { @@ -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', }, }); diff --git a/packages/ibm-products/src/components/UserAvatar/UserAvatar.test.js b/packages/ibm-products/src/components/UserAvatar/UserAvatar.test.js index f4bbe90515..7c73830d29 100644 --- a/packages/ibm-products/src/components/UserAvatar/UserAvatar.test.js +++ b/packages/ibm-products/src/components/UserAvatar/UserAvatar.test.js @@ -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; @@ -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(); }); @@ -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(); + }); });