Skip to content

feat: Tooltip with react-popper #267

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

Merged
merged 12 commits into from
Aug 19, 2022
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
228 changes: 119 additions & 109 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PropsWithChildren } from 'react';
import * as React from 'react';
import TetherComponent from 'react-tether';
import styled, { createGlobalStyle, keyframes } from 'styled-components';
import { Colors, Elevation, MediaQueries } from '../../essentials';
import styled, { keyframes } from 'styled-components';
import { usePopper } from 'react-popper';
import { Placement } from '@popperjs/core/lib/enums';
import { variant } from 'styled-system';
import { Colors, MediaQueries } from '../../essentials';
import { get } from '../../utils/themeGet';
import { Text } from '../Text/Text';
import { TooltipPlacement } from './TooltipPlacement';
import { getAttachmentFromPlacement } from './util/getAttachmentFromPlacement';
import { mapPlacementWithDeprecationWarning, TooltipPlacement } from './TooltipPlacement';

const fadeAnimation = keyframes`
from {
Expand All @@ -18,7 +19,73 @@ const fadeAnimation = keyframes`
}
`;

const TooltipBody = styled.div<Pick<TooltipProps, 'inverted'>>`
const arrowPlacementStyles = variant({
variants: {
bottom: {
right: 'calc(50% - 0.25rem)'
},
'bottom-end': {
right: '0.3rem'
},
'top-start': {
bottom: '-0.5rem',
transform: 'rotate(-180deg)'
},
top: {
bottom: '-0.5rem',
transform: 'rotate(-180deg)',
right: 'calc(50% - 0.25rem)'
},
'top-end': {
bottom: '-0.5rem',
transform: 'rotate(-180deg)',
right: '0.3rem'
},
left: {
top: 'calc(50% - 0.25rem)',
left: 'auto',
right: '-0.5rem',
transform: 'rotate(90deg)'
},
'left-end': {
bottom: '0.5rem',
left: 'auto',
right: '-0.5rem',
transform: 'rotate(90deg)'
},
'left-start': {
top: '0.5rem',
left: 'auto',
right: '-0.5rem',
transform: 'rotate(90deg)'
},
right: {
top: 'calc(50% - 0.25rem)',
left: '-0.25rem',
right: 'auto',
transform: 'rotate(-90deg)'
},
'right-end': {
bottom: '0.5rem',
left: '-0.25rem',
right: 'auto',
transform: 'rotate(-90deg)'
},
'right-start': {
top: '0.5rem',
left: '-0.25rem',
right: 'auto',
transform: 'rotate(-90deg)'
}
}
});

interface TooltipBodyProps {
inverted?: boolean;
variant: string;
}

const TooltipBody = styled.div<TooltipBodyProps>`
position: relative;
background-color: ${p => (p.inverted ? Colors.AUTHENTIC_BLUE_50 : Colors.AUTHENTIC_BLUE_900)};
padding: 0.25rem 0.5rem;
Expand All @@ -45,83 +112,8 @@ const TooltipBody = styled.div<Pick<TooltipProps, 'inverted'>>`
border: 0.25rem solid rgba(0, 0, 0, 0);
border-bottom-color: ${p => (p.inverted ? Colors.AUTHENTIC_BLUE_50 : Colors.AUTHENTIC_BLUE_900)};
margin-left: -0.25rem;
}
`;

const GlobalTetherStyles = createGlobalStyle`
body > .tether-element {
z-index: ${Elevation.TOOLTIP};
}

.tether-target-attached-bottom {
& > ${TooltipBody} {
margin-top: 0.5rem;

&::after {
top: -0.5rem;
}
}
}

.tether-target-attached-top {
& > ${TooltipBody} {
top: -0.5rem;

&::after {
bottom: -0.5rem;
transform: rotate(-180deg);
}
}
}

.tether-target-attached-center {
& > ${TooltipBody} {
&::after {
left: 50%;
}
}
}

.tether-target-attached-left {
& > ${TooltipBody} {
&::after {
left: 1rem;
}
}
}

.tether-target-attached-right {
& > ${TooltipBody} {
&::after {
right: 1rem;
}
}
}

.tether-target-attached-middle.tether-target-attached-right {
& > ${TooltipBody} {
margin-left: 0.5rem;

&::after {
top: calc(50% - 0.25rem);
left: -0.25rem;
right: auto;
transform: rotate(-90deg);
}
}
}

.tether-target-attached-middle.tether-target-attached-left {
& > ${TooltipBody} {
left: -0.5rem;

&::after {
top: calc(50% - 0.25rem);
left: auto;
right: -0.5rem;
transform: rotate(90deg);
}
}
${arrowPlacementStyles}
}
`;

Expand All @@ -131,9 +123,9 @@ interface TooltipProps {
*/
content: React.ReactNode;
/**
* Set the position of where the tooltip is attached to the target, defaults to "top-center"
* Set the position of where the tooltip is attached to the target, defaults to "top"
*/
placement?: TooltipPlacement;
placement?: TooltipPlacement | Placement;
/**
* Adjust the component for display on dark backgrounds
*/
Expand All @@ -147,11 +139,37 @@ interface TooltipProps {
const Tooltip: React.FC<TooltipProps> = ({
content,
children,
placement = 'top-center',
placement = 'top',
alwaysVisible = false,
inverted = false
}: PropsWithChildren<TooltipProps>) => {
const [isVisible, setIsVisible] = React.useState(alwaysVisible);
/**
* triggerReference and contentReference are used with the Popper library in order to get the tooltip styles and attributes
*/
const [triggerReference, setTriggerReference] = React.useState(undefined);
const [contentReference, setContentReference] = React.useState(undefined);


/**
* Map the older placement values to Popper placement as we need to get the correct placement for the tooltip from the Popper library
* without introduce any breaking changes to the Tooltip component.
* TODO: Remove in the next major release.
*/
const mappedPlacement = mapPlacementWithDeprecationWarning(placement);

const { styles, attributes } = usePopper(triggerReference, contentReference, {
placement: mappedPlacement,
modifiers: [
{
name: 'offset',
enabled: true,
options: {
offset: [0, 5]
}
}
]
});

let dynamicContent = content;

Expand All @@ -171,30 +189,22 @@ const Tooltip: React.FC<TooltipProps> = ({

return (
<>
<TetherComponent
{...getAttachmentFromPlacement(placement)}
constraints={[
{
to: 'window',
attachment: 'together'
}
]}
renderTarget={ref =>
React.cloneElement(children as React.ReactElement, {
onMouseOver: () => handleVisibilityChange(true),
onMouseOut: () => handleVisibilityChange(false),
ref
})
}
renderElement={(ref: React.RefObject<HTMLDivElement>) =>
isVisible && (
<TooltipBody ref={ref} inverted={inverted}>
{dynamicContent}
</TooltipBody>
)
}
/>
<GlobalTetherStyles />
{React.cloneElement(children as React.ReactElement, {
onMouseOver: () => handleVisibilityChange(true),
onMouseOut: () => handleVisibilityChange(false),
ref: setTriggerReference
})}
{content && isVisible && (
<TooltipBody
ref={setContentReference}
inverted={inverted}
style={{ ...styles.popper }}
variant={attributes.popper?.['data-popper-placement']}
{...attributes.popper}
>
{dynamicContent}
</TooltipBody>
)}
</>
);
};
Expand Down
21 changes: 21 additions & 0 deletions src/components/Tooltip/TooltipPlacement.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Placement } from '@popperjs/core';
import { deprecatedProperty } from '../../utils/deprecatedProperty';

export type TooltipPlacement =
| 'bottom-left'
| 'bottom-center'
Expand All @@ -7,3 +10,21 @@ export type TooltipPlacement =
| 'top-right'
| 'center-left'
| 'center-right';

const TOOLTIP_TO_POPPER_PLACEMENT_MAP: { [key in TooltipPlacement]: Placement } = {
'bottom-left': 'bottom-start',
'bottom-center': 'bottom',
'bottom-right': 'bottom-end',
'top-left': 'top-start',
'top-center': 'top',
'top-right': 'top-end',
'center-left': 'left',
'center-right': 'right'
};

export const mapPlacementWithDeprecationWarning = (placement: TooltipPlacement | Placement): Placement => {
const mappedPlacement = TOOLTIP_TO_POPPER_PLACEMENT_MAP[placement as TooltipPlacement];
if (mappedPlacement)
deprecatedProperty('Tooltip', placement, `Value '${placement}' for placement`, mappedPlacement);
return mappedPlacement ?? (placement as Placement);
};
6 changes: 5 additions & 1 deletion src/components/Tooltip/docs/Tooltip.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ This component provides informative text to an UI element.

Use the tooltip for user on-boarding, guiding information about a new feature, detailed interactive workflows or for a contextual help (eg. over a button).

## Properties

<TooltipPropsTable />

## Appearance

- The font size is 12px and font weight is regular.
Expand All @@ -49,7 +53,7 @@ Use the tooltip for user on-boarding, guiding information about a new feature, d
It is possible to adjust the position of the tooltip connection to the target with the `placement` prop. Below is a list
of possible options which are represented in the square next to it. It is important to keep in mind that the tooltip
will be moved to a different position if it cannot be shown on the desired side due to screen sizes. Read more about the
Tether library [here](http://tether.io).
Popper library [here](https://popper.js.org/).

<TooltipPlacementExample />

Expand Down
29 changes: 18 additions & 11 deletions src/components/Tooltip/docs/TooltipPlacementExample.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FC } from 'react';
import * as React from 'react';
import styled from 'styled-components';
import { Placement } from '@popperjs/core/lib/enums';
import { RadioButton, Tooltip } from '../..';
import { Colors, MediaQueries } from '../../../essentials';
import { TooltipPlacement } from '../TooltipPlacement';

const TargetSquare = styled.div`
background: ${Colors.BUMPY_MAGENTA_50};
Expand Down Expand Up @@ -35,17 +35,24 @@ const ExampleContainer = styled.div`
`;

const TooltipPlacementExample: FC = () => {
const [placement, setPlacement] = React.useState<TooltipPlacement>('top-center');
const [placement, setPlacement] = React.useState<Placement>('top');

const availablePlacements: TooltipPlacement[] = [
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
'center-left',
'center-right'
const availablePlacements: Placement[] = [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
'right-start',
'right-end',
'left-start',
'left-end',
'top',
'bottom',
'right',
'left',
'auto',
'auto-start',
'auto-end'
];

return (
Expand Down
Loading