Skip to content

Commit

Permalink
Merge pull request #53 from fedspendingtransparency/bug/dev-2698/show…
Browse files Browse the repository at this point in the history
…-tooltip-on-hover

[DEV-2698] Show Tooltip on Hover by Default & Display on Mobile w/ Bottom Positioning
  • Loading branch information
jonhill13 authored Sep 22, 2020
2 parents b616102 + c951563 commit efe3801
Show file tree
Hide file tree
Showing 17 changed files with 196 additions and 134 deletions.
242 changes: 148 additions & 94 deletions components/infoTooltip/TooltipWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

require('../../styles/components/infoTooltip/_tooltipWrapper.scss');

const horizontalPadding = 5;
const baseTooltipWidth = 375;

const propTypes = {
className: PropTypes.string,
children: PropTypes.element,
Expand Down Expand Up @@ -48,18 +51,15 @@ const defaultProps = {
closeTooltip: () => {},
isVisible: false
},
width: baseTooltipWidth,
offsetAdjustments: {
top: -15, // InfoToolTip offset
right: 30, // InfoToolTip offset
top: -15, // So that the arrow points at the middle of the tooltip trigger area 👌
right: 0, // InfoToolTip offset
left: 0
},
styles: {}
};

const horizontalPadding = 20;

const baseTooltipWidth = 375;

const tooltipIcons = {
info: <FontAwesomeIcon className="tooltip__icon" icon="info-circle" />
};
Expand All @@ -69,142 +69,197 @@ export default class TooltipWrapper extends React.Component {

this.state = {
showTooltip: false,
isHoveringOnTooltip: false,
offsetTop: 0,
arrowStyles: {},
arrowDirection: 'left',
spacerStyles: {}
};

this.showTooltip = this.showTooltip.bind(this);
this.closeTooltip = this.closeTooltip.bind(this);
this.measureOffset = throttle(this.measureOffset.bind(this), 16);
this.setTooltipDimensions = throttle(this.setTooltipDimensions.bind(this), 16);
}

componentDidMount() {
this.measureOffset();
if (this.props.tooltipPosition === 'bottom') {
this.positionPointerTop();
}
window.addEventListener("scroll", throttle(this.measureOffset, 500));
window.addEventListener("resize", throttle(this.measureOffset, 100));
this.setTooltipDimensions();
window.addEventListener("scroll", throttle(this.setTooltipDimensions, 500));
window.addEventListener("resize", throttle(this.setTooltipDimensions, 100));
}

componentDidUpdate(prevProps) {
if (
prevProps.wide !== this.props.wide ||
prevProps.tooltipPosition !== this.props.tooltipPosition
) {
this.measureOffset();
}
if (this.props.tooltipPosition === 'bottom' && prevProps.tooltipPosition !== 'bottom') {
this.positionPointerTop();
this.setTooltipDimensions();
}
}

componentWillUnmount() {
window.removeEventListener("scroll", this.measureOffset);
window.removeEventListener("resize", this.measureOffset);
window.removeEventListener("scroll", this.setTooltipDimensions);
window.removeEventListener("resize", this.setTooltipDimensions);
}

onMouseMoveTooltip = () => {
const { onMouseMoveTooltip } = this.props;
if (onMouseMoveTooltip) onMouseMoveTooltip();
}
onMouseLeaveTooltip = () => {
const { onMouseLeaveTooltip } = this.props;
if (onMouseLeaveTooltip) onMouseLeaveTooltip();
}

showTooltip() {
if (!this.props.controlledProps.isControlled) {
this.setState({
showTooltip: true
});
if (this.props.onMouseMoveTooltip) {
this.props.onMouseMoveTooltip();
}
else {
else if (this.props.controlledProps.isControlled) {
this.props.controlledProps.showTooltip();
}
else if (!this.state.isHoveringOnTooltip) {
this.setState({ isHoveringOnTooltip: true });
}
}

closeTooltip() {
if (!this.props.controlledProps.isControlled) {
this.setState({
showTooltip: false
});
onMouseLeaveTooltip = () => {
if (this.props.onMouseLeaveTooltip) {
this.props.onMouseLeaveTooltip();
}
else {
this.props.controlledProps.closeTooltip();
else if (this.state.isHoveringOnTooltip) {
this.setState({ isHoveringOnTooltip: false });
}
}

measureOffset() {
let tooltipWidth = baseTooltipWidth;
let spacerStyle = {};
if (!this.tooltipContainer) return;
const tooltipContainer = this.tooltipContainer;
const ttContainerWidth = tooltipContainer.clientWidth;

const offsetTop = tooltipContainer.offsetTop + this.props.offsetAdjustments.top;
getAvailableHorizontalSpace() {
const totalSpace = window.innerWidth;
const spaceToRight = (totalSpace - tooltipContainer.offsetLeft) - ttContainerWidth;
const spaceToLeft = tooltipContainer.offsetLeft;

const {
offsetLeft: tooltipContainerLeftPosition,
clientWidth: tooltipContainerWidth
} = this.tooltipContainer;
const spaceToRight = (totalSpace - tooltipContainerLeftPosition) - tooltipContainerWidth;
const spaceToLeft = tooltipContainerLeftPosition;
return { right: spaceToRight, left: spaceToLeft, total: totalSpace };
}

if (this.props.wide && this.props.tooltipPosition === 'left') {
tooltipWidth = (spaceToLeft > 800)
getTooltipWidth = () => {
const { right: spaceToRight, left: spaceToLeft, total } = this.getAvailableHorizontalSpace();
if (total < 425) {
// mobile tooltip stylez
return total - (horizontalPadding * 2);
}
else if (this.props.wide && this.props.tooltipPosition === 'left') {
return (spaceToLeft > 800)
? 700
: spaceToLeft - 100;
: spaceToLeft - horizontalPadding;
}
else if (this.props.tooltipPosition === 'bottom') {
return this.props.width;
}
else if (this.props.wide) {
tooltipWidth = (spaceToRight > 800)
return (spaceToRight > 800)
? 700
: spaceToRight - 100;
: spaceToRight - horizontalPadding;
}
return this.props.width;
};

if (this.props.tooltipPosition === 'left') {
const startingPositionLeft = spaceToLeft - tooltipWidth; // minus tooltipWidth b/c right corner of toolTip is flush w/ left edge of toolTip container
spacerStyle = {
top: offsetTop,
left: startingPositionLeft - horizontalPadding,
width: tooltipWidth
getDimensionsForMobile = (isMobile, width) => {
if (isMobile) {
// 8px being 1/2 the height of the arrow.
const top = `${this.tooltipContainer.clientHeight + this.tooltipContainer.offsetTop + 8}px`;
return {
top,
width,
// 1/2 the width of the arrow
left: `${(this.tooltipContainer.clientWidth / 2) - 8}px`
};
}
else {
const startingPositionLeft = spaceToLeft + ttContainerWidth; // plus ttContainerWidth b/c left corner of toolTip is flush w/ right edge of toolTip container
spacerStyle = {
top: offsetTop,
left: startingPositionLeft + horizontalPadding,
width: tooltipWidth
};
return {
...this.state.spacerStyles,
width
};
}

setTooltipDimensions() {
const shouldOverridePositioning = Object.keys(this.props.styles).includes('transform');
if (shouldOverridePositioning && this.tooltipContainer) {
if (this.props.tooltipPosition === 'bottom') {
// make sure we set the arrow styles if the positioning is being overridden.
this.setState({
arrowDirection: 'bottom',
spacerStyle: {
width: this.getTooltipWidth()
}
});
}
else {
// position is being overridden
this.setState({
spacerStyle: {
width: this.getTooltipWidth()
}
});
}
}
/**
* Given a user wants to override the default positioning,
* do not use top
*/
if (this.props?.styles?.transform) {
delete spacerStyle.top;
delete spacerStyle.left;
else if (this.tooltipContainer) {
const tooltipWidth = this.getTooltipWidth();

const { left: spaceToLeft, total } = this.getAvailableHorizontalSpace();
const offsetTop = this.tooltipContainer.offsetTop + this.props.offsetAdjustments.top;
const isMobile = total < 700;
if (this.props.tooltipPosition === 'bottom' || isMobile) {
this.setState({
arrowDirection: 'bottom',
spacerStyle: {
...this.getDimensionsForMobile(isMobile, tooltipWidth)
}
});
}
else if (this.props.tooltipPosition === 'left') {
const startingPositionLeft = spaceToLeft - tooltipWidth; // minus tooltipWidth b/c right corner of toolTip is flush w/ left edge of toolTip container
this.setState({
arrowDirection: 'right',
spacerStyle: {
top: offsetTop,
left: startingPositionLeft - horizontalPadding,
width: tooltipWidth
}
});
}
else {
const startingPositionLeft = spaceToLeft + this.tooltipContainer.clientWidth; // plus ttContainerWidth b/c left corner of toolTip is flush w/ right edge of toolTip container
this.setState({
arrowDirection: 'left',
spacerStyle: {
top: offsetTop,
left: startingPositionLeft + horizontalPadding,
width: tooltipWidth
}
});
}
}
this.setState({ spacerStyle });
}

positionPointerTop = () => {
if (this.tooltipReference) {
const arrowStyles = {
top: '-0.79rem', // half the height of the arrow
left: `${((this.props.width || baseTooltipWidth) / 2) + 8}px`, // add 8 for half the pointer
transform: 'rotate(90deg)'
};
const spacerStyles = {
width: `${this.props.width || baseTooltipWidth}px`
};
this.setState({ arrowStyles, spacerStyles });

showTooltip() {
if (this.props.controlledProps.isControlled) {
this.props.controlledProps.showTooltip();
}
else if (!this.state.showTooltip) {
this.setState({
showTooltip: true
});
}
}

arrowClassName = () => (this.props.tooltipPosition === 'left' ? 'right' : '');
closeTooltip() {
if (this.props.controlledProps.isControlled) {
this.props.controlledProps.closeTooltip();
}
else if (this.state.showTooltip) {
this.setState({
showTooltip: false
});
}
}

render() {
const showTooltip = (this.props.controlledProps.isControlled) ? this.props.controlledProps.isVisible : this.state.showTooltip;
const showTooltip = (
(this.props.controlledProps.isControlled && this.props.controlledProps.isVisible) ||
this.state.showTooltip ||
this.state.isHoveringOnTooltip
);
let tooltip = null;

if (showTooltip) {
Expand All @@ -214,16 +269,15 @@ export default class TooltipWrapper extends React.Component {
className="tooltip"
id="tooltip"
role="tooltip"
onMouseEnter={this.onMouseMoveTooltip}
onMouseMove={this.onMouseMoveTooltip}
onMouseLeave={this.onMouseLeaveTooltip}
ref={(div) => {
this.tooltipReference = div;
}}>
<div
className="tooltip__interior">
<div
className={`tooltip-pointer ${this.arrowClassName()}`}
style={this.state.arrowStyles} />
<div className={`tooltip-pointer ${this.state.arrowDirection}`} />
<div className="tooltip__content">
<div className="tooltip__message">
{this.props.tooltipComponent}
Expand Down
Loading

0 comments on commit efe3801

Please sign in to comment.