@atlaskit/editor-plugin-breakout 15.0.16 → 15.0.17

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,26 @@
1
1
  # @atlaskit/editor-plugin-breakout
2
2
 
3
+ ## 15.0.17
4
+
5
+ ### Patch Changes
6
+
7
+ - [`288e340380dbb`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/288e340380dbb) -
8
+ Behind experiment platform_editor_reduce_event_listener_count: cut the number of native DOM event
9
+ listeners the editor registers per node. React 18 registers a bubble and a capture listener for
10
+ every supported event type on each portal container (~130 listeners) and never removes them, so
11
+ (1) node views whose render() returns null no longer mount a portal at all, and (2) the breakout
12
+ resize handle tooltip portal is deferred until the pointer enters the node instead of being
13
+ mounted for every resizable node up front.
14
+
15
+ Also reduces the work the layout column divider decorations do on every `decorations(state)` read:
16
+ the mousedown listener is unbound when the widget decoration is destroyed, the widget DOM is built
17
+ lazily rather than once per call, the document walk no longer descends into node types that cannot
18
+ contain a layout, and the decoration key is derived from the layout's ordinal rather than its
19
+ position so an edit above a layout no longer invalidates and rebuilds every divider below it. The
20
+ widget now resolves its own layout section and column index from its live position at mousedown.
21
+
22
+ - Updated dependencies
23
+
3
24
  ## 15.0.16
4
25
 
5
26
  ### Patch Changes
@@ -10,7 +10,6 @@ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers
10
10
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
11
  var _react = _interopRequireWildcard(require("react"));
12
12
  var _bindEventListener = require("bind-event-listener");
13
- var _v = _interopRequireDefault(require("uuid/v4"));
14
13
  var _messages = require("@atlaskit/editor-common/messages");
15
14
  var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
16
15
  var _adapter = require("@atlaskit/pragmatic-drag-and-drop/element/adapter");
@@ -20,8 +19,6 @@ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
20
19
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
21
20
  var _tooltip = _interopRequireDefault(require("@atlaskit/tooltip"));
22
21
  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); }
23
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
24
-
25
22
  var getNodeName = function getNodeName(nodeName) {
26
23
  if (nodeName === 'layoutSection') {
27
24
  return 'layout';
@@ -81,6 +78,9 @@ var createPragmaticResizer = exports.createPragmaticResizer = function createPra
81
78
  intl = _ref2.intl,
82
79
  nodeViewPortalProviderAPI = _ref2.nodeViewPortalProviderAPI;
83
80
  var state = 'default';
81
+ // Read once per resizer: `isExperimentEnabled` fires an exposure event, and `registerEvents` is
82
+ // bound to several elements so checking it inside the handler would emit one per element hovered.
83
+ var isDeferredTooltipEnabled = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_reduce_event_listener_count');
84
84
  var isLeftResizeHandleDisabled = (0, _expValEquals.expValEquals)('platform_editor_lovability_resize_dividers_panels', 'isEnabled', true) || (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_lovability_resize_extensions');
85
85
  var createHandle = function createHandle(side) {
86
86
  var handle = document.createElement('div');
@@ -106,22 +106,62 @@ var createPragmaticResizer = exports.createPragmaticResizer = function createPra
106
106
  tooltipContainer.classList.add('pm-breakout-resize-handle-rail-wrapper');
107
107
  handle.appendChild(tooltipContainer);
108
108
  handle.appendChild(handleHitBox);
109
+ var key = crypto.randomUUID();
110
+ var renderTooltip = function renderTooltip() {
111
+ return nodeViewPortalProviderAPI.render(function () {
112
+ return /*#__PURE__*/_react.default.createElement(RailWithTooltip, {
113
+ rail: rail,
114
+ target: target,
115
+ intl: intl
116
+ });
117
+ }, tooltipContainer, key);
118
+ };
109
119
 
110
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
111
- var key = (0, _v.default)();
112
- nodeViewPortalProviderAPI.render(function () {
113
- return /*#__PURE__*/_react.default.createElement(RailWithTooltip, {
114
- rail: rail,
115
- target: target,
116
- intl: intl
117
- });
118
- }, tooltipContainer, key);
120
+ // Only meaningful on the deferred path, where the tooltip mounts on first hover and so may
121
+ // never mount at all. The eager path below mounts unconditionally and does not consult it.
122
+ var isTooltipMounted = false;
123
+ var mountTooltip = function mountTooltip() {
124
+ if (isTooltipMounted) {
125
+ return;
126
+ }
127
+ isTooltipMounted = true;
128
+ // `RailWithTooltip`'s ref moves `rail` into the tooltip wrapper, so the DOM ends up
129
+ // identical to the eager path.
130
+ renderTooltip();
131
+ };
132
+
133
+ // React 18 registers ~130 native event listeners on every portal container (see
134
+ // `listenToAllSupportedEvents`), and never removes them. Mounting this tooltip up front
135
+ // costs that for every resizable node in the document — two containers per node when the
136
+ // left handle is enabled — even though only the hovered handle can ever show a tooltip.
137
+ //
138
+ // Instead, park `rail` directly in the wrapper and mount the tooltip on the first
139
+ // `mouseenter` of the node (see `registerEvents` below). The rail has `opacity: 0` until
140
+ // the pointer is inside the node, so the pre-mount DOM is never visible, and the
141
+ // `mouseenter` fires on the whole node — long before the pointer reaches the 7px handle.
142
+ if (isDeferredTooltipEnabled) {
143
+ // Without the tooltip's `[role="presentation"]` / `.pm-breakout-resize-handle-rail-inside-tooltip`
144
+ // wrappers (both stretched to `width: 100%` by `pragmaticResizerStylesForTooltip`), the rail
145
+ // would size to its content as a flex item. Stretch it explicitly so the pre-mount and
146
+ // post-mount geometry match exactly — `100%` of the 7px handle container either way.
147
+ rail.style.width = '100%';
148
+ tooltipContainer.appendChild(rail);
149
+ } else {
150
+ renderTooltip();
151
+ }
119
152
  return {
120
153
  handle: handle,
121
154
  rail: rail,
122
155
  handleHitBox: handleHitBox,
156
+ mountTooltip: mountTooltip,
123
157
  destroyTooltip: function destroyTooltip() {
124
- return nodeViewPortalProviderAPI.remove(key);
158
+ // The eager path always mounted, so it always removes — unchanged from before. Only
159
+ // the deferred path can reach `destroyTooltip` without having mounted anything.
160
+ if (isDeferredTooltipEnabled && !isTooltipMounted) {
161
+ return;
162
+ }
163
+ nodeViewPortalProviderAPI.remove(key);
164
+ isTooltipMounted = false;
125
165
  }
126
166
  };
127
167
  };
@@ -161,6 +201,10 @@ var createPragmaticResizer = exports.createPragmaticResizer = function createPra
161
201
  return [(0, _bindEventListener.bind)(element, {
162
202
  type: 'mouseenter',
163
203
  listener: function listener() {
204
+ if (isDeferredTooltipEnabled) {
205
+ rightHandle.mountTooltip();
206
+ leftHandle === null || leftHandle === void 0 || leftHandle.mountTooltip();
207
+ }
164
208
  rightHandle.rail.style.setProperty('opacity', '1');
165
209
  leftHandle === null || leftHandle === void 0 || leftHandle.rail.style.setProperty('opacity', '1');
166
210
  }
@@ -1,7 +1,5 @@
1
1
  import React, { useLayoutEffect, useState } from 'react';
2
2
  import { bind } from 'bind-event-listener';
3
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
4
- import uuid from 'uuid/v4';
5
3
  import { breakoutMessages as messages } from '@atlaskit/editor-common/messages';
6
4
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
7
5
  import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
@@ -68,6 +66,9 @@ export const createPragmaticResizer = ({
68
66
  nodeViewPortalProviderAPI
69
67
  }) => {
70
68
  let state = 'default';
69
+ // Read once per resizer: `isExperimentEnabled` fires an exposure event, and `registerEvents` is
70
+ // bound to several elements so checking it inside the handler would emit one per element hovered.
71
+ const isDeferredTooltipEnabled = isExperimentEnabled('platform_editor_reduce_event_listener_count');
71
72
  const isLeftResizeHandleDisabled = expValEquals('platform_editor_lovability_resize_dividers_panels', 'isEnabled', true) || isExperimentEnabled('platform_editor_lovability_resize_extensions');
72
73
  const createHandle = side => {
73
74
  const handle = document.createElement('div');
@@ -93,19 +94,59 @@ export const createPragmaticResizer = ({
93
94
  tooltipContainer.classList.add('pm-breakout-resize-handle-rail-wrapper');
94
95
  handle.appendChild(tooltipContainer);
95
96
  handle.appendChild(handleHitBox);
96
-
97
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
98
- const key = uuid();
99
- nodeViewPortalProviderAPI.render(() => /*#__PURE__*/React.createElement(RailWithTooltip, {
97
+ const key = crypto.randomUUID();
98
+ const renderTooltip = () => nodeViewPortalProviderAPI.render(() => /*#__PURE__*/React.createElement(RailWithTooltip, {
100
99
  rail: rail,
101
100
  target: target,
102
101
  intl: intl
103
102
  }), tooltipContainer, key);
103
+
104
+ // Only meaningful on the deferred path, where the tooltip mounts on first hover and so may
105
+ // never mount at all. The eager path below mounts unconditionally and does not consult it.
106
+ let isTooltipMounted = false;
107
+ const mountTooltip = () => {
108
+ if (isTooltipMounted) {
109
+ return;
110
+ }
111
+ isTooltipMounted = true;
112
+ // `RailWithTooltip`'s ref moves `rail` into the tooltip wrapper, so the DOM ends up
113
+ // identical to the eager path.
114
+ renderTooltip();
115
+ };
116
+
117
+ // React 18 registers ~130 native event listeners on every portal container (see
118
+ // `listenToAllSupportedEvents`), and never removes them. Mounting this tooltip up front
119
+ // costs that for every resizable node in the document — two containers per node when the
120
+ // left handle is enabled — even though only the hovered handle can ever show a tooltip.
121
+ //
122
+ // Instead, park `rail` directly in the wrapper and mount the tooltip on the first
123
+ // `mouseenter` of the node (see `registerEvents` below). The rail has `opacity: 0` until
124
+ // the pointer is inside the node, so the pre-mount DOM is never visible, and the
125
+ // `mouseenter` fires on the whole node — long before the pointer reaches the 7px handle.
126
+ if (isDeferredTooltipEnabled) {
127
+ // Without the tooltip's `[role="presentation"]` / `.pm-breakout-resize-handle-rail-inside-tooltip`
128
+ // wrappers (both stretched to `width: 100%` by `pragmaticResizerStylesForTooltip`), the rail
129
+ // would size to its content as a flex item. Stretch it explicitly so the pre-mount and
130
+ // post-mount geometry match exactly — `100%` of the 7px handle container either way.
131
+ rail.style.width = '100%';
132
+ tooltipContainer.appendChild(rail);
133
+ } else {
134
+ renderTooltip();
135
+ }
104
136
  return {
105
137
  handle,
106
138
  rail,
107
139
  handleHitBox,
108
- destroyTooltip: () => nodeViewPortalProviderAPI.remove(key)
140
+ mountTooltip,
141
+ destroyTooltip: () => {
142
+ // The eager path always mounted, so it always removes — unchanged from before. Only
143
+ // the deferred path can reach `destroyTooltip` without having mounted anything.
144
+ if (isDeferredTooltipEnabled && !isTooltipMounted) {
145
+ return;
146
+ }
147
+ nodeViewPortalProviderAPI.remove(key);
148
+ isTooltipMounted = false;
149
+ }
109
150
  };
110
151
  };
111
152
  const rightHandle = createHandle('right');
@@ -143,6 +184,10 @@ export const createPragmaticResizer = ({
143
184
  return [bind(element, {
144
185
  type: 'mouseenter',
145
186
  listener: () => {
187
+ if (isDeferredTooltipEnabled) {
188
+ rightHandle.mountTooltip();
189
+ leftHandle === null || leftHandle === void 0 ? void 0 : leftHandle.mountTooltip();
190
+ }
146
191
  rightHandle.rail.style.setProperty('opacity', '1');
147
192
  leftHandle === null || leftHandle === void 0 ? void 0 : leftHandle.rail.style.setProperty('opacity', '1');
148
193
  }
@@ -2,8 +2,6 @@ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
3
  import React, { useLayoutEffect, useState } from 'react';
4
4
  import { bind } from 'bind-event-listener';
5
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
6
- import uuid from 'uuid/v4';
7
5
  import { breakoutMessages as messages } from '@atlaskit/editor-common/messages';
8
6
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
9
7
  import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
@@ -71,6 +69,9 @@ export var createPragmaticResizer = function createPragmaticResizer(_ref2) {
71
69
  intl = _ref2.intl,
72
70
  nodeViewPortalProviderAPI = _ref2.nodeViewPortalProviderAPI;
73
71
  var state = 'default';
72
+ // Read once per resizer: `isExperimentEnabled` fires an exposure event, and `registerEvents` is
73
+ // bound to several elements so checking it inside the handler would emit one per element hovered.
74
+ var isDeferredTooltipEnabled = isExperimentEnabled('platform_editor_reduce_event_listener_count');
74
75
  var isLeftResizeHandleDisabled = expValEquals('platform_editor_lovability_resize_dividers_panels', 'isEnabled', true) || isExperimentEnabled('platform_editor_lovability_resize_extensions');
75
76
  var createHandle = function createHandle(side) {
76
77
  var handle = document.createElement('div');
@@ -96,22 +97,62 @@ export var createPragmaticResizer = function createPragmaticResizer(_ref2) {
96
97
  tooltipContainer.classList.add('pm-breakout-resize-handle-rail-wrapper');
97
98
  handle.appendChild(tooltipContainer);
98
99
  handle.appendChild(handleHitBox);
100
+ var key = crypto.randomUUID();
101
+ var renderTooltip = function renderTooltip() {
102
+ return nodeViewPortalProviderAPI.render(function () {
103
+ return /*#__PURE__*/React.createElement(RailWithTooltip, {
104
+ rail: rail,
105
+ target: target,
106
+ intl: intl
107
+ });
108
+ }, tooltipContainer, key);
109
+ };
99
110
 
100
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
101
- var key = uuid();
102
- nodeViewPortalProviderAPI.render(function () {
103
- return /*#__PURE__*/React.createElement(RailWithTooltip, {
104
- rail: rail,
105
- target: target,
106
- intl: intl
107
- });
108
- }, tooltipContainer, key);
111
+ // Only meaningful on the deferred path, where the tooltip mounts on first hover and so may
112
+ // never mount at all. The eager path below mounts unconditionally and does not consult it.
113
+ var isTooltipMounted = false;
114
+ var mountTooltip = function mountTooltip() {
115
+ if (isTooltipMounted) {
116
+ return;
117
+ }
118
+ isTooltipMounted = true;
119
+ // `RailWithTooltip`'s ref moves `rail` into the tooltip wrapper, so the DOM ends up
120
+ // identical to the eager path.
121
+ renderTooltip();
122
+ };
123
+
124
+ // React 18 registers ~130 native event listeners on every portal container (see
125
+ // `listenToAllSupportedEvents`), and never removes them. Mounting this tooltip up front
126
+ // costs that for every resizable node in the document — two containers per node when the
127
+ // left handle is enabled — even though only the hovered handle can ever show a tooltip.
128
+ //
129
+ // Instead, park `rail` directly in the wrapper and mount the tooltip on the first
130
+ // `mouseenter` of the node (see `registerEvents` below). The rail has `opacity: 0` until
131
+ // the pointer is inside the node, so the pre-mount DOM is never visible, and the
132
+ // `mouseenter` fires on the whole node — long before the pointer reaches the 7px handle.
133
+ if (isDeferredTooltipEnabled) {
134
+ // Without the tooltip's `[role="presentation"]` / `.pm-breakout-resize-handle-rail-inside-tooltip`
135
+ // wrappers (both stretched to `width: 100%` by `pragmaticResizerStylesForTooltip`), the rail
136
+ // would size to its content as a flex item. Stretch it explicitly so the pre-mount and
137
+ // post-mount geometry match exactly — `100%` of the 7px handle container either way.
138
+ rail.style.width = '100%';
139
+ tooltipContainer.appendChild(rail);
140
+ } else {
141
+ renderTooltip();
142
+ }
109
143
  return {
110
144
  handle: handle,
111
145
  rail: rail,
112
146
  handleHitBox: handleHitBox,
147
+ mountTooltip: mountTooltip,
113
148
  destroyTooltip: function destroyTooltip() {
114
- return nodeViewPortalProviderAPI.remove(key);
149
+ // The eager path always mounted, so it always removes — unchanged from before. Only
150
+ // the deferred path can reach `destroyTooltip` without having mounted anything.
151
+ if (isDeferredTooltipEnabled && !isTooltipMounted) {
152
+ return;
153
+ }
154
+ nodeViewPortalProviderAPI.remove(key);
155
+ isTooltipMounted = false;
115
156
  }
116
157
  };
117
158
  };
@@ -151,6 +192,10 @@ export var createPragmaticResizer = function createPragmaticResizer(_ref2) {
151
192
  return [bind(element, {
152
193
  type: 'mouseenter',
153
194
  listener: function listener() {
195
+ if (isDeferredTooltipEnabled) {
196
+ rightHandle.mountTooltip();
197
+ leftHandle === null || leftHandle === void 0 || leftHandle.mountTooltip();
198
+ }
154
199
  rightHandle.rail.style.setProperty('opacity', '1');
155
200
  leftHandle === null || leftHandle === void 0 || leftHandle.rail.style.setProperty('opacity', '1');
156
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-breakout",
3
- "version": "15.0.16",
3
+ "version": "15.0.17",
4
4
  "description": "Breakout plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",