UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

408 lines (400 loc) 18.6 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn"; import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf"; import _inherits from "@babel/runtime/helpers/inherits"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; 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 _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } import React from 'react'; import { bind } from 'bind-event-listener'; import createFocusTrap from 'focus-trap'; import rafSchedule from 'raf-schd'; import { createPortal, flushSync } from 'react-dom'; import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { calculatePlacement, calculatePosition as _calculatePosition, findOverflowScrollParent, validatePosition } from './utils'; // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/react/no-class-components var Popup = /*#__PURE__*/function (_React$Component) { function Popup() { var _window; var _this; _classCallCheck(this, Popup); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _callSuper(this, Popup, [].concat(args)); _defineProperty(_this, "rafIds", new Set()); _defineProperty(_this, "state", { validPosition: true }); _defineProperty(_this, "popupRef", /*#__PURE__*/React.createRef()); _defineProperty(_this, "placement", ['', '']); _defineProperty(_this, "handleRef", function (popup) { if (!popup) { return; } _this.initPopup(popup); }); _defineProperty(_this, "scheduledUpdatePosition", rafSchedule(function (_props) { _this.updatePosition(_this.props); })); _defineProperty(_this, "onResize", function () { return _this.scheduledUpdatePosition(_this.props); }); _defineProperty(_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 */ _defineProperty(_this, "initFocusTrap", rafSchedule(function () { var popup = _this.popupRef.current; if (!popup) { return; } var defaultTrapConfig = { clickOutsideDeactivates: true, escapeDeactivates: true, initialFocus: popup, fallbackFocus: popup, returnFocusOnDeactivate: false }; var trapConfig = typeof _this.props.focusTrap === 'boolean' ? defaultTrapConfig : _objectSpread(_objectSpread({}, defaultTrapConfig), _this.props.focusTrap); _this.focusTrap = createFocusTrap(popup, editorExperiment('platform_editor_block_menu', true) ? trapConfig : defaultTrapConfig); _this.focusTrap.activate(); })); return _this; } _inherits(Popup, _React$Component); return _createClass(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, minPopupMargin = props.minPopupMargin, 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 = 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 = _calculatePosition({ placement: placement, popup: popup, target: target, stick: stick, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion offset: offset, allowOutOfBounds: allowOutOfBounds, rect: rect, boundariesElement: boundariesElement || document.body, minPopupMargin: minPopupMargin, scrollableElement: stick && (expValEquals('platform_editor_fix_scrolling_popup_position', 'isEnabled', true) || expValEquals('create_work_item_modernization_exp', 'isEnabled', true)) ? this.scrollElement : undefined }); 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: 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) { 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 && // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion !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) { if (this.popupRef.current) { var _this$resizeObserver; (_this$resizeObserver = this.resizeObserver) === null || _this$resizeObserver === void 0 || _this$resizeObserver.unobserve(this.popupRef.current); } this.popupRef.current = popup; var target = this.props.target; var overflowScrollParent = 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(); } if (this.popupRef.current) { var _this$resizeObserver2; (_this$resizeObserver2 = this.resizeObserver) === null || _this$resizeObserver2 === void 0 || _this$resizeObserver2.observe(this.popupRef.current); } } }, { 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(); } } /** * Idempotent scroll element setup — tears down any previous listener/observer * then attaches to the current scroll parent. */ }, { key: "initScrollElement", value: function initScrollElement() { var _this$unbindScroll; var _this$props = this.props, stick = _this$props.stick, target = _this$props.target, scrollableElement = _this$props.scrollableElement; (_this$unbindScroll = this.unbindScroll) === null || _this$unbindScroll === void 0 || _this$unbindScroll.call(this); if (this.scrollElement && this.resizeObserver) { this.resizeObserver.unobserve(this.scrollElement); } this.scrollElement = scrollableElement; if (stick && !this.scrollElement && target) { this.scrollElement = findOverflowScrollParent(target); } if (this.scrollElement) { if (this.resizeObserver) { this.resizeObserver.observe(this.scrollElement); } this.unbindScroll = bind(this.scrollElement, { type: 'scroll', listener: this.onResize }); } } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps) { this.handleChangedFocusTrapProp(prevProps); if ((expValEquals('platform_editor_fix_scrolling_popup_position', 'isEnabled', true) || expValEquals('create_work_item_modernization_exp', 'isEnabled', true)) && prevProps.scrollableElement !== this.props.scrollableElement) { this.initScrollElement(); } if (this.props !== prevProps) { this.scheduledUpdatePosition(prevProps); } } }, { key: "componentDidMount", value: function componentDidMount() { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners window.addEventListener('resize', this.onResize); if (expValEquals('platform_editor_fix_scrolling_popup_position', 'isEnabled', true) || expValEquals('create_work_item_modernization_exp', 'isEnabled', true)) { this.initScrollElement(); return; } var stick = this.props.stick; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion var target = this.props.target; var scrollParentElement = findOverflowScrollParent(target); if (scrollParentElement && this.resizeObserver) { this.resizeObserver.observe(scrollParentElement); } if (stick) { this.scrollElement = scrollParentElement; } else { this.scrollElement = this.props.scrollableElement; } if (this.scrollElement) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners this.scrollElement.addEventListener('scroll', this.onResize); } } }, { key: "componentWillUnmount", value: function componentWillUnmount() { var _this$unbindScroll2, _this$resizeObserver3; // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners window.removeEventListener('resize', this.onResize); (_this$unbindScroll2 = this.unbindScroll) === null || _this$unbindScroll2 === void 0 || _this$unbindScroll2.call(this); if (this.scrollElement) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners this.scrollElement.removeEventListener('scroll', this.onResize); } (_this$resizeObserver3 = this.resizeObserver) === null || _this$resizeObserver3 === void 0 || _this$resizeObserver3.disconnect(); this.scheduledUpdatePosition.cancel(); this.destroyFocusTrap(); var onUnmount = this.props.onUnmount; if (onUnmount) { onUnmount(); } } }, { key: "renderPopup", value: function renderPopup() { var _this$props$ariaLabel, _this3 = this; var position = this.state.position; var shouldRenderPopup = this.props.shouldRenderPopup; if (shouldRenderPopup && !shouldRenderPopup(position || {})) { return null; } /** * https://a11y.atlassian.net/browse/A11Y-9995 * We set aria-label to undefined if it's null, no more 'Popup' fallback. * It is meaningless for screen readers and causes confusion. */ var ariaLabel = fg('_editor_a11y_aria_label_removal_popup') ? (_this$props$ariaLabel = this.props.ariaLabel) !== null && _this$props$ariaLabel !== void 0 ? _this$props$ariaLabel : undefined : this.props.ariaLabel === null ? undefined : this.props.ariaLabel || 'Popup'; var getRole = function getRole() { // Provide a valid role only when aria-label is present to satisfy a11y rules, as when aria-label is present, role is required // use role = dialog as default role, as dialog role itself is not a parent role that requires specific children to function as some other ARIA roles(menu) do // if set default role to menu, tons of integration tests will fail as many of our popup usages do not have children that satisfy menu role requirements if (ariaLabel) { return _this3.props.role || 'dialog'; } return undefined; }; return /*#__PURE__*/React.createElement("div", { ref: this.handleRef, style: _objectSpread(_objectSpread({ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 position: 'absolute', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 zIndex: this.props.zIndex || akEditorFloatingPanelZIndex }, position), this.props.style), role: getRole(), "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$props2 = this.props, target = _this$props2.target, mountTo = _this$props2.mountTo; var validPosition = this.state.validPosition; if (!target || !validPosition) { return null; } if (mountTo) { return /*#__PURE__*/createPortal(this.renderPopup(), mountTo); } // Without mountTo property renders popup as is, // which means it will be cropped by "overflow: hidden" container. return this.renderPopup(); } }]); }(React.Component); _defineProperty(Popup, "defaultProps", { offset: [0, 0], allowOutOfBound: false }); export { Popup as default };