Skip to content

Commit

Permalink
Merge pull request #2578 from contentful/homer-2272
Browse files Browse the repository at this point in the history
feat(Tooltip): support ReactElement as content
  • Loading branch information
miguelcrespo authored Sep 19, 2023
2 parents b60f972 + a1f2b40 commit 346339f
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 23 deletions.
6 changes: 6 additions & 0 deletions .changeset/breezy-geese-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@contentful/f36-avatar': minor
'@contentful/f36-tooltip': minor
---

Tooltips can now render ReactElement as their Content
10 changes: 8 additions & 2 deletions packages/components/avatar/src/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ 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 {
Tooltip,
type TooltipInternalProps,
type WithEnhancedContent,
} from '@contentful/f36-tooltip';

import { convertSizeToPixels, getAvatarStyles } from './Avatar.styles';
import type { ColorVariant } from './utils';
Expand All @@ -20,7 +24,9 @@ export interface AvatarProps extends CommonProps {
/**
* A tooltipProps attribute used to conditionally render the tooltip around root element
*/
tooltipProps?: Omit<TooltipProps, 'children'>;
tooltipProps?: CommonProps &
WithEnhancedContent &
Omit<TooltipInternalProps, 'children'>;
variant?: Variant;
colorVariant?: ColorVariant;
icon?: React.ReactElement;
Expand Down
2 changes: 2 additions & 0 deletions packages/components/tooltip/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { Tooltip } from '@contentful/f36-tooltip';
## Content guidelines

- Use short and clear messages as the `Tooltip`’s content
- The Tooltip component allows you to pass React elements as content. However, this should be used with care. ReactElement as content is best suited for text formatting purposes. It should not be used for interactive elements like links, buttons, or form elements. Additionally, extensive images should be avoided, except for text-decorator icons that give semantic meaning to the text shown in the tooltip (e.g., warning signs or other relevant icons).
- When using `ReactElement` as the content, it's recommended to use the `ScreenReaderOnly` component when displaying critical information.

## Accessibility

Expand Down
23 changes: 23 additions & 0 deletions packages/components/tooltip/src/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { axe } from '@/scripts/test/axeHelper';

import { Tooltip } from './Tooltip';
import { Paragraph } from '@contentful/f36-typography';

jest.mock('@contentful/f36-core', () => {
const actual = jest.requireActual('@contentful/f36-core');
Expand Down Expand Up @@ -126,4 +127,26 @@ describe('Tooltip', () => {

expect(results).toHaveNoViolations();
});

it('render a React Element as children', async () => {
const user = userEvent.setup();

const { container } = render(
<Tooltip
label="With React Element"
id="Tooltip"
content={<Paragraph>Ich bin ein Paragraph</Paragraph>}
>
<span>Hover me</span>
</Tooltip>,
);
await user.hover(screen.getByText('Hover me'));

const results = await axe(container);

expect(results).toHaveNoViolations();
expect(screen.getByRole('tooltip').textContent).toBe(
'Ich bin ein Paragraph',
);
});
});
38 changes: 31 additions & 7 deletions packages/components/tooltip/src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
type MouseEvent,
type FocusEvent,
type CSSProperties,
ReactElement,
} from 'react';
import { usePopper } from 'react-popper';
import type { Placement } from '@popperjs/core';
Expand All @@ -18,7 +19,29 @@ import { getStyles } from './Tooltip.styles';

export type TooltipPlacement = Placement;

export interface TooltipProps extends CommonProps {
export type WithEnhancedContent =
| {
/**
* Content of the tooltip
*/
content: ReactElement;
/**
* Accesible label property, only required when using ReactElement as content
*/
label: string;
}
| {
/**
* Content of the tooltip
*/
content: string;
/**
* Accesible label property, only required when using ReactElement as content
*/
label?: string;
};

export type TooltipInternalProps = {
/**
* Child nodes to be rendered in the component and that will show the tooltip when they are hovered
*/
Expand All @@ -27,10 +50,6 @@ export interface TooltipProps extends CommonProps {
* HTML element used to wrap the target of the tooltip
*/
as?: React.ElementType;
/**
* Content of the tooltip
*/
content?: string;
/**
* A unique id of the tooltip
*/
Expand Down Expand Up @@ -94,13 +113,18 @@ export interface TooltipProps extends CommonProps {
* @default false
*/
isDisabled?: boolean;
}
};

export type TooltipProps = CommonProps &
TooltipInternalProps &
WithEnhancedContent;

export const Tooltip = ({
children,
className,
as: HtmlTag = 'span',
content,
label,
id,
isVisible = false,
hideDelay = 0,
Expand Down Expand Up @@ -210,7 +234,7 @@ export const Tooltip = ({
}}
{...attributes.popper}
>
<span>{content}</span>
<span aria-label={label}>{content}</span>
<span
className={styles.tooltipArrow}
data-placement={
Expand Down
6 changes: 5 additions & 1 deletion packages/components/tooltip/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { Tooltip } from './Tooltip';
export type { TooltipProps } from './Tooltip';
export type {
TooltipInternalProps,
TooltipProps,
WithEnhancedContent,
} from './Tooltip';
31 changes: 30 additions & 1 deletion packages/components/tooltip/stories/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { SectionHeading } from '@contentful/f36-typography';
import { Heading, Paragraph, SectionHeading } from '@contentful/f36-typography';
import { Tooltip } from '../src/Tooltip';
import { TextLink } from '@contentful/f36-text-link';
import { Flex } from '@contentful/f36-core';
import tokens from '@contentful/f36-tokens';

export default {
title: 'Components/Tooltip',
Expand Down Expand Up @@ -212,3 +213,31 @@ export const WithDelays = () => {
</>
);
};

export const WithReactElement = () => {
return (
<>
<SectionHeading as="h3" marginBottom="spacingS">
With React Elements as children
</SectionHeading>

<Flex marginBottom="spacingS">
<Tooltip
label="Render a paragrah with a React Element"
content={
<Flex flexDirection="column">
<Heading style={{ color: tokens.colorWhite }}>
I am a Heading
</Heading>
<Paragraph style={{ color: tokens.colorWhite }}>
I am a paragraph
</Paragraph>
</Flex>
}
>
<TextLink href="/">Hover me</TextLink>
</Tooltip>
</Flex>
</>
);
};
12 changes: 0 additions & 12 deletions packages/forma-36-codemod/transforms/v4-tooltip.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
const { modifyPropsCodemod } = require('./common/modify-props-codemod');
const { hasProperty, getProperty } = require('../utils');

module.exports = modifyPropsCodemod({
componentName: 'Tooltip',
beforeRename: (attributes) => {
if (
hasProperty(attributes, { propertyName: 'content' }) &&
typeof getProperty(attributes, { propertyName: 'content' } !== 'string')
) {
console.error(
'Value of property "content" on Tooltip component should be a string.',
);
}
return attributes;
},
renameMap: {
place: 'placement',
containerElement: 'as',
Expand Down

1 comment on commit 346339f

@vercel
Copy link

@vercel vercel bot commented on 346339f Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.