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

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.
@@ -249,8 +249,9 @@ const BlockMenu = ({
249
249
  if (!isMenuOpen) {
250
250
  return;
251
251
  }
252
+ onDropdownOpenChanged(true);
252
253
  setMenuHeight(((_popupRef$current = popupRef.current) === null || _popupRef$current === void 0 ? void 0 : _popupRef$current.clientHeight) || FALLBACK_MENU_HEIGHT);
253
- }, [isMenuOpen]);
254
+ }, [isMenuOpen, onDropdownOpenChanged]);
254
255
  const hasFocus = (_ref2 = (editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) ||
255
256
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
256
257
  document.activeElement === targetHandleRef || popupRef.current && (
@@ -339,6 +340,24 @@ const BlockMenu = ({
339
340
  stick: true
340
341
  // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
341
342
  ,
343
+ onPositionCalculated: fg('platform_editor_blocks_patch_8') ? position => {
344
+ if (position.left === undefined) {
345
+ // Already placed on the gutter side (using `right`) - nothing to correct.
346
+ return position;
347
+ }
348
+ // Popup prefers whichever side has more room, which flips the menu onto the
349
+ // content side when the gutter is too narrow to fit it. Force it back onto the
350
+ // gutter side instead - it can still overlap the content once there, but it
351
+ // must not start on top of the node. `position.left` is shifted by the same
352
+ // distance regardless of the offset parent/scroll it was calculated against,
353
+ // since that just moves both by the same amount.
354
+ return {
355
+ ...position,
356
+ left: position.left - DEFAULT_MENU_WIDTH - DRAG_HANDLE_WIDTH - 2 * DRAG_HANDLE_OFFSET_PADDING
357
+ };
358
+ } : undefined
359
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
360
+ ,
342
361
  offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, targetHandleHeightOffset],
343
362
  focusTrap: openedViaKeyboard ?
344
363
  // Only enable focus trap when opened via keyboard to make sure the focus is on the first focusable menu item
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
8
8
  import { DIRECTION } from '@atlaskit/editor-common/types';
9
9
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
10
10
  import ArrowDownIcon from '@atlaskit/icon/core/arrow-down';
11
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
11
12
  import { useBlockMenu } from './block-menu-provider';
12
13
  import { BLOCK_MENU_ITEM_NAME } from './consts';
13
- import { fixBlockMenuPositionAndScroll } from './utils/fixBlockMenuPositionAndScroll';
14
+ import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
14
15
  const MoveDownDropdownItemContent = ({
15
16
  api
16
17
  }) => {
@@ -20,7 +21,10 @@ const MoveDownDropdownItemContent = ({
20
21
  const {
21
22
  moveUpRef,
22
23
  moveDownRef,
23
- getFirstSelectedDomNode
24
+ getFirstSelectedDomNode,
25
+ getMovedBlockDomNode,
26
+ getSelectedBlockDomNode,
27
+ anchorMetricsRef
24
28
  } = useBlockMenu();
25
29
  const {
26
30
  canMoveDown
@@ -41,6 +45,7 @@ const MoveDownDropdownItemContent = ({
41
45
  }
42
46
  }, [canMoveDown, moveUpRef, moveDownRef]);
43
47
  const handleClick = () => {
48
+ const positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
44
49
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
45
50
  tr
46
51
  }) => {
@@ -59,10 +64,7 @@ const MoveDownDropdownItemContent = ({
59
64
  });
60
65
  return tr;
61
66
  });
62
- requestAnimationFrame(() => {
63
- const newFirstNode = getFirstSelectedDomNode();
64
- fixBlockMenuPositionAndScroll(newFirstNode);
65
- });
67
+ scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
66
68
  };
67
69
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
68
70
  triggerRef: moveDownRef,
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
8
8
  import { DIRECTION } from '@atlaskit/editor-common/types';
9
9
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
10
10
  import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
11
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
11
12
  import { useBlockMenu } from './block-menu-provider';
12
13
  import { BLOCK_MENU_ITEM_NAME } from './consts';
13
- import { fixBlockMenuPositionAndScroll } from './utils/fixBlockMenuPositionAndScroll';
14
+ import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
14
15
  const MoveUpDropdownItemContent = ({
15
16
  api
16
17
  }) => {
@@ -20,7 +21,10 @@ const MoveUpDropdownItemContent = ({
20
21
  const {
21
22
  moveUpRef,
22
23
  moveDownRef,
23
- getFirstSelectedDomNode
24
+ getFirstSelectedDomNode,
25
+ getMovedBlockDomNode,
26
+ getSelectedBlockDomNode,
27
+ anchorMetricsRef
24
28
  } = useBlockMenu();
25
29
  const {
26
30
  canMoveUp
@@ -39,6 +43,7 @@ const MoveUpDropdownItemContent = ({
39
43
  }
40
44
  }, [canMoveUp, moveDownRef, moveUpRef]);
41
45
  const handleClick = () => {
46
+ const positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
42
47
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
43
48
  tr
44
49
  }) => {
@@ -57,10 +62,7 @@ const MoveUpDropdownItemContent = ({
57
62
  });
58
63
  return tr;
59
64
  });
60
- requestAnimationFrame(() => {
61
- const newFirstNode = getFirstSelectedDomNode();
62
- fixBlockMenuPositionAndScroll(newFirstNode);
63
- });
65
+ scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
64
66
  };
65
67
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
66
68
  triggerRef: moveUpRef,
@@ -1,6 +1,156 @@
1
1
  import { getDocument } from '@atlaskit/browser-apis';
2
2
  import { BLOCK_MENU_TEST_ID } from '@atlaskit/editor-common/block-menu';
3
- export const fixBlockMenuPositionAndScroll = firstSelectedNode => {
3
+ import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
4
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
5
+ const POPUP_WRAPPER_TEST_ID = 'popup-wrapper';
6
+ const EDITOR_CONTENT_CONTAINER_SELECTOR = '[data-testid="editor-content-container"]';
7
+ /**
8
+ * Breathing room left between the revealed block and the fold it just crossed, so the block does
9
+ * not end up flush against the edge of the viewport.
10
+ */
11
+ const REVEAL_MARGIN = 8;
12
+
13
+ /**
14
+ * Where the drag handle and the popup sit relative to their block. The drag handle is removed from
15
+ * the DOM as soon as the pointer moves onto the block menu, so this has to be measured while the
16
+ * menu opens rather than when a menu item is clicked.
17
+ */
18
+
19
+ /**
20
+ * Minimum scroll needed to bring the drag handle back inside the viewport.
21
+ * Zero means the handle is already fully visible.
22
+ */
23
+ export const getScrollDistanceToRevealAnchor = (dragHandleTop, dragHandleHeight, viewport) => {
24
+ if (dragHandleTop < viewport.top) {
25
+ return dragHandleTop - viewport.top;
26
+ }
27
+ const dragHandleBottom = dragHandleTop + dragHandleHeight;
28
+ if (dragHandleBottom > viewport.bottom) {
29
+ return dragHandleBottom - viewport.bottom;
30
+ }
31
+ return 0;
32
+ };
33
+
34
+ /**
35
+ * The editor content container is not always the element that actually scrolls - depending on the
36
+ * product it can be an ancestor, or the document itself. Pick the closest one that really scrolls.
37
+ */
38
+ const getScrollContainer = (doc, selectedBlock) => {
39
+ const editorContentContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
40
+ if (editorContentContainer && editorContentContainer.scrollHeight > editorContentContainer.clientHeight) {
41
+ return editorContentContainer;
42
+ }
43
+ let ancestor = selectedBlock.parentElement;
44
+ while (ancestor) {
45
+ var _doc$defaultView;
46
+ const overflowY = (_doc$defaultView = doc.defaultView) === null || _doc$defaultView === void 0 ? void 0 : _doc$defaultView.getComputedStyle(ancestor).overflowY;
47
+ if ((overflowY === 'auto' || overflowY === 'scroll') && ancestor.scrollHeight > ancestor.clientHeight) {
48
+ return ancestor;
49
+ }
50
+ ancestor = ancestor.parentElement;
51
+ }
52
+ return doc.scrollingElement;
53
+ };
54
+ const getScrollViewport = (doc, scrollContainer) => {
55
+ if (scrollContainer === doc.scrollingElement) {
56
+ var _doc$defaultView$inne, _doc$defaultView2;
57
+ return {
58
+ 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,
59
+ top: 0
60
+ };
61
+ }
62
+ const {
63
+ top,
64
+ bottom
65
+ } = scrollContainer.getBoundingClientRect();
66
+ return {
67
+ bottom,
68
+ top
69
+ };
70
+ };
71
+
72
+ /**
73
+ * Float based node views - a media group is one - collapse their top level wrapper to zero height,
74
+ * so the visual bottom of the block has to come from its children.
75
+ */
76
+ const getBlockBottom = block => {
77
+ const rect = block.getBoundingClientRect();
78
+ if (rect.height > 0) {
79
+ return rect.bottom;
80
+ }
81
+ return Array.from(block.children).reduce((bottom, child) => Math.max(bottom, child.getBoundingClientRect().bottom), rect.bottom);
82
+ };
83
+ const setPopupTop = (blockMenuEl, targetTop) => {
84
+ var _blockMenuEl$closest;
85
+ const popupWrapper = (_blockMenuEl$closest = blockMenuEl.closest(`[data-testid="${POPUP_WRAPPER_TEST_ID}"]`)) !== null && _blockMenuEl$closest !== void 0 ? _blockMenuEl$closest : blockMenuEl.parentElement;
86
+ if (!(popupWrapper instanceof HTMLElement)) {
87
+ return;
88
+ }
89
+ const distance = blockMenuEl.getBoundingClientRect().top - targetTop;
90
+ const hasTopProperty = popupWrapper.style.top !== '';
91
+ const hasBottomProperty = popupWrapper.style.bottom !== '';
92
+ if (hasBottomProperty && !hasTopProperty) {
93
+ popupWrapper.style.bottom = `${parseFloat(popupWrapper.style.bottom || '0') + distance}px`;
94
+ } else {
95
+ popupWrapper.style.top = `${parseFloat(popupWrapper.style.top || '0') - distance}px`;
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Measured while the menu opens, when the popup is still aligned to the drag handle. Moves that
101
+ * keep the handle visible deliberately leave the popup where it is, so this alignment - not the
102
+ * drifted one - is what the popup is restored to when a fold is crossed.
103
+ */
104
+ export const getBlockMenuAnchorMetrics = selectedBlock => {
105
+ var _doc$querySelector;
106
+ const doc = getDocument();
107
+ const blockMenuEl = doc === null || doc === void 0 ? void 0 : doc.querySelector(`[data-testid="${BLOCK_MENU_TEST_ID}"]`);
108
+ if (!doc || !selectedBlock || !blockMenuEl) {
109
+ return;
110
+ }
111
+ const blockMenuRect = blockMenuEl.getBoundingClientRect();
112
+ const dragHandleRect = (_doc$querySelector = doc.querySelector(DRAG_HANDLE_SELECTOR)) === null || _doc$querySelector === void 0 ? void 0 : _doc$querySelector.getBoundingClientRect();
113
+ if (blockMenuRect.height === 0 || !(dragHandleRect !== null && dragHandleRect !== void 0 && dragHandleRect.height)) {
114
+ return;
115
+ }
116
+ const blockTop = selectedBlock.getBoundingClientRect().top;
117
+ return {
118
+ blockHeight: getBlockBottom(selectedBlock) - blockTop,
119
+ dragHandleHeight: dragHandleRect.height,
120
+ dragHandleOffsetFromBlock: dragHandleRect.top - blockTop,
121
+ popupOffsetFromBlock: blockMenuRect.top - blockTop,
122
+ popupHeight: blockMenuRect.height
123
+ };
124
+ };
125
+
126
+ /**
127
+ * Captured before the move transaction, so the popup can be realigned to the block after it.
128
+ */
129
+ export const getBlockMenuPositionSnapshot = (selectedBlock, anchorMetrics) => {
130
+ const doc = getDocument();
131
+ if (!doc || !selectedBlock) {
132
+ return;
133
+ }
134
+ const scrollContainer = getScrollContainer(doc, selectedBlock);
135
+ const metrics = anchorMetrics !== null && anchorMetrics !== void 0 ? anchorMetrics : getBlockMenuAnchorMetrics(selectedBlock);
136
+ if (!scrollContainer || !metrics) {
137
+ return;
138
+ }
139
+ return {
140
+ ...metrics,
141
+ scrollContainer,
142
+ scrollTop: scrollContainer.scrollTop
143
+ };
144
+ };
145
+
146
+ /**
147
+ * Called after a move up/down transaction.
148
+ *
149
+ * With a `positionSnapshot` the popup is only sticky when it has to be: the editor stays put while
150
+ * the moved node's drag handle is still visible, and scrolls the minimum needed to reveal the
151
+ * handle otherwise - realigning the popup to the revealed handle.
152
+ */
153
+ export const fixBlockMenuPositionAndScroll = (selectedNode, positionSnapshot) => {
4
154
  const doc = getDocument();
5
155
  if (!doc) {
6
156
  return;
@@ -9,13 +159,54 @@ export const fixBlockMenuPositionAndScroll = firstSelectedNode => {
9
159
  if (!(blockMenuEl !== null && blockMenuEl !== void 0 && blockMenuEl.parentElement)) {
10
160
  return;
11
161
  }
12
- const scrollableContainer = doc.querySelector('[data-testid="editor-content-container"]');
13
- if (!firstSelectedNode || !scrollableContainer) {
162
+ if (positionSnapshot) {
163
+ if (!selectedNode) {
164
+ return;
165
+ }
166
+ // Reuse the container resolved when the snapshot was taken - re-resolving it independently
167
+ // here could pick a different element (e.g. for a node near an overflow boundary), applying
168
+ // positionSnapshot.scrollTop to the wrong container.
169
+ const scrollContainer = positionSnapshot.scrollContainer;
170
+
171
+ // The move transaction scrolls the new selection into view - undo that, only a drag handle
172
+ // leaving the viewport is allowed to move the editor.
173
+ scrollContainer.scrollTo({
174
+ behavior: 'instant',
175
+ top: positionSnapshot.scrollTop
176
+ });
177
+ const viewport = getScrollViewport(doc, scrollContainer);
178
+ const anchorTop = selectedNode.getBoundingClientRect().top + positionSnapshot.dragHandleOffsetFromBlock;
179
+
180
+ // Whether to scroll at all is decided by the drag handle alone - a block taller than the
181
+ // viewport is expected to be cut off while its handle stays usable.
182
+ if (getScrollDistanceToRevealAnchor(anchorTop, positionSnapshot.dragHandleHeight, viewport) === 0) {
183
+ return;
184
+ }
185
+
186
+ // Once we do scroll, reveal the block and the popup, not just the handle sized sliver of it.
187
+ // popupOffsetFromBlock is relative to the block - re-express it relative to the handle (the
188
+ // anchor everything else here is measured from) to build one span covering all three. The
189
+ // handle itself (0 to dragHandleHeight) always stays inside that span, so its visibility is
190
+ // never sacrificed to make room for the block or the popup.
191
+ const popupTopFromHandle = positionSnapshot.popupOffsetFromBlock - positionSnapshot.dragHandleOffsetFromBlock;
192
+ const revealTop = Math.min(0, popupTopFromHandle);
193
+ const revealBottom = Math.max(positionSnapshot.dragHandleHeight, positionSnapshot.blockHeight - positionSnapshot.dragHandleOffsetFromBlock, popupTopFromHandle + positionSnapshot.popupHeight);
194
+ const revealHeight = Math.min(revealBottom - revealTop, Math.max(viewport.bottom - viewport.top - 2 * REVEAL_MARGIN, 0));
195
+ const scrollDistance = getScrollDistanceToRevealAnchor(anchorTop + revealTop - REVEAL_MARGIN, revealHeight + 2 * REVEAL_MARGIN, viewport);
196
+ scrollContainer.scrollBy({
197
+ behavior: 'instant',
198
+ top: scrollDistance
199
+ });
200
+ setPopupTop(blockMenuEl, selectedNode.getBoundingClientRect().top + positionSnapshot.popupOffsetFromBlock);
201
+ return;
202
+ }
203
+ const scrollableContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
204
+ if (!selectedNode || !scrollableContainer) {
14
205
  return;
15
206
  }
16
207
  const parentElement = blockMenuEl.parentElement;
17
208
  const currentTop = parentElement.getBoundingClientRect().top;
18
- const distance = firstSelectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
209
+ const distance = selectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
19
210
  scrollableContainer.scrollBy({
20
211
  behavior: 'instant',
21
212
  top: distance
@@ -31,4 +222,22 @@ export const fixBlockMenuPositionAndScroll = firstSelectedNode => {
31
222
  const currentTopValue = parseFloat(parentElement.style.top || '0');
32
223
  parentElement.style.top = `${currentTopValue + topDifference}px`;
33
224
  }
225
+ };
226
+
227
+ /**
228
+ * Shared by move-up.tsx and move-down.tsx: schedules the post-move fix for the frame after
229
+ * ProseMirror has re-rendered.
230
+ *
231
+ * Falls back to the pre-PR always-scroll path (matching gate-off, via `getFirstSelectedDomNode`)
232
+ * whenever a `positionSnapshot` wasn't captured - e.g. a media/file thumbnail reporting zero height
233
+ * for a frame when the menu opened - rather than silently doing nothing.
234
+ */
235
+ export const scheduleBlockMenuPositionFix = (positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode) => {
236
+ requestAnimationFrame(() => {
237
+ if (fg('platform_editor_blocks_patch_8') && positionSnapshot) {
238
+ fixBlockMenuPositionAndScroll(getMovedBlockDomNode(), positionSnapshot);
239
+ return;
240
+ }
241
+ fixBlockMenuPositionAndScroll(getFirstSelectedDomNode());
242
+ });
34
243
  };
@@ -1,10 +1,21 @@
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
  var BlockMenuContext = /*#__PURE__*/createContext({
3
5
  onDropdownOpenChanged: function onDropdownOpenChanged() {},
4
6
  moveDownRef: /*#__PURE__*/React.createRef(),
5
7
  moveUpRef: /*#__PURE__*/React.createRef(),
6
8
  getFirstSelectedDomNode: function getFirstSelectedDomNode() {
7
9
  return undefined;
10
+ },
11
+ getMovedBlockDomNode: function getMovedBlockDomNode() {
12
+ return undefined;
13
+ },
14
+ getSelectedBlockDomNode: function getSelectedBlockDomNode() {
15
+ return undefined;
16
+ },
17
+ anchorMetricsRef: {
18
+ current: undefined
8
19
  }
9
20
  });
10
21
  export var useBlockMenu = function useBlockMenu() {
@@ -20,6 +31,7 @@ export var BlockMenuProvider = function BlockMenuProvider(_ref) {
20
31
  editorView = _ref.editorView;
21
32
  var moveUpRef = useRef(null);
22
33
  var moveDownRef = useRef(null);
34
+ var anchorMetricsRef = useRef(undefined);
23
35
  var getFirstSelectedDomNode = useCallback(function () {
24
36
  var _api$selection;
25
37
  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;
@@ -30,25 +42,74 @@ export var BlockMenuProvider = function BlockMenuProvider(_ref) {
30
42
  }
31
43
  }
32
44
  }, [api, editorView]);
45
+ var getTopLevelBlockDomNodeAt = useCallback(function (from) {
46
+ if (!editorView) {
47
+ return;
48
+ }
49
+ var nodeDOM = editorView.nodeDOM(from);
50
+ var domAtPos = nodeDOM instanceof Element ? nodeDOM : editorView.domAtPos(from).node;
51
+ var element = domAtPos instanceof Element ? domAtPos : domAtPos.parentElement;
52
+ if (!element || !editorView.dom.contains(element)) {
53
+ return;
54
+ }
55
+ var block = element;
56
+ while (block.parentElement && block.parentElement !== editorView.dom) {
57
+ block = block.parentElement;
58
+ }
59
+ return block;
60
+ }, [editorView]);
61
+ var getSelectedBlockDomNode = useCallback(function () {
62
+ var _api$blockControls, _ref2, _blockControlsState$a, _blockControlsState$a2, _blockControlsState$m, _blockControlsState$p;
63
+ var blockControlsState = api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState();
64
+ // The editor selection is not necessarily in the block the menu was opened for.
65
+ 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;
66
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
67
+ }, [api, getTopLevelBlockDomNodeAt]);
68
+ var getMovedBlockDomNode = useCallback(function () {
69
+ // A move transaction maps its preserved selection onto the node it just moved, so this is
70
+ // where that node ended up - unlike the block menu state, which keeps stale positions.
71
+ var from = editorView === null || editorView === void 0 ? void 0 : editorView.state.selection.from;
72
+ return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
73
+ }, [editorView, getTopLevelBlockDomNodeAt]);
33
74
  var onDropdownOpenChanged = useCallback(function (isOpen) {
34
- if (!isOpen) {
35
- // On Dropdown closed, return focus to editor
36
- setTimeout(function () {
37
- return requestAnimationFrame(function () {
38
- api === null || api === void 0 || api.core.actions.focus({
39
- scrollIntoView: false
75
+ anchorMetricsRef.current = undefined;
76
+ if (isOpen) {
77
+ if (fg('platform_editor_blocks_patch_8')) {
78
+ // The popup lays itself out next to the drag handle over the next frame or two.
79
+ var remainingFrames = 3;
80
+ var _captureAnchorMetrics = function captureAnchorMetrics() {
81
+ requestAnimationFrame(function () {
82
+ anchorMetricsRef.current = getBlockMenuAnchorMetrics(getSelectedBlockDomNode());
83
+ remainingFrames -= 1;
84
+ if (!anchorMetricsRef.current && remainingFrames > 0) {
85
+ _captureAnchorMetrics();
86
+ }
40
87
  });
41
- });
42
- }, 1);
88
+ };
89
+ _captureAnchorMetrics();
90
+ }
91
+ return;
43
92
  }
44
- }, [api]);
93
+
94
+ // On Dropdown closed, return focus to editor
95
+ setTimeout(function () {
96
+ return requestAnimationFrame(function () {
97
+ api === null || api === void 0 || api.core.actions.focus({
98
+ scrollIntoView: false
99
+ });
100
+ });
101
+ }, 1);
102
+ }, [api, getSelectedBlockDomNode]);
45
103
  return /*#__PURE__*/React.createElement(BlockMenuContext.Provider, {
46
104
  // 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)
47
105
  value: {
48
106
  onDropdownOpenChanged: onDropdownOpenChanged,
49
107
  moveDownRef: moveDownRef,
50
108
  moveUpRef: moveUpRef,
51
- getFirstSelectedDomNode: getFirstSelectedDomNode
109
+ getFirstSelectedDomNode: getFirstSelectedDomNode,
110
+ getMovedBlockDomNode: getMovedBlockDomNode,
111
+ getSelectedBlockDomNode: getSelectedBlockDomNode,
112
+ anchorMetricsRef: anchorMetricsRef
52
113
  }
53
114
  }, children);
54
115
  };
@@ -1,7 +1,10 @@
1
1
  /* block-menu.tsx generated by @compiled/babel-plugin v2.0.0 */
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import "./block-menu.compiled.css";
4
5
  import { ax, ix } from "@compiled/react/runtime";
6
+ 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; }
7
+ 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; }
5
8
  import React, { useContext, useEffect, useRef } from 'react';
6
9
  import { injectIntl } from 'react-intl';
7
10
  import { cx } from '@atlaskit/css';
@@ -257,8 +260,9 @@ var BlockMenu = function BlockMenu(_ref5) {
257
260
  if (!isMenuOpen) {
258
261
  return;
259
262
  }
263
+ onDropdownOpenChanged(true);
260
264
  setMenuHeight(((_popupRef$current = popupRef.current) === null || _popupRef$current === void 0 ? void 0 : _popupRef$current.clientHeight) || FALLBACK_MENU_HEIGHT);
261
- }, [isMenuOpen]);
265
+ }, [isMenuOpen, onDropdownOpenChanged]);
262
266
  var hasFocus = (_ref8 = (editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) ||
263
267
  // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
264
268
  document.activeElement === targetHandleRef || popupRef.current && (
@@ -347,6 +351,23 @@ var BlockMenu = function BlockMenu(_ref5) {
347
351
  stick: true
348
352
  // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
349
353
  ,
354
+ onPositionCalculated: fg('platform_editor_blocks_patch_8') ? function (position) {
355
+ if (position.left === undefined) {
356
+ // Already placed on the gutter side (using `right`) - nothing to correct.
357
+ return position;
358
+ }
359
+ // Popup prefers whichever side has more room, which flips the menu onto the
360
+ // content side when the gutter is too narrow to fit it. Force it back onto the
361
+ // gutter side instead - it can still overlap the content once there, but it
362
+ // must not start on top of the node. `position.left` is shifted by the same
363
+ // distance regardless of the offset parent/scroll it was calculated against,
364
+ // since that just moves both by the same amount.
365
+ return _objectSpread(_objectSpread({}, position), {}, {
366
+ left: position.left - DEFAULT_MENU_WIDTH - DRAG_HANDLE_WIDTH - 2 * DRAG_HANDLE_OFFSET_PADDING
367
+ });
368
+ } : undefined
369
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
370
+ ,
350
371
  offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, targetHandleHeightOffset],
351
372
  focusTrap: openedViaKeyboard ?
352
373
  // Only enable focus trap when opened via keyboard to make sure the focus is on the first focusable menu item
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
8
8
  import { DIRECTION } from '@atlaskit/editor-common/types';
9
9
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
10
10
  import ArrowDownIcon from '@atlaskit/icon/core/arrow-down';
11
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
11
12
  import { useBlockMenu } from './block-menu-provider';
12
13
  import { BLOCK_MENU_ITEM_NAME } from './consts';
13
- import { fixBlockMenuPositionAndScroll } from './utils/fixBlockMenuPositionAndScroll';
14
+ import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
14
15
  var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
15
16
  var api = _ref.api;
16
17
  var _useIntl = useIntl(),
@@ -18,7 +19,10 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
18
19
  var _useBlockMenu = useBlockMenu(),
19
20
  moveUpRef = _useBlockMenu.moveUpRef,
20
21
  moveDownRef = _useBlockMenu.moveDownRef,
21
- getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode;
22
+ getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
23
+ getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
24
+ getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
25
+ anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
22
26
  var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls'], function (_ref2) {
23
27
  var _blockControlsState$b;
24
28
  var blockControlsState = _ref2.blockControlsState;
@@ -36,6 +40,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
36
40
  }
37
41
  }, [canMoveDown, moveUpRef, moveDownRef]);
38
42
  var handleClick = function handleClick() {
43
+ var positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
39
44
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
40
45
  var _api$analytics, _api$blockControls;
41
46
  var tr = _ref3.tr;
@@ -53,10 +58,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
53
58
  });
54
59
  return tr;
55
60
  });
56
- requestAnimationFrame(function () {
57
- var newFirstNode = getFirstSelectedDomNode();
58
- fixBlockMenuPositionAndScroll(newFirstNode);
59
- });
61
+ scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
60
62
  };
61
63
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
62
64
  triggerRef: moveDownRef,
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
8
8
  import { DIRECTION } from '@atlaskit/editor-common/types';
9
9
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
10
10
  import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
11
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
11
12
  import { useBlockMenu } from './block-menu-provider';
12
13
  import { BLOCK_MENU_ITEM_NAME } from './consts';
13
- import { fixBlockMenuPositionAndScroll } from './utils/fixBlockMenuPositionAndScroll';
14
+ import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
14
15
  var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
15
16
  var api = _ref.api;
16
17
  var _useIntl = useIntl(),
@@ -18,7 +19,10 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
18
19
  var _useBlockMenu = useBlockMenu(),
19
20
  moveUpRef = _useBlockMenu.moveUpRef,
20
21
  moveDownRef = _useBlockMenu.moveDownRef,
21
- getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode;
22
+ getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
23
+ getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
24
+ getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
25
+ anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
22
26
  var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls'], function (_ref2) {
23
27
  var _blockControlsState$b;
24
28
  var blockControlsState = _ref2.blockControlsState;
@@ -34,6 +38,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
34
38
  }
35
39
  }, [canMoveUp, moveDownRef, moveUpRef]);
36
40
  var handleClick = function handleClick() {
41
+ var positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
37
42
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
38
43
  var _api$analytics, _api$blockControls;
39
44
  var tr = _ref3.tr;
@@ -51,10 +56,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
51
56
  });
52
57
  return tr;
53
58
  });
54
- requestAnimationFrame(function () {
55
- var newFirstNode = getFirstSelectedDomNode();
56
- fixBlockMenuPositionAndScroll(newFirstNode);
57
- });
59
+ scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
58
60
  };
59
61
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
60
62
  triggerRef: moveUpRef,