@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
335 lines (328 loc) • 15.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
Object.defineProperty(exports, "findOverflowScrollParent", {
enumerable: true,
get: function get() {
return _utils.findOverflowScrollParent;
}
});
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireDefault(require("react"));
var _focusTrap = _interopRequireDefault(require("focus-trap"));
var _rafSchd = _interopRequireDefault(require("raf-schd"));
var _reactDom = require("react-dom");
var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
var _utils = require("./utils");
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) { (0, _defineProperty2.default)(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 _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
var Popup = exports.default = /*#__PURE__*/function (_React$Component) {
(0, _inherits2.default)(Popup, _React$Component);
var _super = _createSuper(Popup);
function Popup() {
var _window;
var _this;
(0, _classCallCheck2.default)(this, Popup);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _super.call.apply(_super, [this].concat(args));
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "rafIds", new Set());
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "state", {
validPosition: true
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "popupRef", /*#__PURE__*/_react.default.createRef());
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "placement", ['', '']);
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "handleRef", function (popup) {
if (!popup) {
return;
}
_this.initPopup(popup);
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "scheduledUpdatePosition", (0, _rafSchd.default)(function (props) {
_this.updatePosition(_this.props);
}));
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onResize", function () {
return _this.scheduledUpdatePosition(_this.props);
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "resizeObserver", (_window = window) !== null && _window !== void 0 && _window.ResizeObserver ? new ResizeObserver(function () {
_this.scheduledUpdatePosition(_this.props);
}) : undefined);
/**
* Raf scheduled so that it also occurs after the initial update position
*/
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "initFocusTrap", (0, _rafSchd.default)(function () {
var popup = _this.popupRef.current;
if (!popup) {
return;
}
var trapConfig = {
clickOutsideDeactivates: true,
escapeDeactivates: true,
initialFocus: popup,
fallbackFocus: popup,
returnFocusOnDeactivate: false
};
_this.focusTrap = (0, _focusTrap.default)(popup, trapConfig);
_this.focusTrap.activate();
}));
return _this;
}
(0, _createClass2.default)(Popup, [{
key: "calculatePosition",
value:
/**
* Calculates new popup position
*/
function calculatePosition(props, popup) {
var target = props.target,
fitHeight = props.fitHeight,
fitWidth = props.fitWidth,
boundariesElement = props.boundariesElement,
offset = props.offset,
onPositionCalculated = props.onPositionCalculated,
onPlacementChanged = props.onPlacementChanged,
alignX = props.alignX,
alignY = props.alignY,
stick = props.stick,
forcePlacement = props.forcePlacement,
allowOutOfBounds = props.allowOutOfBounds,
rect = props.rect,
preventOverflow = props.preventOverflow,
absoluteOffset = props.absoluteOffset;
if (!target || !popup) {
return {};
}
var placement = (0, _utils.calculatePlacement)(target, boundariesElement || document.body, fitWidth, fitHeight, alignX, alignY, forcePlacement, preventOverflow);
if (onPlacementChanged && this.placement.join('') !== placement.join('')) {
onPlacementChanged(placement);
this.placement = placement;
}
var position = (0, _utils.calculatePosition)({
placement: placement,
popup: popup,
target: target,
stick: stick,
offset: offset,
allowOutOfBounds: allowOutOfBounds,
rect: rect
});
position = onPositionCalculated ? onPositionCalculated(position) : position;
if (typeof position.top !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.top) {
position.top = position.top + absoluteOffset.top;
}
if (typeof position.bottom !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.bottom) {
position.bottom = position.bottom + absoluteOffset.bottom;
}
if (typeof position.right !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.right) {
position.right = position.right + absoluteOffset.right;
}
if (typeof position.left !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.left) {
position.left = position.left + absoluteOffset.left;
}
return {
position: position,
validPosition: (0, _utils.validatePosition)(target)
};
}
}, {
key: "updatePosition",
value: function updatePosition() {
var _this2 = this;
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state;
var popup = state.popup;
var _this$calculatePositi = this.calculatePosition(props, popup),
position = _this$calculatePositi.position,
validPosition = _this$calculatePositi.validPosition;
if (position && validPosition) {
(0, _reactDom.flushSync)(function () {
_this2.setState({
position: position,
validPosition: validPosition
});
});
}
}
}, {
key: "cannotSetPopup",
value: function cannotSetPopup(popup, target, overflowScrollParent) {
/**
* Check whether:
* 1. Popup's offset targets which means whether or not its possible to correctly position popup along with given target.
* 2. Popup is inside "overflow: scroll" container, but its offset parent isn't.
*
* Currently Popup isn't capable of position itself correctly in case 2,
* Add "position: relative" to "overflow: scroll" container or to some other FloatingPanel wrapper inside it.
*/
return !target || document.body.contains(target) && popup.offsetParent && !popup.offsetParent.contains(target) || overflowScrollParent && !overflowScrollParent.contains(popup.offsetParent);
}
/**
* Popup initialization.
* Checks whether it's possible to position popup along given target, and if it's not throws an error.
*/
}, {
key: "initPopup",
value: function initPopup(popup) {
this.popupRef.current = popup;
var target = this.props.target;
var overflowScrollParent = (0, _utils.findOverflowScrollParent)(popup);
if (this.cannotSetPopup(popup, target, overflowScrollParent)) {
return;
}
this.setState({
popup: popup
});
/**
* Some plugins (like image) have async rendering of component in floating toolbar(which is popup).
* Now, floating toolbar position depends on it's size.
* Size of floating toolbar changes, when async component renders.
* There is currently, no way to re position floating toolbar or
* better to not show floating toolbar till all the async component are ready to render.
* Also, it is not even Popup's responsibility to take care of it as popup's children are passed
* as a prop.
* So, calling scheduledUpdatePosition to position popup on next request animation frame,
* which is currently working for most of the floating toolbar and other popups.
*/
this.scheduledUpdatePosition(this.props);
if (this.props.focusTrap) {
this.initFocusTrap();
}
}
}, {
key: "UNSAFE_componentWillReceiveProps",
value: function UNSAFE_componentWillReceiveProps(newProps) {
// We are delaying `updatePosition` otherwise it happens before the children
// get rendered and we end up with a wrong position
this.scheduledUpdatePosition(newProps);
}
}, {
key: "destroyFocusTrap",
value:
/**
* Cancels the initialisation of the focus trap if it has not yet occured
* Deactivates the focus trap if it exists
*/
function destroyFocusTrap() {
var _this$focusTrap;
this.initFocusTrap.cancel();
(_this$focusTrap = this.focusTrap) === null || _this$focusTrap === void 0 || _this$focusTrap.deactivate();
}
/**
* Handle pausing, unpausing, and initialising (if not yet initialised) of the focus trap
*/
}, {
key: "handleChangedFocusTrapProp",
value: function handleChangedFocusTrapProp(prevProps) {
if (prevProps.focusTrap !== this.props.focusTrap) {
// If currently set to disable, then pause the trap if it exists
if (!this.props.focusTrap) {
var _this$focusTrap2;
return (_this$focusTrap2 = this.focusTrap) === null || _this$focusTrap2 === void 0 ? void 0 : _this$focusTrap2.pause();
}
// If set to enabled and trap already exists, unpause
if (this.focusTrap) {
this.focusTrap.unpause();
}
// Else initialise the focus trap
return this.initFocusTrap();
}
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
this.handleChangedFocusTrapProp(prevProps);
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
window.addEventListener('resize', this.onResize);
var stick = this.props.stick;
this.scrollParentElement = (0, _utils.findOverflowScrollParent)(this.props.target);
if (this.scrollParentElement && this.resizeObserver) {
this.resizeObserver.observe(this.scrollParentElement);
}
if (stick) {
this.scrollElement = this.scrollParentElement;
} else {
this.scrollElement = this.props.scrollableElement;
}
if (this.scrollElement) {
this.scrollElement.addEventListener('scroll', this.onResize);
}
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
if (this.scrollElement) {
this.scrollElement.removeEventListener('scroll', this.onResize);
}
if (this.scrollParentElement && this.resizeObserver) {
this.resizeObserver.unobserve(this.scrollParentElement);
}
this.scheduledUpdatePosition.cancel();
this.destroyFocusTrap();
var onUnmount = this.props.onUnmount;
if (onUnmount) {
onUnmount();
}
}
}, {
key: "renderPopup",
value: function renderPopup() {
var position = this.state.position;
var shouldRenderPopup = this.props.shouldRenderPopup;
if (shouldRenderPopup && !shouldRenderPopup(position || {})) {
return null;
}
//In some cases we don't want to use default "Popup" text as an aria-label. It might be tedious for screen reader users.
var ariaLabel = this.props.ariaLabel === null ? undefined : this.props.ariaLabel || 'Popup';
return /*#__PURE__*/_react.default.createElement("div", {
ref: this.handleRef,
style: _objectSpread(_objectSpread({
position: 'absolute',
zIndex: this.props.zIndex || _editorSharedStyles.akEditorFloatingPanelZIndex
}, position), this.props.style),
"aria-label": ariaLabel,
"data-testid": "popup-wrapper"
// Indicates component is an editor pop. Required for focus handling in Message.tsx
,
"data-editor-popup": true
}, this.props.children);
}
}, {
key: "render",
value: function render() {
var _this$props = this.props,
target = _this$props.target,
mountTo = _this$props.mountTo;
var validPosition = this.state.validPosition;
if (!target || !validPosition) {
return null;
}
if (mountTo) {
return /*#__PURE__*/(0, _reactDom.createPortal)(this.renderPopup(), mountTo);
}
// Without mountTo property renders popup as is,
// which means it will be cropped by "overflow: hidden" container.
return this.renderPopup();
}
}]);
return Popup;
}(_react.default.Component);
(0, _defineProperty2.default)(Popup, "defaultProps", {
offset: [0, 0],
allowOutOfBound: false
});