src/components/HelpText/index.js
import React, { useRef, useState, useEffect, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { useWindowScrolling } from '@rainbow-modules/hooks';
import { useUniqueIdentifier, useDisclosure, useWindowResize } from '../../libs/hooks';
import InternalOverlay from '../InternalOverlay';
import RenderIf from '../RenderIf';
import AssistiveText from '../AssistiveText';
import { ESCAPE_KEY } from '../../libs/constants';
import {
StyledTooltip,
StyledTitle,
StyledIconContainer,
StyledText,
StyledButton,
} from './styled';
import {
ErrorIcon,
InfoIcon,
QuestionIcon,
WarningIcon,
ErrorInverseIcon,
InfoInverseIcon,
QuestionInverseIcon,
WarningInverseIcon,
} from './icons';
const iconMap = {
question: QuestionIcon,
info: InfoIcon,
error: ErrorIcon,
warning: WarningIcon,
};
const inverseIconMap = {
question: <QuestionInverseIcon />,
info: <InfoInverseIcon />,
error: <ErrorInverseIcon />,
warning: <WarningInverseIcon />,
};
/**
* HelpText is a popup that displays information related to an element.
*/
const HelpText = React.forwardRef((props, ref) => {
const { id, title, text, variant, tabIndex, iconSize, className, style } = props;
const triggerRef = useRef();
const helpTextId = useUniqueIdentifier('help-text');
const [isFocused, setIsFocused] = useState(false);
const isHoverTooltip = useRef(false);
const isClickTooltip = useRef(false);
const { isOpen, open: openOverlay, close: closeOverlay } = useDisclosure(false);
useImperativeHandle(ref, () => ({
close: closeOverlay,
}));
useEffect(() => {
if (isFocused) {
openOverlay();
} else {
closeOverlay();
}
}, [closeOverlay, isFocused, openOverlay]);
useWindowResize(() => closeOverlay(), isOpen);
useWindowScrolling(closeOverlay, isOpen);
const handleBlur = () => {
if (!isClickTooltip.current) {
setIsFocused(false);
}
};
const handleButtonMouseLeave = () => {
if (!isFocused) {
setTimeout(() => {
if (!isHoverTooltip.current) closeOverlay();
}, 50);
}
};
const handleTooltipMouseDown = () => {
isClickTooltip.current = true;
};
const handleTooltipMouseUp = () => {
isClickTooltip.current = false;
setTimeout(() => triggerRef.current.focus());
};
const handleTooltipMouseEnter = () => {
isHoverTooltip.current = true;
};
const handleTooltipMouseLeave = () => {
isHoverTooltip.current = false;
if (!isFocused) {
closeOverlay();
}
};
const handleKeyPressed = event => {
if (event.keyCode === ESCAPE_KEY) {
event.preventDefault();
closeOverlay();
}
};
const Icon = iconMap[variant] || iconMap.info;
const inverseIcon = inverseIconMap[variant] || inverseIconMap.info;
return (
<>
<StyledButton
id={id}
className={className}
style={style}
ref={triggerRef}
onMouseEnter={openOverlay}
onMouseLeave={handleButtonMouseLeave}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
onKeyDown={handleKeyPressed}
type="button"
tabIndex={tabIndex}
ariaLabelledby={helpTextId}
variant={variant}
iconSize={iconSize}
>
<Icon isFocused={isFocused} iconSize={iconSize} />
<AssistiveText text={variant} />
</StyledButton>
<RenderIf isTrue={text}>
<InternalOverlay
isVisible={isOpen}
render={() => {
return (
<StyledTooltip
id={helpTextId}
role="tooltip"
onMouseDown={handleTooltipMouseDown}
onMouseUp={handleTooltipMouseUp}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
>
<RenderIf isTrue={title}>
<StyledTitle variant={variant}>
<StyledIconContainer>{inverseIcon}</StyledIconContainer>
{title}
</StyledTitle>
</RenderIf>
<StyledText>{text}</StyledText>
</StyledTooltip>
);
}}
triggerElementRef={triggerRef}
/>
</RenderIf>
</>
);
});
HelpText.propTypes = {
/** The id of the outer element. */
id: PropTypes.string,
/** A CSS class for the outer element, in addition to the component's base classes. */
className: PropTypes.string,
/** An object with custom style applied to the outer element. */
style: PropTypes.object,
/** Displayed the title of component. */
title: PropTypes.string,
/** Displayed the help message */
text: PropTypes.node,
/** The variant changes the appearance of the button. Accepted variants include question, info, error and warning */
variant: PropTypes.oneOf(['question', 'info', 'error', 'warning']),
/** Specifies the tab order of an element (when the tab button is used for navigating). */
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** The size of the icon */
iconSize: PropTypes.oneOf(['small', 'medium']),
};
HelpText.defaultProps = {
id: undefined,
className: undefined,
style: undefined,
title: undefined,
text: undefined,
variant: 'info',
tabIndex: undefined,
iconSize: 'medium',
};
export default HelpText;