File: //home/arjun/projects/buyercall/node_modules/react-interactive/lib/index.js
'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'];