Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tooltip support for avatar and avatar group components [] #2549

Merged
merged 2 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/components/avatar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"dependencies": {
"@contentful/f36-core": "^4.48.0",
"@contentful/f36-image": "^4.0.0-alpha.0",
"@contentful/f36-tokens": "^4.0.0",
"@contentful/f36-menu": "^4.48.0",
"@contentful/f36-tokens": "^4.0.0",
"@contentful/f36-tooltip": "^4.48.0",
"emotion": "^10.0.17"
},
"peerDependencies": {
Expand Down
19 changes: 18 additions & 1 deletion packages/components/avatar/src/Avatar/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Avatar } from './Avatar';
import { CheckCircleIcon } from '@contentful/f36-icons';
import userEvent from '@testing-library/user-event';
import { Avatar } from './Avatar';

jest.mock('@contentful/f36-image', () => ({
// eslint-disable-next-line jsx-a11y/alt-text
Expand All @@ -26,4 +27,20 @@ describe('Avatar', () => {
const src = 'https://example.com/image.jpg';
render(<Avatar src={src} icon={<CheckCircleIcon variant="positive" />} />);
});

it('renders no tooltip when no props are provided', async () => {
const user = userEvent.setup();
render(<Avatar />);
await user.hover(screen.getByTestId('cf-ui-avatar-fallback'));

expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument();
});

it('renders with tooltip when props are provided', async () => {
const user = userEvent.setup();
render(<Avatar tooltipProps={{ content: 'some content' }} />);
await user.hover(screen.getByTestId('cf-ui-avatar-fallback'));

expect(screen.getByRole('tooltip').textContent).toBe('some content');
});
});
24 changes: 21 additions & 3 deletions packages/components/avatar/src/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { forwardRef } from 'react';
import { cx } from 'emotion';

import { type CommonProps } from '@contentful/f36-core';
import { Image, type ImageProps } from '@contentful/f36-image';
import { Tooltip, type TooltipProps } from '@contentful/f36-tooltip';

import { convertSizeToPixels, getAvatarStyles } from './Avatar.styles';
import type { ColorVariant } from './utils';

Expand All @@ -14,6 +17,10 @@ export interface AvatarProps extends CommonProps {
isLoading?: boolean;
size?: Size;
src?: ImageProps['src'];
/**
* A tooltipProps attribute used to conditionally render the tooltip around root element
*/
tooltipProps?: Omit<TooltipProps, 'children'>;
variant?: Variant;
colorVariant?: ColorVariant;
icon?: React.ReactElement;
Expand All @@ -23,13 +30,14 @@ function _Avatar(
{
alt = '',
className,
colorVariant,
icon = null,
isLoading = false,
size = 'medium',
src,
testId = 'cf-ui-avatar',
tooltipProps,
variant = 'user',
colorVariant,
icon = null,
...otherProps
}: AvatarProps,
forwardedRef: React.Ref<HTMLDivElement>,
Expand All @@ -38,7 +46,8 @@ function _Avatar(
const isFallback = Boolean(!isLoading && !src);
const styles = getAvatarStyles({ isFallback, size, variant, colorVariant });
const sizePixels = convertSizeToPixels(size);
return (

const Content = () => (
<div
className={cx(styles.root, className, {
[styles.imageContainer]: icon !== null,
Expand All @@ -63,6 +72,15 @@ function _Avatar(
{icon !== null && <span className={styles.overlayIcon}>{icon}</span>}
</div>
);

if (tooltipProps)
return (
<Tooltip {...tooltipProps}>
<Content />
</Tooltip>
);

return <Content />;
}

export const Avatar = forwardRef(_Avatar);
45 changes: 45 additions & 0 deletions packages/components/avatar/src/AvatarGroup/AvatarGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('AvatarGroup', () => {

expect(screen.getAllByRole('img')).toHaveLength(4);
});

it('renders the rest of the avatars in a menu with image and name', async () => {
const user = UserEvent.setup();
render(
Expand All @@ -68,6 +69,50 @@ describe('AvatarGroup', () => {
expect(screen.getAllByRole('img')).toHaveLength(5);
});

it('renders the avatars with tooltip', async () => {
const user = UserEvent.setup();
render(
<AvatarGroup>
<Avatar
alt="Marge Simpson"
src={imgUrl}
tooltipProps={{ content: 'Marge Simpson' }}
/>
<Avatar alt="Maggie Simpson" src={imgUrl} />
<Avatar alt="Lisa Simpson" src={imgUrl} />
<Avatar alt="Homer Simpson" src={imgUrl} />
<Avatar alt="Bart Simpson" src={imgUrl} />
</AvatarGroup>,
);

// Renders the tooltip for the presented avatars
await user.hover(screen.getAllByTestId('cf-ui-avatar')[0]);
expect(screen.getByRole('tooltip').textContent).toBe('Marge Simpson');
});

it('renders the avatars with tooltip in dropdown', async () => {
const user = UserEvent.setup();
render(
<AvatarGroup>
<Avatar alt="Marge Simpson" src={imgUrl} />
<Avatar alt="Maggie Simpson" src={imgUrl} />
<Avatar
alt="Lisa Simpson"
src={imgUrl}
tooltipProps={{ content: 'Lisa Simpson' }}
/>
<Avatar alt="Homer Simpson" src={imgUrl} />
<Avatar alt="Bart Simpson" src={imgUrl} />
</AvatarGroup>,
);

await user.click(screen.getByRole('button', { name: '3' }));
// Hover over the 1st avatar in dropdown: 2 visible avatars + button
await user.hover(screen.getAllByTestId('cf-ui-avatar')[4]);

expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument();
});

it('has no a11y issues', async () => {
const { container } = render(
<AvatarGroup>
Expand Down
18 changes: 10 additions & 8 deletions packages/components/avatar/src/AvatarGroup/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import React, { forwardRef } from 'react';
import { cx } from 'emotion';

import { Stack, type CommonProps } from '@contentful/f36-core';
import { Menu } from '@contentful/f36-menu';

import { type AvatarProps } from '../Avatar';
import { getAvatarGroupStyles } from './AvatarGroup.styles';

import { cx } from 'emotion';

export interface AvatarGroupProps extends CommonProps {
maxVisibleChildren?: number;
size?: 'small' | 'medium';
variant?: 'stacked' | 'spaced';
children?:
| React.ReactElement<AvatarProps>[]
| React.ReactElement<AvatarProps>;
maxVisibleChildren?: number;
size?: 'small' | 'medium';
variant?: 'stacked' | 'spaced';
}

function _AvatarGroup(
{
children,
className,
maxVisibleChildren = 3,
size = 'medium',
variant = 'spaced',
testId = 'cf-ui-avatar-group',
maxVisibleChildren = 3,
className,
variant = 'spaced',
}: AvatarGroupProps,
forwardedRef: React.Ref<HTMLDivElement>,
) {
Expand Down Expand Up @@ -85,6 +86,7 @@ function _AvatarGroup(
{React.cloneElement(child as React.ReactElement, {
key: `avatar-menuitem-${index}`,
size: 'tiny',
tooltipProps: undefined,
})}
{(child as React.ReactElement).props.alt}
</Menu.Item>
Expand Down
19 changes: 19 additions & 0 deletions packages/components/avatar/stories/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ export const Overview: Story<AvatarProps> = (args) => {
icon={<CheckCircleIcon variant="positive" />}
/>
</Flex>

<SectionHeading as="h4">Tooltip properties</SectionHeading>
<Flex
alignItems="center"
flexDirection="row"
gap="spacingS"
marginBottom="spacingM"
>
<Avatar
{...args}
size="large"
variant="user"
colorVariant="gray"
tooltipProps={{
content: 'Contentful Avatar',
placement: 'bottom',
}}
/>
</Flex>
</>
);
};
Expand Down
43 changes: 43 additions & 0 deletions packages/components/avatar/stories/AvatarGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,49 @@ export const Overview: Story<AvatarProps> = (args) => {
<Avatar {...args} alt="Prof. Daniel Düsentrieb" variant="user" />
</AvatarGroup>

<SectionHeading as="h3" marginBottom="spacingS">
Avatar Group spaced with menu and tooltip
</SectionHeading>

<AvatarGroup>
<Avatar
{...args}
alt="Lisa Simpson"
variant="user"
tooltipProps={{ content: 'Lisa Simpson', placement: 'bottom' }}
/>
<Avatar
{...args}
alt="Apu Nahasapeemapetilon"
variant="user"
tooltipProps={{
content: 'Apu Nahasapeemapetilon',
placement: 'bottom',
}}
/>
<Avatar
{...args}
alt="Arnie Pye"
variant="user"
tooltipProps={{ content: 'Arnie Pye', placement: 'bottom' }}
/>
<Avatar
{...args}
alt="Dr. Julius Hibbert"
variant="user"
tooltipProps={{ content: 'Dr. Julius Hibbert', placement: 'bottom' }}
/>
<Avatar
{...args}
alt="Prof. Daniel Düsentrieb"
variant="user"
tooltipProps={{
content: 'Prof. Daniel Düsentrieb',
placement: 'bottom',
}}
/>
</AvatarGroup>

<SectionHeading as="h3" marginBottom="spacingS">
Avatar Group spaced with custom visible children
</SectionHeading>
Expand Down
Loading