diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 178b9d419..12c5585eb 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -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 { @@ -18,7 +19,73 @@ const fadeAnimation = keyframes` } `; -const TooltipBody = styled.div>` +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` position: relative; background-color: ${p => (p.inverted ? Colors.AUTHENTIC_BLUE_50 : Colors.AUTHENTIC_BLUE_900)}; padding: 0.25rem 0.5rem; @@ -45,83 +112,8 @@ const TooltipBody = styled.div>` 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} } `; @@ -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 */ @@ -147,11 +139,37 @@ interface TooltipProps { const Tooltip: React.FC = ({ content, children, - placement = 'top-center', + placement = 'top', alwaysVisible = false, inverted = false }: PropsWithChildren) => { 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; @@ -171,30 +189,22 @@ const Tooltip: React.FC = ({ return ( <> - - React.cloneElement(children as React.ReactElement, { - onMouseOver: () => handleVisibilityChange(true), - onMouseOut: () => handleVisibilityChange(false), - ref - }) - } - renderElement={(ref: React.RefObject) => - isVisible && ( - - {dynamicContent} - - ) - } - /> - + {React.cloneElement(children as React.ReactElement, { + onMouseOver: () => handleVisibilityChange(true), + onMouseOut: () => handleVisibilityChange(false), + ref: setTriggerReference + })} + {content && isVisible && ( + + {dynamicContent} + + )} ); }; diff --git a/src/components/Tooltip/TooltipPlacement.ts b/src/components/Tooltip/TooltipPlacement.ts index 949af115c..39a67fcc4 100644 --- a/src/components/Tooltip/TooltipPlacement.ts +++ b/src/components/Tooltip/TooltipPlacement.ts @@ -1,3 +1,6 @@ +import { Placement } from '@popperjs/core'; +import { deprecatedProperty } from '../../utils/deprecatedProperty'; + export type TooltipPlacement = | 'bottom-left' | 'bottom-center' @@ -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); +}; diff --git a/src/components/Tooltip/docs/Tooltip.mdx b/src/components/Tooltip/docs/Tooltip.mdx index 019c2a447..c0abb16cb 100644 --- a/src/components/Tooltip/docs/Tooltip.mdx +++ b/src/components/Tooltip/docs/Tooltip.mdx @@ -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 + + + ## Appearance - The font size is 12px and font weight is regular. @@ -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/). diff --git a/src/components/Tooltip/docs/TooltipPlacementExample.tsx b/src/components/Tooltip/docs/TooltipPlacementExample.tsx index 6116a28b8..c8b6d8fa5 100644 --- a/src/components/Tooltip/docs/TooltipPlacementExample.tsx +++ b/src/components/Tooltip/docs/TooltipPlacementExample.tsx @@ -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}; @@ -35,17 +35,24 @@ const ExampleContainer = styled.div` `; const TooltipPlacementExample: FC = () => { - const [placement, setPlacement] = React.useState('top-center'); + const [placement, setPlacement] = React.useState('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 ( diff --git a/src/components/Tooltip/docs/TooltipPropsTable.tsx b/src/components/Tooltip/docs/TooltipPropsTable.tsx index 6c7198c74..6d54205b4 100644 --- a/src/components/Tooltip/docs/TooltipPropsTable.tsx +++ b/src/components/Tooltip/docs/TooltipPropsTable.tsx @@ -1,29 +1,32 @@ -import React, { FC } from 'react'; - +import * as React from 'react'; import { PropsTable } from '../../../docs/PropsTable'; -export const TooltipPropsTable: FC = () => { +export const TooltipPropsTable = () => { const props = [ { name: 'content', - type: 'React.ReactNode', - description: 'The content that will be shown inside of the tooltip body.' + type: 'React.ReactNode | string', + description: 'The content that will be shown inside of the tooltip body', + defaultValue: '-' }, { name: 'placement', - type: 'TooltipPlacement (see the Placement section below for values)', - description: 'Set the position of where the tooltip is attached to the target.', - defaultValue: 'top-center' + type: + '"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"', + description: 'Set the position of where the tooltip is attached to the target', + defaultValue: 'top' }, { name: 'inverted', type: 'boolean', - description: 'Adjust the component for display on dark backgrounds.' + description: 'Adjust color for display on a dark background', + defaultValue: 'false' }, { name: 'alwaysVisible', type: 'boolean', - description: 'Force the tooltip to always be visible, regardless of user interaction.' + description: 'Force the tooltip to always be visible, regardless of user interaction', + defaultValue: 'false' } ]; return ; diff --git a/src/components/Tooltip/util/getAttachmentFromPlacement.ts b/src/components/Tooltip/util/getAttachmentFromPlacement.ts deleted file mode 100644 index 22e194b3c..000000000 --- a/src/components/Tooltip/util/getAttachmentFromPlacement.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TooltipPlacement } from '../TooltipPlacement'; - -// we need to convert the users `placement` prop to react-tether `attachment` and `targetAttachment` -export const getAttachmentFromPlacement = ( - placement: TooltipPlacement -): { attachment: string; targetAttachment: string } => { - switch (placement) { - case 'bottom-left': - return { - attachment: 'top left', - targetAttachment: 'bottom left' - }; - - case 'bottom-center': - return { - attachment: 'top center', - targetAttachment: 'bottom center' - }; - case 'bottom-right': - return { - attachment: 'top right', - targetAttachment: 'bottom right' - }; - case 'top-left': - return { - attachment: 'bottom left', - targetAttachment: 'top left' - }; - case 'top-center': - return { - attachment: 'bottom center', - targetAttachment: 'top center' - }; - case 'top-right': - return { - attachment: 'bottom right', - targetAttachment: 'top right' - }; - case 'center-left': - return { - attachment: 'middle right', - targetAttachment: 'middle left' - }; - case 'center-right': - return { - attachment: 'middle left', - targetAttachment: 'middle right' - }; - default: - return { - attachment: 'bottom center', - targetAttachment: 'top center' - }; - } -};