@enact/ui
Version:
A collection of simplified unstyled cross-platform UI components for Enact
606 lines (596 loc) • 25.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Transition = Transition;
exports["default"] = exports.TransitionBase = void 0;
var _handle = require("@enact/core/handle");
var _propTypes = _interopRequireDefault(require("@enact/core/internal/prop-types"));
var _kind = _interopRequireDefault(require("@enact/core/kind"));
var _util = require("@enact/core/util");
var _react = require("react");
var _propTypes2 = _interopRequireDefault(require("prop-types"));
var _Resizable = require("../Resizable");
var _TransitionModule = _interopRequireDefault(require("./Transition.module.css"));
var _jsxRuntime = require("react/jsx-runtime");
var _excluded = ["css", "childRef", "children", "innerStyle"],
_excluded2 = ["children", "direction", "duration", "noAnimation", "onHide", "onShow", "timingFunction", "type", "visible"];
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, 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); } /**
* A component that can transition its children components onto the screen.
*
* Transitions whether that's from off the edge of the screen or hidden inside or behind an
* already-on-screen component. You can switch types of transitions using the `type` property,
* change the direction they come in from using the `direction` property, or even adjust the
* transition timing function using `timingFunction`.
*
* @example
* <Transition visible={true} type="slide">
* <div>Set `visible` above to `false` to hide this element.</div>
* </Transition>
*
* @module ui/Transition
* @exports Transition
* @exports TransitionBase
*/
var formatter = function formatter(duration) {
return typeof duration === 'number' ? duration + 'ms' : duration;
};
// Shared empty style object reused when no per-type tweaks are needed.
var EMPTY_STYLE = {};
/**
* The stateless structure of the component.
*
* In case you want to provide all of the state yourself.
* In general, you'll probably want to use the `Transition` instead of `TransitionBase`.
*
* @class TransitionBase
* @ui
* @memberof ui/Transition
* @public
*/
var TransitionBase = exports.TransitionBase = (0, _kind["default"])({
name: 'TransitionBase',
propTypes: /** @lends ui/Transition.TransitionBase.prototype */{
/**
* Provide a function to get the reference to the child node (the one with the content) at
* render time.
*
*Useful if you need to measure or interact with the node directly.
*
* @type {Object|Function}
* @default null
* @public
*/
childRef: _propTypes["default"].ref,
/**
* The node to be transitioned.
*
* @type {Node}
* @public
*/
children: _propTypes2["default"].node,
/**
* The height of the transition when `type` is set to `'clip'`, used when direction is
* 'left' or 'right'.
*
* @type {Number}
* @default null
* @public
*/
clipHeight: _propTypes2["default"].number,
/**
* The width of the transition when `type` is set to `'clip'`, used when direction is 'left'
* or 'right'.
*
* @type {Number}
* @default null
* @public
*/
clipWidth: _propTypes2["default"].number,
/**
* Customizes the component by mapping the supplied collection of CSS class names to the
* corresponding internal elements and states of this component.
*
* The following classes are supported:
*
* * `transition` - The root component class
* * `inner` - The element inside the transition. This is the container for the transitioning content.
* * `shown` - Applied when content is present (visible), related to the `visible` prop/state
* * `hidden` - Applied when content is not present (hiding), related to the `visible` prop/state
* * `slide` - Applied when the `slide` `type` is set
* * `fade` - Applied when the `fade` `type` is set
* * `clip` - Applied when the `clip` `type` is set
* * `up` - Applied when the `direction` `up` is set
* * `right` - Applied when the `direction` `right` is set
* * `down` - Applied when the `direction` `down` is set
* * `left` - Applied when the `direction` `left` is set
* * `short` - Applied when the `duration` `short` is set
* * `medium` - Applied when the `duration` `medium` is set
* * `long` - Applied when the `duration` `long` is set
* * `ease` - Applied when the `timingFunction` `ease` is set
* * `ease-in` - Applied when the `timingFunction` `ease-in` is set
* * `ease-out` - Applied when the `timingFunction` `ease-out` is set
* * `ease-in-out` - Applied when the `timingFunction` `ease-in-out` is set
* * `ease-in-quart` - Applied when the `timingFunction` `ease-in-quart` is set
* * `ease-out-quart` - Applied when the `timingFunction` `ease-out-quart` is set
* * `linear` - Applied when the `timingFunction` `linear` is set
*
* @type {Object}
* @public
*/
css: _propTypes2["default"].object,
/**
* Sets the direction of transition. Where the component will move *to*; the destination.
* Supported directions are: `'up'`, `'right'`, `'down'`, `'left'`.
*
* @type {String}
* @default 'up'
* @public
*/
direction: _propTypes2["default"].oneOf(['up', 'right', 'down', 'left']),
/**
* Controls how long the transition should take.
* Supported preset durations are: `'short'` (250ms), `'medium'` (500ms), and `'long'` (1s).
* `'medium'` (500ms) is default when no others are specified.
* Any valid CSS duration value is also accepted, e.g. "200ms" or "3s". Pure numeric values
* are also supported and treated as milliseconds.
*
* @type {String|Number}
* @default 'medium'
* @public
*/
duration: _propTypes2["default"].oneOfType([_propTypes2["default"].string, _propTypes2["default"].number]),
/**
* Disables transition animation.
*
* When `false`, visibility changes animate.
*
* @type {Boolean}
* @default false
* @public
*/
noAnimation: _propTypes2["default"].bool,
/**
* The transition timing function.
*
* * Supported function names are: `ease`, `ease-in`, `ease-out`, `ease-in-out`, `ease-in-quart`,
* `ease-out-quart`, and `linear`.
*
* @type {String}
* @default 'ease-in-out'
* @public
*/
timingFunction: _propTypes2["default"].oneOf(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'ease-in-quart', 'ease-out-quart', 'linear']),
/**
* The type of transition to affect the content.
*
* * Supported types are: `'slide'`, `'clip'`, and `'fade'`.
*
* Details on types:
* * `'slide'` - Typically used for bringing something which is off the edge of the screen,
* and not visible, onto the screen. Think of a popup, toast, notification, dialog, or
* an overlaying menu. This requires no re-rendering or repainting of the screen during
* the transition, making it very performant. However, this does not affect layout at
* all, which makes it less useful for transitioning from a place already on the
* screen.
* * `'clip'` - This is useful for showing a component that transitions-in from a location
* that is already on the screen. Examples would be an expanding header or an
* accordion. This type does affect layout, its current size will push other sibling
* elements to make room for itself. Because of this, repainting the layout does happen
* during transition.
* * `'fade'` - Fade the components onto the screen, from 0 opacity (completely invisible)
* to 1 (full visibility). Pretty basic, but useful for fading on/off a tooltip, a
* menu, a panel, or even view contents. This does not affect layout at all.
*
* @type {String}
* @default 'slide'
* @public
*/
type: _propTypes2["default"].oneOf(['slide', 'clip', 'fade']),
/**
* Sets the visibility of the component, which determines whether it's on screen or off.
*
* @type {Boolean}
* @default true
* @public
*/
visible: _propTypes2["default"].bool
},
defaultProps: {
noAnimation: false,
direction: 'up',
duration: 'medium',
timingFunction: 'ease-in-out',
type: 'slide',
visible: true
},
styles: {
css: _TransitionModule["default"],
className: 'transition',
publicClassNames: true
},
computed: {
className: function className(_ref) {
var css = _ref.css,
direction = _ref.direction,
duration = _ref.duration,
noAnimation = _ref.noAnimation,
timingFunction = _ref.timingFunction,
type = _ref.type,
visible = _ref.visible,
styler = _ref.styler;
return styler.append(visible ? 'shown' : 'hidden', direction && css[direction], !noAnimation && duration && css[duration], !noAnimation && timingFunction && css[timingFunction], css[type]);
},
innerStyle: function innerStyle(_ref2) {
var clipWidth = _ref2.clipWidth,
css = _ref2.css,
direction = _ref2.direction,
duration = _ref2.duration,
type = _ref2.type;
if (type === 'clip' && (direction === 'left' || direction === 'right')) {
return {
width: clipWidth
};
}
if ((type === 'fade' || type === 'slide') && duration && !css[duration]) {
// If it's a number, assume it's milliseconds, if not, assume it's already a CSS duration string (like "200ms" or "2s")
return {
transitionDuration: formatter(duration)
};
}
return EMPTY_STYLE;
},
style: function style(_ref3) {
var clipHeight = _ref3.clipHeight,
css = _ref3.css,
direction = _ref3.direction,
duration = _ref3.duration,
type = _ref3.type,
visible = _ref3.visible,
_style = _ref3.style;
if (type !== 'clip') return _style;
var merged = _style ? _objectSpread(_objectSpread({}, _style), {}, {
overflow: 'hidden'
}) : {
overflow: 'hidden'
};
if (visible && (direction === 'up' || direction === 'down')) {
merged.height = clipHeight;
}
// If duration isn't a known named string, assume it is a CSS duration value
if (duration && !css[duration]) {
// If it's a number, assume it's milliseconds, if not, assume it's already a CSS duration string (like "200ms" or "2s")
merged.transitionDuration = formatter(duration);
}
return merged;
}
},
render: function render(_ref4) {
var css = _ref4.css,
childRef = _ref4.childRef,
children = _ref4.children,
innerStyle = _ref4.innerStyle,
rest = _objectWithoutProperties(_ref4, _excluded);
delete rest.clipHeight;
delete rest.clipWidth;
delete rest.direction;
delete rest.duration;
delete rest.noAnimation;
delete rest.timingFunction;
delete rest.type;
delete rest.visible;
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", _objectSpread(_objectSpread({}, rest), {}, {
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
className: css.inner,
style: innerStyle,
ref: childRef,
children: children
})
}));
}
});
var TRANSITION_STATE = {
INIT: 0,
// closed and unmeasured
MEASURE: 1,
// open but need to measure
READY: 2 // measured and ready
};
/**
* A stateful component that allows for applying transitions to its child items via configurable
* properties and events.
*
* @function Transition
* @ui
* @memberof ui/Transition
* @public
*/
function Transition(_ref5) {
var children = _ref5.children,
_ref5$direction = _ref5.direction,
direction = _ref5$direction === void 0 ? 'up' : _ref5$direction,
_ref5$duration = _ref5.duration,
duration = _ref5$duration === void 0 ? 'medium' : _ref5$duration,
_ref5$noAnimation = _ref5.noAnimation,
noAnimation = _ref5$noAnimation === void 0 ? false : _ref5$noAnimation,
onHide = _ref5.onHide,
onShow = _ref5.onShow,
_ref5$timingFunction = _ref5.timingFunction,
timingFunction = _ref5$timingFunction === void 0 ? 'ease-in-out' : _ref5$timingFunction,
_ref5$type = _ref5.type,
type = _ref5$type === void 0 ? 'slide' : _ref5$type,
_ref5$visible = _ref5.visible,
visible = _ref5$visible === void 0 ? true : _ref5$visible,
rest = _objectWithoutProperties(_ref5, _excluded2);
var _useState = (0, _react.useState)(function () {
return {
initialHeight: null,
initialWidth: null,
renderState: visible ? TRANSITION_STATE.READY : TRANSITION_STATE.INIT
};
}),
_useState2 = _slicedToArray(_useState, 2),
state = _useState2[0],
setState = _useState2[1];
var prevVisibleRef = (0, _react.useRef)(visible);
// Adjust state during render when `visible` flips on. The functional equivalent of the
// class's getDerivedStateFromProps. Reading/writing the ref here is intentional.
// https://react.dev/reference/react/useState#storing-information-from-previous-renders
if (!prevVisibleRef.current && visible) {
// eslint-disable-line react-hooks/refs
prevVisibleRef.current = true;
setState({
initialHeight: null,
initialWidth: null,
renderState: TRANSITION_STATE.MEASURE
});
}
// Latest props for handlers that fire asynchronously (transitionend, observer callbacks)
var currentProps = _objectSpread({
children: children,
direction: direction,
duration: duration,
noAnimation: noAnimation,
onHide: onHide,
onShow: onShow,
timingFunction: timingFunction,
type: type,
visible: visible
}, rest);
(0, _util.checkPropTypes)(Transition, currentProps);
var propsRef = (0, _react.useRef)();
propsRef.current = currentProps; // eslint-disable-line react-hooks/refs
var childNodeRef = (0, _react.useRef)(null);
var measuringJobRef = (0, _react.useRef)(null);
if (measuringJobRef.current == null) {
measuringJobRef.current = new _util.Job(function () {
setState(function (prev) {
return prev.renderState === TRANSITION_STATE.MEASURE ? prev : _objectSpread(_objectSpread({}, prev), {}, {
renderState: TRANSITION_STATE.MEASURE
});
});
});
}
var measureInner = (0, _react.useCallback)(function () {
var node = childNodeRef.current;
if (!node) return;
var initialHeight = node.scrollHeight;
var initialWidth = node.scrollWidth;
setState(function (prev) {
if (prev.initialHeight === initialHeight && prev.initialWidth === initialWidth && prev.renderState === TRANSITION_STATE.READY) {
return prev;
}
return {
initialHeight: initialHeight,
initialWidth: initialWidth,
renderState: TRANSITION_STATE.READY
};
});
}, []);
var childRef = (0, _react.useCallback)(function (node) {
childNodeRef.current = node;
}, []);
// Measurement effect — covers MEASURE → READY commits and the resize-driven
// re-measure path (handleResize nulls initialHeight, this re-fires).
(0, _react.useLayoutEffect)(function () {
if (state.renderState === TRANSITION_STATE.MEASURE) {
measuringJobRef.current.stop();
measureInner();
} else if (visible && state.initialHeight == null) {
// Component should be visible but doesn't have a height — measure now.
measureInner();
}
}, [state.renderState, state.initialHeight, visible, measureInner]);
(0, _react.useEffect)(function () {
var job = measuringJobRef.current;
if (!propsRef.current.visible) {
job.idle();
}
return function () {
return job.stop();
};
}, []);
// ResizeContext registration — the context value is itself the register fn.
var resizeRegister = (0, _react.use)(_Resizable.ResizeContext);
var handleResize = (0, _react.useCallback)(function () {
// Null initialHeight so the next layout commit re-measures in a separate tick.
// the useLayoutEffect above re-runs when state.initialHeight transitions to null.
setState(function (prev) {
return prev.initialHeight == null ? prev : _objectSpread(_objectSpread({}, prev), {}, {
initialHeight: null
});
});
}, []);
(0, _react.useEffect)(function () {
if (typeof resizeRegister !== 'function') return;
var registry = resizeRegister(handleResize);
return function () {
if (registry) registry.unregister();
};
}, [resizeRegister, handleResize]);
var prevVisibleForEffectRef = (0, _react.useRef)(visible);
(0, _react.useEffect)(function () {
var prev = prevVisibleForEffectRef.current;
prevVisibleForEffectRef.current = visible;
if (!noAnimation || prev === visible) return;
if (!prev && visible) {
(0, _handle.forwardCustom)('onShow')(null, propsRef.current);
} else if (prev && !visible) {
(0, _handle.forwardCustom)('onHide')(null, propsRef.current);
}
}, [visible, noAnimation]);
var handleTransitionEnd = (0, _react.useCallback)(function (ev) {
(0, _handle.forward)('onTransitionEnd', ev, propsRef.current);
if (ev.target === childNodeRef.current) {
var eventName = propsRef.current.visible ? 'onShow' : 'onHide';
(0, _handle.forward)(eventName, {
type: eventName,
currentTarget: ev.currentTarget
}, propsRef.current);
}
}, []);
// If we are deferring children, don't render any.
if (state.renderState === TRANSITION_STATE.INIT) {
return null;
}
var childProps = _objectSpread(_objectSpread({}, rest), {}, {
children: children,
direction: direction,
duration: duration,
noAnimation: noAnimation,
timingFunction: timingFunction,
type: type
});
// If we're transitioning to visible but don't have a measurement yet, create
// the transition container with its children so we can measure. Measuring
// will cause a state change to trigger the animation.
if (state.renderState === TRANSITION_STATE.MEASURE) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(TransitionBase, _objectSpread(_objectSpread({}, childProps), {}, {
childRef: childRef,
visible: false
}));
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)(TransitionBase, _objectSpread(_objectSpread({}, childProps), {}, {
childRef: childRef,
visible: visible,
clipHeight: state.initialHeight,
clipWidth: state.initialWidth,
onTransitionEnd: handleTransitionEnd
}));
}
Transition.displayName = 'Transition';
Transition.propTypes = /** @lends ui/Transition.Transition.prototype */{
/**
* The node to be transitioned.
*
* @type {Node}
* @public
*/
children: _propTypes2["default"].node,
/**
* The direction of transition (i.e. where the component will move *to*; the destination).
*
* * Supported directions are: `'up'`, `'right'`, `'down'`, `'left'`.
*
* @type {String}
* @default 'up'
* @public
*/
direction: _propTypes2["default"].oneOf(['up', 'right', 'down', 'left']),
/**
* Controls how long the transition should take.
*
* * Supported preset durations are: `'short'` (250ms), `'medium'` (500ms), and `'long'` (1s).
* `'medium'` (500ms) is default when no others are specified.
*
* Any valid CSS duration value is also accepted, e.g. "200ms" or "3s". Pure numeric values
* are also supported and treated as milliseconds.
*
* @type {String|Number}
* @default 'medium'
* @public
*/
duration: _propTypes2["default"].oneOfType([_propTypes2["default"].string, _propTypes2["default"].number]),
/**
* Disables transition animation.
*
* When `false`, visibility changes animate.
*
* @type {Boolean}
* @default false
* @public
*/
noAnimation: _propTypes2["default"].bool,
/**
* Called after transition for hiding is finished.
*
* @type {Function}
* @public
*/
onHide: _propTypes2["default"].func,
/**
* Called after transition for showing is finished.
*
* @type {Function}
* @public
*/
onShow: _propTypes2["default"].func,
/**
* The transition timing function.
* Supported function names are: `ease`, `ease-in`, `ease-out`, `ease-in-out`, `ease-in-quart`,
* `ease-out-quart`, and `linear`.
*
* @type {String}
* @default 'ease-in-out'
* @public
*/
timingFunction: _propTypes2["default"].oneOf(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'ease-in-quart', 'ease-out-quart', 'linear']),
/**
* The type of transition to affect the content.
*
* * Supported types are: `'slide'`, `'clip'`, and `'fade'`.
*
* Details on types:
* * `'slide'` - Typically used for bringing something which is off the edge of the screen,
* and not visible, onto the screen. Think of a popup, toast, notification, dialog, or
* an overlaying menu. This requires no re-rendering or repainting of the screen during
* the transition, making it very performant. However, this does not affect layout at
* all, which makes it less useful for transitioning from a place already on the
* screen.
* * `'clip'` - This is useful for showing a component that transitions-in from a location
* that is already on the screen. Examples would be an expanding header or an
* accordion. This type does affect layout, its current size will push other sibling
* elements to make room for itself. Because of this, repainting the layout does happen
* during transition.
* * `'fade'` - Fade the components onto the screen, from 0 opacity (completely invisible)
* to 1 (full visibility). Pretty basic, but useful for fading on/off a tooltip, a
* menu, a panel, or even view contents. This does not affect layout at all.
*
* @type {String}
* @default 'slide'
* @public
*/
type: _propTypes2["default"].oneOf(['slide', 'clip', 'fade']),
/**
* The visibility of the component, which determines whether it's on the screen or off.
*
* @type {Boolean}
* @default true
* @public
*/
visible: _propTypes2["default"].bool
};
var _default = exports["default"] = Transition;