"use strict";

exports.__esModule = true;
exports.TooltipPopup = undefined;

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

exports.useTooltip = useTooltip;
exports.default = Tooltip;

var _react = require("react");

var _react2 = _interopRequireDefault(_react);

var _autoId = require("@reach/auto-id");

var _utils = require("@reach/utils");

var _portal = require("@reach/portal");

var _portal2 = _interopRequireDefault(_portal);

var _visuallyHidden = require("@reach/visually-hidden");

var _visuallyHidden2 = _interopRequireDefault(_visuallyHidden);

var _rect = require("@reach/rect");

var _propTypes = require("prop-types");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } ////////////////////////////////////////////////////////////////////////////////
// Welcome to @reach/tooltip!
//
// Quick definitions:
//
// - "on rest" or "rested on": describes when the element receives keyboard
//   focus or mouse hover after a short delay (and hopefully soon, touch
//   longpress).
//
// - "activation": describes a mouse click, keyboard enter, or keyboard space.
//
// Only one tooltip can be visible at a time, so we use a global state chart to
// describe the various states and transitions between states that are
// possible.  With the all the timeouts involved with tooltips it's important to
// "make impossible states impossible" with a state machine.
//
// It's also okay to use these module globals because you don't server render
// tooltips. None of the state is changed outside of user events.
//
// There are a few features that are important to understand.
//
// 1. Tooltips don't show up until the user has rested on one, we don't
//    want tooltips popupping up as you move your mouse around the page.
//
// 2. Once any tooltip becomes visible, other tooltips nearby should skip
//    resting and display immediately.
//
// 3. Tooltips stick around for a little bit after blur/mouseleave.

/* eslint-disable default-case */

////////////////////////////////////////////////////////////////////////////////
var chart = {
  initial: "idle",
  states: {
    idle: {
      enter: clearContextId,
      on: {
        mouseenter: "focused",
        focus: "focused"
      }
    },
    focused: {
      enter: startRestTimer,
      leave: clearRestTimer,
      on: {
        mousemove: "focused",
        mouseleave: "idle",
        mousedown: "dismissed",
        blur: "idle",
        rest: "visible"
      }
    },
    visible: {
      on: {
        mouseleave: "leavingVisible",
        blur: "leavingVisible",
        mousedown: "dismissed",
        selectWithKeyboard: "dismissed"
      }
    },
    leavingVisible: {
      enter: startLeavingVisibleTimer,
      leave: function leave() {
        clearLeavingVisibleTimer();
        clearContextId();
      },
      on: {
        mouseenter: "visible",
        focus: "visible",
        timecomplete: "idle"
      }
    },
    dismissed: {
      leave: function leave() {
        // allows us to come on back later w/o entering something else first
        context.id = null;
      },
      on: {
        mouseleave: "idle",
        blur: "idle"
      }
    }
  }
};

// chart context allows us to persist some data around, in Tooltip all we use
// is the id of the current tooltip being interacted with.
var context = { id: null };
var state = chart.initial;

////////////////////////////////////////////////////////////////////////////////
// Finds the next state from the current state + action. If the chart doesn't
// describe that transition, it will throw.
//
// It also manages lifecycles of the machine, (enter/leave hooks on the state
// chart)
function transition(action, newContext) {
  var stateDef = chart.states[state];
  var nextState = stateDef.on[action];

  if (!nextState) {
    throw new Error("Unknown state for action \"" + action + "\" from state \"" + state + "\"");
  }

  if (stateDef.leave) {
    stateDef.leave();
  }

  if (newContext) {
    context = newContext;
  }

  var nextDef = chart.states[nextState];
  if (nextDef.enter) {
    nextDef.enter();
  }

  state = nextState;
  notify();
}

////////////////////////////////////////////////////////////////////////////////
// Subscriptions:
//
// We could require apps to render a <TooltipProvider> around the app and use
// React context to notify Tooltips of changes to our state machine, instead
// we manage subscriptions ourselves and simplify the Tooltip API.
//
// Maybe if default context could take a hook (instead of just a static value)
// that was rendered at the root for us, that'd be cool! But it doesn't.
var subscriptions = [];

function subscribe(fn) {
  subscriptions.push(fn);
  return function () {
    subscriptions.splice(subscriptions.indexOf(fn), 1);
  };
}

function notify() {
  subscriptions.forEach(function (fn) {
    return fn(state, context);
  });
}

////////////////////////////////////////////////////////////////////////////////
// Timeouts:

// Manages when the user "rests" on an element. Keeps the interface from being
// flashing tooltips all the time as the user moves the mouse around the screen.
var restTimeout = void 0;
function startRestTimer() {
  clearTimeout(restTimeout);
  restTimeout = setTimeout(function () {
    return transition("rest");
  }, 100);
}

function clearRestTimer() {
  clearTimeout(restTimeout);
}

// Manages the delay to hide the tooltip after rest leaves.
var leavingVisibleTimer = void 0;
function startLeavingVisibleTimer() {
  clearTimeout(leavingVisibleTimer);
  leavingVisibleTimer = setTimeout(function () {
    return transition("timecomplete");
  }, 500);
}

function clearLeavingVisibleTimer() {
  clearTimeout(leavingVisibleTimer);
}

////////////////////////////////////////////////////////////////////////////////
// Rando helpers

// allows us to come on back later w/o entering something else first after the
// user leaves or dismisses
function clearContextId() {
  context.id = null;
}

// Avoids focus/mouseEnter from fighting with each other since you can have
// one element focused and another mouseentered at the same time. The first
// one sets the context.id and then the second one is ignored (for a single
// element, most recent focus/mouseenter wins between elements).
function isActive(id) {
  return context.id === id;
}

////////////////////////////////////////////////////////////////////////////////
// THE HOOK!
function useTooltip() {
  var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
      onMouseEnter = _ref.onMouseEnter,
      onMouseMove = _ref.onMouseMove,
      onMouseLeave = _ref.onMouseLeave,
      onFocus = _ref.onFocus,
      onBlur = _ref.onBlur,
      onKeyDown = _ref.onKeyDown,
      onMouseDown = _ref.onMouseDown,
      ref = _ref.ref;

  var _useState = (0, _react.useState)(state === "visible"),
      isVisible = _useState[0],
      setIsVisible = _useState[1];

  // hopefully they always pass a ref if they ever pass one


  var triggerRef = ref || (0, _react.useRef)();
  var triggerRect = (0, _rect.useRect)(triggerRef, isVisible);
  var id = "tooltip:" + (0, _autoId.useId)();

  (0, _react.useEffect)(function () {
    return subscribe(function () {
      if (context.id === id && (state === "visible" || state === "leavingVisible")) {
        setIsVisible(true);
      } else {
        setIsVisible(false);
      }
    });
  }, [id]);

  var handleMouseEnter = function handleMouseEnter() {
    if (isActive(id) &&
    // enables leaving then entering again before the times up
    state !== "leavingVisible") {
      return;
    }

    switch (state) {
      case "idle":
      case "leavingVisible":
        {
          transition("mouseenter", { id: id });
        }
    }
  };

  var handleMouseMove = function handleMouseMove() {
    switch (state) {
      case "focused":
        {
          transition("mousemove", { id: id });
        }
    }
  };

  var handleFocus = function handleFocus() {
    if (isActive(id)) return;
    switch (state) {
      case "idle":
      case "leavingVisible":
        {
          transition("focus", { id: id });
        }
    }
  };

  var handleMouseLeave = function handleMouseLeave() {
    switch (state) {
      case "focused":
      case "visible":
      case "dismissed":
        {
          transition("mouseleave");
        }
    }
  };

  var handleBlur = function handleBlur() {
    // Allow quick click from one tool to another
    if (context.id !== id) return;
    switch (state) {
      case "focused":
      case "visible":
      case "dismissed":
        {
          transition("blur");
        }
    }
  };

  var handleMouseDown = function handleMouseDown() {
    // Allow quick click from one tool to another
    if (context.id !== id) return;
    switch (state) {
      case "focused":
      case "visible":
        {
          transition("mousedown");
        }
    }
  };

  var handleKeyDown = function handleKeyDown(event) {
    if (event.key === "Enter" || event.key === " ") {
      switch (state) {
        case "visible":
          {
            transition("selectWithKeyboard");
          }
      }
    }
  };

  var trigger = {
    "aria-labelledby": id,
    "data-reach-tooltip-trigger": "",
    ref: triggerRef,
    onMouseEnter: (0, _utils.wrapEvent)(onMouseEnter, handleMouseEnter),
    onMouseMove: (0, _utils.wrapEvent)(onMouseMove, handleMouseMove),
    onFocus: (0, _utils.wrapEvent)(onFocus, handleFocus),
    onBlur: (0, _utils.wrapEvent)(onFocus, handleBlur),
    onMouseLeave: (0, _utils.wrapEvent)(onMouseLeave, handleMouseLeave),
    onKeyDown: (0, _utils.wrapEvent)(onKeyDown, handleKeyDown),
    onMouseDown: (0, _utils.wrapEvent)(onMouseDown, handleMouseDown)
  };

  var tooltip = {
    id: id,
    triggerRect: triggerRect,
    isVisible: isVisible
  };

  return [trigger, tooltip, isVisible];
}

////////////////////////////////////////////////////////////////////////////////
function Tooltip(_ref2) {
  var children = _ref2.children,
      label = _ref2.label,
      ariaLabel = _ref2.ariaLabel,
      rest = _objectWithoutProperties(_ref2, ["children", "label", "ariaLabel"]);

  var _useTooltip = useTooltip(),
      trigger = _useTooltip[0],
      tooltip = _useTooltip[1];

  return _react2.default.createElement(
    _react.Fragment,
    null,
    (0, _react.cloneElement)(_react.Children.only(children), trigger),
    _react2.default.createElement(TooltipPopup, _extends({
      label: label,
      ariaLabel: ariaLabel
    }, tooltip, rest))
  );
}

process.env.NODE_ENV !== "production" ? Tooltip.propTypes = {
  children: _propTypes.node.isRequired,
  label: _propTypes.node.isRequired,
  ariaLabel: _propTypes.string
} : void 0;

////////////////////////////////////////////////////////////////////////////////
var TooltipPopup = (0, _react.forwardRef)(function TooltipPopup(_ref3, forwardRef) {
  var label = _ref3.label,
      ariaLabel = _ref3.ariaLabel,
      position = _ref3.position,
      isVisible = _ref3.isVisible,
      id = _ref3.id,
      triggerRect = _ref3.triggerRect,
      rest = _objectWithoutProperties(_ref3, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect"]);

  return isVisible ? _react2.default.createElement(
    _portal2.default,
    null,
    _react2.default.createElement(TooltipContent, _extends({
      label: label,
      ariaLabel: ariaLabel,
      position: position,
      isVisible: isVisible,
      id: id,
      triggerRect: triggerRect,
      ref: forwardRef
    }, rest))
  ) : null;
});

exports.TooltipPopup = TooltipPopup;
process.env.NODE_ENV !== "production" ? TooltipPopup.propTypes = {
  label: _propTypes.node.isRequired,
  ariaLabel: _propTypes.string,
  position: _propTypes.func
} : void 0;

// Need a separate component so that useRect works inside the portal
var TooltipContent = (0, _react.forwardRef)(function TooltipContent(_ref4, forwardRef) {
  var label = _ref4.label,
      ariaLabel = _ref4.ariaLabel,
      _ref4$position = _ref4.position,
      position = _ref4$position === undefined ? positionDefault : _ref4$position,
      isVisible = _ref4.isVisible,
      id = _ref4.id,
      triggerRect = _ref4.triggerRect,
      style = _ref4.style,
      rest = _objectWithoutProperties(_ref4, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect", "style"]);

  var useAriaLabel = ariaLabel != null;
  var tooltipRef = (0, _react.useRef)();
  var tooltipRect = (0, _rect.useRect)(tooltipRef, isVisible);
  return _react2.default.createElement(
    _react.Fragment,
    null,
    _react2.default.createElement("div", _extends({
      "data-reach-tooltip": true,
      role: useAriaLabel ? undefined : "tooltip",
      id: useAriaLabel ? undefined : id,
      children: label,
      style: _extends({}, style, getStyles(position, triggerRect, tooltipRect)),
      ref: function ref(node) {
        tooltipRef.current = node;
        if (forwardRef) forwardRef(node);
      }
    }, rest)),
    useAriaLabel && _react2.default.createElement(
      _visuallyHidden2.default,
      { role: "tooltip", id: id },
      ariaLabel
    )
  );
});

////////////////////////////////////////////////////////////////////////////////
// TODO: research longpress tooltips on Android, iOS, implement here.
// - Probably want to position it by default above, since your thumb
//   is below and would cover it
// - I'm thinking after longpress, display the tooltip and cancel any
//   click events. Then on touchend, hide the tooltip after a timer
//   in case their hand is obstructing it, then can remove and read
//   the tooltip

// feels awkward when it's perfectly aligned w/ the trigger
var OFFSET = 8;

var getStyles = function getStyles(position, triggerRect, tooltipRect) {
  var haventMeasuredTooltipYet = !tooltipRect;
  if (haventMeasuredTooltipYet) {
    return { visibility: "hidden" };
  }
  return position(triggerRect, tooltipRect);
};

var positionDefault = function positionDefault(triggerRect, tooltipRect) {
  var styles = {
    left: triggerRect.left + window.scrollX + "px",
    top: triggerRect.top + triggerRect.height + window.scrollY + "px"
  };

  var collisions = {
    top: triggerRect.top - tooltipRect.height < 0,
    right: window.innerWidth < triggerRect.left + tooltipRect.width,
    bottom: window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET,
    left: triggerRect.left - tooltipRect.width < 0
  };

  var directionRight = collisions.right && !collisions.left;
  var directionUp = collisions.bottom && !collisions.top;

  return _extends({}, styles, {
    left: directionRight ? triggerRect.right + OFFSET / 2 - tooltipRect.width + window.scrollX + "px" : triggerRect.left - OFFSET / 2 + window.scrollX + "px",
    top: directionUp ? triggerRect.top - OFFSET - tooltipRect.height + window.scrollY + "px" : triggerRect.top + OFFSET + triggerRect.height + window.scrollY + "px"
  });
};