@atlaskit/editor-plugin-paste-options-toolbar 9.0.4 → 9.0.6

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,19 @@
1
1
  # @atlaskit/editor-plugin-paste-options-toolbar
2
2
 
3
+ ## 9.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [`2b33e02e33a67`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2b33e02e33a67) -
8
+ [ux] [EDITOR-5880] paste menu position fix so it sticks on screen when pasting large content
9
+ - Updated dependencies
10
+
11
+ ## 9.0.5
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+
3
17
  ## 9.0.4
4
18
 
5
19
  ### Patch Changes
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.TEXT_HIGHLIGHT_CLASS = exports.PASTE_TOOLBAR_MENU_ID = exports.PASTE_TOOLBAR_ITEM_CLASS = exports.PASTE_TOOLBAR_CLASS = exports.PASTE_OPTIONS_TEST_ID = exports.PASTE_OPTIONS_META_ID = exports.PASTE_MENU_GAP = exports.PASTE_HIGHLIGHT_DECORATION_KEY = exports.EDITOR_WRAPPER_CLASS = void 0;
6
+ exports.TEXT_HIGHLIGHT_CLASS = exports.PASTE_TOOLBAR_MENU_ID = exports.PASTE_TOOLBAR_ITEM_CLASS = exports.PASTE_TOOLBAR_CLASS = exports.PASTE_OPTIONS_TEST_ID = exports.PASTE_OPTIONS_META_ID = exports.PASTE_MENU_GAP_TOP = exports.PASTE_MENU_GAP_HORIZONTAL = exports.PASTE_HIGHLIGHT_DECORATION_KEY = exports.EDITOR_WRAPPER_CLASS = void 0;
7
7
  var PASTE_TOOLBAR_CLASS = exports.PASTE_TOOLBAR_CLASS = 'ak-editor-paste-toolbar';
8
8
  var PASTE_TOOLBAR_MENU_ID = exports.PASTE_TOOLBAR_MENU_ID = 'ak-editor-paste-toolbar-item-dropdownList';
9
9
  var TEXT_HIGHLIGHT_CLASS = exports.TEXT_HIGHLIGHT_CLASS = 'text-highlight';
@@ -12,6 +12,5 @@ var PASTE_TOOLBAR_ITEM_CLASS = exports.PASTE_TOOLBAR_ITEM_CLASS = 'ak-editor-pas
12
12
  var EDITOR_WRAPPER_CLASS = exports.EDITOR_WRAPPER_CLASS = 'akEditor';
13
13
  var PASTE_OPTIONS_TEST_ID = exports.PASTE_OPTIONS_TEST_ID = 'paste-options-testid';
14
14
  var PASTE_OPTIONS_META_ID = exports.PASTE_OPTIONS_META_ID = 'paste-options$';
15
-
16
- // Gap (in px) between the right edge of the pasted content and the left edge of the paste menu.
17
- var PASTE_MENU_GAP = exports.PASTE_MENU_GAP = 12;
15
+ var PASTE_MENU_GAP_HORIZONTAL = exports.PASTE_MENU_GAP_HORIZONTAL = 8;
16
+ var PASTE_MENU_GAP_TOP = exports.PASTE_MENU_GAP_TOP = 24;
@@ -41,26 +41,44 @@ function getTargetElement(editorView, pos) {
41
41
 
42
42
  /**
43
43
  * Adjusts the vertical position of the paste menu to align with the top of the
44
- * pasted content using the exact coordinates at the paste start position.
44
+ * pasted content using the exact coordinates at the paste start position,
45
+ * and sticks the menu to the top of the scroll container when the pasted
46
+ * content scrolls above the visible area.
45
47
  *
46
- * The Popup's vertical placement may place the popup below the target element
47
- * (alignY="bottom"). This override computes the correct top position using
48
- * coordsAtPos for the paste start, then converts to the Popup's coordinate
49
- * system by calculating the delta from the target element's bottom (where the
50
- * Popup positions by default) to the paste start coordinates.
48
+ * The Popup uses alignY="bottom", which positions the popup below the target
49
+ * element's bottom edge. This override:
50
+ *
51
+ * 1. Shifts the popup from the target's bottom edge to align with the paste
52
+ * start position.
53
+ * 2. When the paste start scrolls above the scroll container, clamps the menu
54
+ * to the scroll container's top edge (sticky-top).
55
+ * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
56
+ * above the visible area.
51
57
  */
52
- function onPositionCalculated(editorView, pasteStartPos, targetElement) {
58
+ function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
53
59
  return function (position) {
54
60
  var _position$top;
55
61
  var startCoords = editorView.coordsAtPos(pasteStartPos);
62
+ var endCoords = editorView.coordsAtPos(pasteEndPos);
56
63
  var targetRect = targetElement.getBoundingClientRect();
57
64
 
58
65
  // The Popup places the menu at the target's bottom edge by default.
59
- // We need to shift it up so it aligns with the paste start position.
66
+ // We shift it up so it aligns with the paste start position.
60
67
  // Both coordinates are in viewport space, so the delta is offset-parent agnostic.
61
68
  var topDelta = startCoords.top - (targetRect.top + targetRect.height);
69
+ var adjustedTop = ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta;
70
+
71
+ // Sticky-top: clamp to the scroll container's top edge when the paste
72
+ // start has scrolled above the visible area, but only while some pasted
73
+ // content is still visible.
74
+ if (scrollableElement) {
75
+ var scrollContainerTop = scrollableElement.getBoundingClientRect().top;
76
+ if (startCoords.top < scrollContainerTop && endCoords.bottom > scrollContainerTop) {
77
+ adjustedTop += scrollContainerTop - startCoords.top + _constants.PASTE_MENU_GAP_TOP;
78
+ }
79
+ }
62
80
  return _objectSpread(_objectSpread({}, position), {}, {
63
- top: ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta
81
+ top: adjustedTop
64
82
  });
65
83
  };
66
84
  }
@@ -102,15 +120,17 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
102
120
  (0, _commands.showToolbar)(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
103
121
  }, [lastContentPasted, editorView]);
104
122
  var _useSharedPluginState2 = (0, _hooks.useSharedPluginStateWithSelector)(api, ['pasteOptionsToolbarPlugin'], function (states) {
105
- var _pluginState$showTool, _pluginState$pasteSta;
123
+ var _pluginState$showTool, _pluginState$pasteSta, _pluginState$pasteEnd;
106
124
  var pluginState = states.pasteOptionsToolbarPluginState;
107
125
  return {
108
126
  showToolbar: (_pluginState$showTool = pluginState === null || pluginState === void 0 ? void 0 : pluginState.showToolbar) !== null && _pluginState$showTool !== void 0 ? _pluginState$showTool : false,
109
- pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0
127
+ pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0,
128
+ pasteEndPos: (_pluginState$pasteEnd = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteEndPos) !== null && _pluginState$pasteEnd !== void 0 ? _pluginState$pasteEnd : 0
110
129
  };
111
130
  }),
112
131
  isToolbarShown = _useSharedPluginState2.showToolbar,
113
- pasteStartPos = _useSharedPluginState2.pasteStartPos;
132
+ pasteStartPos = _useSharedPluginState2.pasteStartPos,
133
+ pasteEndPos = _useSharedPluginState2.pasteEndPos;
114
134
  var aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
115
135
  var visibleAiActionKeys = (0, _hasVisibleButton.getVisibleKeys)(aiSurfaceComponents, ['button', 'menu-item']);
116
136
  (0, _react.useEffect)(function () {
@@ -150,6 +170,15 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
150
170
  handleDismiss();
151
171
  }
152
172
  }, [handleDismiss]);
173
+
174
+ // Find the actual scroll container using the same utility the Popup's
175
+ // stick prop uses internally. We pass this as the scrollableElement prop
176
+ // so the Popup attaches its built-in scroll listener, which calls
177
+ // scheduledUpdatePosition (RAF-throttled) on each scroll event — triggering
178
+ // onPositionCalculated with fresh viewport coordinates.
179
+ var targetForScroll = isToolbarShown ? getTargetElement(editorView, pasteStartPos) : null;
180
+ var overflowScrollParent = targetForScroll ? (0, _ui.findOverflowScrollParent)(targetForScroll) : false;
181
+ var effectiveScrollableElement = overflowScrollParent || scrollableElement;
153
182
  var pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(_toolbar.PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
154
183
  var anyComponentVisible = (0, _hasVisibleButton.hasVisibleButton)(pasteMenuComponents);
155
184
  if (!isToolbarShown) {
@@ -166,12 +195,13 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
166
195
  target: target,
167
196
  mountTo: mountTo,
168
197
  boundariesElement: boundariesElement,
169
- scrollableElement: scrollableElement,
198
+ scrollableElement: effectiveScrollableElement,
199
+ minPopupMargin: _constants.PASTE_MENU_GAP_HORIZONTAL,
170
200
  zIndex: _editorSharedStyles.akEditorFloatingPanelZIndex,
171
201
  alignX: "end",
172
202
  alignY: "bottom",
173
- offset: [_constants.PASTE_MENU_GAP, 0],
174
- onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, target),
203
+ offset: [_constants.PASTE_MENU_GAP_HORIZONTAL, 0],
204
+ onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
175
205
  handleClickOutside: handleClickOutside,
176
206
  handleEscapeKeydown: handleDismiss
177
207
  }, /*#__PURE__*/_react.default.createElement(_toolbar.EditorToolbarProvider, {
@@ -6,6 +6,5 @@ export const PASTE_TOOLBAR_ITEM_CLASS = 'ak-editor-paste-toolbar-item';
6
6
  export const EDITOR_WRAPPER_CLASS = 'akEditor';
7
7
  export const PASTE_OPTIONS_TEST_ID = 'paste-options-testid';
8
8
  export const PASTE_OPTIONS_META_ID = 'paste-options$';
9
-
10
- // Gap (in px) between the right edge of the pasted content and the left edge of the paste menu.
11
- export const PASTE_MENU_GAP = 12;
9
+ export const PASTE_MENU_GAP_HORIZONTAL = 8;
10
+ export const PASTE_MENU_GAP_TOP = 24;
@@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useRef } from 'react';
2
2
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
3
3
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
4
4
  import { EditorToolbarProvider, PASTE_MENU } from '@atlaskit/editor-common/toolbar';
5
- import { Popup } from '@atlaskit/editor-common/ui';
5
+ import { findOverflowScrollParent, Popup } from '@atlaskit/editor-common/ui';
6
6
  import { withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react';
7
7
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
8
8
  import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
9
9
  import { ToolbarDropdownMenuProvider } from '@atlaskit/editor-toolbar';
10
10
  import { hideToolbar, highlightContent, showToolbar } from '../../editor-commands/commands';
11
- import { PASTE_MENU_GAP } from '../../pm-plugins/constants';
11
+ import { PASTE_MENU_GAP_HORIZONTAL, PASTE_MENU_GAP_TOP } from '../../pm-plugins/constants';
12
12
  import { ToolbarDropdownOption } from '../../types/types';
13
13
  import { isToolbarVisible } from '../toolbar';
14
14
  import { getVisibleKeys, hasVisibleButton } from './hasVisibleButton';
@@ -28,27 +28,45 @@ function getTargetElement(editorView, pos) {
28
28
 
29
29
  /**
30
30
  * Adjusts the vertical position of the paste menu to align with the top of the
31
- * pasted content using the exact coordinates at the paste start position.
31
+ * pasted content using the exact coordinates at the paste start position,
32
+ * and sticks the menu to the top of the scroll container when the pasted
33
+ * content scrolls above the visible area.
32
34
  *
33
- * The Popup's vertical placement may place the popup below the target element
34
- * (alignY="bottom"). This override computes the correct top position using
35
- * coordsAtPos for the paste start, then converts to the Popup's coordinate
36
- * system by calculating the delta from the target element's bottom (where the
37
- * Popup positions by default) to the paste start coordinates.
35
+ * The Popup uses alignY="bottom", which positions the popup below the target
36
+ * element's bottom edge. This override:
37
+ *
38
+ * 1. Shifts the popup from the target's bottom edge to align with the paste
39
+ * start position.
40
+ * 2. When the paste start scrolls above the scroll container, clamps the menu
41
+ * to the scroll container's top edge (sticky-top).
42
+ * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
43
+ * above the visible area.
38
44
  */
39
- export function onPositionCalculated(editorView, pasteStartPos, targetElement) {
45
+ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
40
46
  return position => {
41
47
  var _position$top;
42
48
  const startCoords = editorView.coordsAtPos(pasteStartPos);
49
+ const endCoords = editorView.coordsAtPos(pasteEndPos);
43
50
  const targetRect = targetElement.getBoundingClientRect();
44
51
 
45
52
  // The Popup places the menu at the target's bottom edge by default.
46
- // We need to shift it up so it aligns with the paste start position.
53
+ // We shift it up so it aligns with the paste start position.
47
54
  // Both coordinates are in viewport space, so the delta is offset-parent agnostic.
48
55
  const topDelta = startCoords.top - (targetRect.top + targetRect.height);
56
+ let adjustedTop = ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta;
57
+
58
+ // Sticky-top: clamp to the scroll container's top edge when the paste
59
+ // start has scrolled above the visible area, but only while some pasted
60
+ // content is still visible.
61
+ if (scrollableElement) {
62
+ const scrollContainerTop = scrollableElement.getBoundingClientRect().top;
63
+ if (startCoords.top < scrollContainerTop && endCoords.bottom > scrollContainerTop) {
64
+ adjustedTop += scrollContainerTop - startCoords.top + PASTE_MENU_GAP_TOP;
65
+ }
66
+ }
49
67
  return {
50
68
  ...position,
51
- top: ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta
69
+ top: adjustedTop
52
70
  };
53
71
  };
54
72
  }
@@ -93,13 +111,15 @@ export const PasteActionsMenu = ({
93
111
  }, [lastContentPasted, editorView]);
94
112
  const {
95
113
  showToolbar: isToolbarShown,
96
- pasteStartPos
114
+ pasteStartPos,
115
+ pasteEndPos
97
116
  } = useSharedPluginStateWithSelector(api, ['pasteOptionsToolbarPlugin'], states => {
98
- var _pluginState$showTool, _pluginState$pasteSta;
117
+ var _pluginState$showTool, _pluginState$pasteSta, _pluginState$pasteEnd;
99
118
  const pluginState = states.pasteOptionsToolbarPluginState;
100
119
  return {
101
120
  showToolbar: (_pluginState$showTool = pluginState === null || pluginState === void 0 ? void 0 : pluginState.showToolbar) !== null && _pluginState$showTool !== void 0 ? _pluginState$showTool : false,
102
- pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0
121
+ pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0,
122
+ pasteEndPos: (_pluginState$pasteEnd = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteEndPos) !== null && _pluginState$pasteEnd !== void 0 ? _pluginState$pasteEnd : 0
103
123
  };
104
124
  });
105
125
  const aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
@@ -141,6 +161,15 @@ export const PasteActionsMenu = ({
141
161
  handleDismiss();
142
162
  }
143
163
  }, [handleDismiss]);
164
+
165
+ // Find the actual scroll container using the same utility the Popup's
166
+ // stick prop uses internally. We pass this as the scrollableElement prop
167
+ // so the Popup attaches its built-in scroll listener, which calls
168
+ // scheduledUpdatePosition (RAF-throttled) on each scroll event — triggering
169
+ // onPositionCalculated with fresh viewport coordinates.
170
+ const targetForScroll = isToolbarShown ? getTargetElement(editorView, pasteStartPos) : null;
171
+ const overflowScrollParent = targetForScroll ? findOverflowScrollParent(targetForScroll) : false;
172
+ const effectiveScrollableElement = overflowScrollParent || scrollableElement;
144
173
  const pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
145
174
  const anyComponentVisible = hasVisibleButton(pasteMenuComponents);
146
175
  if (!isToolbarShown) {
@@ -157,12 +186,13 @@ export const PasteActionsMenu = ({
157
186
  target: target,
158
187
  mountTo: mountTo,
159
188
  boundariesElement: boundariesElement,
160
- scrollableElement: scrollableElement,
189
+ scrollableElement: effectiveScrollableElement,
190
+ minPopupMargin: PASTE_MENU_GAP_HORIZONTAL,
161
191
  zIndex: akEditorFloatingPanelZIndex,
162
192
  alignX: "end",
163
193
  alignY: "bottom",
164
- offset: [PASTE_MENU_GAP, 0],
165
- onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, target),
194
+ offset: [PASTE_MENU_GAP_HORIZONTAL, 0],
195
+ onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
166
196
  handleClickOutside: handleClickOutside,
167
197
  handleEscapeKeydown: handleDismiss
168
198
  }, /*#__PURE__*/React.createElement(EditorToolbarProvider, {
@@ -6,6 +6,5 @@ export var PASTE_TOOLBAR_ITEM_CLASS = 'ak-editor-paste-toolbar-item';
6
6
  export var EDITOR_WRAPPER_CLASS = 'akEditor';
7
7
  export var PASTE_OPTIONS_TEST_ID = 'paste-options-testid';
8
8
  export var PASTE_OPTIONS_META_ID = 'paste-options$';
9
-
10
- // Gap (in px) between the right edge of the pasted content and the left edge of the paste menu.
11
- export var PASTE_MENU_GAP = 12;
9
+ export var PASTE_MENU_GAP_HORIZONTAL = 8;
10
+ export var PASTE_MENU_GAP_TOP = 24;
@@ -5,13 +5,13 @@ import React, { useCallback, useEffect, useRef } from 'react';
5
5
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
6
6
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
7
7
  import { EditorToolbarProvider, PASTE_MENU } from '@atlaskit/editor-common/toolbar';
8
- import { Popup } from '@atlaskit/editor-common/ui';
8
+ import { findOverflowScrollParent, Popup } from '@atlaskit/editor-common/ui';
9
9
  import { withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react';
10
10
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
11
11
  import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
12
12
  import { ToolbarDropdownMenuProvider } from '@atlaskit/editor-toolbar';
13
13
  import { hideToolbar, highlightContent, showToolbar } from '../../editor-commands/commands';
14
- import { PASTE_MENU_GAP } from '../../pm-plugins/constants';
14
+ import { PASTE_MENU_GAP_HORIZONTAL, PASTE_MENU_GAP_TOP } from '../../pm-plugins/constants';
15
15
  import { ToolbarDropdownOption } from '../../types/types';
16
16
  import { isToolbarVisible } from '../toolbar';
17
17
  import { getVisibleKeys, hasVisibleButton } from './hasVisibleButton';
@@ -31,26 +31,44 @@ function getTargetElement(editorView, pos) {
31
31
 
32
32
  /**
33
33
  * Adjusts the vertical position of the paste menu to align with the top of the
34
- * pasted content using the exact coordinates at the paste start position.
34
+ * pasted content using the exact coordinates at the paste start position,
35
+ * and sticks the menu to the top of the scroll container when the pasted
36
+ * content scrolls above the visible area.
35
37
  *
36
- * The Popup's vertical placement may place the popup below the target element
37
- * (alignY="bottom"). This override computes the correct top position using
38
- * coordsAtPos for the paste start, then converts to the Popup's coordinate
39
- * system by calculating the delta from the target element's bottom (where the
40
- * Popup positions by default) to the paste start coordinates.
38
+ * The Popup uses alignY="bottom", which positions the popup below the target
39
+ * element's bottom edge. This override:
40
+ *
41
+ * 1. Shifts the popup from the target's bottom edge to align with the paste
42
+ * start position.
43
+ * 2. When the paste start scrolls above the scroll container, clamps the menu
44
+ * to the scroll container's top edge (sticky-top).
45
+ * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
46
+ * above the visible area.
41
47
  */
42
- export function onPositionCalculated(editorView, pasteStartPos, targetElement) {
48
+ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
43
49
  return function (position) {
44
50
  var _position$top;
45
51
  var startCoords = editorView.coordsAtPos(pasteStartPos);
52
+ var endCoords = editorView.coordsAtPos(pasteEndPos);
46
53
  var targetRect = targetElement.getBoundingClientRect();
47
54
 
48
55
  // The Popup places the menu at the target's bottom edge by default.
49
- // We need to shift it up so it aligns with the paste start position.
56
+ // We shift it up so it aligns with the paste start position.
50
57
  // Both coordinates are in viewport space, so the delta is offset-parent agnostic.
51
58
  var topDelta = startCoords.top - (targetRect.top + targetRect.height);
59
+ var adjustedTop = ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta;
60
+
61
+ // Sticky-top: clamp to the scroll container's top edge when the paste
62
+ // start has scrolled above the visible area, but only while some pasted
63
+ // content is still visible.
64
+ if (scrollableElement) {
65
+ var scrollContainerTop = scrollableElement.getBoundingClientRect().top;
66
+ if (startCoords.top < scrollContainerTop && endCoords.bottom > scrollContainerTop) {
67
+ adjustedTop += scrollContainerTop - startCoords.top + PASTE_MENU_GAP_TOP;
68
+ }
69
+ }
52
70
  return _objectSpread(_objectSpread({}, position), {}, {
53
- top: ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta
71
+ top: adjustedTop
54
72
  });
55
73
  };
56
74
  }
@@ -92,15 +110,17 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
92
110
  showToolbar(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
93
111
  }, [lastContentPasted, editorView]);
94
112
  var _useSharedPluginState2 = useSharedPluginStateWithSelector(api, ['pasteOptionsToolbarPlugin'], function (states) {
95
- var _pluginState$showTool, _pluginState$pasteSta;
113
+ var _pluginState$showTool, _pluginState$pasteSta, _pluginState$pasteEnd;
96
114
  var pluginState = states.pasteOptionsToolbarPluginState;
97
115
  return {
98
116
  showToolbar: (_pluginState$showTool = pluginState === null || pluginState === void 0 ? void 0 : pluginState.showToolbar) !== null && _pluginState$showTool !== void 0 ? _pluginState$showTool : false,
99
- pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0
117
+ pasteStartPos: (_pluginState$pasteSta = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteStartPos) !== null && _pluginState$pasteSta !== void 0 ? _pluginState$pasteSta : 0,
118
+ pasteEndPos: (_pluginState$pasteEnd = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteEndPos) !== null && _pluginState$pasteEnd !== void 0 ? _pluginState$pasteEnd : 0
100
119
  };
101
120
  }),
102
121
  isToolbarShown = _useSharedPluginState2.showToolbar,
103
- pasteStartPos = _useSharedPluginState2.pasteStartPos;
122
+ pasteStartPos = _useSharedPluginState2.pasteStartPos,
123
+ pasteEndPos = _useSharedPluginState2.pasteEndPos;
104
124
  var aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
105
125
  var visibleAiActionKeys = getVisibleKeys(aiSurfaceComponents, ['button', 'menu-item']);
106
126
  useEffect(function () {
@@ -140,6 +160,15 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
140
160
  handleDismiss();
141
161
  }
142
162
  }, [handleDismiss]);
163
+
164
+ // Find the actual scroll container using the same utility the Popup's
165
+ // stick prop uses internally. We pass this as the scrollableElement prop
166
+ // so the Popup attaches its built-in scroll listener, which calls
167
+ // scheduledUpdatePosition (RAF-throttled) on each scroll event — triggering
168
+ // onPositionCalculated with fresh viewport coordinates.
169
+ var targetForScroll = isToolbarShown ? getTargetElement(editorView, pasteStartPos) : null;
170
+ var overflowScrollParent = targetForScroll ? findOverflowScrollParent(targetForScroll) : false;
171
+ var effectiveScrollableElement = overflowScrollParent || scrollableElement;
143
172
  var pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
144
173
  var anyComponentVisible = hasVisibleButton(pasteMenuComponents);
145
174
  if (!isToolbarShown) {
@@ -156,12 +185,13 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
156
185
  target: target,
157
186
  mountTo: mountTo,
158
187
  boundariesElement: boundariesElement,
159
- scrollableElement: scrollableElement,
188
+ scrollableElement: effectiveScrollableElement,
189
+ minPopupMargin: PASTE_MENU_GAP_HORIZONTAL,
160
190
  zIndex: akEditorFloatingPanelZIndex,
161
191
  alignX: "end",
162
192
  alignY: "bottom",
163
- offset: [PASTE_MENU_GAP, 0],
164
- onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, target),
193
+ offset: [PASTE_MENU_GAP_HORIZONTAL, 0],
194
+ onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
165
195
  handleClickOutside: handleClickOutside,
166
196
  handleEscapeKeydown: handleDismiss
167
197
  }, /*#__PURE__*/React.createElement(EditorToolbarProvider, {
@@ -6,4 +6,5 @@ export declare const PASTE_TOOLBAR_ITEM_CLASS = "ak-editor-paste-toolbar-item";
6
6
  export declare const EDITOR_WRAPPER_CLASS = "akEditor";
7
7
  export declare const PASTE_OPTIONS_TEST_ID = "paste-options-testid";
8
8
  export declare const PASTE_OPTIONS_META_ID = "paste-options$";
9
- export declare const PASTE_MENU_GAP = 12;
9
+ export declare const PASTE_MENU_GAP_HORIZONTAL = 8;
10
+ export declare const PASTE_MENU_GAP_TOP = 24;
@@ -11,15 +11,21 @@ interface PasteActionsMenuProps {
11
11
  }
12
12
  /**
13
13
  * Adjusts the vertical position of the paste menu to align with the top of the
14
- * pasted content using the exact coordinates at the paste start position.
14
+ * pasted content using the exact coordinates at the paste start position,
15
+ * and sticks the menu to the top of the scroll container when the pasted
16
+ * content scrolls above the visible area.
15
17
  *
16
- * The Popup's vertical placement may place the popup below the target element
17
- * (alignY="bottom"). This override computes the correct top position using
18
- * coordsAtPos for the paste start, then converts to the Popup's coordinate
19
- * system by calculating the delta from the target element's bottom (where the
20
- * Popup positions by default) to the paste start coordinates.
18
+ * The Popup uses alignY="bottom", which positions the popup below the target
19
+ * element's bottom edge. This override:
20
+ *
21
+ * 1. Shifts the popup from the target's bottom edge to align with the paste
22
+ * start position.
23
+ * 2. When the paste start scrolls above the scroll container, clamps the menu
24
+ * to the scroll container's top edge (sticky-top).
25
+ * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
26
+ * above the visible area.
21
27
  */
22
- export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, targetElement: HTMLElement): (position: {
28
+ export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
23
29
  bottom?: number;
24
30
  left?: number;
25
31
  right?: number;
@@ -6,4 +6,5 @@ export declare const PASTE_TOOLBAR_ITEM_CLASS = "ak-editor-paste-toolbar-item";
6
6
  export declare const EDITOR_WRAPPER_CLASS = "akEditor";
7
7
  export declare const PASTE_OPTIONS_TEST_ID = "paste-options-testid";
8
8
  export declare const PASTE_OPTIONS_META_ID = "paste-options$";
9
- export declare const PASTE_MENU_GAP = 12;
9
+ export declare const PASTE_MENU_GAP_HORIZONTAL = 8;
10
+ export declare const PASTE_MENU_GAP_TOP = 24;
@@ -11,15 +11,21 @@ interface PasteActionsMenuProps {
11
11
  }
12
12
  /**
13
13
  * Adjusts the vertical position of the paste menu to align with the top of the
14
- * pasted content using the exact coordinates at the paste start position.
14
+ * pasted content using the exact coordinates at the paste start position,
15
+ * and sticks the menu to the top of the scroll container when the pasted
16
+ * content scrolls above the visible area.
15
17
  *
16
- * The Popup's vertical placement may place the popup below the target element
17
- * (alignY="bottom"). This override computes the correct top position using
18
- * coordsAtPos for the paste start, then converts to the Popup's coordinate
19
- * system by calculating the delta from the target element's bottom (where the
20
- * Popup positions by default) to the paste start coordinates.
18
+ * The Popup uses alignY="bottom", which positions the popup below the target
19
+ * element's bottom edge. This override:
20
+ *
21
+ * 1. Shifts the popup from the target's bottom edge to align with the paste
22
+ * start position.
23
+ * 2. When the paste start scrolls above the scroll container, clamps the menu
24
+ * to the scroll container's top edge (sticky-top).
25
+ * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
26
+ * above the visible area.
21
27
  */
22
- export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, targetElement: HTMLElement): (position: {
28
+ export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
23
29
  bottom?: number;
24
30
  left?: number;
25
31
  right?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-paste-options-toolbar",
3
- "version": "9.0.4",
3
+ "version": "9.0.6",
4
4
  "description": "Paste options toolbar for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -38,10 +38,10 @@
38
38
  "@atlaskit/editor-shared-styles": "^3.10.0",
39
39
  "@atlaskit/editor-toolbar": "^0.19.0",
40
40
  "@atlaskit/editor-ui-control-model": "^1.1.0",
41
- "@atlaskit/icon": "^32.0.0",
41
+ "@atlaskit/icon": "^32.1.0",
42
42
  "@atlaskit/platform-feature-flags": "^1.1.0",
43
43
  "@atlaskit/primitives": "^18.0.0",
44
- "@atlaskit/tmp-editor-statsig": "^37.0.0",
44
+ "@atlaskit/tmp-editor-statsig": "^38.1.0",
45
45
  "@atlaskit/tokens": "^11.1.0",
46
46
  "@babel/runtime": "^7.0.0",
47
47
  "@compiled/react": "^0.20.0",
@@ -49,7 +49,7 @@
49
49
  "react-intl-next": "npm:react-intl@^5.18.1"
50
50
  },
51
51
  "peerDependencies": {
52
- "@atlaskit/editor-common": "^112.2.0",
52
+ "@atlaskit/editor-common": "^112.3.0",
53
53
  "react": "^18.2.0",
54
54
  "react-dom": "^18.2.0"
55
55
  },