diff --git a/UNRELEASED.md b/UNRELEASED.md index f06d139c69e..e17bbb33143 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -26,5 +26,6 @@ - Converted `BulkActionButton` into a functional component ([#2542](https://github.com/Shopify/polaris-react/pull/2542)) - Converted `Focus` into a functional component ([#2540](https://github.com/Shopify/polaris-react/pull/2540)) +- Converted `Tooltip` into a functional component ([#2543](https://github.com/Shopify/polaris-react/pull/2543)) ### Deprecations diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index f54906aff86..624b2fe3d87 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,9 +1,10 @@ -import React from 'react'; -import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; +import React, {useEffect, useState, useRef} from 'react'; import {findFirstFocusableNode} from '@shopify/javascript-utilities/focus'; import {PreferredPosition} from '../PositionedOverlay'; import {Portal} from '../Portal'; +import {useUniqueId} from '../../utilities/unique-id'; +import {useToggle} from '../../utilities/use-toggle'; import {TooltipOverlay} from './components'; import styles from './Tooltip.scss'; @@ -28,122 +29,93 @@ export interface TooltipProps { activatorWrapper?: string; } -interface State { - active: boolean; - activatorNode: HTMLElement | null; -} - -const getUniqueID = createUniqueIDFactory('TooltipContent'); - -export class Tooltip extends React.PureComponent { - state: State = { - active: Boolean(this.props.active), - activatorNode: null, - }; - - private id = getUniqueID(); - private activatorContainer: HTMLElement | null = null; - private mouseEntered = false; - - componentDidMount() { - this.setAccessibilityAttributes(); - } - - componentDidUpdate() { - this.setAccessibilityAttributes(); - } - - render() { - const {id} = this; - - const { - children, - content, - light, - preferredPosition = 'below', - activatorWrapper: WrapperComponent = 'span' as any, - } = this.props; - - const {active, activatorNode} = this.state; +export function Tooltip({ + children, + content, + light, + active: originalActive, + preferredPosition = 'below', + activatorWrapper = 'span', +}: TooltipProps) { + const WrapperComponent: any = activatorWrapper; + const {value: active, setTrue: handleFocus, setFalse: handleBlur} = useToggle( + Boolean(originalActive), + ); + const [activatorNode, setActivatorNode] = useState(null); + + const id = useUniqueId('TooltipContent'); + const activatorContainer = useRef(null); + const mouseEntered = useRef(false); + + useEffect(() => { + const firstFocusable = activatorContainer.current + ? findFirstFocusableNode(activatorContainer.current) + : null; + const accessibilityNode = firstFocusable || activatorContainer.current; + + if (!accessibilityNode) return; - const portal = activatorNode ? ( - - -
- {content} -
-
-
- ) : null; - - return ( - + - {children} - {portal} - - ); - } - - private setActivator = (node: HTMLElement | null) => { +
+ {content} +
+ + + ) : null; + + return ( + + {children} + {portal} + + ); + + function setActivator(node: HTMLElement | null) { + const activatorContainerRef: any = activatorContainer; if (node == null) { - this.activatorContainer = null; - this.setState({activatorNode: null}); + activatorContainerRef.current = null; + setActivatorNode(null); return; } - this.setState({activatorNode: node.firstElementChild as HTMLElement}); - this.activatorContainer = node; - }; - - private handleFocus = () => { - this.setState({active: true}); - }; - - private handleBlur = () => { - this.setState({active: false}); - }; + setActivatorNode(node.firstElementChild as HTMLElement); + activatorContainerRef.current = node; + } - private handleMouseEnter = () => { - this.mouseEntered = true; - this.setState({active: true}); - }; + function handleMouseEnter() { + mouseEntered.current = true; + handleFocus(); + } - private handleMouseLeave = () => { - this.mouseEntered = false; - this.setState({active: false}); - }; + function handleMouseLeave() { + mouseEntered.current = false; + handleBlur(); + } // https://github.com/facebook/react/issues/10109 // Mouseenter event not triggered when cursor moves from disabled button - private handleMouseEnterFix = () => { - !this.mouseEntered && this.handleMouseEnter(); - }; - - private setAccessibilityAttributes() { - const {activatorContainer, id} = this; - if (activatorContainer == null) { - return; - } - - const firstFocusable = findFirstFocusableNode(activatorContainer); - const accessibilityNode = firstFocusable || activatorContainer; - - accessibilityNode.tabIndex = 0; - accessibilityNode.setAttribute('aria-describedby', id); + function handleMouseEnterFix() { + !mouseEntered.current && handleMouseEnter(); } }