@moxy/react-page-swapper
Version:
An orchestrator that eases out the implementation of page transitions
251 lines (216 loc) • 7.3 kB
JavaScript
import _omit from "lodash/omit";
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import React, { Component, createRef } from 'react';
import { findDOMNode } from 'react-dom';
import { TransitionGroup } from 'react-transition-group';
import PropTypes from 'prop-types';
import memoizeOne from 'memoize-one';
import SwapTransition from './SwapTransition';
import { getRandomNodeKey } from './node-key';
import { lockContainerSize, buildEnterStyle, buildExitStyle } from './layout';
export default class PageSwapper extends Component {
constructor(_props) {
super(_props);
_defineProperty(this, "state", {});
_defineProperty(this, "containerRef", createRef());
_defineProperty(this, "ref", createRef());
_defineProperty(this, "remainingSwapAcks", 0);
_defineProperty(this, "raf", void 0);
_defineProperty(this, "halfSwap", void 0);
_defineProperty(this, "buildContainerProps", memoizeOne(props => _omit(props, ['node', 'nodeKey', 'mode', 'animation', 'children', 'updateScroll', 'onSwapBegin', 'onSwapEnd'])));
_defineProperty(this, "handleRef", ref => {
if (ref) {
this.ref.current = ref;
}
});
_defineProperty(this, "handleEntered", () => {
this.remainingSwapAcks -= 1;
if (!this.isSwapping()) {
this.finishSwap();
}
});
_defineProperty(this, "handleExited", () => {
this.remainingSwapAcks -= 1;
if (this.halfSwap) {
this.halfSwap();
} else {
this.finishSwap();
}
});
this.state = this.buildState();
}
componentDidUpdate() {
if (!this.isSwapping()) {
if (this.isOutOfSync()) {
this.beginSwap();
} else {
this.maybeUpdateNode();
}
}
}
componentWillUnmount() {
cancelAnimationFrame(this.raf);
}
render() {
const {
children
} = this.props;
const {
node,
nodeKey,
prevNodeKey,
in: inProp,
mode,
animation,
style
} = this.state;
return /*#__PURE__*/React.createElement(TransitionGroup, Object.assign({
ref: this.containerRef
}, this.buildContainerProps(this.props)), nodeKey && /*#__PURE__*/React.createElement(SwapTransition, {
key: nodeKey,
in: inProp,
node: node,
nodeKey: nodeKey,
prevNodeKey: prevNodeKey,
mode: mode,
animation: animation,
style: style,
ref: this.handleRef,
onEntered: this.handleEntered,
onExited: this.handleExited
}, children));
}
isSwapping() {
return this.remainingSwapAcks > 0;
}
isOutOfSync() {
const currentNodeKey = this.state.nodeKey;
const nodeKey = this.props.nodeKey || getRandomNodeKey(this.props.node);
return nodeKey !== currentNodeKey;
}
beginSwap() {
var _this$props$onSwapBeg, _this$props, _document$activeEleme;
const state = this.buildState();
const {
nodeKey,
prevNodeKey
} = state;
const element = findDOMNode(this.ref.current);
const containerElement = findDOMNode(this.containerRef.current);
(_this$props$onSwapBeg = (_this$props = this.props).onSwapBegin) === null || _this$props$onSwapBeg === void 0 ? void 0 : _this$props$onSwapBeg.call(_this$props, {
nodeKey: prevNodeKey,
nextNodeKey: nodeKey
});
this.remainingSwapAcks = 2;
cancelAnimationFrame(this.raf); // Prepare exiting:
// - Lock the container size
// - Blur activeElement if any
// - Apply the animation and out-of-flow styles
const unlockSize = lockContainerSize(containerElement);
(_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.blur();
this.setState({
animation: state.animation,
style: buildExitStyle(element)
}, () => {
// Need to wait an animation frame so that the styles are applied
// This is especially necessary for Safari, to avoid "flickering"
this.raf = requestAnimationFrame(() => {
if (state.mode === 'out-in') {
// Finally start the swap by making the current node exit.
this.setState({
node: null,
nodeKey: null
}); // Declare what to do after it exists, which will be used on the handleExited function
this.halfSwap = () => {
// Make the new node enter.
this.setState(state, () => {
// Now that we have the current node, its dimensions are being counted
// towards the document flow, meaning we can now update the scroll
// and unlock size
this.props.updateScroll({
nodeKey
});
unlockSize();
});
};
} else {
this.halfSwap = undefined; // Finally start the swap!
this.setState(state, () => {
// Now that we have the current node, its dimensions are being counted
// towards the document flow, meaning we can now update the scroll
// and unlock size
this.props.updateScroll({
nodeKey
});
unlockSize();
});
}
});
});
}
finishSwap() {
var _this$props$onSwapEnd, _this$props2;
const {
nodeKey,
prevNodeKey
} = this.state;
(_this$props$onSwapEnd = (_this$props2 = this.props).onSwapEnd) === null || _this$props$onSwapEnd === void 0 ? void 0 : _this$props$onSwapEnd.call(_this$props2, {
nodeKey,
prevNodeKey
});
if (this.isOutOfSync()) {
this.beginSwap();
} else {
this.maybeUpdateNode();
}
}
maybeUpdateNode() {
if (this.props.node !== this.state.node) {
this.setState({
node: this.props.node
});
}
}
buildState() {
var _this$props$nodeKey;
const {
node,
mode,
animation
} = this.props;
const {
nodeKey: currentNodeKey
} = this.state;
const nodeKey = (_this$props$nodeKey = this.props.nodeKey) !== null && _this$props$nodeKey !== void 0 ? _this$props$nodeKey : getRandomNodeKey(node);
const modeStr = typeof mode === 'function' ? mode({
prevNodeKey: currentNodeKey,
nodeKey
}) : mode;
const animationStr = typeof animation === 'function' ? animation({
prevNodeKey: currentNodeKey,
nodeKey
}) : animation;
return {
node,
nodeKey,
prevNodeKey: currentNodeKey,
mode: modeStr,
animation: animationStr,
style: buildEnterStyle()
};
} // eslint-disable-next-line react/sort-comp
}
_defineProperty(PageSwapper, "propTypes", {
node: PropTypes.element.isRequired,
nodeKey: PropTypes.string,
mode: PropTypes.oneOfType([PropTypes.oneOf(['simultaneous', 'out-in']), PropTypes.func]),
animation: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
children: PropTypes.func.isRequired,
updateScroll: PropTypes.func,
onSwapBegin: PropTypes.func,
onSwapEnd: PropTypes.func
});
_defineProperty(PageSwapper, "defaultProps", {
mode: 'simultaneous',
updateScroll: () => window.scrollTo(0, 0)
});