@enact/ui
Version:
A collection of simplified unstyled cross-platform UI components for Enact
503 lines (488 loc) • 20.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.TransitionGroup = void 0;
var _propTypes = _interopRequireDefault(require("@enact/core/internal/prop-types"));
var _handle = require("@enact/core/handle");
var _propTypes2 = _interopRequireDefault(require("prop-types"));
var _eqBy = _interopRequireDefault(require("ramda/src/eqBy"));
var _findIndex = _interopRequireDefault(require("ramda/src/findIndex"));
var _identity = _interopRequireDefault(require("ramda/src/identity"));
var _prop = _interopRequireDefault(require("ramda/src/prop"));
var _propEq = _interopRequireDefault(require("ramda/src/propEq"));
var _remove = _interopRequireDefault(require("ramda/src/remove"));
var _unionWith = _interopRequireDefault(require("ramda/src/unionWith"));
var _useWith = _interopRequireDefault(require("ramda/src/useWith"));
var _react = require("react");
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == typeof e || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } /*
* Exports the {@link ui/ViewManager.TransitionGroup} component.
*/ // Using string refs from the source code of ReactTransitionGroup
/**
* Returns the index of a child in an array found by `key` matching
*
* @param {Object} child React element to find
* @param {Object[]} children Array of React elements
* @returns {Number} Index of child
* @method
* @private
*/ // eslint-disable-next-line react-hooks/rules-of-hooks
var indexOfChild = (0, _useWith["default"])(_findIndex["default"], [(0, _propEq["default"])('key'), _identity["default"]]);
/**
* Returns an array of non-null children
*
* @param {Object[]} children Array of React children
*
* @returns {Object[]} Array of children
* @private
*/
var mapChildren = function mapChildren(children) {
var result = children && _react.Children.toArray(children);
return result ? result.filter(function (c) {
return !!c;
}) : [];
};
/**
* Merges two arrays of children without any duplicates (by `key`)
*
* @param {Object[]} a Set of children
* @param {Object[]} b Set of children
* @returns {Object[]} Merged set of children
* @method
* @private
*/
var mergeChildren = (0, _unionWith["default"])((0, _eqBy["default"])((0, _prop["default"])('key')));
// Cached event forwarders
var forwardOnAppear = (0, _handle.forward)('onAppear');
var forwardOnEnter = (0, _handle.forward)('onEnter');
var forwardOnLeave = (0, _handle.forward)('onLeave');
var forwardOnStay = (0, _handle.forward)('onStay');
/**
* Manages the transition of added and removed child components. Children that are added are
* transitioned in and those removed are transition out via optional callbacks on the child.
*
* Ported from [ReactTransitionGroup]
* (https://facebook.github.io/react/docs/animation.html#low-level-api-reacttransitiongroup).
* Currently somewhat specialized for the purposes of ViewManager.
*
* @class TransitionGroup
* @memberof ui/ViewManager
* @private
*/
var TransitionGroup = exports.TransitionGroup = /*#__PURE__*/function (_Component) {
function TransitionGroup(props) {
var _this;
_classCallCheck(this, TransitionGroup);
_this = _callSuper(this, TransitionGroup, [props]);
_this.performAppear = function (key) {
_this.currentlyTransitioningKeys[key] = true;
var component = _this.groupRefs[key];
if (component.componentWillAppear) {
component.componentWillAppear(_this._handleDoneAppearing.bind(_this, key));
} else {
_this._handleDoneAppearing(key);
}
};
_this._handleDoneAppearing = function (key) {
var component = _this.groupRefs[key];
if (component.componentDidAppear) {
component.componentDidAppear();
}
forwardOnAppear({
type: 'onAppear',
view: component
}, _this.props);
_this.completeTransition({
key: key,
noForwarding: true
});
var currentChildMapping = mapChildren(_this.props.children);
if (!currentChildMapping || !currentChildMapping.find(function (child) {
return child.key === key;
})) {
// This was removed before it had fully appeared. Remove it.
_this.performLeave(key);
}
};
_this.performEnter = function (key) {
_this.currentlyTransitioningKeys[key] = true;
var component = _this.groupRefs[key];
if (component.componentWillEnter) {
component.componentWillEnter(_this._handleDoneEntering.bind(_this, key));
} else {
_this._handleDoneEntering(key);
}
};
_this._handleDoneEntering = function (key) {
var component = _this.groupRefs[key];
if (component.componentDidEnter) {
component.componentDidEnter();
}
forwardOnEnter({
type: 'onEnter',
view: component
}, _this.props);
_this.completeTransition({
key: key
});
};
_this.performStay = function (key) {
var component = _this.groupRefs[key];
if (component.componentWillStay) {
component.componentWillStay(_this._handleDoneStaying.bind(_this, key));
} else {
_this._handleDoneStaying(key);
}
};
_this._handleDoneStaying = function (key) {
var component = _this.groupRefs[key];
if (component.componentDidStay) {
component.componentDidStay();
}
forwardOnStay({
type: 'onStay',
view: component
}, _this.props);
};
_this.performLeave = function (key) {
_this.currentlyTransitioningKeys[key] = true;
var component = _this.groupRefs[key];
if (component.componentWillLeave) {
component.componentWillLeave(_this._handleDoneLeaving.bind(_this, key));
} else {
// Note that this is somewhat dangerous b/c it calls setState()
// again, effectively mutating the component before all the work
// is done.
_this._handleDoneLeaving(key);
}
};
_this._handleDoneLeaving = function (key) {
var component = _this.groupRefs[key];
if (component.componentDidLeave) {
component.componentDidLeave();
}
forwardOnLeave({
type: 'onLeave',
view: component
}, _this.props);
_this.completeTransition({
key: key
});
_this.setState(function (state) {
var index = indexOfChild(key, state.children);
return {
children: (0, _remove["default"])(index, 1, state.children)
};
});
};
_this.storeRefs = function (key) {
return function (node) {
_this.groupRefs[key] = node;
};
};
/* This code is the same as @enact/core/WithRef. */
_this.getNodeRef = function () {
var _refNode$parentElemen;
var refNode = _this.nodeRef.current;
var attributeSelector = "[data-transitiongroup-id=\"".concat(refNode.getAttribute('data-transitiongroup-target'), "\"]");
/* The intended code is to search for the referrer element via a single querySelector call. But unit tests cannot handle :has() properly.
const selector = `:scope ${attributeSelector}, :scope :has(${attributeSelector})`;
return refNode?.parentElement?.querySelector(selector) || null;
*/
var targetNode = (refNode === null || refNode === void 0 || (_refNode$parentElemen = refNode.parentElement) === null || _refNode$parentElemen === void 0 ? void 0 : _refNode$parentElemen.querySelector(attributeSelector)) || null;
for (var current = targetNode; current; current = current.parentElement) {
var _current;
if (((_current = current) === null || _current === void 0 ? void 0 : _current.parentElement) === (refNode === null || refNode === void 0 ? void 0 : refNode.parentElement)) {
return current;
}
}
};
_this.state = {
firstRender: true,
children: []
};
_this.hasMounted = false;
_this.currentlyTransitioningKeys = {};
_this.keysToEnter = [];
_this.keysToLeave = [];
_this.keysToStay = [];
_this.groupRefs = {};
_this.nodeRef = /*#__PURE__*/(0, _react.createRef)();
_this.refNodeId = '#transition#group#';
return _this;
}
_inherits(TransitionGroup, _Component);
return _createClass(TransitionGroup, [{
key: "componentDidMount",
value: function componentDidMount() {
var _this2 = this;
this.hasMounted = true;
// this isn't used by ViewManager or View at the moment but leaving it around for future
// flexibility
this.state.children.forEach(function (child) {
return _this2.performAppear(child.key);
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
this.reconcileUnmountedChildren(prevState.children, this.state.children);
this.reconcileChildren(prevState.activeChildren, this.state.activeChildren);
}
}, {
key: "reconcileUnmountedChildren",
value: function reconcileUnmountedChildren(prevChildMapping, nextChildMapping) {
var _this3 = this;
var nextChildKeys = nextChildMapping.map(function (c) {
return c.key;
});
var prevChildKeys = prevChildMapping.map(function (c) {
return c.key;
});
// `state.children` represents the mounted children. if a view change happens during a
// transition causing the View to be unmounted before it fires its callback, the
// currentlyTransitioningKeys map will be out of sync. To manage that, we check for keys
// that have fallen out of the `children` array and manually clean them up from the map.
prevChildKeys.filter(function (key) {
return !nextChildKeys.includes(key);
}).forEach(function (key) {
return _this3.completeTransition({
key: key
});
});
}
}, {
key: "reconcileChildren",
value: function reconcileChildren(prevActiveChildMapping, nextActiveChildMapping) {
var _this4 = this;
var size = this.props.size;
var nextChildKeys = nextActiveChildMapping.map(function (c) {
return c.key;
});
var prevChildKeys = prevActiveChildMapping.map(function (c) {
return c.key;
});
var droppedKeys = prevChildKeys.filter(function (key) {
return !nextChildKeys.includes(key);
});
// if children haven't changed, there's nothing to reconcile
if (prevActiveChildMapping.length === nextActiveChildMapping.length && droppedKeys.length === 0) {
return;
}
// remove any "dropped" children from the list of transitioning children
droppedKeys.forEach(function (key) {
return _this4.completeTransition({
key: key
});
});
// mark any new child as entering
nextChildKeys.forEach(function (key, index) {
var hasPrev = prevChildKeys.includes(key);
if (!hasPrev || _this4.currentlyTransitioningKeys[key]) {
// flag a view to enter if it's new (!hasPrev), or if it's not new (hasPrev) but is
// re-entering (is currently transitioning)
_this4.keysToEnter.push(key);
} else if (index < size - 1) {
// keep views that are less than size minus the "transition out" buffer
_this4.keysToStay.push(key);
} else {
// everything else is leaving
_this4.keysToLeave.push(key);
}
});
// mark any previous child not remaining as leaving
prevChildKeys.forEach(function (key) {
var hasNext = nextChildKeys.includes(key);
var isRendered = Boolean(_this4.groupRefs[key]);
// flag a view to leave if it isn't in the new set (!hasNext) and it exists (isRendered)
if (!hasNext && isRendered) {
_this4.keysToLeave.push(key);
}
});
if (this.keysToEnter.length || this.keysToLeave.length) {
(0, _handle.forwardCustom)('onWillTransition')(null, this.props);
}
// once the component has been updated, start the enter transition for new children,
var keysToEnter = this.keysToEnter;
this.keysToEnter = [];
keysToEnter.forEach(this.performEnter);
// ... the stay transition for any children remaining,
var keysToStay = this.keysToStay;
this.keysToStay = [];
keysToStay.forEach(this.performStay);
// ... and the leave transition for departing children
var keysToLeave = this.keysToLeave;
this.keysToLeave = [];
keysToLeave.forEach(this.performLeave);
}
}, {
key: "completeTransition",
value: function completeTransition(_ref) {
var key = _ref.key,
_ref$noForwarding = _ref.noForwarding,
noForwarding = _ref$noForwarding === void 0 ? false : _ref$noForwarding;
if (key in this.currentlyTransitioningKeys) {
delete this.currentlyTransitioningKeys[key];
if (!noForwarding && Object.keys(this.currentlyTransitioningKeys).length === 0) {
(0, _handle.forwardCustom)('onTransition')(null, this.props);
}
}
}
}, {
key: "render",
value: function render() {
var _this5 = this;
// support wrapping arbitrary children with a component that supports the necessary
// lifecycle methods to animate transitions
var childrenToRender = this.state.children.map(function (child, index) {
var isLeaving = child.props['data-index'] !== _this5.props.currentIndex && typeof child.props['data-index'] !== 'undefined';
return /*#__PURE__*/(0, _react.cloneElement)(_this5.props.childFactory(child), {
key: child.key,
ref: _this5.storeRefs(child.key),
leaving: isLeaving,
appearing: !_this5.hasMounted,
getParentRef: _this5.getNodeRef,
renderedIndex: index
});
});
// Do not forward TransitionGroup props to primitive DOM nodes
var props = Object.assign({}, this.props);
props.ref = this.props.componentRef;
props['data-transitiongroup-id'] = this.refNodeId;
delete props.childFactory;
delete props.component;
delete props.componentRef;
delete props.currentIndex;
delete props.onAppear;
delete props.onEnter;
delete props.onLeave;
delete props.onStay;
delete props.onTransition;
delete props.onWillTransition;
delete props.size;
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [/*#__PURE__*/(0, _react.createElement)(this.props.component, props, childrenToRender), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
"data-transitiongroup-target": this.refNodeId,
ref: this.nodeRef,
style: {
display: 'none'
}
})]
});
}
}], [{
key: "getDerivedStateFromProps",
value: function getDerivedStateFromProps(props, state) {
var children = mapChildren(props.children).slice(0, props.size);
if (state.firstRender) {
return {
activeChildren: children,
children: children,
firstRender: false
};
}
return {
activeChildren: children,
children: mergeChildren(children, state.children).slice(0, props.size)
};
}
}]);
}(_react.Component);
TransitionGroup.propTypes = /** @lends ui/ViewManager.TransitionGroup.prototype */{
children: _propTypes2["default"].node.isRequired,
/**
* Adapts children to be compatible with TransitionGroup
*
* @type {Function}
*/
childFactory: _propTypes2["default"].func,
/**
* Type of component wrapping the children.
*
* May be a DOM node or a custom React component.
*
* @type {String|Component}
* @default 'div'
*/
component: _propTypes["default"].renderable,
/**
* Called with a reference to {@link ui/ViewManager.TransitionGroup.component|component}
*
* @type {Object|Function}
* @private
*/
componentRef: _propTypes["default"].ref,
/**
* Current Index the ViewManager is on
*
* @type {Number}
*/
currentIndex: _propTypes2["default"].number,
/**
* Called when each view is rendered during initial construction.
*
* @type {Function}
*/
onAppear: _propTypes2["default"].func,
/**
* Called when each view completes its transition into the viewport.
*
* @type {Function}
*/
onEnter: _propTypes2["default"].func,
/**
* Called when each view completes its transition out of the viewport.
*
* @type {Function}
*/
onLeave: _propTypes2["default"].func,
/**
* Called when each view completes its transition within the viewport.
*
* @type {Function}
*/
onStay: _propTypes2["default"].func,
/**
* Called once when all views have completed their transition.
*
* @type {Function}
*/
onTransition: _propTypes2["default"].func,
/**
* Called once before views begin their transition.
*
* @type {Function}
*/
onWillTransition: _propTypes2["default"].func,
/**
* Maximum number of rendered children.
*
* Used to limit how many visible transitions are active at any time.
* A value of 1 would prevent any exit transitions whereas a value of 2,
* the default, would ensure that only 1 view is transitioning on and 1 view is
* transitioning off at a time.
*
* @type {Number}
* @default 2
*/
size: _propTypes2["default"].number
};
TransitionGroup.defaultProps = {
childFactory: _identity["default"],
component: 'div',
size: 2
};
var _default = exports["default"] = TransitionGroup;
;