@atlaskit/editor-plugin-block-menu 15.0.11 → 15.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-plugin-block-menu
2
2
 
3
+ ## 15.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 15.0.12
10
+
11
+ ### Patch Changes
12
+
13
+ - [`91559dfb45b53`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/91559dfb45b53) -
14
+ Behind platform_editor_blocks_patch_8, make the block menu popup conditionally sticky after Move
15
+ up / Move down: the editor no longer scrolls while the moved block's drag handle stays visible,
16
+ and scrolls by the minimum needed to bring the drag handle, block and popup back into view -
17
+ realigning the popup to it - when the handle crosses a fold. The scroll distance accounts for the
18
+ popup's own height, so a popup taller than the moved block is no longer cut off at the fold.
19
+
3
20
  ## 15.0.11
4
21
 
5
22
  ### Patch Changes
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.useBlockMenu = exports.BlockMenuProvider = void 0;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
10
+ var _fixBlockMenuPositionAndScroll = require("./utils/fixBlockMenuPositionAndScroll");
9
11
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
10
12
  var BlockMenuContext = /*#__PURE__*/(0, _react.createContext)({
11
13
  onDropdownOpenChanged: function onDropdownOpenChanged() {},
@@ -13,6 +15,15 @@ var BlockMenuContext = /*#__PURE__*/(0, _react.createContext)({
13
15
  moveUpRef: /*#__PURE__*/_react.default.createRef(),
14
16
  getFirstSelectedDomNode: function getFirstSelectedDomNode() {
15
17
  return undefined;
18
+ },
19
+ getMovedBlockDomNode: function getMovedBlockDomNode() {
20
+ return undefined;
21
+ },
22
+ getSelectedBlockDomNode: function getSelectedBlockDomNode() {
23
+ return undefined;
24
+ },
25
+ anchorMetricsRef: {
26
+ current: undefined
16
27
  }
17
28
  });
18
29
  var useBlockMenu = exports.useBlockMenu = function useBlockMenu() {
@@ -28,6 +39,7 @@ var BlockMenuProvider = exports.BlockMenuProvider = function BlockMenuProvider(_
28
39
  editorView = _ref.editorView;
29
40
  var moveUpRef = (0, _react.useRef)(null);
30
41
  var moveDownRef = (0, _react.useRef)(null);
42
+ var anchorMetricsRef = (0, _react.useRef)(undefined);
31
43
  var getFirstSelectedDomNode = (0, _react.useCallback)(function () {
32
44
  var _api$selection;
33
45
  var from = api === null || api === void 0 || (_api$selection = api.selection) === null || _api$selection === void 0 || (_api$selection = _api$selection.sharedState.currentState()) === null || _api$selection === void 0 || (_api$selection = _api$selection.selection) === null || _api$selection === void 0 ? void 0 : _api$selection.from;
@@ -38,25 +50,74 @@ var BlockMenuProvider = exports.BlockMenuProvider = function BlockMenuProvider(_
38
50
  }
39
51
  }
40
52
  }, [api, editorView]);
53
+ var getTopLevelBlockDomNodeAt = (0, _react.useCallback)(function (from) {
54
+ if (!editorView) {
55
+ return;
56
+ }
57
+ var nodeDOM = editorView.nodeDOM(from);
58
+ var domAtPos = nodeDOM instanceof Element ? nodeDOM : editorView.domAtPos(from).node;
59
+ var element = domAtPos instanceof Element ? domAtPos : domAtPos.parentElement;
60
+ if (!element || !editorView.dom.contains(element)) {
61
+ return;
62
+ }
63
+ var block = element;
64
+ while (block.parentElement && block.parentElement !== editorView.dom) {
65
+ block = block.parentElement;
66
+ }
67
+ return block;
68
+ }, [editorView]);
69
+ var getSelectedBlockDomNode = (0, _react.useCallback)(function () {
70
+ var _api$blockControls, _ref2, _blockControlsState$a, _blockControlsState$a2, _blockControlsState$m, _blockControlsState$p;
71
+ var blockControlsState = api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState();
72
+ // The editor selection is not necessarily in the block the menu was opened for.
73
+ var from = (_ref2 = (_blockControlsState$a = blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$a2 = blockControlsState.activeNode) === null || _blockControlsState$a2 === void 0 ? void 0 : _blockControlsState$a2.pos) !== null && _blockControlsState$a !== void 0 ? _blockControlsState$a : blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$m = blockControlsState.menuTriggerByNode) === null || _blockControlsState$m === void 0 ? void 0 : _blockControlsState$m.pos) !== null && _ref2 !== void 0 ? _ref2 : blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$p = blockControlsState.preservedSelection) === null || _blockControlsState$p === void 0 ? void 0 : _blockControlsState$p.from;
74
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
75
+ }, [api, getTopLevelBlockDomNodeAt]);
76
+ var getMovedBlockDomNode = (0, _react.useCallback)(function () {
77
+ // A move transaction maps its preserved selection onto the node it just moved, so this is
78
+ // where that node ended up - unlike the block menu state, which keeps stale positions.
79
+ var from = editorView === null || editorView === void 0 ? void 0 : editorView.state.selection.from;
80
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
81
+ }, [editorView, getTopLevelBlockDomNodeAt]);
41
82
  var onDropdownOpenChanged = (0, _react.useCallback)(function (isOpen) {
42
- if (!isOpen) {
43
- // On Dropdown closed, return focus to editor
44
- setTimeout(function () {
45
- return requestAnimationFrame(function () {
46
- api === null || api === void 0 || api.core.actions.focus({
47
- scrollIntoView: false
83
+ anchorMetricsRef.current = undefined;
84
+ if (isOpen) {
85
+ if ((0, _fg.fg)('platform_editor_blocks_patch_8')) {
86
+ // The popup lays itself out next to the drag handle over the next frame or two.
87
+ var remainingFrames = 3;
88
+ var _captureAnchorMetrics = function captureAnchorMetrics() {
89
+ requestAnimationFrame(function () {
90
+ anchorMetricsRef.current = (0, _fixBlockMenuPositionAndScroll.getBlockMenuAnchorMetrics)(getSelectedBlockDomNode());
91
+ remainingFrames -= 1;
92
+ if (!anchorMetricsRef.current && remainingFrames > 0) {
93
+ _captureAnchorMetrics();
94
+ }
48
95
  });
49
- });
50
- }, 1);
96
+ };
97
+ _captureAnchorMetrics();
98
+ }
99
+ return;
51
100
  }
52
- }, [api]);
101
+
102
+ // On Dropdown closed, return focus to editor
103
+ setTimeout(function () {
104
+ return requestAnimationFrame(function () {
105
+ api === null || api === void 0 || api.core.actions.focus({
106
+ scrollIntoView: false
107
+ });
108
+ });
109
+ }, 1);
110
+ }, [api, getSelectedBlockDomNode]);
53
111
  return /*#__PURE__*/_react.default.createElement(BlockMenuContext.Provider, {
54
112
  // eslint-disable-next-line @atlassian/perf-linting/no-inline-context-value, @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
55
113
  value: {
56
114
  onDropdownOpenChanged: onDropdownOpenChanged,
57
115
  moveDownRef: moveDownRef,
58
116
  moveUpRef: moveUpRef,
59
- getFirstSelectedDomNode: getFirstSelectedDomNode
117
+ getFirstSelectedDomNode: getFirstSelectedDomNode,
118
+ getMovedBlockDomNode: getMovedBlockDomNode,
119
+ getSelectedBlockDomNode: getSelectedBlockDomNode,
120
+ anchorMetricsRef: anchorMetricsRef
60
121
  }
61
122
  }, children);
62
123
  };
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", {
9
9
  exports.default = void 0;
10
10
  require("./block-menu.compiled.css");
11
11
  var _runtime = require("@compiled/react/runtime");
12
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
12
13
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
13
14
  var _react = _interopRequireWildcard(require("react"));
14
15
  var _reactIntl = require("react-intl");
@@ -33,6 +34,8 @@ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
33
34
  var _blockMenuProvider = require("./block-menu-provider");
34
35
  var _BlockMenuRenderer = require("./block-menu-renderer/BlockMenuRenderer");
35
36
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
37
+ 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; }
38
+ 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; }
36
39
  var styles = {
37
40
  base: "_2rko12b0 _bfhk1bhr _16qs130s",
38
41
  maxWidthStyles: "_p12fnklw",
@@ -266,8 +269,9 @@ var BlockMenu = function BlockMenu(_ref5) {
266
269
  if (!isMenuOpen) {
267
270
  return;
268
271
  }
272
+ onDropdownOpenChanged(true);
269
273
  setMenuHeight(((_popupRef$current = popupRef.current) === null || _popupRef$current === void 0 ? void 0 : _popupRef$current.clientHeight) || FALLBACK_MENU_HEIGHT);
270
- }, [isMenuOpen]);
274
+ }, [isMenuOpen, onDropdownOpenChanged]);
271
275
  var hasFocus = (_ref8 = (editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) ||
272
276
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
273
277
  document.activeElement === targetHandleRef || popupRef.current && (
@@ -356,6 +360,23 @@ var BlockMenu = function BlockMenu(_ref5) {
356
360
  stick: true
357
361
  // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
358
362
  ,
363
+ onPositionCalculated: (0, _fg.fg)('platform_editor_blocks_patch_8') ? function (position) {
364
+ if (position.left === undefined) {
365
+ // Already placed on the gutter side (using `right`) - nothing to correct.
366
+ return position;
367
+ }
368
+ // Popup prefers whichever side has more room, which flips the menu onto the
369
+ // content side when the gutter is too narrow to fit it. Force it back onto the
370
+ // gutter side instead - it can still overlap the content once there, but it
371
+ // must not start on top of the node. `position.left` is shifted by the same
372
+ // distance regardless of the offset parent/scroll it was calculated against,
373
+ // since that just moves both by the same amount.
374
+ return _objectSpread(_objectSpread({}, position), {}, {
375
+ left: position.left - DEFAULT_MENU_WIDTH - _styles.DRAG_HANDLE_WIDTH - 2 * DRAG_HANDLE_OFFSET_PADDING
376
+ });
377
+ } : undefined
378
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
379
+ ,
359
380
  offset: [_styles.DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, targetHandleHeightOffset],
360
381
  focusTrap: openedViaKeyboard ?
361
382
  // Only enable focus trap when opened via keyboard to make sure the focus is on the first focusable menu item
@@ -16,6 +16,7 @@ var _messages = require("@atlaskit/editor-common/messages");
16
16
  var _types = require("@atlaskit/editor-common/types");
17
17
  var _editorToolbar = require("@atlaskit/editor-toolbar");
18
18
  var _arrowDown = _interopRequireDefault(require("@atlaskit/icon/core/arrow-down"));
19
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
19
20
  var _blockMenuProvider = require("./block-menu-provider");
20
21
  var _consts = require("./consts");
21
22
  var _fixBlockMenuPositionAndScroll = require("./utils/fixBlockMenuPositionAndScroll");
@@ -27,7 +28,10 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
27
28
  var _useBlockMenu = (0, _blockMenuProvider.useBlockMenu)(),
28
29
  moveUpRef = _useBlockMenu.moveUpRef,
29
30
  moveDownRef = _useBlockMenu.moveDownRef,
30
- getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode;
31
+ getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
32
+ getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
33
+ getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
34
+ anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
31
35
  var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['blockControls'], function (_ref2) {
32
36
  var _blockControlsState$b;
33
37
  var blockControlsState = _ref2.blockControlsState;
@@ -45,6 +49,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
45
49
  }
46
50
  }, [canMoveDown, moveUpRef, moveDownRef]);
47
51
  var handleClick = function handleClick() {
52
+ var positionSnapshot = (0, _fg.fg)('platform_editor_blocks_patch_8') ? (0, _fixBlockMenuPositionAndScroll.getBlockMenuPositionSnapshot)(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
48
53
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
49
54
  var _api$analytics, _api$blockControls;
50
55
  var tr = _ref3.tr;
@@ -62,10 +67,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
62
67
  });
63
68
  return tr;
64
69
  });
65
- requestAnimationFrame(function () {
66
- var newFirstNode = getFirstSelectedDomNode();
67
- (0, _fixBlockMenuPositionAndScroll.fixBlockMenuPositionAndScroll)(newFirstNode);
68
- });
70
+ (0, _fixBlockMenuPositionAndScroll.scheduleBlockMenuPositionFix)(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
69
71
  };
70
72
  return /*#__PURE__*/_react.default.createElement(_editorToolbar.ToolbarDropdownItem, {
71
73
  triggerRef: moveDownRef,
@@ -16,6 +16,7 @@ var _messages = require("@atlaskit/editor-common/messages");
16
16
  var _types = require("@atlaskit/editor-common/types");
17
17
  var _editorToolbar = require("@atlaskit/editor-toolbar");
18
18
  var _arrowUp = _interopRequireDefault(require("@atlaskit/icon/core/arrow-up"));
19
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
19
20
  var _blockMenuProvider = require("./block-menu-provider");
20
21
  var _consts = require("./consts");
21
22
  var _fixBlockMenuPositionAndScroll = require("./utils/fixBlockMenuPositionAndScroll");
@@ -27,7 +28,10 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
27
28
  var _useBlockMenu = (0, _blockMenuProvider.useBlockMenu)(),
28
29
  moveUpRef = _useBlockMenu.moveUpRef,
29
30
  moveDownRef = _useBlockMenu.moveDownRef,
30
- getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode;
31
+ getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
32
+ getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
33
+ getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
34
+ anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
31
35
  var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['blockControls'], function (_ref2) {
32
36
  var _blockControlsState$b;
33
37
  var blockControlsState = _ref2.blockControlsState;
@@ -43,6 +47,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
43
47
  }
44
48
  }, [canMoveUp, moveDownRef, moveUpRef]);
45
49
  var handleClick = function handleClick() {
50
+ var positionSnapshot = (0, _fg.fg)('platform_editor_blocks_patch_8') ? (0, _fixBlockMenuPositionAndScroll.getBlockMenuPositionSnapshot)(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
46
51
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
47
52
  var _api$analytics, _api$blockControls;
48
53
  var tr = _ref3.tr;
@@ -60,10 +65,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
60
65
  });
61
66
  return tr;
62
67
  });
63
- requestAnimationFrame(function () {
64
- var newFirstNode = getFirstSelectedDomNode();
65
- (0, _fixBlockMenuPositionAndScroll.fixBlockMenuPositionAndScroll)(newFirstNode);
66
- });
68
+ (0, _fixBlockMenuPositionAndScroll.scheduleBlockMenuPositionFix)(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
67
69
  };
68
70
  return /*#__PURE__*/_react.default.createElement(_editorToolbar.ToolbarDropdownItem, {
69
71
  triggerRef: moveUpRef,
@@ -1,12 +1,166 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
- exports.fixBlockMenuPositionAndScroll = void 0;
7
+ exports.scheduleBlockMenuPositionFix = exports.getScrollDistanceToRevealAnchor = exports.getBlockMenuPositionSnapshot = exports.getBlockMenuAnchorMetrics = exports.fixBlockMenuPositionAndScroll = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
7
9
  var _browserApis = require("@atlaskit/browser-apis");
8
10
  var _blockMenu = require("@atlaskit/editor-common/block-menu");
9
- var fixBlockMenuPositionAndScroll = exports.fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScroll(firstSelectedNode) {
11
+ var _styles = require("@atlaskit/editor-common/styles");
12
+ var _fg = require("@atlaskit/platform-feature-flags/fg");
13
+ 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; }
14
+ 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; }
15
+ var POPUP_WRAPPER_TEST_ID = 'popup-wrapper';
16
+ var EDITOR_CONTENT_CONTAINER_SELECTOR = '[data-testid="editor-content-container"]';
17
+ /**
18
+ * Breathing room left between the revealed block and the fold it just crossed, so the block does
19
+ * not end up flush against the edge of the viewport.
20
+ */
21
+ var REVEAL_MARGIN = 8;
22
+
23
+ /**
24
+ * Where the drag handle and the popup sit relative to their block. The drag handle is removed from
25
+ * the DOM as soon as the pointer moves onto the block menu, so this has to be measured while the
26
+ * menu opens rather than when a menu item is clicked.
27
+ */
28
+
29
+ /**
30
+ * Minimum scroll needed to bring the drag handle back inside the viewport.
31
+ * Zero means the handle is already fully visible.
32
+ */
33
+ var getScrollDistanceToRevealAnchor = exports.getScrollDistanceToRevealAnchor = function getScrollDistanceToRevealAnchor(dragHandleTop, dragHandleHeight, viewport) {
34
+ if (dragHandleTop < viewport.top) {
35
+ return dragHandleTop - viewport.top;
36
+ }
37
+ var dragHandleBottom = dragHandleTop + dragHandleHeight;
38
+ if (dragHandleBottom > viewport.bottom) {
39
+ return dragHandleBottom - viewport.bottom;
40
+ }
41
+ return 0;
42
+ };
43
+
44
+ /**
45
+ * The editor content container is not always the element that actually scrolls - depending on the
46
+ * product it can be an ancestor, or the document itself. Pick the closest one that really scrolls.
47
+ */
48
+ var getScrollContainer = function getScrollContainer(doc, selectedBlock) {
49
+ var editorContentContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
50
+ if (editorContentContainer && editorContentContainer.scrollHeight > editorContentContainer.clientHeight) {
51
+ return editorContentContainer;
52
+ }
53
+ var ancestor = selectedBlock.parentElement;
54
+ while (ancestor) {
55
+ var _doc$defaultView;
56
+ var overflowY = (_doc$defaultView = doc.defaultView) === null || _doc$defaultView === void 0 ? void 0 : _doc$defaultView.getComputedStyle(ancestor).overflowY;
57
+ if ((overflowY === 'auto' || overflowY === 'scroll') && ancestor.scrollHeight > ancestor.clientHeight) {
58
+ return ancestor;
59
+ }
60
+ ancestor = ancestor.parentElement;
61
+ }
62
+ return doc.scrollingElement;
63
+ };
64
+ var getScrollViewport = function getScrollViewport(doc, scrollContainer) {
65
+ if (scrollContainer === doc.scrollingElement) {
66
+ var _doc$defaultView$inne, _doc$defaultView2;
67
+ return {
68
+ bottom: (_doc$defaultView$inne = (_doc$defaultView2 = doc.defaultView) === null || _doc$defaultView2 === void 0 ? void 0 : _doc$defaultView2.innerHeight) !== null && _doc$defaultView$inne !== void 0 ? _doc$defaultView$inne : 0,
69
+ top: 0
70
+ };
71
+ }
72
+ var _scrollContainer$getB = scrollContainer.getBoundingClientRect(),
73
+ top = _scrollContainer$getB.top,
74
+ bottom = _scrollContainer$getB.bottom;
75
+ return {
76
+ bottom: bottom,
77
+ top: top
78
+ };
79
+ };
80
+
81
+ /**
82
+ * Float based node views - a media group is one - collapse their top level wrapper to zero height,
83
+ * so the visual bottom of the block has to come from its children.
84
+ */
85
+ var getBlockBottom = function getBlockBottom(block) {
86
+ var rect = block.getBoundingClientRect();
87
+ if (rect.height > 0) {
88
+ return rect.bottom;
89
+ }
90
+ return Array.from(block.children).reduce(function (bottom, child) {
91
+ return Math.max(bottom, child.getBoundingClientRect().bottom);
92
+ }, rect.bottom);
93
+ };
94
+ var setPopupTop = function setPopupTop(blockMenuEl, targetTop) {
95
+ var _blockMenuEl$closest;
96
+ var popupWrapper = (_blockMenuEl$closest = blockMenuEl.closest("[data-testid=\"".concat(POPUP_WRAPPER_TEST_ID, "\"]"))) !== null && _blockMenuEl$closest !== void 0 ? _blockMenuEl$closest : blockMenuEl.parentElement;
97
+ if (!(popupWrapper instanceof HTMLElement)) {
98
+ return;
99
+ }
100
+ var distance = blockMenuEl.getBoundingClientRect().top - targetTop;
101
+ var hasTopProperty = popupWrapper.style.top !== '';
102
+ var hasBottomProperty = popupWrapper.style.bottom !== '';
103
+ if (hasBottomProperty && !hasTopProperty) {
104
+ popupWrapper.style.bottom = "".concat(parseFloat(popupWrapper.style.bottom || '0') + distance, "px");
105
+ } else {
106
+ popupWrapper.style.top = "".concat(parseFloat(popupWrapper.style.top || '0') - distance, "px");
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Measured while the menu opens, when the popup is still aligned to the drag handle. Moves that
112
+ * keep the handle visible deliberately leave the popup where it is, so this alignment - not the
113
+ * drifted one - is what the popup is restored to when a fold is crossed.
114
+ */
115
+ var getBlockMenuAnchorMetrics = exports.getBlockMenuAnchorMetrics = function getBlockMenuAnchorMetrics(selectedBlock) {
116
+ var _doc$querySelector;
117
+ var doc = (0, _browserApis.getDocument)();
118
+ var blockMenuEl = doc === null || doc === void 0 ? void 0 : doc.querySelector("[data-testid=\"".concat(_blockMenu.BLOCK_MENU_TEST_ID, "\"]"));
119
+ if (!doc || !selectedBlock || !blockMenuEl) {
120
+ return;
121
+ }
122
+ var blockMenuRect = blockMenuEl.getBoundingClientRect();
123
+ var dragHandleRect = (_doc$querySelector = doc.querySelector(_styles.DRAG_HANDLE_SELECTOR)) === null || _doc$querySelector === void 0 ? void 0 : _doc$querySelector.getBoundingClientRect();
124
+ if (blockMenuRect.height === 0 || !(dragHandleRect !== null && dragHandleRect !== void 0 && dragHandleRect.height)) {
125
+ return;
126
+ }
127
+ var blockTop = selectedBlock.getBoundingClientRect().top;
128
+ return {
129
+ blockHeight: getBlockBottom(selectedBlock) - blockTop,
130
+ dragHandleHeight: dragHandleRect.height,
131
+ dragHandleOffsetFromBlock: dragHandleRect.top - blockTop,
132
+ popupOffsetFromBlock: blockMenuRect.top - blockTop,
133
+ popupHeight: blockMenuRect.height
134
+ };
135
+ };
136
+
137
+ /**
138
+ * Captured before the move transaction, so the popup can be realigned to the block after it.
139
+ */
140
+ var getBlockMenuPositionSnapshot = exports.getBlockMenuPositionSnapshot = function getBlockMenuPositionSnapshot(selectedBlock, anchorMetrics) {
141
+ var doc = (0, _browserApis.getDocument)();
142
+ if (!doc || !selectedBlock) {
143
+ return;
144
+ }
145
+ var scrollContainer = getScrollContainer(doc, selectedBlock);
146
+ var metrics = anchorMetrics !== null && anchorMetrics !== void 0 ? anchorMetrics : getBlockMenuAnchorMetrics(selectedBlock);
147
+ if (!scrollContainer || !metrics) {
148
+ return;
149
+ }
150
+ return _objectSpread(_objectSpread({}, metrics), {}, {
151
+ scrollContainer: scrollContainer,
152
+ scrollTop: scrollContainer.scrollTop
153
+ });
154
+ };
155
+
156
+ /**
157
+ * Called after a move up/down transaction.
158
+ *
159
+ * With a `positionSnapshot` the popup is only sticky when it has to be: the editor stays put while
160
+ * the moved node's drag handle is still visible, and scrolls the minimum needed to reveal the
161
+ * handle otherwise - realigning the popup to the revealed handle.
162
+ */
163
+ var fixBlockMenuPositionAndScroll = exports.fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScroll(selectedNode, positionSnapshot) {
10
164
  var doc = (0, _browserApis.getDocument)();
11
165
  if (!doc) {
12
166
  return;
@@ -15,13 +169,54 @@ var fixBlockMenuPositionAndScroll = exports.fixBlockMenuPositionAndScroll = func
15
169
  if (!(blockMenuEl !== null && blockMenuEl !== void 0 && blockMenuEl.parentElement)) {
16
170
  return;
17
171
  }
18
- var scrollableContainer = doc.querySelector('[data-testid="editor-content-container"]');
19
- if (!firstSelectedNode || !scrollableContainer) {
172
+ if (positionSnapshot) {
173
+ if (!selectedNode) {
174
+ return;
175
+ }
176
+ // Reuse the container resolved when the snapshot was taken - re-resolving it independently
177
+ // here could pick a different element (e.g. for a node near an overflow boundary), applying
178
+ // positionSnapshot.scrollTop to the wrong container.
179
+ var scrollContainer = positionSnapshot.scrollContainer;
180
+
181
+ // The move transaction scrolls the new selection into view - undo that, only a drag handle
182
+ // leaving the viewport is allowed to move the editor.
183
+ scrollContainer.scrollTo({
184
+ behavior: 'instant',
185
+ top: positionSnapshot.scrollTop
186
+ });
187
+ var viewport = getScrollViewport(doc, scrollContainer);
188
+ var anchorTop = selectedNode.getBoundingClientRect().top + positionSnapshot.dragHandleOffsetFromBlock;
189
+
190
+ // Whether to scroll at all is decided by the drag handle alone - a block taller than the
191
+ // viewport is expected to be cut off while its handle stays usable.
192
+ if (getScrollDistanceToRevealAnchor(anchorTop, positionSnapshot.dragHandleHeight, viewport) === 0) {
193
+ return;
194
+ }
195
+
196
+ // Once we do scroll, reveal the block and the popup, not just the handle sized sliver of it.
197
+ // popupOffsetFromBlock is relative to the block - re-express it relative to the handle (the
198
+ // anchor everything else here is measured from) to build one span covering all three. The
199
+ // handle itself (0 to dragHandleHeight) always stays inside that span, so its visibility is
200
+ // never sacrificed to make room for the block or the popup.
201
+ var popupTopFromHandle = positionSnapshot.popupOffsetFromBlock - positionSnapshot.dragHandleOffsetFromBlock;
202
+ var revealTop = Math.min(0, popupTopFromHandle);
203
+ var revealBottom = Math.max(positionSnapshot.dragHandleHeight, positionSnapshot.blockHeight - positionSnapshot.dragHandleOffsetFromBlock, popupTopFromHandle + positionSnapshot.popupHeight);
204
+ var revealHeight = Math.min(revealBottom - revealTop, Math.max(viewport.bottom - viewport.top - 2 * REVEAL_MARGIN, 0));
205
+ var scrollDistance = getScrollDistanceToRevealAnchor(anchorTop + revealTop - REVEAL_MARGIN, revealHeight + 2 * REVEAL_MARGIN, viewport);
206
+ scrollContainer.scrollBy({
207
+ behavior: 'instant',
208
+ top: scrollDistance
209
+ });
210
+ setPopupTop(blockMenuEl, selectedNode.getBoundingClientRect().top + positionSnapshot.popupOffsetFromBlock);
211
+ return;
212
+ }
213
+ var scrollableContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
214
+ if (!selectedNode || !scrollableContainer) {
20
215
  return;
21
216
  }
22
217
  var parentElement = blockMenuEl.parentElement;
23
218
  var currentTop = parentElement.getBoundingClientRect().top;
24
- var distance = firstSelectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
219
+ var distance = selectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
25
220
  scrollableContainer.scrollBy({
26
221
  behavior: 'instant',
27
222
  top: distance
@@ -37,4 +232,22 @@ var fixBlockMenuPositionAndScroll = exports.fixBlockMenuPositionAndScroll = func
37
232
  var currentTopValue = parseFloat(parentElement.style.top || '0');
38
233
  parentElement.style.top = "".concat(currentTopValue + topDifference, "px");
39
234
  }
235
+ };
236
+
237
+ /**
238
+ * Shared by move-up.tsx and move-down.tsx: schedules the post-move fix for the frame after
239
+ * ProseMirror has re-rendered.
240
+ *
241
+ * Falls back to the pre-PR always-scroll path (matching gate-off, via `getFirstSelectedDomNode`)
242
+ * whenever a `positionSnapshot` wasn't captured - e.g. a media/file thumbnail reporting zero height
243
+ * for a frame when the menu opened - rather than silently doing nothing.
244
+ */
245
+ var scheduleBlockMenuPositionFix = exports.scheduleBlockMenuPositionFix = function scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode) {
246
+ requestAnimationFrame(function () {
247
+ if ((0, _fg.fg)('platform_editor_blocks_patch_8') && positionSnapshot) {
248
+ fixBlockMenuPositionAndScroll(getMovedBlockDomNode(), positionSnapshot);
249
+ return;
250
+ }
251
+ fixBlockMenuPositionAndScroll(getFirstSelectedDomNode());
252
+ });
40
253
  };
@@ -1,9 +1,16 @@
1
1
  import React, { useCallback, createContext, useContext, useRef } from 'react';
2
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
3
+ import { getBlockMenuAnchorMetrics } from './utils/fixBlockMenuPositionAndScroll';
2
4
  const BlockMenuContext = /*#__PURE__*/createContext({
3
5
  onDropdownOpenChanged: () => {},
4
6
  moveDownRef: /*#__PURE__*/React.createRef(),
5
7
  moveUpRef: /*#__PURE__*/React.createRef(),
6
- getFirstSelectedDomNode: () => undefined
8
+ getFirstSelectedDomNode: () => undefined,
9
+ getMovedBlockDomNode: () => undefined,
10
+ getSelectedBlockDomNode: () => undefined,
11
+ anchorMetricsRef: {
12
+ current: undefined
13
+ }
7
14
  });
8
15
  export const useBlockMenu = () => {
9
16
  const context = useContext(BlockMenuContext);
@@ -19,6 +26,7 @@ export const BlockMenuProvider = ({
19
26
  }) => {
20
27
  const moveUpRef = useRef(null);
21
28
  const moveDownRef = useRef(null);
29
+ const anchorMetricsRef = useRef(undefined);
22
30
  const getFirstSelectedDomNode = useCallback(() => {
23
31
  var _api$selection, _api$selection$shared, _api$selection$shared2;
24
32
  const from = api === null || api === void 0 ? void 0 : (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : (_api$selection$shared = _api$selection.sharedState.currentState()) === null || _api$selection$shared === void 0 ? void 0 : (_api$selection$shared2 = _api$selection$shared.selection) === null || _api$selection$shared2 === void 0 ? void 0 : _api$selection$shared2.from;
@@ -29,23 +37,72 @@ export const BlockMenuProvider = ({
29
37
  }
30
38
  }
31
39
  }, [api, editorView]);
40
+ const getTopLevelBlockDomNodeAt = useCallback(from => {
41
+ if (!editorView) {
42
+ return;
43
+ }
44
+ const nodeDOM = editorView.nodeDOM(from);
45
+ const domAtPos = nodeDOM instanceof Element ? nodeDOM : editorView.domAtPos(from).node;
46
+ const element = domAtPos instanceof Element ? domAtPos : domAtPos.parentElement;
47
+ if (!element || !editorView.dom.contains(element)) {
48
+ return;
49
+ }
50
+ let block = element;
51
+ while (block.parentElement && block.parentElement !== editorView.dom) {
52
+ block = block.parentElement;
53
+ }
54
+ return block;
55
+ }, [editorView]);
56
+ const getSelectedBlockDomNode = useCallback(() => {
57
+ var _api$blockControls, _ref, _blockControlsState$a, _blockControlsState$a2, _blockControlsState$m, _blockControlsState$p;
58
+ const blockControlsState = api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState();
59
+ // The editor selection is not necessarily in the block the menu was opened for.
60
+ const from = (_ref = (_blockControlsState$a = blockControlsState === null || blockControlsState === void 0 ? void 0 : (_blockControlsState$a2 = blockControlsState.activeNode) === null || _blockControlsState$a2 === void 0 ? void 0 : _blockControlsState$a2.pos) !== null && _blockControlsState$a !== void 0 ? _blockControlsState$a : blockControlsState === null || blockControlsState === void 0 ? void 0 : (_blockControlsState$m = blockControlsState.menuTriggerByNode) === null || _blockControlsState$m === void 0 ? void 0 : _blockControlsState$m.pos) !== null && _ref !== void 0 ? _ref : blockControlsState === null || blockControlsState === void 0 ? void 0 : (_blockControlsState$p = blockControlsState.preservedSelection) === null || _blockControlsState$p === void 0 ? void 0 : _blockControlsState$p.from;
61
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
62
+ }, [api, getTopLevelBlockDomNodeAt]);
63
+ const getMovedBlockDomNode = useCallback(() => {
64
+ // A move transaction maps its preserved selection onto the node it just moved, so this is
65
+ // where that node ended up - unlike the block menu state, which keeps stale positions.
66
+ const from = editorView === null || editorView === void 0 ? void 0 : editorView.state.selection.from;
67
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
68
+ }, [editorView, getTopLevelBlockDomNodeAt]);
32
69
  const onDropdownOpenChanged = useCallback(isOpen => {
33
- if (!isOpen) {
34
- // On Dropdown closed, return focus to editor
35
- setTimeout(() => requestAnimationFrame(() => {
36
- api === null || api === void 0 ? void 0 : api.core.actions.focus({
37
- scrollIntoView: false
38
- });
39
- }), 1);
70
+ anchorMetricsRef.current = undefined;
71
+ if (isOpen) {
72
+ if (fg('platform_editor_blocks_patch_8')) {
73
+ // The popup lays itself out next to the drag handle over the next frame or two.
74
+ let remainingFrames = 3;
75
+ const captureAnchorMetrics = () => {
76
+ requestAnimationFrame(() => {
77
+ anchorMetricsRef.current = getBlockMenuAnchorMetrics(getSelectedBlockDomNode());
78
+ remainingFrames -= 1;
79
+ if (!anchorMetricsRef.current && remainingFrames > 0) {
80
+ captureAnchorMetrics();
81
+ }
82
+ });
83
+ };
84
+ captureAnchorMetrics();
85
+ }
86
+ return;
40
87
  }
41
- }, [api]);
88
+
89
+ // On Dropdown closed, return focus to editor
90
+ setTimeout(() => requestAnimationFrame(() => {
91
+ api === null || api === void 0 ? void 0 : api.core.actions.focus({
92
+ scrollIntoView: false
93
+ });
94
+ }), 1);
95
+ }, [api, getSelectedBlockDomNode]);
42
96
  return /*#__PURE__*/React.createElement(BlockMenuContext.Provider, {
43
97
  // eslint-disable-next-line @atlassian/perf-linting/no-inline-context-value, @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
44
98
  value: {
45
99
  onDropdownOpenChanged,
46
100
  moveDownRef,
47
101
  moveUpRef,
48
- getFirstSelectedDomNode
102
+ getFirstSelectedDomNode,
103
+ getMovedBlockDomNode,
104
+ getSelectedBlockDomNode,
105
+ anchorMetricsRef
49
106
  }
50
107
  }, children);
51
108
  };