client/components/tooltip/index.tsx

import './index.scss';
import React from 'react';

import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

interface TooltipButtonProps {
  icon: IconDefinition
  tooltip?: string
  onClick?: VoidFunction
  className?: string
}

/**
 * How many pixels a label MUST be off the edge of the screen.
 * this class guarantees it never goes beyond this limit.
 */
const CORNER_OFFSET = 6;

function goesBeforePage(el: HTMLElement, parentBounds: DOMRect, offset: number) {
  return parentBounds.x + offset < CORNER_OFFSET
}

function goesAfterPage(el: HTMLElement, parentBounds: DOMRect, _: number) { // eslint-disable-line @typescript-eslint/no-unused-vars
  return parentBounds.x + el.offsetWidth - document.documentElement.offsetWidth > CORNER_OFFSET
}

/**
 * A component to render a clickable button to the DOM with an
 * optional tooltip that's shown when the button is hovered.
 */
export default function TooltipButton({tooltip, icon, className, ...props}: TooltipButtonProps) {
  const [offsets, setOffsets] = React.useState([0, null]);
  const labelStyle = {left: `${offsets[0]}px`}
  if (offsets[1]) {
    labelStyle['--pointer-offset'] = offsets[1];
  }

  // Adjust the position of the tooltip label such that it stays in the bounds
  // of the page.
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (ref.current) {
      const elem = ref.current as unknown as HTMLElement;
      const button = elem.parentElement as HTMLElement;
      const bounds = button.getBoundingClientRect();

      let offset = -(elem.offsetWidth - button.offsetWidth) / 2;
      let pointerOffset: any = null // eslint-disable-line @typescript-eslint/no-explicit-any
      if (goesBeforePage(elem, bounds, offset)) {
        offset = CORNER_OFFSET - bounds.x;
        pointerOffset = bounds.x + (button.offsetWidth / 2)
      } else if (goesAfterPage(elem, bounds, offset)) {
        offset = -elem.offsetWidth + bounds.width - CORNER_OFFSET
        // NOTE for some reason, right hand side works with pixels, left
        // with just units :?
        pointerOffset = `${elem.offsetWidth - (button.offsetWidth / 2)}px`
      }
      setOffsets([offset, pointerOffset]);
    }
  }, [ref.current])

  const onKeyPress = (e: React.KeyboardEvent) => {
    if (props.onClick && e.key === 'Enter') {
      props.onClick()
    }
  }

  const onClick = (e: React.MouseEvent) => {
    if (props.onClick) {
      props.onClick()
    }
    // don't keep focus on clicked tooltip
    // WARN for some reason... blur() isn't a method on currentTarget
    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    const target = e.currentTarget as any
    target.blur()
  }

  return (
    <div className={["tooltip", className].join(' ')} role="button"
         style={{cursor: (props.onClick) ? "pointer" : undefined}}
         onClick={onClick} tabIndex={0}
         onKeyPress={onKeyPress} >
      <FontAwesomeIcon {...props} icon={icon} className='icon' />
      {tooltip &&
        <span ref={ref} style={labelStyle} className="label">{tooltip}</span>}
    </div>
  )
}