@atlaskit/editor-plugin-block-controls 13.4.0 → 13.4.1

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,14 @@
1
1
  # @atlaskit/editor-plugin-block-controls
2
2
 
3
+ ## 13.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`82270dfb1e67e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/82270dfb1e67e) -
8
+ Migrate block-controls drag-handle render/unmount to the React 18/19 createRoot API behind
9
+ nike_r19_render_unmount
10
+ - Updated dependencies
11
+
3
12
  ## 13.4.0
4
13
 
5
14
  ### Minor Changes
@@ -8,10 +8,10 @@ exports.unmountDecorations = exports.getNodeTypeWithLevel = exports.getNodeAncho
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
- var _reactDom = _interopRequireDefault(require("react-dom"));
12
11
  var _uuid = _interopRequireDefault(require("uuid"));
13
12
  var _nodeTypeUtils = require("@atlaskit/editor-common/utils/node-type-utils");
14
13
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
14
+ var _reactRootRegistry = require("./react-root-registry");
15
15
  var _validation = require("./utils/validation");
16
16
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
17
17
 
@@ -60,6 +60,6 @@ var unmountDecorations = exports.unmountDecorations = function unmountDecoration
60
60
  // as it was more responsive and causes less re-rendering
61
61
  var decorationsToRemove = document.querySelectorAll("[".concat(selector, "=\"true\"]"));
62
62
  decorationsToRemove.forEach(function (el) {
63
- _reactDom.default.unmountComponentAtNode(el);
63
+ (0, _reactRootRegistry.unmountFromMountPoint)(el);
64
64
  });
65
65
  };
@@ -8,7 +8,6 @@ exports.findHandleDec = exports.findActiveDragHandleNodeDec = exports.emptyParag
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _react = require("react");
10
10
  var _bindEventListener = require("bind-event-listener");
11
- var _reactDom = _interopRequireDefault(require("react-dom"));
12
11
  var _uuid = _interopRequireDefault(require("uuid"));
13
12
  var _view = require("@atlaskit/editor-prosemirror/view");
14
13
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
@@ -17,6 +16,7 @@ var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
17
16
  var _consts = require("../ui/consts");
18
17
  var _dragHandle = require("../ui/drag-handle");
19
18
  var _decorationsCommon = require("./decorations-common");
19
+ var _reactRootRegistry = require("./react-root-registry");
20
20
  var _marks = require("./utils/marks");
21
21
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
22
22
 
@@ -157,7 +157,7 @@ var dragHandleDecoration = exports.dragHandleDecoration = function dragHandleDec
157
157
  // );
158
158
 
159
159
  if ((0, _experiments.editorExperiment)('platform_editor_controls', 'variant1')) {
160
- _reactDom.default.render( /*#__PURE__*/(0, _react.createElement)(_dragHandle.DragHandleWithVisibility, {
160
+ (0, _reactRootRegistry.renderToMountPoint)( /*#__PURE__*/(0, _react.createElement)(_dragHandle.DragHandleWithVisibility, {
161
161
  view: view,
162
162
  api: api,
163
163
  formatMessage: formatMessage,
@@ -169,7 +169,7 @@ var dragHandleDecoration = exports.dragHandleDecoration = function dragHandleDec
169
169
  anchorRectCache: anchorRectCache
170
170
  }), element);
171
171
  } else {
172
- _reactDom.default.render( /*#__PURE__*/(0, _react.createElement)(_dragHandle.DragHandle, {
172
+ (0, _reactRootRegistry.renderToMountPoint)( /*#__PURE__*/(0, _react.createElement)(_dragHandle.DragHandle, {
173
173
  view: view,
174
174
  api: api,
175
175
  formatMessage: formatMessage,
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.unmountFromMountPoint = exports.renderToMountPoint = void 0;
8
+ var _reactDom = _interopRequireWildcard(require("react-dom"));
9
+ var _client = require("react-dom/client");
10
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
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); }
12
+ /**
13
+ * Shared registry of React roots created via `createRoot`.
14
+ *
15
+ * Both `decorations-drag-handle.ts` (render) and `decorations-common.ts`
16
+ * (unmount) must operate on the **same** root for a given DOM element, so
17
+ * the WeakMap lives here and is imported by both modules.
18
+ */
19
+ var reactRoots = new WeakMap();
20
+
21
+ /**
22
+ * Mounts `element` into `mountPoint`.
23
+ *
24
+ * When `nike_r19_render_unmount` is **on**, uses the React 18/19 `createRoot`
25
+ * API wrapped in `flushSync` so the render is synchronous – matching the
26
+ * legacy `ReactDOM.render` timing that ProseMirror decoration widgets depend
27
+ * on (focus management, anchor positioning, block-menu visibility all read
28
+ * the rendered DOM immediately after the callback returns).
29
+ *
30
+ * When the gate is **off**, falls back to the legacy `ReactDOM.render` path.
31
+ */
32
+ var renderToMountPoint = exports.renderToMountPoint = function renderToMountPoint(element, mountPoint) {
33
+ if ((0, _platformFeatureFlags.fg)('nike_r19_render_unmount')) {
34
+ var root = reactRoots.get(mountPoint);
35
+ if (!root) {
36
+ root = (0, _client.createRoot)(mountPoint);
37
+ reactRoots.set(mountPoint, root);
38
+ }
39
+ var activeRoot = root;
40
+
41
+ // flushSync makes createRoot render synchronously, matching legacy
42
+ // ReactDOM.render timing that the widget/focus/positioning code depends on.
43
+ (0, _reactDom.flushSync)(function () {
44
+ return activeRoot.render(element);
45
+ });
46
+ } else {
47
+ _reactDom.default.render(element, mountPoint);
48
+ }
49
+ };
50
+
51
+ /**
52
+ * Unmounts the React tree at `mountPoint`.
53
+ *
54
+ * When `nike_r19_render_unmount` is **on**, looks up the root in the shared
55
+ * registry, calls `root.unmount()`, and removes the entry.
56
+ *
57
+ * When the gate is **off**, falls back to `ReactDOM.unmountComponentAtNode`.
58
+ */
59
+ var unmountFromMountPoint = exports.unmountFromMountPoint = function unmountFromMountPoint(mountPoint) {
60
+ if ((0, _platformFeatureFlags.fg)('nike_r19_render_unmount')) {
61
+ var root = reactRoots.get(mountPoint);
62
+ if (root) {
63
+ root.unmount();
64
+ reactRoots.delete(mountPoint);
65
+ }
66
+ } else {
67
+ _reactDom.default.unmountComponentAtNode(mountPoint);
68
+ }
69
+ };
@@ -1011,7 +1011,11 @@ var DragHandle = exports.DragHandle = function DragHandle(_ref2) {
1011
1011
  (_cleanUpTransitionLis2 = cleanUpTransitionListener) === null || _cleanUpTransitionLis2 === void 0 || _cleanUpTransitionLis2();
1012
1012
  };
1013
1013
  }, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType, docDepForReliableAnchor]);
1014
+ var isHandleShown = positionStylesOld.display !== 'none';
1014
1015
  (0, _react.useEffect)(function () {
1016
+ if ((0, _platformFeatureFlags.fg)('nike_r19_render_unmount')) {
1017
+ return;
1018
+ }
1015
1019
  if (handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && buttonRef.current) {
1016
1020
  var id = requestAnimationFrame(function () {
1017
1021
  var _buttonRef$current;
@@ -1023,6 +1027,43 @@ var DragHandle = exports.DragHandle = function DragHandle(_ref2) {
1023
1027
  };
1024
1028
  }
1025
1029
  }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
1030
+ (0, _react.useEffect)(function () {
1031
+ if (!(0, _platformFeatureFlags.fg)('nike_r19_render_unmount')) {
1032
+ return;
1033
+ }
1034
+ if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && isHandleShown && buttonRef.current)) {
1035
+ return;
1036
+ }
1037
+
1038
+ // isHandleShown means the handle has been positioned, but an ancestor can still be
1039
+ // visibility:hidden or display:none for a frame, in which case focus() is a silent no-op.
1040
+ // Focus now, and if activeElement shows it did not land, retry on the next few frames.
1041
+ var rafId;
1042
+ var attempts = 0;
1043
+ var MAX_ATTEMPTS = 5;
1044
+ var _focusHandle = function focusHandle() {
1045
+ var _getDocument2;
1046
+ var button = buttonRef.current;
1047
+ if (!button) {
1048
+ return;
1049
+ }
1050
+ button.focus();
1051
+ // getDocument() is the same document handleKeyDownNew reads activeElement from, so
1052
+ // this confirms Space/Enter will act on the handle rather than silently no-op.
1053
+ if (((_getDocument2 = (0, _browserApis.getDocument)()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement) === button || attempts >= MAX_ATTEMPTS) {
1054
+ return;
1055
+ }
1056
+ attempts++;
1057
+ rafId = requestAnimationFrame(_focusHandle);
1058
+ };
1059
+ _focusHandle();
1060
+ return function () {
1061
+ if (rafId !== undefined) {
1062
+ cancelAnimationFrame(rafId);
1063
+ }
1064
+ view.focus();
1065
+ };
1066
+ }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view, isHandleShown]);
1026
1067
  (0, _react.useEffect)(function () {
1027
1068
  if (typeof start !== 'number' || !selection) {
1028
1069
  return;
@@ -1,9 +1,9 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import ReactDOM from 'react-dom';
3
2
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
4
3
  import uuid from 'uuid';
5
4
  import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
6
5
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
6
+ import { unmountFromMountPoint } from './react-root-registry';
7
7
  import { isFontSizeMarkActive } from './utils/validation';
8
8
  export const TYPE_DROP_TARGET_DEC = 'drop-target-decoration';
9
9
  export const TYPE_HANDLE_DEC = 'drag-handle';
@@ -44,6 +44,6 @@ export const unmountDecorations = (nodeViewPortalProviderAPI, selector, key) =>
44
44
  // as it was more responsive and causes less re-rendering
45
45
  const decorationsToRemove = document.querySelectorAll(`[${selector}="true"]`);
46
46
  decorationsToRemove.forEach(el => {
47
- ReactDOM.unmountComponentAtNode(el);
47
+ unmountFromMountPoint(el);
48
48
  });
49
49
  };
@@ -1,6 +1,5 @@
1
1
  import { createElement } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
- import ReactDOM from 'react-dom';
4
3
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
5
4
  import uuid from 'uuid';
6
5
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
@@ -10,6 +9,7 @@ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
10
9
  import { ACTIVE_DRAG_HANDLE_ATTR } from '../ui/consts';
11
10
  import { DragHandle, DragHandleWithVisibility } from '../ui/drag-handle';
12
11
  import { TYPE_HANDLE_DEC, TYPE_NODE_DEC, unmountDecorations } from './decorations-common';
12
+ import { renderToMountPoint } from './react-root-registry';
13
13
  import { getMatchingBlockMarks } from './utils/marks';
14
14
  const TYPE_ACTIVE_HANDLE_DEC = 'active-drag-handle-node';
15
15
 
@@ -144,7 +144,7 @@ export const dragHandleDecoration = ({
144
144
  // );
145
145
 
146
146
  if (editorExperiment('platform_editor_controls', 'variant1')) {
147
- ReactDOM.render( /*#__PURE__*/createElement(DragHandleWithVisibility, {
147
+ renderToMountPoint( /*#__PURE__*/createElement(DragHandleWithVisibility, {
148
148
  view,
149
149
  api,
150
150
  formatMessage,
@@ -156,7 +156,7 @@ export const dragHandleDecoration = ({
156
156
  anchorRectCache
157
157
  }), element);
158
158
  } else {
159
- ReactDOM.render( /*#__PURE__*/createElement(DragHandle, {
159
+ renderToMountPoint( /*#__PURE__*/createElement(DragHandle, {
160
160
  view,
161
161
  api,
162
162
  formatMessage,
@@ -0,0 +1,60 @@
1
+ import ReactDOM, { flushSync } from 'react-dom';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
4
+
5
+ /**
6
+ * Shared registry of React roots created via `createRoot`.
7
+ *
8
+ * Both `decorations-drag-handle.ts` (render) and `decorations-common.ts`
9
+ * (unmount) must operate on the **same** root for a given DOM element, so
10
+ * the WeakMap lives here and is imported by both modules.
11
+ */
12
+ const reactRoots = new WeakMap();
13
+
14
+ /**
15
+ * Mounts `element` into `mountPoint`.
16
+ *
17
+ * When `nike_r19_render_unmount` is **on**, uses the React 18/19 `createRoot`
18
+ * API wrapped in `flushSync` so the render is synchronous – matching the
19
+ * legacy `ReactDOM.render` timing that ProseMirror decoration widgets depend
20
+ * on (focus management, anchor positioning, block-menu visibility all read
21
+ * the rendered DOM immediately after the callback returns).
22
+ *
23
+ * When the gate is **off**, falls back to the legacy `ReactDOM.render` path.
24
+ */
25
+ export const renderToMountPoint = (element, mountPoint) => {
26
+ if (fg('nike_r19_render_unmount')) {
27
+ let root = reactRoots.get(mountPoint);
28
+ if (!root) {
29
+ root = createRoot(mountPoint);
30
+ reactRoots.set(mountPoint, root);
31
+ }
32
+ const activeRoot = root;
33
+
34
+ // flushSync makes createRoot render synchronously, matching legacy
35
+ // ReactDOM.render timing that the widget/focus/positioning code depends on.
36
+ flushSync(() => activeRoot.render(element));
37
+ } else {
38
+ ReactDOM.render(element, mountPoint);
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Unmounts the React tree at `mountPoint`.
44
+ *
45
+ * When `nike_r19_render_unmount` is **on**, looks up the root in the shared
46
+ * registry, calls `root.unmount()`, and removes the entry.
47
+ *
48
+ * When the gate is **off**, falls back to `ReactDOM.unmountComponentAtNode`.
49
+ */
50
+ export const unmountFromMountPoint = mountPoint => {
51
+ if (fg('nike_r19_render_unmount')) {
52
+ const root = reactRoots.get(mountPoint);
53
+ if (root) {
54
+ root.unmount();
55
+ reactRoots.delete(mountPoint);
56
+ }
57
+ } else {
58
+ ReactDOM.unmountComponentAtNode(mountPoint);
59
+ }
60
+ };
@@ -992,7 +992,11 @@ export const DragHandle = ({
992
992
  (_cleanUpTransitionLis2 = cleanUpTransitionListener) === null || _cleanUpTransitionLis2 === void 0 ? void 0 : _cleanUpTransitionLis2();
993
993
  };
994
994
  }, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType, docDepForReliableAnchor]);
995
+ const isHandleShown = positionStylesOld.display !== 'none';
995
996
  useEffect(() => {
997
+ if (fg('nike_r19_render_unmount')) {
998
+ return;
999
+ }
996
1000
  if (handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && buttonRef.current) {
997
1001
  const id = requestAnimationFrame(() => {
998
1002
  var _buttonRef$current;
@@ -1004,6 +1008,43 @@ export const DragHandle = ({
1004
1008
  };
1005
1009
  }
1006
1010
  }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
1011
+ useEffect(() => {
1012
+ if (!fg('nike_r19_render_unmount')) {
1013
+ return;
1014
+ }
1015
+ if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && isHandleShown && buttonRef.current)) {
1016
+ return;
1017
+ }
1018
+
1019
+ // isHandleShown means the handle has been positioned, but an ancestor can still be
1020
+ // visibility:hidden or display:none for a frame, in which case focus() is a silent no-op.
1021
+ // Focus now, and if activeElement shows it did not land, retry on the next few frames.
1022
+ let rafId;
1023
+ let attempts = 0;
1024
+ const MAX_ATTEMPTS = 5;
1025
+ const focusHandle = () => {
1026
+ var _getDocument2;
1027
+ const button = buttonRef.current;
1028
+ if (!button) {
1029
+ return;
1030
+ }
1031
+ button.focus();
1032
+ // getDocument() is the same document handleKeyDownNew reads activeElement from, so
1033
+ // this confirms Space/Enter will act on the handle rather than silently no-op.
1034
+ if (((_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement) === button || attempts >= MAX_ATTEMPTS) {
1035
+ return;
1036
+ }
1037
+ attempts++;
1038
+ rafId = requestAnimationFrame(focusHandle);
1039
+ };
1040
+ focusHandle();
1041
+ return () => {
1042
+ if (rafId !== undefined) {
1043
+ cancelAnimationFrame(rafId);
1044
+ }
1045
+ view.focus();
1046
+ };
1047
+ }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view, isHandleShown]);
1007
1048
  useEffect(() => {
1008
1049
  if (typeof start !== 'number' || !selection) {
1009
1050
  return;
@@ -1,11 +1,11 @@
1
1
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
2
  import _createClass from "@babel/runtime/helpers/createClass";
3
3
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
- import ReactDOM from 'react-dom';
5
4
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
6
5
  import uuid from 'uuid';
7
6
  import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
8
7
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
8
+ import { unmountFromMountPoint } from './react-root-registry';
9
9
  import { isFontSizeMarkActive } from './utils/validation';
10
10
  export var TYPE_DROP_TARGET_DEC = 'drop-target-decoration';
11
11
  export var TYPE_HANDLE_DEC = 'drag-handle';
@@ -52,6 +52,6 @@ export var unmountDecorations = function unmountDecorations(nodeViewPortalProvid
52
52
  // as it was more responsive and causes less re-rendering
53
53
  var decorationsToRemove = document.querySelectorAll("[".concat(selector, "=\"true\"]"));
54
54
  decorationsToRemove.forEach(function (el) {
55
- ReactDOM.unmountComponentAtNode(el);
55
+ unmountFromMountPoint(el);
56
56
  });
57
57
  };
@@ -1,7 +1,6 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { createElement } from 'react';
3
3
  import { bind } from 'bind-event-listener';
4
- import ReactDOM from 'react-dom';
5
4
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
6
5
  import uuid from 'uuid';
7
6
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
@@ -11,6 +10,7 @@ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
11
10
  import { ACTIVE_DRAG_HANDLE_ATTR } from '../ui/consts';
12
11
  import { DragHandle, DragHandleWithVisibility } from '../ui/drag-handle';
13
12
  import { TYPE_HANDLE_DEC, TYPE_NODE_DEC, unmountDecorations } from './decorations-common';
13
+ import { renderToMountPoint } from './react-root-registry';
14
14
  import { getMatchingBlockMarks } from './utils/marks';
15
15
  var TYPE_ACTIVE_HANDLE_DEC = 'active-drag-handle-node';
16
16
 
@@ -149,7 +149,7 @@ export var dragHandleDecoration = function dragHandleDecoration(_ref) {
149
149
  // );
150
150
 
151
151
  if (editorExperiment('platform_editor_controls', 'variant1')) {
152
- ReactDOM.render( /*#__PURE__*/createElement(DragHandleWithVisibility, {
152
+ renderToMountPoint( /*#__PURE__*/createElement(DragHandleWithVisibility, {
153
153
  view: view,
154
154
  api: api,
155
155
  formatMessage: formatMessage,
@@ -161,7 +161,7 @@ export var dragHandleDecoration = function dragHandleDecoration(_ref) {
161
161
  anchorRectCache: anchorRectCache
162
162
  }), element);
163
163
  } else {
164
- ReactDOM.render( /*#__PURE__*/createElement(DragHandle, {
164
+ renderToMountPoint( /*#__PURE__*/createElement(DragHandle, {
165
165
  view: view,
166
166
  api: api,
167
167
  formatMessage: formatMessage,
@@ -0,0 +1,62 @@
1
+ import ReactDOM, { flushSync } from 'react-dom';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
4
+
5
+ /**
6
+ * Shared registry of React roots created via `createRoot`.
7
+ *
8
+ * Both `decorations-drag-handle.ts` (render) and `decorations-common.ts`
9
+ * (unmount) must operate on the **same** root for a given DOM element, so
10
+ * the WeakMap lives here and is imported by both modules.
11
+ */
12
+ var reactRoots = new WeakMap();
13
+
14
+ /**
15
+ * Mounts `element` into `mountPoint`.
16
+ *
17
+ * When `nike_r19_render_unmount` is **on**, uses the React 18/19 `createRoot`
18
+ * API wrapped in `flushSync` so the render is synchronous – matching the
19
+ * legacy `ReactDOM.render` timing that ProseMirror decoration widgets depend
20
+ * on (focus management, anchor positioning, block-menu visibility all read
21
+ * the rendered DOM immediately after the callback returns).
22
+ *
23
+ * When the gate is **off**, falls back to the legacy `ReactDOM.render` path.
24
+ */
25
+ export var renderToMountPoint = function renderToMountPoint(element, mountPoint) {
26
+ if (fg('nike_r19_render_unmount')) {
27
+ var root = reactRoots.get(mountPoint);
28
+ if (!root) {
29
+ root = createRoot(mountPoint);
30
+ reactRoots.set(mountPoint, root);
31
+ }
32
+ var activeRoot = root;
33
+
34
+ // flushSync makes createRoot render synchronously, matching legacy
35
+ // ReactDOM.render timing that the widget/focus/positioning code depends on.
36
+ flushSync(function () {
37
+ return activeRoot.render(element);
38
+ });
39
+ } else {
40
+ ReactDOM.render(element, mountPoint);
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Unmounts the React tree at `mountPoint`.
46
+ *
47
+ * When `nike_r19_render_unmount` is **on**, looks up the root in the shared
48
+ * registry, calls `root.unmount()`, and removes the entry.
49
+ *
50
+ * When the gate is **off**, falls back to `ReactDOM.unmountComponentAtNode`.
51
+ */
52
+ export var unmountFromMountPoint = function unmountFromMountPoint(mountPoint) {
53
+ if (fg('nike_r19_render_unmount')) {
54
+ var root = reactRoots.get(mountPoint);
55
+ if (root) {
56
+ root.unmount();
57
+ reactRoots.delete(mountPoint);
58
+ }
59
+ } else {
60
+ ReactDOM.unmountComponentAtNode(mountPoint);
61
+ }
62
+ };
@@ -1007,7 +1007,11 @@ export var DragHandle = function DragHandle(_ref2) {
1007
1007
  (_cleanUpTransitionLis2 = cleanUpTransitionListener) === null || _cleanUpTransitionLis2 === void 0 || _cleanUpTransitionLis2();
1008
1008
  };
1009
1009
  }, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType, docDepForReliableAnchor]);
1010
+ var isHandleShown = positionStylesOld.display !== 'none';
1010
1011
  useEffect(function () {
1012
+ if (fg('nike_r19_render_unmount')) {
1013
+ return;
1014
+ }
1011
1015
  if (handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && buttonRef.current) {
1012
1016
  var id = requestAnimationFrame(function () {
1013
1017
  var _buttonRef$current;
@@ -1019,6 +1023,43 @@ export var DragHandle = function DragHandle(_ref2) {
1019
1023
  };
1020
1024
  }
1021
1025
  }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
1026
+ useEffect(function () {
1027
+ if (!fg('nike_r19_render_unmount')) {
1028
+ return;
1029
+ }
1030
+ if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && isHandleShown && buttonRef.current)) {
1031
+ return;
1032
+ }
1033
+
1034
+ // isHandleShown means the handle has been positioned, but an ancestor can still be
1035
+ // visibility:hidden or display:none for a frame, in which case focus() is a silent no-op.
1036
+ // Focus now, and if activeElement shows it did not land, retry on the next few frames.
1037
+ var rafId;
1038
+ var attempts = 0;
1039
+ var MAX_ATTEMPTS = 5;
1040
+ var _focusHandle = function focusHandle() {
1041
+ var _getDocument2;
1042
+ var button = buttonRef.current;
1043
+ if (!button) {
1044
+ return;
1045
+ }
1046
+ button.focus();
1047
+ // getDocument() is the same document handleKeyDownNew reads activeElement from, so
1048
+ // this confirms Space/Enter will act on the handle rather than silently no-op.
1049
+ if (((_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement) === button || attempts >= MAX_ATTEMPTS) {
1050
+ return;
1051
+ }
1052
+ attempts++;
1053
+ rafId = requestAnimationFrame(_focusHandle);
1054
+ };
1055
+ _focusHandle();
1056
+ return function () {
1057
+ if (rafId !== undefined) {
1058
+ cancelAnimationFrame(rafId);
1059
+ }
1060
+ view.focus();
1061
+ };
1062
+ }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view, isHandleShown]);
1022
1063
  useEffect(function () {
1023
1064
  if (typeof start !== 'number' || !selection) {
1024
1065
  return;
@@ -0,0 +1,22 @@
1
+ import type { ReactElement } from 'react';
2
+ /**
3
+ * Mounts `element` into `mountPoint`.
4
+ *
5
+ * When `nike_r19_render_unmount` is **on**, uses the React 18/19 `createRoot`
6
+ * API wrapped in `flushSync` so the render is synchronous – matching the
7
+ * legacy `ReactDOM.render` timing that ProseMirror decoration widgets depend
8
+ * on (focus management, anchor positioning, block-menu visibility all read
9
+ * the rendered DOM immediately after the callback returns).
10
+ *
11
+ * When the gate is **off**, falls back to the legacy `ReactDOM.render` path.
12
+ */
13
+ export declare const renderToMountPoint: (element: ReactElement, mountPoint: Element) => void;
14
+ /**
15
+ * Unmounts the React tree at `mountPoint`.
16
+ *
17
+ * When `nike_r19_render_unmount` is **on**, looks up the root in the shared
18
+ * registry, calls `root.unmount()`, and removes the entry.
19
+ *
20
+ * When the gate is **off**, falls back to `ReactDOM.unmountComponentAtNode`.
21
+ */
22
+ export declare const unmountFromMountPoint: (mountPoint: Element) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-controls",
3
- "version": "13.4.0",
3
+ "version": "13.4.1",
4
4
  "description": "Block controls plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -48,7 +48,7 @@
48
48
  "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^4.1.0",
49
49
  "@atlaskit/primitives": "^22.2.0",
50
50
  "@atlaskit/theme": "^26.1.0",
51
- "@atlaskit/tmp-editor-statsig": "^135.7.0",
51
+ "@atlaskit/tmp-editor-statsig": "^135.10.0",
52
52
  "@atlaskit/tokens": "^16.3.0",
53
53
  "@atlaskit/tooltip": "^24.0.0",
54
54
  "@babel/runtime": "^7.0.0",
@@ -59,7 +59,7 @@
59
59
  "uuid": "^3.1.0"
60
60
  },
61
61
  "peerDependencies": {
62
- "@atlaskit/editor-common": "^116.45.0",
62
+ "@atlaskit/editor-common": "^116.47.0",
63
63
  "react": "^18.2.0 || ^19.2.0",
64
64
  "react-dom": "^18.2.0 || ^19.2.0",
65
65
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
@@ -137,6 +137,9 @@
137
137
  "platform-dst-top-layer-tooltip": {
138
138
  "type": "boolean",
139
139
  "referenceOnly": true
140
+ },
141
+ "nike_r19_render_unmount": {
142
+ "type": "boolean"
140
143
  }
141
144
  },
142
145
  "devDependencies": {