'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

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; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _objectAssign = require('object-assign');

var _objectAssign2 = _interopRequireDefault(_objectAssign);

var _propTypes = require('./propTypes');

var _compareProps = require('./compareProps');

var _compareProps2 = _interopRequireDefault(_compareProps);

var _mergeAndExtractProps2 = require('./mergeAndExtractProps');

var _mergeAndExtractProps3 = _interopRequireDefault(_mergeAndExtractProps2);

var _extractStyle = require('./extractStyle');

var _recursiveNodeCheck = require('./recursiveNodeCheck');

var _recursiveNodeCheck2 = _interopRequireDefault(_recursiveNodeCheck);

var _inputTracker = require('./inputTracker');

var _inputTracker2 = _interopRequireDefault(_inputTracker);

var _notifier = require('./notifier');

var _syntheticClick = require('./syntheticClick');

var _syntheticClick2 = _interopRequireDefault(_syntheticClick);

var _constants = require('./constants');

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

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Interactive = function (_React$Component) {
  _inherits(Interactive, _React$Component);

  function Interactive(props) {
    _classCallCheck(this, Interactive);

    // state is always an object with two keys, `iState` and `focus`
    var _this = _possibleConstructorReturn(this, (Interactive.__proto__ || Object.getPrototypeOf(Interactive)).call(this, props));

    _this.refCallback = function (node) {
      _this.refNode = node;
      if (node) {
        var prevTopNode = _this.topNode;
        // if `as` is a component, then the `refNode` is the span wrapper, so get its firstChild
        if (typeof _this.p.props.as !== 'string') _this.topNode = node.firstChild;else _this.topNode = node;
        _this.tagName = _this.topNode.tagName.toLowerCase();
        _this.type = _this.topNode.type && _this.topNode.type.toLowerCase();
        _this.enterKeyTrigger = (0, _constants.enterKeyTrigger)(_this.tagName, _this.type);
        _this.spaceKeyTrigger = (0, _constants.spaceKeyTrigger)(_this.tagName, _this.type);
        // if as is a react component then won't have access to tag in componentWillReceiveProps,
        // so check if click listener needs to be set again here (after this.tagName is set)
        if (_this.setClickListener(_this.p.props)) _this.p.passThroughProps.onClick = _this.handleEvent;
        // if node is a new node then call manageFocus to keep browser in sync with RI,
        // note: above assignments can't be in this if statement b/c node could have mutated,
        // node should maintain focus state when mutated
        if (prevTopNode !== _this.topNode) {
          _this.manageFocus('refCallback');
          // if refDOMNode prop, pass along new DOM node
          _this.p.props.refDOMNode && _this.p.props.refDOMNode(_this.topNode);
        }
      }
    };

    _this.handleEvent = function (e) {
      if (!_this.isValidEvent(e)) return;

      if (_constants.mouseEvents[e.type]) {
        if (_this.handleMouseEvent(e) === 'terminate') return;
      } else if (_constants.touchEvents[e.type] || e.type === 'touchmove' || e.type === 'touchtapcancel') {
        if (_this.handleTouchEvent(e) === 'terminate') return;
      } else if (e.type === 'click') {
        if (_this.handleClickEvent(e) === 'terminate') return;
      } else if (_this.handleOtherEvent(e) === 'terminate') return;

      // compute the new state object and pass it as an argument to updateState,
      // which calls setState and state change callbacks if needed
      _this.updateState(_this.computeState(), _this.p.props, e);
    };

    _this.handleNotifyOfNext = function (e) {
      var updateState = false;

      switch (e.type) {
        case 'scroll':
        case 'mouseenter':
        case 'mutation':
          // check mouse position, if it's still on RI, then reNotifyOfNext, else updateState
          if (_this.track.mouseOn && _this.checkMousePosition() === 'mouseOn') {
            return 'reNotifyOfNext';
          }
          _this.track.mouseOn = false;
          _this.track.buttonDown = false;
          updateState = true;
          break;

        case 'touchstart':
          // cancel tap if extra touch point, or when touch someplace else on the screen
          // check topNode and children to make sure they weren't the target
          if (_this.p.props.extraTouchNoTap) {
            if (_this.track.touches.active < _this.maxTapPoints && (0, _recursiveNodeCheck2.default)(_this.topNode, function (node) {
              return e.target === node;
            })) {
              return 'reNotifyOfNext';
            }
            updateState = _this.handleTouchEvent({ type: 'touchtapcancel' }) === 'updateState';
          }
          break;

        case 'dragstart':
          // use setTimeout because notifier drag event will fire before the drag event on RI,
          // so w/o timeout when this intance of RI is dragged it would go:
          // active -> force normal from notifier drag -> active from RI's drag event,
          // but the timeout will allow time for RI's drag event to fire before force normal
          _this.manageSetTimeout('dragstart', function () {
            if (!_this.track.drag) {
              _this.forceTrackIState('normal');
              _this.updateState(_this.computeState(), _this.p.props, e, true);
            }
          }, 30);
          break;

        // window focus event
        case 'focus':
          // reinstate previous focus state if this window focus event is followed by
          // an element focus event, otherwise cancel focus reinstatement
          if (_this.track.previousFocus !== false) {
            _this.track.reinstateFocus = true;
            _this.manageSetTimeout('windowFocus', function () {
              _this.track.reinstateFocus = false;
            }, _constants.queueTime);
          }
          break;

        // window blur event to preserve the focus state
        case 'blur':
          // clear the timer set in manageNotifyOfNext that was set to cancel this notification
          _this.cancelTimeout('elementBlur');
          // notifiy of the next window focus event (re-entering the app/window/tab)
          if (!_this.track.notifyOfNext.focus) {
            _this.track.notifyOfNext.focus = (0, _notifier.notifyOfNext)('focus', _this.handleNotifyOfNext);
          }
          break;
        default:
      }

      if (updateState) _this.updateState(_this.computeState(), _this.p.props, e, true);
      delete _this.track.notifyOfNext[e.type];
      return null;
    };

    _this.state = {
      // iState is always 1 of 5 strings:
      // 'normal', 'hover', 'hoverActive', 'touchActive', 'keyActive'
      iState: 'normal',
      // focus is always 1 of 4 values: false, 'tab', 'mouse' or 'touch'
      focus: false
    };

    // things to keep track of so RI knows what to do when
    _this.track = {
      touchDown: false,
      recentTouch: false,
      touches: { points: {}, active: 0 },
      mouseOn: false,
      buttonDown: false,
      clickType: 'reset',
      focus: false,
      previousFocus: false,
      reinstateFocus: false,
      focusTransition: 'reset',
      focusStateOnMouseDown: false,
      spaceKeyDown: false,
      enterKeyDown: false,
      drag: false,
      updateTopNode: false,
      notifyOfNext: {},
      timeoutIDs: {},
      state: _this.state
    };

    // the node returned by the ref callback
    _this.refNode = null;
    // the actual top DOM node of `as`, needed when `as` is wrapped in a span (is ReactComponent)
    _this.topNode = null;
    // tagName and type properties of topNode, updated in refCallback
    _this.tagName = typeof props.as === 'string' && props.as || '';
    _this.type = props.type || '';
    // if the topNode is triggered by the enter key, and/or the space bar
    _this.enterKeyTrigger = false;
    _this.spaceKeyTrigger = false;

    // maximum number of touch points where a tap is still possible, updated in propsSetup
    _this.maxTapPoints = 1;

    // the event handlers to pass down as props to the element/component
    _this.eventHandlers = _this.setupEventHandlers();

    // this.p is used to store things that are a deterministic function of props
    // to avoid recalculating every time they are needed, it can be thought of as a pure
    // extension to props and is only updated in the constructor and componentWillReceiveProps
    _this.p = { sameProps: false };
    // set properties of `this.p`
    _this.propsSetup(props);
    // if initialState prop, update state.iState for initial render, note that state.focus
    // will be updated in componentDidMount b/c can't call focus until have ref to DOM node
    if (_this.p.props.initialState && _this.p.props.initialState.iState) {
      _this.forceTrackIState(_this.p.props.initialState.iState);
      _this.state = _this.computeState();
    }
    return _this;
  }

  _createClass(Interactive, [{
    key: 'componentDidMount',
    value: function componentDidMount() {
      // enter focus state if initialState.focus - called here instead of constructor
      // because can't call focus until have ref to DOM node
      if (this.p.props.initialState && this.p.props.initialState.focus !== undefined) {
        this.forceState({ focus: this.p.props.initialState.focus });
      }
    }
  }, {
    key: 'componentWillReceiveProps',
    value: function componentWillReceiveProps(nextProps) {
      // set if the `topNode` needs to be updated in componentDidUpdate => `as` is different
      // and not a string, note that if `as` is a new string, then the `refCallback`
      // will be called by React so no need to do anything in componentDidUpdate
      this.track.updateTopNode = this.props.as !== nextProps.as && typeof this.props.as !== 'string' && typeof nextProps.as !== 'string';

      // check if nextProps are the same as this.props
      this.p.sameProps = false;
      if (!nextProps.mutableProps && (0, _compareProps2.default)(this.props, nextProps)) {
        this.p.sameProps = true;
      } else {
        // if not same props, do props setup => set properties of `this.p`
        this.propsSetup(nextProps);
      }

      // if `forceState` prop, then force update state
      if (this.p.props.forceState) this.forceState(this.p.props.forceState);
    }
  }, {
    key: 'shouldComponentUpdate',
    value: function shouldComponentUpdate(nextProps, nextState) {
      // or statement, returns true on first true value, returns false if all are false
      return (
        // return true if props have changed since last render
        !this.p.sameProps && nextProps !== this.props ||
        // always update if there are interactive children
        nextProps.interactiveChild ||
        // if `iState` changed, AND the `style` or `className` for the new `iState` is different,
        // prevents renders when switching b/t two states that have the same `style` and `className`
        nextState.iState !== this.state.iState && (this.p[nextState.iState + 'Style'].style !== this.p[this.state.iState + 'Style'].style || this.p[nextState.iState + 'Style'].className !== this.p[this.state.iState + 'Style'].className) ||
        // if `focus` state changed (always update to work with default style)
        nextState.focus !== this.state.focus
      );
    }
  }, {
    key: 'componentDidUpdate',
    value: function componentDidUpdate() {
      // `refCallback` isn't called by React when `as` is a component because the span wrapper
      // remains the same element and is not re-mounted in the DOM, so need to call refCallback here
      // if `as` is new and a component (`updateTopNode` was set in componentWillReceiveProps).
      if (this.track.updateTopNode) {
        this.track.updateTopNode = false;
        this.refCallback(this.refNode);
      }
    }
  }, {
    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      var _this2 = this;

      Object.keys(this.track.notifyOfNext).forEach(function (eType) {
        (0, _notifier.cancelNotifyOfNext)(eType, _this2.track.notifyOfNext[eType]);
      });
      Object.keys(this.track.timeoutIDs).forEach(function (timer) {
        window.clearTimeout(_this2.track.timeoutIDs[timer]);
      });
    }

    // determine event handlers to use based on the device type - only determined once in constructor

  }, {
    key: 'setupEventHandlers',
    value: function setupEventHandlers() {
      var _this3 = this;

      var eventHandlers = {};
      Object.keys(_constants.otherEvents).forEach(function (event) {
        eventHandlers[_constants.otherEvents[event]] = _this3.handleEvent;
      });

      // if the device has touch, set touch event listeners
      if (_constants.deviceHasTouch) {
        Object.keys(_constants.touchEvents).forEach(function (event) {
          eventHandlers[_constants.touchEvents[event]] = _this3.handleEvent;
        });
      }
      // if the device has a mouse, set mouse event listeners
      if (_constants.deviceHasMouse) {
        Object.keys(_constants.mouseEvents).forEach(function (event) {
          eventHandlers[_constants.mouseEvents[event]] = _this3.handleEvent;
        });
      }
      return eventHandlers;
    }

    // returns true if a click listener should be set, called from propsSetup and refCallback

  }, {
    key: 'setClickListener',
    value: function setClickListener(props) {
      // set click listener when there is an onClick prop
      if (props.onClick) return true;
      if (_constants.deviceHasTouch) {
        // set click listener when the element is focusable - this is to correct a bug
        // in Chrome on iOS where it will sometimes, when it is under stress, fire focus and
        // click events without firing a touch event on the document - the result is the focus event
        // will cause RI to enter the focus from tab state errantly, and then the click event will
        // toggle focus off making the correction, so have to listen for click events
        if (props.tabIndex) return true;
        // set click listener when the element has a knownRoleTag, i.e. the browser
        // has a click event handler so preventDefault() needs to be called when the
        // browser sends a click event after RI has canceled tap (e.g. touchTapTimer expired, etc)
        if (_constants.knownRoleTags[this.tagName]) return true;
      }
      return false;
    }

    // find and set the top DOM node of `as`

  }, {
    key: 'propsSetup',


    // setup `this.p`, only called from constructor and componentWillReceiveProps
    value: function propsSetup(props) {
      var _mergeAndExtractProps = (0, _mergeAndExtractProps3.default)(props, _constants.knownProps),
          mergedProps = _mergeAndExtractProps.mergedProps,
          passThroughProps = _mergeAndExtractProps.passThroughProps;

      (0, _extractStyle.setActiveAndFocusProps)(mergedProps);

      // if focus state prop and no tabIndex, then add a tabIndex so RI is focusable by browser
      if (passThroughProps.tabIndex === null) delete passThroughProps.tabIndex;else if (!passThroughProps.tabIndex && (mergedProps.focus || mergedProps.focusFromTab || mergedProps.focusFromMouse || mergedProps.focusFromTouch || mergedProps.onClick)) {
        mergedProps.tabIndex = '0';
        passThroughProps.tabIndex = '0';
      }

      // if onClick prop but it's not clear what the role of the element is then add role="button"
      if (passThroughProps.role === null) delete passThroughProps.role;else if (mergedProps.onClick && !mergedProps.role && typeof mergedProps.as === 'string' && !_constants.knownRoleTags[mergedProps.as]) {
        mergedProps.role = 'button';
        passThroughProps.role = 'button';
      }

      // maximum number of touch points where a tap is still possible
      this.maxTapPoints = mergedProps.onTapFour && 4 || mergedProps.onTapThree && 3 || mergedProps.onTapTwo && 2 || 1;

      // add onClick handler to passThroughProps if it's required
      if (this.setClickListener(mergedProps)) passThroughProps.onClick = this.handleEvent;

      //  add onTouchMove handler to passThroughProps if it's required
      if (_constants.deviceHasTouch && (mergedProps.touchActiveTapOnly || mergedProps.onLongPress || mergedProps.onTouchMove)) {
        passThroughProps.onTouchMove = this.handleEvent;
      }

      // add other event handlers to passThroughProps
      (0, _objectAssign2.default)(passThroughProps, this.eventHandlers);

      this.p.normalStyle = (0, _extractStyle.extractStyle)(mergedProps, 'normal');
      this.p.hoverStyle = (0, _extractStyle.extractStyle)(mergedProps, 'hover');
      this.p.hoverActiveStyle = (0, _extractStyle.extractStyle)(mergedProps, 'hoverActive');
      this.p.touchActiveStyle = (0, _extractStyle.extractStyle)(mergedProps, 'touchActive');
      this.p.keyActiveStyle = (0, _extractStyle.extractStyle)(mergedProps, 'keyActive');
      this.p.tabFocusStyle = (0, _extractStyle.extractStyle)(mergedProps, 'focusFromTab');
      this.p.mouseFocusStyle = (0, _extractStyle.extractStyle)(mergedProps, 'focusFromMouse');
      this.p.touchFocusStyle = (0, _extractStyle.extractStyle)(mergedProps, 'focusFromTouch');
      this.p.passThroughProps = passThroughProps;
      this.p.props = mergedProps;
    }

    // keep track of running timeouts so can clear in componentWillUnmount

  }, {
    key: 'manageSetTimeout',
    value: function manageSetTimeout(type, cb, delay) {
      var _this4 = this;

      if (this.track.timeoutIDs[type] !== undefined) {
        window.clearTimeout(this.track.timeoutIDs[type]);
      }
      this.track.timeoutIDs[type] = window.setTimeout(function () {
        delete _this4.track.timeoutIDs[type];
        cb();
      }, delay);
    }
  }, {
    key: 'cancelTimeout',
    value: function cancelTimeout(type) {
      if (this.track.timeoutIDs[type] !== undefined) {
        window.clearTimeout(this.track.timeoutIDs[type]);
        delete this.track.timeoutIDs[type];
      }
    }

    // force set this.track properties based on iState

  }, {
    key: 'forceTrackIState',
    value: function forceTrackIState(iState) {
      if (this.computeState().iState !== iState) {
        this.track.mouseOn = iState === 'hover' || iState === 'hoverActive';
        this.track.buttonDown = iState === 'hoverActive';
        this.track.touchDown = iState === 'touchActive';
        this.track.spaceKeyDown = iState === 'keyActive';
        this.track.enterKeyDown = iState === 'keyActive';
        this.track.drag = false;
      }
    }

    // force set new state

  }, {
    key: 'forceState',
    value: function forceState(newState) {
      // set this.track properties to match new iState
      if (newState.iState !== undefined) this.forceTrackIState(newState.iState);

      // if new focus state, call manageFocus and return b/c focus calls updateState
      if (newState.focus !== undefined && newState.focus !== this.track.state.focus) {
        this.track.focus = newState.focus;
        this.manageFocus(newState.focus ? 'forceStateFocusTrue' : 'forceStateFocusFalse');
        return;
      }

      // update state with new computed state and dummy 'event' that caused state change
      this.updateState(this.computeState(), this.p.props, (0, _constants.dummyEvent)('forcestate'));
    }

    // compute the state based on what's set in `this.track`, returns a new state object
    // note: use the respective active state when drag is true (i.e. dragging the element)

  }, {
    key: 'computeState',
    value: function computeState() {
      var _track = this.track,
          mouseOn = _track.mouseOn,
          buttonDown = _track.buttonDown,
          touchDown = _track.touchDown,
          focus = _track.focus,
          drag = _track.drag;

      var focusKeyDown = focus && (this.track.enterKeyDown && this.enterKeyTrigger || this.track.spaceKeyDown && this.spaceKeyTrigger);
      var newState = { focus: focus };
      if (!mouseOn && !buttonDown && !touchDown && !focusKeyDown && !drag) newState.iState = 'normal';else if (mouseOn && !buttonDown && !touchDown && !focusKeyDown && !drag) {
        newState.iState = 'hover';
      } else if (mouseOn && buttonDown && !touchDown && !focusKeyDown || drag && !touchDown) {
        newState.iState = 'hoverActive';
      } else if (focusKeyDown && !touchDown) newState.iState = 'keyActive';else if (touchDown || drag) newState.iState = 'touchActive';
      return newState;
    }

    // takes a new state, calls setState and the state change callbacks

  }, {
    key: 'updateState',
    value: function updateState(newState, props, event, dontManageNotifyOfNext) {
      if (!dontManageNotifyOfNext) this.manageNotifyOfNext(newState);
      var prevIState = this.track.state.iState;
      var nextIState = newState.iState;
      var iStateChange = nextIState !== prevIState;
      var focusChange = newState.focus !== this.track.state.focus;

      // early return if state doesn't need to change
      if (!iStateChange && !focusChange) return;

      // create new prev and next state objects with immutable values
      var prevState = {
        iState: prevIState,
        focus: this.track.state.focus
      };
      var nextState = {
        iState: nextIState,
        focus: newState.focus
      };

      // call onStateChange prop callback
      props.onStateChange && props.onStateChange({ prevState: prevState, nextState: nextState, event: event });

      // track new state because setState is asyncrounous
      this.track.state = newState;

      // only place that setState is called
      this.setState(newState, props.setStateCallback && props.setStateCallback.bind(this, { prevState: prevState, nextState: nextState }));
    }

    // handles all events - first checks if it's a valid event, then calls the specific
    // type of event handler (to set the proper this.track properties),
    // and at the end calls this.updateState(...)

  }, {
    key: 'isValidEvent',


    // checks if the event is a valid event or not, returns true / false respectivly
    value: function isValidEvent(e) {
      // if it's a known click event then return true
      if (e.type === 'click' && this.track.clickType !== 'reset') return true;
      // if it's a focus/blur event and this Interactive instance is not the target then return true
      if ((e.type === 'focus' || e.type === 'blur') && e.target !== this.topNode) return true;

      // refCallbackFocus calls focus when there is a new top DOM node and RI is already in the
      // focus state to keep the browser's focus state in sync with RI's, so reset and return false
      if (e.type === 'focus' && this.track.focusTransition === 'refCallbackFocus') {
        e.stopPropagation();
        this.track.focusTransition = 'reset';
        return false;
      }

      // if the focusTransition is a force blur and RI is not currently in the focus state,
      // then the force blur is to keep the browser focus state in sync with RI's focus state,
      // so reset the focusTransition and return false, no need to do anything
      // else because the blur event was only for the benefit of the browser, not RI
      if (e.type === 'blur' && this.track.focusTransition === 'focusForceBlur' && !this.track.state.focus) {
        e.stopPropagation();
        this.track.focusTransition = 'reset';
        return false;
      }

      // if the device is touchOnly or a hybrid
      if (_constants.deviceHasTouch) {
        // reject click events that are from touch interactions, unless no active or touchActive prop
        // if no active or touchActive prop, then let the browser determine what is a click from touch
        // this allows for edge taps that don't fire touch events on RI (only click events)
        // so the click event is allowed through when WebkitTapHightlightColor indicates a click
        if (e.type === 'click' && (_inputTracker2.default.touch.recentTouch || _inputTracker2.default.touch.touchOnScreen) && (this.p.props.active || this.p.props.touchActive)) {
          e.preventDefault();
          e.stopPropagation();
          return false;
        }
        // reject unknown focus events from touch interactions
        if (e.type === 'focus') {
          if (this.track.focusTransition === 'reset' && (_inputTracker2.default.touch.recentTouch || !this.track.touchDown && _inputTracker2.default.touch.touchOnScreen)) {
            e.preventDefault();
            e.stopPropagation();
            this.manageFocus('focusForceBlur');
            return false;
          }
        }
      }

      if (_constants.deviceType === 'hybrid') {
        // reject mouse events from touch interactions
        if (/mouse/.test(e.type) && (_inputTracker2.default.touch.touchOnScreen || _inputTracker2.default.touch.recentTouch)) {
          e.preventDefault();
          e.stopPropagation();
          return false;
        }
      }

      return true;
    }

    // notifyOfNext plugs the holes in the events fired by the browser on the RI element,
    // in some situations the browser fails to fire the necessary event leaving RI stuck
    // in the wrong state (a not normal iState), so sign up to be notified of the next global event
    // and do some checks (in handleNotifyOfNext) to confirm RI is in the correct state,
    // note that notifyOfNext only while not in the normal state makes the notifier O(1) instead of
    // O(n), where n is the number of mounted RI components

  }, {
    key: 'manageNotifyOfNext',
    value: function manageNotifyOfNext(newState) {
      var _this5 = this;

      // set notifyOfNext
      var setNON = function setNON(eType) {
        if (!_this5.track.notifyOfNext[eType]) {
          _this5.track.notifyOfNext[eType] = (0, _notifier.notifyOfNext)(eType, _this5.handleNotifyOfNext);
        }
      };
      // cancel notifyOfNext
      var cancelNON = function cancelNON(eType) {
        if (_this5.track.notifyOfNext[eType]) {
          (0, _notifier.cancelNotifyOfNext)(eType, _this5.track.notifyOfNext[eType]);
          delete _this5.track.notifyOfNext[eType];
        }
      };

      if (_constants.deviceHasMouse) {
        // if not in the normal state and not dragging, then set notifyOfNext, otherwise cancel
        var shouldSetNON = newState.iState !== 'normal' && !this.track.drag;

        // check mouse position on document mouseenter to prevent from sticking in
        // the hover state after switching to another app/window, moving the mouse,
        // and then switching  back (so the mouse is no longer over the element)
        shouldSetNON ? setNON('mouseenter') : cancelNON('mouseenter');

        // the dragstart event on an element fires after a short delay, so it is possible to
        // start dragging an element and have the mouseenter another element putting it in the
        // hoverActive state before the dragstart event fires (after the dragstart event
        // no other mouse events are fired), so sign up for next global dragstart to force intro
        // normal state while another element is being dragged
        shouldSetNON ? setNON('dragstart') : cancelNON('dragstart');

        // the scroll listener provides a minor improvement to accuracy by exiting the hover state
        // as soon as the mouse is scrolled off an element instead of waiting for the scrolling to end
        // only set as a passive listener as the improvement is not worth it if it hurts performance
        if (_constants.passiveEventSupport) {
          shouldSetNON ? setNON('scroll') : cancelNON('scroll');
        }

        // if the mouse is on RI, then sign up for next DOM mutation event, which could
        // move the mouse off of RI (by changing the layout of the page)
        // without firing a mouseleave event (because the mouse never moved)
        this.track.mouseOn ? setNON('mutation') : cancelNON('mutation');
      }

      if (_constants.deviceHasTouch) {
        // cancel tap when touch someplace else on the screen
        newState.iState === 'touchActive' ? this.p.props.extraTouchNoTap && setNON('touchstart') : cancelNON('touchstart');
      }

      // notify of next setup for maintaining correct focusFrom when switching apps/windows,
      // if exiting the focus state, notify of the next window blur (leaving the app/window/tab)
      // event if it immediately follows this event, otherwise cancel the notify of next
      if (this.track.state.focus && !newState.focus) {
        setNON('blur');
        this.manageSetTimeout('elementBlur', function () {
          _this5.track.previousFocus = false;
          cancelNON('blur');
        }, _constants.queueTime);
      }
    }
  }, {
    key: 'checkMousePosition',


    // check the mouse position relative to the RI element on the page
    value: function checkMousePosition(e) {
      if (!_constants.deviceHasMouse) return null;

      var mouseX = e && e.clientX || _inputTracker2.default.mouse.clientX;
      var mouseY = e && e.clientY || _inputTracker2.default.mouse.clientY;
      function mouseOnNode(node) {
        var rect = node.getBoundingClientRect();
        return mouseX >= rect.left - 1 && mouseX <= rect.right + 1 && mouseY >= rect.top - 1 && mouseY <= rect.bottom + 1;
      }

      var mouseOn = true;

      if (!_inputTracker2.default.mouse.mouseOnDocument) {
        mouseOn = false;
      } else if (!this.p.props.nonContainedChild) {
        mouseOn = mouseOnNode(this.topNode);
      } else {
        // if the nonContainedChild prop is present, then do a recursive check of the node and its
        // children until the mouse is on a node or all children are checked,
        // this is useful when the children aren't inside of the parent on the page
        mouseOn = (0, _recursiveNodeCheck2.default)(this.topNode, mouseOnNode);
      }

      return mouseOn ? 'mouseOn' : 'mouseOff';
    }

    // check to see if a focusTransition is necessary and update this.track.focusTransition
    // returns 'terminate' if handleEvent should terminate, returns 'updateState'
    // if handleEvent should continue and call updateState this time through
    // focus event lifecycle:
    // - browser calls focus -> onFocus listener triggered
    // - RI calls focus (using manageFocus) -> set focusTransition -> onFocus listener triggered
    // - RI event handler uses track.focusTransition to determine if the focus event is:
    //   - not a valid event (in isValidEvent)
    //     - sent from RI to keep browser focus in sync with RI -> reset focusTransition -> end
    //     - errant -> call blur to keep browser in sync, set focusTransition to focusForceBlur -> end
    //   - a valid event
    //     - sent from RI -> reset focusTransition -> RI enters the focus state w/ focus
    //       based on the focusTransition
    //     - sent from browser -> RI enters the focus state w/ focus set to 'tab'
    // - browser calls blur -> onBlur listener triggered
    // - RI calls blur (using manageFocus) -> set focusTransition -> onBlur listener triggered
    // - RI event handler uses track.focusTransition to determine if the blur event is:
    //   - not a valid event (in isValidEvent)
    //     - a force blur to keep the browser focus state in sync -> reset focusTransition -> end
    //       (if it's a force blur meant for both RI and the browser, then it's a valid event)
    //   - eveything else -> reset focusTransition -> RI leaves focus state

  }, {
    key: 'manageFocus',
    value: function manageFocus(type, e) {
      var _this6 = this;

      // if this exact event has already been used for focus/blur by another instance of Interactive
      // i.e. a child and the event is bubbling, then don't manage focus and return updateState
      if (e && (_inputTracker.focusRegistry.focus === e || _inputTracker.focusRegistry.blur === e)) return 'updateState';

      // is the DOM node tag blurable for toggle focus
      var tagIsBlurable = !_constants.nonBlurrableTags[this.tagName] && !this.p.props.focusToggleOff;
      // is the node focusable, if there is a focus or tabIndex prop, or it's non-blurable, then it is
      var tagIsFocusable = this.p.props.tabIndex || _constants.knownRoleTags[this.tagName];

      // calls focus/blur to transition focus, returns 'terminate' if focus/blur call is made
      // because focus/blur event handler called updateState,
      // returns 'updateState' if not allowed to make specified transition, so RI will continue
      // to updateState this time through handleEvent
      var focusTransition = function focusTransition(event, transitionAs, force) {
        if (force === 'force' || event === 'focus' && tagIsFocusable || event === 'blur' && tagIsBlurable) {
          // if the manageFocus call is from a browser event (i.e. will bubble), register it
          if (e) {
            _inputTracker.focusRegistry[event] = e;
            // reset event registry after bubbling has finished because React reuses events so
            // future event equality checks may give a false positive if not reset
            _this6.manageSetTimeout('focusRegistry', function () {
              _inputTracker.focusRegistry[event] = null;
            }, 0);
          }
          _this6.track.focusTransition = transitionAs;
          _this6.topNode[event]();
          // if focusTransition has changed, then the focus/blur call was sucessful so terminate
          if (_this6.track.focusTransition !== transitionAs) {
            return 'terminate';
          }
        }
        _this6.track.focusTransition = 'reset';
        return 'updateState';
      };

      // toggles focus by calling focusTransition, returns focusTransition's return
      var toggleFocus = function toggleFocus(toggleAs, force) {
        if (_this6.track.state.focus) return focusTransition('blur', toggleAs + 'Blur', force);
        return focusTransition('focus', toggleAs + 'Focus', force);
      };

      switch (type) {
        case 'mousedown':
          return focusTransition('focus', 'mouseDownFocus');
        case 'mouseup':
          // blur only if focus was not initiated on the preceding mousedown,
          if (this.track.focusStateOnMouseDown) return focusTransition('blur', 'mouseUpBlur');
          this.track.focusTransition = 'reset';
          return 'updateState';
        case 'touchclick':
          return toggleFocus('touchClick');
        case 'forceStateFocusTrue':
          // setTimeout because React misses focus calls made during componentWillReceiveProps,
          // which is where forceState calls come from (the browser receives the focus call
          // but not React), so have to call focus asyncrounsly so React receives it
          this.manageSetTimeout('forceStateFocusTrue', function () {
            !_this6.track.state.focus && focusTransition('focus', 'forceStateFocus', 'force');
          }, 0);
          return 'terminate';
        case 'forceStateFocusFalse':
          // same as forceStateFocusTrue, but for focus false
          this.manageSetTimeout('forceStateFocusFalse', function () {
            _this6.track.state.focus && focusTransition('blur', 'forceStateBlur', 'force');
          }, 0);
          return 'terminate';
        case 'refCallback':
          // if in the focus state and RI has a new topDOMNode, then call focus() on `this.topNode`
          // to keep the browser focus state in sync with RI's focus state
          if (this.track.state.focus) return focusTransition('focus', 'refCallbackFocus', 'force');
          this.track.focusTransition = 'reset';
          return 'terminate';
        case 'focusForceBlur':
          return focusTransition('blur', 'focusForceBlur', 'force');
        default:
          return 'updateState';
      }
    }

    // returns 'terminate' if the caller (this.handleEvent) should not call updateState(...)

  }, {
    key: 'handleMouseEvent',
    value: function handleMouseEvent(e) {
      switch (e.type) {
        case 'mouseenter':
          (0, _inputTracker.updateMouseFromRI)(e);
          this.p.props.onMouseEnter && this.p.props.onMouseEnter(e);
          this.track.mouseOn = true;
          this.track.buttonDown = e.buttons === 1;
          return 'updateState';
        case 'mouseleave':
          (0, _inputTracker.updateMouseFromRI)(e);
          this.p.props.onMouseLeave && this.p.props.onMouseLeave(e);
          this.track.mouseOn = false;
          this.track.buttonDown = false;
          return 'updateState';
        case 'mousemove':
          this.p.props.onMouseMove && this.p.props.onMouseMove(e);
          // early return for mouse move
          if (this.track.mouseOn && this.track.buttonDown === (e.buttons === 1)) return 'terminate';
          this.track.mouseOn = true;
          this.track.buttonDown = e.buttons === 1;
          return 'updateState';
        case 'mousedown':
          this.p.props.onMouseDown && this.p.props.onMouseDown(e);
          this.track.mouseOn = true;
          this.track.buttonDown = true;
          // track focus state on mousedown to know if should blur on mouseup
          this.track.focusStateOnMouseDown = this.track.state.focus;
          return this.manageFocus('mousedown', e);
        case 'mouseup':
          {
            this.p.props.onMouseUp && this.p.props.onMouseUp(e);
            this.track.buttonDown = false;
            var manageFocusReturn = this.manageFocus('mouseup', e);
            this.manageClick('mouseClick');
            return manageFocusReturn;
          }
        default:
          return 'terminate';
      }
    }

    // returns 'terminate' if the caller (this.handleEvent) should not call updateState(...)
    // note that a touch interaction lasts from the start of the first touch point on RI,
    // until removal of the last touch point on RI, and then the touch interaction is reset

  }, {
    key: 'handleTouchEvent',
    value: function handleTouchEvent(e) {
      var _this7 = this;

      // reset mouse trackers
      this.track.mouseOn = false;
      this.track.buttonDown = false;

      // reset touch interaction tracking, called when there are no more touches on the target
      var resetTouchInteraction = function resetTouchInteraction() {
        _this7.track.touchDown = false;
        _this7.track.touches = { points: {}, active: 0 };
        // clear the touchTapTimer if it's running
        _this7.cancelTimeout('touchTapTimer');
      };

      // track recent touch, called from touchend and touchcancel
      var recentTouch = function recentTouch() {
        _this7.track.recentTouch = true;
        _this7.manageSetTimeout('recentTouchTimer', function () {
          _this7.track.recentTouch = false;
        }, _constants.queueTime);
      };

      // returns true if there are extra touches on the screen
      var extraTouches = function extraTouches() {
        return (
          // if extraTouchNoTap prop and also touching someplace else on the screen, or
          _this7.p.props.extraTouchNoTap && e.touches.length !== _this7.track.touches.active ||
          // more touches on RI than maxTapPoints
          _this7.track.touches.active > _this7.maxTapPoints
        );
      };

      // returns true if a touch point has moved more than is allowed for a tap
      var touchMoved = function touchMoved(endTouch, startTouch, numberOfPoints) {
        return Math.abs(endTouch.clientX - startTouch.startX) >= 15 + 3 * numberOfPoints || Math.abs(endTouch.clientY - startTouch.startY) >= 15 + 3 * numberOfPoints;
      };

      // log touch position for each touch point that is part of the touch event
      var logTouchCoordsAs = function logTouchCoordsAs(logAs) {
        for (var i = 0; i < e.changedTouches.length; i++) {
          var point = _this7.track.touches.points[e.changedTouches[i].identifier] || {};
          point[logAs + 'X'] = e.changedTouches[i].clientX;
          point[logAs + 'Y'] = e.changedTouches[i].clientY;
          _this7.track.touches.points[e.changedTouches[i].identifier] = point;
        }
      };

      switch (e.type) {
        case 'touchstart':
          {
            this.p.props.onTouchStart && this.p.props.onTouchStart(e);
            // update number of active touches
            this.track.touches.active += e.changedTouches.length;
            if (this.track.touches.tapCanceled) return 'terminate';
            var newTouchDown = !this.track.touchDown;
            this.track.touchDown = true;
            // cancel tap if there was already a touchend in this interaction or there are extra touches
            if (this.track.touches.touchend || extraTouches()) {
              // recursively call handleTouchEvent with a touchtapcancel event to set track properties,
              // call handleTouchEvent directly don't go through handleEvent so updateState isn't called
              return this.handleTouchEvent({ type: 'touchtapcancel' }) === 'updateState' || newTouchDown ? 'updateState' : 'terminate';
            }

            // if going from no touch to touch, set touchTapTimer
            if (newTouchDown) {
              e.persist();
              this.manageSetTimeout('touchTapTimer', function () {
                // if the timer finishes then call onLongPress callback and
                // fire a touchtapcancel event to cancel the tap,
                // because this goes through handleEvent, updateState will be called if needed
                _this7.p.props.onLongPress && _this7.p.props.onLongPress(e);
                _this7.handleEvent((0, _constants.dummyEvent)('touchtapcancel'));
              }, this.p.props.tapTimeCutoff);
            }

            // log touch start position
            logTouchCoordsAs('start');
            return 'updateState';
          }

        case 'touchmove':
          this.p.props.onTouchMove && this.p.props.onTouchMove(e);
          if (this.track.touches.tapCanceled) return 'terminate';
          // cancel tap if there are extra touches
          if (extraTouches()) return this.handleTouchEvent({ type: 'touchtapcancel' });

          // if touchActiveTapOnly or onLongPress prop,
          // check to see if the touch moved enough to cancel tap
          if (this.p.props.touchActiveTapOnly || this.p.props.onLongPress) {
            for (var i = 0; i < e.changedTouches.length; i++) {
              var touch = this.track.touches.points[e.changedTouches[i].identifier];
              if (touch && touchMoved(e.changedTouches[i], touch, this.maxTapPoints)) {
                return this.handleTouchEvent({ type: 'touchtapcancel' });
              }
            }
          }
          return 'terminate';

        case 'touchend':
          // start recent touch timer
          recentTouch();
          this.p.props.onTouchEnd && this.p.props.onTouchEnd(e);
          // update number of active touches
          this.track.touches.active -= e.changedTouches.length;
          // if a touch event was dropped somewhere, i.e.
          // cumulative length of changed touches for touchstarts !== touchends, then reset
          if (this.track.touches.active < 0 || e.touches.length === 0 && this.track.touches.active > 0) {
            resetTouchInteraction();
            return 'updateState';
          }

          // track that there has been a touchend in this touch interaction
          this.track.touches.touchend = true;

          // check to see if tap is already canceled or should be canceled
          if (this.track.touches.active === 0 && (this.track.touches.tapCanceled || extraTouches())) {
            resetTouchInteraction();
            return 'updateState';
          } else if (this.track.touches.tapCanceled) return 'terminate';else if (extraTouches()) return this.handleTouchEvent({ type: 'touchtapcancel' });

          // log touch end position
          logTouchCoordsAs('client');

          // if there are no remaining touches, then process the touch interaction
          if (this.track.touches.active === 0) {
            var touches = this.track.touches.points;
            var touchKeys = Object.keys(touches);
            var count = touchKeys.length;

            // determine if there was a tap and number of touch points for the tap
            // if every touch point hasn't moved, set tapTouchPoints to count
            var tapTouchPoints = touchKeys.every(function (touch) {
              return !touchMoved(touches[touch], touches[touch], count);
            }) ? count : 0;

            // reset the touch interaction
            resetTouchInteraction();

            switch (tapTouchPoints) {
              case 1:
                {
                  var manageFocusReturn = 'updateState';
                  // if no active or touchActive prop, let the browser handle click events
                  if (this.p.props.active || this.p.props.touchActive) {
                    manageFocusReturn = this.manageFocus('touchclick', e);
                    this.manageClick('tapClick');
                  }
                  return manageFocusReturn;
                }
              case 2:
                this.p.props.onTapTwo && this.p.props.onTapTwo(e);
                break;
              case 3:
                this.p.props.onTapThree && this.p.props.onTapThree(e);
                break;
              case 4:
                this.p.props.onTapFour && this.p.props.onTapFour(e);
                break;
              default:
            }
          }
          return 'updateState';

        case 'touchcancel':
          recentTouch();
          this.p.props.onTouchCancel && this.p.props.onTouchCancel(e);
          this.track.touches.active -= e.changedTouches.length;

          // if there are no remaining touches, then reset the touch interaction
          if (this.track.touches.active === 0) {
            resetTouchInteraction();
            return 'updateState';
          }

          // cancel tap and return whatever touchtapcancel says todo
          return this.handleTouchEvent({ type: 'touchtapcancel' });

        // cancel tap for this touch interaction
        case 'touchtapcancel':
          // clear the touchTapTimer if it's running
          this.cancelTimeout('touchTapTimer');
          if (this.track.touchDown) {
            // set the tap event to canceled
            this.track.touches.tapCanceled = true;
            if (this.p.props.touchActiveTapOnly) {
              // if touchActiveTapOnly prop, exit the touchActive state and updateState
              this.track.touchDown = false;
              return 'updateState';
            }
          }
          return 'terminate';
        default:
          return 'terminate';
      }
    }

    // called in anticipation of a click event (before it's fired) to track the source
    // of the click event (mouse, touch, key), and synthetically call node.click() if needed

  }, {
    key: 'manageClick',
    value: function manageClick(type) {
      var _this8 = this;

      // clear clickType timer if it's running
      this.cancelTimeout('clickType');

      // timer to reset the clickType,
      // when it's left to the browser to call click(), the browser has queueTime
      // to add the click event to the queue for it to be recognized as a known click event
      var setClickTypeTimer = function setClickTypeTimer() {
        _this8.manageSetTimeout('clickType', function () {
          _this8.track.clickType = 'reset';
        }, _constants.queueTime);
      };

      switch (type) {
        case 'mouseClick':
          this.track.clickType = 'mouseClick';
          // let the browser call click() for mouse interactions
          setClickTypeTimer();
          break;
        case 'tapClick':
          this.track.clickType = 'tapClick';
          // for touch interactions, use syntheticClick to call node.click() now and
          // block the subsequent click event created by the browser if there is one
          (0, _syntheticClick2.default)(this.topNode);
          this.track.clickType = 'reset';
          break;
        case 'keyClick':
          this.track.clickType = 'keyClick';
          // if the element has a known interactive role (a, button, input, etc),
          // then let the browser call click() for keyClick interactions (enter key and/or space bar)
          if (_constants.knownRoleTags[this.tagName]) {
            setClickTypeTimer();

            // if the element doesn't have a known interactive role, but there is an onClick prop,
            // then call node.click() directly as the browser won't fire a click event
            // from a keyClick interaction
          } else if (this.p.props.onClick) {
            this.topNode.click();
            this.track.clickType = 'reset';
          }
          break;
        default:
      }
    }

    // returns 'terminate' if the caller (this.handleEvent) should not call updateState(...)
    // in almost cases this will return terminate as click events don't change state,
    // the one exception is an unknown but valid click event from a touch interaction,
    // which will need to manageFocus, and then return whatever manageFocus says to do

  }, {
    key: 'handleClickEvent',
    value: function handleClickEvent(e) {
      // clear clickType timer if running
      this.cancelTimeout('clickType');
      var returnValue = 'terminate';
      // if this is an unknown click event, make some assumptions
      if (this.track.clickType === 'reset') {
        // unknown click event on a form submit input with a recentEnterKeyDown on the document
        // is considered to be a keyClick (when you press enter to submit a form
        // but focus is not on the submit button)
        var enterKeyFormSubmit = this.tagName === 'input' && this.type === 'submit' && _inputTracker2.default.key.recentEnterKeyDown;
        if (enterKeyFormSubmit) this.track.clickType = 'keyClick';else if (_inputTracker2.default.touch.recentTouch || _inputTracker2.default.touch.touchOnScreen || _constants.deviceType === 'touchOnly') {
          // if there is a recent touch on the document,
          // or this is a unknown synthetic click event on a touchOnly device
          returnValue = this.manageFocus('touchclick', e);
          this.track.keyClick = 'tapClick';
          // else this is a unknown synthetic click event on a mouseOnly or hybrid device
        } else this.track.keyClick = 'mouseClick';
      }

      // focus is not called on touch tap with links that open in a new window
      // on pages that have been navigated to with pushState (only tested react router).
      // So need to simulate a previous focus state of touch and a window blur event by
      // signing up to be notified of next window focus event.
      // Note that if navigated to www.example.tld/some-page with pushState link (e.g. RR Link)
      // then focus is not called on tap, but if do a fresh page load for www.example.tld/some-page
      // then focus is called on tap before opening the link in a new window (which is really weird).
      // Note that focus not called means the browser doesn't respect focus calls generated by RI
      // (and the browser may not generate a focus call itself, results varied by browser).
      // This is only a problem on Android Chrome because despite not calling focus on link tap,
      // upon returning to the window, focus is called on the element putting it
      // into the focusFromTab state, when it should be in the focusFromTouch state.
      if (this.p.props.target === '_blank' && this.track.clickType === 'tapClick' && !this.track.notifyOfNext.focus) {
        this.track.previousFocus = 'touch';
        this.track.notifyOfNext.focus = (0, _notifier.notifyOfNext)('focus', this.handleNotifyOfNext);
      }

      // call onClick handler and pass in clickType (mouseClick, tapClick, keyClick) as 2nd argument
      this.p.props.onClick && this.p.props.onClick(e, this.track.clickType);
      this.track.clickType = 'reset';
      return returnValue;
    }

    // returns 'terminate' if the caller (this.handleEvent) should not call updateState(...)

  }, {
    key: 'handleOtherEvent',
    value: function handleOtherEvent(e) {
      switch (e.type) {
        case 'focus':
          this.p.props.onFocus && this.p.props.onFocus(e);

          // if this instance of RI is not the focus target, then don't enter the focus state
          if (e.target !== this.topNode) return 'terminate';

          // if this is a known focusTransition or focus is false,
          // then set focus based on the type of focusTransition,
          if (this.track.focusTransition !== 'reset' || !this.track.focus) {
            var focusTransition = this.track.focusTransition.toLowerCase();
            if (/mouse/.test(focusTransition)) {
              this.track.focus = 'mouse';
            } else if (/touch/.test(focusTransition) || this.track.touchDown) {
              this.track.focus = 'touch';
            } else if (this.track.reinstateFocus) {
              this.track.focus = this.track.previousFocus;
            } else if (!/forcestate/.test(focusTransition)) {
              this.track.focus = 'tab';
            }
          }

          // if there was a timer set by a recent window focus event, clear it
          this.cancelTimeout('windowFocus');
          // only reinstate focus from window blur/focus for next focus event
          this.track.reinstateFocus = false;

          this.track.focusTransition = 'reset';
          return 'updateState';
        case 'blur':
          this.p.props.onBlur && this.p.props.onBlur(e);
          if (e.target !== this.topNode) return 'terminate';
          this.track.focusTransition = 'reset';
          this.track.previousFocus = this.track.focus;
          this.track.focus = false;
          this.track.spaceKeyDown = false;
          this.track.enterKeyDown = false;
          return 'updateState';
        case 'keydown':
          this.p.props.onKeyDown && this.p.props.onKeyDown(e);
          if (!this.track.focus) return 'terminate';
          if (e.key === ' ') this.track.spaceKeyDown = true;else if (e.key === 'Enter') {
            this.track.enterKeyDown = true;
            if (this.enterKeyTrigger) this.manageClick('keyClick');
          } else return 'terminate';
          return 'updateState';
        case 'keyup':
          this.p.props.onKeyUp && this.p.props.onKeyUp(e);
          if (!this.track.focus) return 'terminate';
          if (e.key === 'Enter') this.track.enterKeyDown = false;else if (e.key === ' ') {
            this.track.spaceKeyDown = false;
            if (this.spaceKeyTrigger) this.manageClick('keyClick');
          } else return 'terminate';
          return 'updateState';
        case 'dragstart':
          this.p.props.onDragStart && this.p.props.onDragStart(e);
          this.track.drag = true;
          return 'updateState';
        case 'dragend':
          this.p.props.onDragEnd && this.p.props.onDragEnd(e);
          this.forceTrackIState('normal');
          return 'updateState';
        default:
          return 'terminate';
      }
    }
  }, {
    key: 'computeStyle',
    value: function computeStyle() {
      // build style object, priority order: state styles, style prop, default styles
      var style = {};
      // add default styles first:
      // if focusFromTab prop provided, then reset browser focus style,
      // otherwise only reset it when focus is not from tab
      if (!this.p.props.useBrowserOutlineFocus && (this.p.props.focusFromTab || this.state.focus !== 'tab' && !_constants.nonBlurrableTags[this.tagName])) {
        style.outline = 0;
        style.outlineOffset = 0;
      }
      // if touchActive or active prop provided, then reset webkit tap highlight style
      if ((this.p.props.touchActive || this.p.props.active) && _constants.deviceHasTouch) {
        style.WebkitTapHighlightColor = 'rgba(0, 0, 0, 0)';
      }
      // set cursor to pointer if clicking does something
      var lowerAs = typeof this.p.props.as === 'string' && this.p.props.as.toLowerCase();
      if (!this.p.props.useBrowserCursor && (this.p.props.onClick || lowerAs !== 'input' && this.p.props.tabIndex && (this.p.mouseFocusStyle.style || this.p.mouseFocusStyle.className) || lowerAs === 'input' && (this.p.props.type === 'checkbox' || this.p.props.type === 'radio' || this.p.props.type === 'submit') || lowerAs === 'button' || lowerAs === 'a' || lowerAs === 'area' || lowerAs === 'select') && !this.p.props.disabled) {
        style.cursor = 'pointer';
      }

      // add style prop styles second:
      (0, _objectAssign2.default)(style, this.p.props.style);

      // add iState and focus state styles third:
      // focus has priority over iState styles unless overridden in stylePriority
      var hasPriority = this.state.iState === 'keyActive' || this.p.props.stylePriority && this.p.props.stylePriority[this.state.iState];
      var iStateStyle = this.p[this.state.iState + 'Style'].style;
      var focusStyle = this.state.focus ? this.p[this.state.focus + 'FocusStyle'].style : null;
      if (hasPriority) {
        (0, _objectAssign2.default)(style, focusStyle, iStateStyle);
      } else {
        (0, _objectAssign2.default)(style, iStateStyle, focusStyle);
      }
      return style;
    }
  }, {
    key: 'computeClassName',
    value: function computeClassName() {
      // build className string, union of class names from className prop, iState className,
      // and focus className (if in the focus state)
      return (0, _extractStyle.joinClasses)(this.p.props.className || '', this.p[this.state.iState + 'Style'].className, this.state.focus ? this.p[this.state.focus + 'FocusStyle'].className : '');
    }

    // compute children when there is an interactiveChild prop, returns the new children

  }, {
    key: 'computeChildren',
    value: function computeChildren() {
      var _this9 = this;

      // convert this.state.focus to the string focusFrom[Type] for use later
      var focusFrom = this.state.focus && 'focusFrom' + this.state.focus.charAt(0).toUpperCase() + this.state.focus.slice(1);
      // does the current iState style have priority over the focus state style
      var iStateStylePriority = this.p.props.stylePriority && this.p.props.stylePriority[this.state.iState];

      var computeChildStyle = function computeChildStyle(props) {
        var style = props.style ? _extends({}, props.style) : {};
        (0, _extractStyle.setActiveAndFocusProps)(props);
        var iStateStyle = (0, _extractStyle.extractStyle)(props, _this9.state.iState);
        var focusStyle = _this9.state.focus && (0, _extractStyle.extractStyle)(props, focusFrom);

        return {
          className: (0, _extractStyle.joinClasses)(props.className || '', iStateStyle.className, focusStyle && focusStyle.className || ''),
          style: iStateStylePriority && (0, _objectAssign2.default)(style, focusStyle.style, iStateStyle.style) || (0, _objectAssign2.default)(style, iStateStyle.style, focusStyle.style)
        };
      };

      // recurse and map children, if child is an Interactive component, then don't recurse into
      // it's children
      var recursiveMap = function recursiveMap(children) {
        return _react2.default.Children.map(children, function (child) {
          if (!_react2.default.isValidElement(child)) return child;

          // if the child should not be shown, then return null
          if (child.props.showOnParent) {
            var showOn = child.props.showOnParent.split(' ');
            if (!showOn.some(function (el) {
              return el === _this9.state.iState || /Active/.test(_this9.state.iState) && el === 'active' || _this9.state.focus && (el === focusFrom || el === 'focus');
            })) {
              return null;
            }
          }

          var childPropKeys = Object.keys(child.props);

          // if the child doesn't have any interactive child props, then return the child
          if (!childPropKeys.some(function (key) {
            return _constants.childInteractiveProps[key];
          })) {
            if (child.type === Interactive) return child;
            // if the child is not an Interactive component, then still recuse into its children
            return _react2.default.cloneElement(child, {}, recursiveMap(child.props.children));
          }

          var newChildProps = {};
          var childStyleProps = {};
          // separate child props to pass through (newChildProps), from props used
          // to compute the child's style (childStyleProps)
          childPropKeys.forEach(function (key) {
            if (!_constants.childInteractiveProps[key]) newChildProps[key] = child.props[key];else if (key !== 'showOnParent') {
              childStyleProps['' + key.slice(8).charAt(0).toLowerCase() + key.slice(9)] = child.props[key];
            }
          });

          childStyleProps.style = child.props.style;
          childStyleProps.className = child.props.className;

          var _computeChildStyle = computeChildStyle(childStyleProps),
              style = _computeChildStyle.style,
              className = _computeChildStyle.className;

          newChildProps.style = style;
          if (className) newChildProps.className = className;

          // can't use cloneElement because not possible to delete existing child prop,
          // e.g. need to delete the prop onParentHover from the child
          return _react2.default.createElement(child.type, newChildProps, child.type === Interactive ? child.props.children : recursiveMap(child.props.children));
        });
      };

      return recursiveMap(this.p.props.children);
    }
  }, {
    key: 'render',
    value: function render() {
      // props to pass down:
      // passThroughProps (includes event handlers)
      // style
      // className
      this.p.passThroughProps.style = this.computeStyle();
      var className = this.computeClassName();
      if (className) this.p.passThroughProps.className = className;

      var children = this.p.props.interactiveChild ? this.computeChildren() : this.p.props.children;

      // if `as` is a string (i.e. DOM tag name), then add the ref to props and render `as`
      if (typeof this.p.props.as === 'string') {
        this.p.passThroughProps.ref = this.refCallback;
        return _react2.default.createElement(this.p.props.as, this.p.passThroughProps, children);
      }
      // If `as` is a ReactClass or a ReactFunctionalComponent, then wrap it in a span
      // so can access the DOM node without breaking encapsulation
      return _react2.default.createElement('span', {
        ref: this.refCallback,
        style: this.p.props.wrapperStyle,
        className: this.p.props.wrapperClassName
      }, _react2.default.createElement(this.p.props.as, this.p.passThroughProps, children));
    }
  }]);

  return Interactive;
}(_react2.default.Component);

Interactive.propTypes = _propTypes.propTypes;
Interactive.defaultProps = _propTypes.defaultProps;
exports.default = Interactive;
module.exports = exports['default'];