Skip to content

Commit

Permalink
Converted Tooltip to a functional component
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewMusgrave committed Dec 9, 2019
1 parent 21603b8 commit 6b22620
Showing 1 changed file with 94 additions and 104 deletions.
198 changes: 94 additions & 104 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React from 'react';
import React, {
memo,
useEffect,
useState,
useRef,
RefObject,
useCallback,
} from 'react';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';
import {findFirstFocusableNode} from '@shopify/javascript-utilities/focus';

import {PreferredPosition} from '../PositionedOverlay';
import {Portal} from '../Portal';
import {useUniqueId} from '../../utilities/unique-id';
import {useFocused} from '../../utilities/use-focused';
import {TooltipOverlay} from './components';
import styles from './Tooltip.scss';

Expand All @@ -28,122 +37,103 @@ export interface TooltipProps {
activatorWrapper?: string;
}

interface State {
active: boolean;
activatorNode: HTMLElement | null;
}

const getUniqueID = createUniqueIDFactory('TooltipContent');

export class Tooltip extends React.PureComponent<TooltipProps, State> {
state: State = {
active: Boolean(this.props.active),
activatorNode: null,
};
export function Tooltip({
children,
content,
light,
active: originalActive,
preferredPosition = 'below',
activatorWrapper = 'span',
}: TooltipProps) {
const WrapperComponent: any = activatorWrapper;
const [active, {handleFocus, handleBlur}] = useFocused(
Boolean(originalActive),
);
const [activatorNode, setActivatorNode] = useState<HTMLElement | null>(null);

const id = useUniqueId('TooltipContent');
const activatorContainer = useRef<HTMLElement>(null);
const mouseEntered = useRef(false);

// Check this impl
const setAccessibilityAttributes = useCallback(() => {
if (activatorContainer == null) {
return;
}

private id = getUniqueID();
private activatorContainer: HTMLElement | null;
private mouseEntered = false;
const firstFocusable = activatorContainer.current
? findFirstFocusableNode(activatorContainer.current)
: null;
const accessibilityNode = firstFocusable || activatorContainer.current;

componentDidMount() {
this.setAccessibilityAttributes();
}
if (!accessibilityNode) return;

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;

const portal = activatorNode ? (
<Portal idPrefix="tooltip">
<TooltipOverlay
id={id}
preferredPosition={preferredPosition}
activator={activatorNode}
active={active}
onClose={noop}
light={light}
>
<div className={styles.Label} testID="TooltipOverlayLabel">
{content}
</div>
</TooltipOverlay>
</Portal>
) : null;

return (
<WrapperComponent
testID="WrapperComponent"
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onMouseLeave={this.handleMouseLeave}
onMouseOver={this.handleMouseEnterFix}
ref={this.setActivator}
accessibilityNode.tabIndex = 0;
accessibilityNode.setAttribute('aria-describedby', id);
}, [id]);

useEffect(() => {
setAccessibilityAttributes();
}, [setAccessibilityAttributes]);

const portal = activatorNode ? (
<Portal idPrefix="tooltip">
<TooltipOverlay
id={id}
preferredPosition={preferredPosition}
activator={activatorNode}
active={active}
onClose={noop}
light={light}
>
{children}
{portal}
</WrapperComponent>
);
}

private setActivator = (node: HTMLElement | null) => {
<div className={styles.Label} testID="TooltipOverlayLabel">
{content}
</div>
</TooltipOverlay>
</Portal>
) : null;

return (
<WrapperComponent
testID="WrapperComponent"
onFocus={handleFocus}
onBlur={handleBlur}
onMouseLeave={handleMouseLeave}
onMouseOver={handleMouseEnterFix}
ref={setActivator}
>
{children}
{portal}
</WrapperComponent>
);

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});
};
// maybe add pre focus and pre blur????
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();
}
}

Expand Down

0 comments on commit 6b22620

Please sign in to comment.