UNPKG

@enact/ui

Version:

A collection of simplified unstyled cross-platform UI components for Enact

503 lines (488 loc) 20.1 kB
"use strict"; 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;