@atlaskit/editor-plugin-show-diff 16.0.2 → 16.0.3

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,30 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 16.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`0265ca1688db1`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0265ca1688db1) -
8
+ [EDITOR-8971] `VanillaTooltip` takes an optional container to append its tooltip to, instead of
9
+ appending it inside the trigger. Opt-in per caller, and used by the diff contributor tag; every
10
+ other tooltip is unchanged.
11
+
12
+ Diff contributor tag fixes, all behind `confluence_ncs_step_diffing_version_history`:
13
+ - The tag's full label no longer renders off screen. Its tooltip is hoisted out of the tag, so
14
+ Popper and the browser measure a top-layer popover from the same origin — inside the trigger
15
+ they disagreed whenever an ancestor was transformed, such as a wide image.
16
+ - Stepping to a change reveals only that change's tag, not every tag the active range touches.
17
+ - A contributor no longer gets two tags inside one navigation stop. Two of their changes that
18
+ merely touched each kept a tag, and the trailing one could be hovered but never stepped to; it
19
+ now folds into the leading tag and stays a hover target. Tags for different contributors sharing
20
+ a stop are still kept apart.
21
+ - A tag on deleted content no longer disappears when the document changes. Any change remaps the
22
+ decoration, which rebuilds its widget and fired the teardown — but the deleted-content widget's
23
+ DOM is reused as-is and runs no mount callback, so the tag was faded out of a change that was
24
+ still the active one. The tag is now kept whenever its host is still in a live editor.
25
+
26
+ - Updated dependencies
27
+
3
28
  ## 16.0.2
4
29
 
5
30
  ### Patch Changes
@@ -27,6 +27,7 @@ var _decorationKeys = require("../decorations/decorationKeys");
27
27
  var _extractContributorTags = require("../decorations/extractContributorTags");
28
28
  var _getAttrChangeRanges = require("../decorations/utils/getAttrChangeRanges");
29
29
  var _getMarkChangeRanges = require("../decorations/utils/getMarkChangeRanges");
30
+ var _getScrollableDecorations = require("../getScrollableDecorations");
30
31
  var _isExtendedEnabled = require("../isExtendedEnabled");
31
32
  var _diffBySteps = require("./diffBySteps");
32
33
  var _groupChangesByBlock = require("./groupChangesByBlock");
@@ -788,7 +789,10 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
788
789
  });
789
790
  var decorationSet = _view.DecorationSet.empty.add(tr.doc, decorations);
790
791
  return {
791
- contributorTags: showContributorTags ? (0, _extractContributorTags.extractContributorTags)(decorationSet, contributors) : [],
792
+ contributorTags: showContributorTags ? (0, _extractContributorTags.extractContributorTags)(decorationSet, contributors, activeIndexPos,
793
+ // The stops the step buttons walk, so a stop cannot hold a second tag for one
794
+ // contributor that navigation can never reach.
795
+ (0, _getScrollableDecorations.getScrollableDecorations)(decorationSet, tr.doc, diffType)) : [],
792
796
  decorations: decorationSet,
793
797
  diffDescriptors: (0, _decorationKeys.extractDiffDescriptors)(decorationSet)
794
798
  };
@@ -48,7 +48,26 @@ var mountContributorTag = exports.mountContributorTag = function mountContributo
48
48
  diffId: diffId
49
49
  }));
50
50
  controller.mount(host);
51
- return controller;
51
+
52
+ /**
53
+ * Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
54
+ * change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
55
+ * and runs no mount callback, so tearing down here left an active change with no tag
56
+ * (EDITOR-8971).
57
+ *
58
+ * Deferred, because the host is still attached while that update is in flight. A host outside a
59
+ * live editor root is a real removal — including the view being destroyed, which drops the root.
60
+ */
61
+ return {
62
+ destroy: function destroy() {
63
+ return queueMicrotask(function () {
64
+ var _mountContext$getEdit;
65
+ if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
66
+ controller.destroy();
67
+ }
68
+ });
69
+ }
70
+ };
52
71
  };
53
72
  var unmountContributorTag = exports.unmountContributorTag = function unmountContributorTag(mount) {
54
73
  mount === null || mount === void 0 || mount.destroy();
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.extractContributorTags = void 0;
8
+ var _toArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toArray"));
8
9
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
10
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
11
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
@@ -56,12 +57,48 @@ var isSameBlockChange = function isSameBlockChange(inner, block) {
56
57
  return inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
57
58
  };
58
59
 
60
+ /**
61
+ * Which of two changes in the same range leads, and so is the one a tag captions. Position first,
62
+ * then `side`, since deleted content shares its `from` with the content that replaced it and paints
63
+ * above; then the narrower range, so a change nested in another captions itself.
64
+ */
65
+ var byLeadingPosition = function byLeadingPosition(a, b) {
66
+ var _a$decoration$spec$si, _b$decoration$spec$si;
67
+ return a.decoration.from - b.decoration.from || ((_a$decoration$spec$si = a.decoration.spec.side) !== null && _a$decoration$spec$si !== void 0 ? _a$decoration$spec$si : 0) - ((_b$decoration$spec$si = b.decoration.spec.side) !== null && _b$decoration$spec$si !== void 0 ? _b$decoration$spec$si : 0) || a.decoration.to - a.decoration.from - (b.decoration.to - b.decoration.from);
68
+ };
69
+
70
+ /** The targets a navigation range wholly covers, leader first. */
71
+ var containedBy = function containedBy(targets, _ref2) {
72
+ var from = _ref2.from,
73
+ to = _ref2.to;
74
+ return targets.filter(function (_ref3) {
75
+ var decoration = _ref3.decoration;
76
+ return from <= decoration.from && decoration.to <= to;
77
+ }).sort(byLeadingPosition);
78
+ };
79
+
80
+ /**
81
+ * The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
82
+ * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
83
+ * containment, and the leading change of what it covers.
84
+ */
85
+ var resolveActiveTarget = function resolveActiveTarget(surviving, activeIndexPos) {
86
+ return containedBy(surviving, activeIndexPos)[0];
87
+ };
88
+
59
89
  /**
60
90
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
61
91
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
62
92
  * into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
93
+ *
94
+ * `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
95
+ * Absent, each tag keeps the active state its own decoration was drawn with.
96
+ *
97
+ * `stops`, when given, is the navigation stop list the step buttons walk
98
+ * (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
99
+ * leading one would ever be reachable. Absent, every decoration keeps its own tag.
63
100
  */
64
- var extractContributorTags = exports.extractContributorTags = function extractContributorTags(decorations, contributors) {
101
+ var extractContributorTags = exports.extractContributorTags = function extractContributorTags(decorations, contributors, activeIndexPos, stops) {
65
102
  if (!contributors) {
66
103
  return [];
67
104
  }
@@ -94,25 +131,30 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
94
131
  } finally {
95
132
  _iterator.f();
96
133
  }
97
- var inlineTargets = targets.filter(function (_ref2) {
98
- var decoration = _ref2.decoration;
134
+ var inlineTargets = targets.filter(function (_ref4) {
135
+ var decoration = _ref4.decoration;
99
136
  return decoration.spec.decorationType === 'inline';
100
137
  });
101
- var blockTargets = targets.filter(function (_ref3) {
102
- var decoration = _ref3.decoration;
138
+ var blockTargets = targets.filter(function (_ref5) {
139
+ var decoration = _ref5.decoration;
103
140
  return decoration.spec.decorationType === 'block';
104
141
  });
105
- var widgetTargets = targets.filter(function (_ref4) {
106
- var decoration = _ref4.decoration;
142
+ var widgetTargets = targets.filter(function (_ref6) {
143
+ var decoration = _ref6.decoration;
107
144
  return decoration.spec.decorationType === 'widget';
108
145
  });
109
146
  var linkedDiffIds = new Map();
110
147
  var folded = new Set();
111
148
  var fold = function fold(host, target) {
112
- var _linkedDiffIds$get;
149
+ var _linkedDiffIds$get, _linkedDiffIds$get2;
113
150
  folded.add(target);
114
151
  var hostDiffId = host.decoration.spec.diffId;
115
- linkedDiffIds.set(hostDiffId, [].concat((0, _toConsumableArray2.default)((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), [target.decoration.spec.diffId]));
152
+ var targetDiffId = target.decoration.spec.diffId;
153
+ // A target that already hosts folded tags hands them up, so no hover target is orphaned when
154
+ // one host folds into another.
155
+ var inherited = (_linkedDiffIds$get = linkedDiffIds.get(targetDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : [];
156
+ linkedDiffIds.delete(targetDiffId);
157
+ linkedDiffIds.set(hostDiffId, [].concat((0, _toConsumableArray2.default)((_linkedDiffIds$get2 = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get2 !== void 0 ? _linkedDiffIds$get2 : []), [targetDiffId], (0, _toConsumableArray2.default)(inherited)));
116
158
  };
117
159
  var _loop = function _loop() {
118
160
  var target = _targets[_i];
@@ -122,8 +164,8 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
122
164
  }
123
165
 
124
166
  // A block outranks both other kinds, so it is tried first.
125
- var block = blockTargets.find(function (_ref5) {
126
- var decoration = _ref5.decoration;
167
+ var block = blockTargets.find(function (_ref7) {
168
+ var decoration = _ref7.decoration;
127
169
  return isSameBlockChange(target.decoration, decoration);
128
170
  });
129
171
  if (block) {
@@ -132,8 +174,8 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
132
174
  }
133
175
  if (decorationType === 'inline') {
134
176
  // The deleted half renders in front of this one, so that half captions the change.
135
- var leadingWidget = widgetTargets.find(function (_ref6) {
136
- var decoration = _ref6.decoration;
177
+ var leadingWidget = widgetTargets.find(function (_ref8) {
178
+ var decoration = _ref8.decoration;
137
179
  return leadsReplacement(decoration) && isSameChange(decoration, target.decoration);
138
180
  });
139
181
  if (leadingWidget) {
@@ -146,8 +188,8 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
146
188
  if (leadsReplacement(target.decoration)) {
147
189
  return 0; // continue
148
190
  }
149
- var host = inlineTargets.find(function (_ref7) {
150
- var decoration = _ref7.decoration;
191
+ var host = inlineTargets.find(function (_ref9) {
192
+ var decoration = _ref9.decoration;
151
193
  return isSameChange(target.decoration, decoration);
152
194
  });
153
195
  if (host) {
@@ -159,14 +201,72 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
159
201
  _ret = _loop();
160
202
  if (_ret === 0) continue;
161
203
  }
162
- return targets.filter(function (target) {
204
+
205
+ // One tag per contributor per navigation stop. The folds above only catch a replacement's two
206
+ // halves and a block's own highlights; two of one contributor's changes that merely touch are a
207
+ // single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
208
+ // drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
209
+ // contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
210
+ // still captions each, rather than crediting one for the other's change.
211
+ var _iterator2 = _createForOfIteratorHelper(stops !== null && stops !== void 0 ? stops : []),
212
+ _step2;
213
+ try {
214
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
215
+ var stop = _step2.value;
216
+ var byContributor = new Map();
217
+ var _iterator3 = _createForOfIteratorHelper(containedBy(targets.filter(function (target) {
218
+ return !folded.has(target);
219
+ }), stop)),
220
+ _step3;
221
+ try {
222
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
223
+ var _target$decoration$sp, _byContributor$get;
224
+ var target = _step3.value;
225
+ var key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
226
+ byContributor.set(key, [].concat((0, _toConsumableArray2.default)((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), [target]));
227
+ }
228
+ } catch (err) {
229
+ _iterator3.e(err);
230
+ } finally {
231
+ _iterator3.f();
232
+ }
233
+ var _iterator4 = _createForOfIteratorHelper(byContributor.values()),
234
+ _step4;
235
+ try {
236
+ var _loop2 = function _loop2() {
237
+ var _step4$value = (0, _toArray2.default)(_step4.value),
238
+ leader = _step4$value[0],
239
+ trailing = _arrayLikeToArray(_step4$value).slice(1);
240
+ trailing.forEach(function (target) {
241
+ return fold(leader, target);
242
+ });
243
+ };
244
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
245
+ _loop2();
246
+ }
247
+ } catch (err) {
248
+ _iterator4.e(err);
249
+ } finally {
250
+ _iterator4.f();
251
+ }
252
+ }
253
+ } catch (err) {
254
+ _iterator2.e(err);
255
+ } finally {
256
+ _iterator2.f();
257
+ }
258
+ var surviving = targets.filter(function (target) {
163
259
  return !folded.has(target);
164
- }).map(function (_ref8) {
165
- var contributor = _ref8.contributor,
166
- spec = _ref8.decoration.spec;
260
+ });
261
+ var activeTarget = activeIndexPos === undefined ? undefined : resolveActiveTarget(surviving, activeIndexPos);
262
+ return surviving.map(function (target) {
263
+ var contributor = target.contributor,
264
+ spec = target.decoration.spec;
167
265
  var connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
168
266
  var connectedContributor = connected ? toTagContributor(connected) : undefined;
169
267
  var linked = linkedDiffIds.get(spec.diffId);
268
+ // Only the navigated tag reveals; with no active range, the decoration's own state stands.
269
+ var isActive = activeIndexPos === undefined ? spec.isActive : target === activeTarget;
170
270
  return _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
171
271
  contributor: toTagContributor(contributor),
172
272
  diffId: spec.diffId
@@ -174,8 +274,8 @@ var extractContributorTags = exports.extractContributorTags = function extractCo
174
274
  connectedContributor: connectedContributor
175
275
  } : {}), spec.colorScheme ? {
176
276
  colorScheme: spec.colorScheme
177
- } : {}), spec.isActive !== undefined ? {
178
- isActive: spec.isActive
277
+ } : {}), isActive !== undefined ? {
278
+ isActive: isActive
179
279
  } : {}), spec.isInserted !== undefined ? {
180
280
  isInserted: spec.isInserted
181
281
  } : {}), linked ? {
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.buildContributorTagDom = exports.TAG_EXIT_FALLBACK_MS = exports.MAX_TAG_WIDTH = exports.CONTRIBUTOR_TAG_Z_INDEX = exports.CONTRIBUTOR_TAG_TESTID = exports.CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = exports.CONTRIBUTOR_TAG_NAME_TESTID = exports.CONTRIBUTOR_TAG_CLASS = void 0;
7
+ exports.buildContributorTagDom = exports.TAG_EXIT_FALLBACK_MS = exports.MAX_TAG_WIDTH = exports.CONTRIBUTOR_TAG_Z_INDEX = exports.CONTRIBUTOR_TAG_TOOLTIP_STYLES = exports.CONTRIBUTOR_TAG_TESTID = exports.CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = exports.CONTRIBUTOR_TAG_NAME_TESTID = exports.CONTRIBUTOR_TAG_CLASS = void 0;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
9
  var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
10
10
  var CONTRIBUTOR_TAG_TESTID = exports.CONTRIBUTOR_TAG_TESTID = 'diff-contributor-tag';
@@ -49,6 +49,30 @@ var CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = exports.CONTRIBUTOR_TAG_REVEALED_ATTRIB
49
49
  */
50
50
  var TAG_EXIT_FALLBACK_MS = exports.TAG_EXIT_FALLBACK_MS = 1200;
51
51
 
52
+ /**
53
+ * The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
54
+ * hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
55
+ * `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
56
+ * look every other vanilla tooltip has; keep the two in step.
57
+ */
58
+ var CONTRIBUTOR_TAG_TOOLTIP_STYLES = exports.CONTRIBUTOR_TAG_TOOLTIP_STYLES = {
59
+ boxSizing: 'border-box',
60
+ maxWidth: '240px',
61
+ backgroundColor: "var(--ds-background-neutral-bold, #292A2E)",
62
+ // A `popover` is given one by the UA stylesheet.
63
+ border: 'none',
64
+ borderRadius: "var(--ds-radius-small, 3px)",
65
+ color: "var(--ds-text-inverse, #FFFFFF)",
66
+ font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
67
+ fontFamily: "var(--ds-font-family-body, \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
68
+ overflowWrap: 'break-word',
69
+ paddingBlock: "var(--ds-space-050, 4px)",
70
+ paddingInline: "var(--ds-space-075, 6px)",
71
+ whiteSpace: 'normal',
72
+ // A tooltip is never a hit target — it hangs over the document's own content.
73
+ pointerEvents: 'none'
74
+ };
75
+
52
76
  // Positioned against the host widget its decoration renders on the change's first character:
53
77
  // `bottom: 100%` lifts the tag onto the line above, `inset-inline-start` starts it on that
54
78
  // character.
@@ -400,7 +400,7 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
400
400
  }, {
401
401
  key: "syncTooltip",
402
402
  value: function syncTooltip() {
403
- var _this$tooltip2, _this$dom$tag$querySe;
403
+ var _this$tooltip2;
404
404
  var content = this.dom ? this.fullLabel : undefined;
405
405
  if (content === this.tooltipContent) {
406
406
  return;
@@ -411,17 +411,39 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
411
411
  if (!content || !this.dom) {
412
412
  return;
413
413
  }
414
- this.tooltip = new _vanillaTooltip.VanillaTooltip(this.dom.tag, content, undefined,
415
- // `ak-editor-vanilla-tooltip-default` opts into the shared `VanillaTooltip` look, defined as
416
- // `vanillaTooltipDefaultStyles` in both EditorContentContainer stylesheets — keep in sync when
417
- // renaming. Those styles are scoped under `.ProseMirror`, and the tag renders inside it.
418
- 'ak-editor-vanilla-tooltip-default');
414
+ this.tooltip = new _vanillaTooltip.VanillaTooltip(this.dom.tag, content,
415
+ // Generated id.
416
+ undefined,
417
+ // No class: the look is inline, because a hoisted tooltip is outside the `.ProseMirror`
418
+ // scope `VANILLA_TOOLTIP_DEFAULT_CLASS` is keyed on — see `CONTRIBUTOR_TAG_TOOLTIP_STYLES`.
419
+ '',
420
+ // Default delay, no `onShow`, and the default `top` placement.
421
+ undefined, _buildContributorTagDom.CONTRIBUTOR_TAG_TOOLTIP_STYLES, undefined, undefined, this.resolveTooltipContainer());
419
422
  // `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
420
423
  // second time — the hidden child already announces it, and names the tag with it.
421
424
  this.dom.tag.removeAttribute('aria-describedby');
422
- // The popover is a child of the trigger, so its text would be a third copy of the same
423
- // sentence. It stays visible for sighted users, who need the part of the name the tag clipped.
424
- (_this$dom$tag$querySe = this.dom.tag.querySelector(':scope > [role="tooltip"]')) === null || _this$dom$tag$querySe === void 0 || _this$dom$tag$querySe.setAttribute('aria-hidden', 'true');
425
+ // Out of the tag but still in the document, so its text would otherwise be a third copy of the
426
+ // same sentence. It stays visible for sighted users, who need the part of the name the tag
427
+ // clipped.
428
+ this.tooltip.element.setAttribute('aria-hidden', 'true');
429
+ }
430
+
431
+ /**
432
+ * Where the tooltip is appended, rather than inside the tag.
433
+ *
434
+ * Popper and the browser only agree on a top-layer popover's origin when nothing above it is
435
+ * transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
436
+ * that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
437
+ * (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
438
+ * the tooltip inside the editor it belongs to; the document body covers an appearance that has
439
+ * no content area.
440
+ */
441
+ }, {
442
+ key: "resolveTooltipContainer",
443
+ value: function resolveTooltipContainer() {
444
+ var _editorRoot$closest;
445
+ var editorRoot = this.options.getEditorRoot();
446
+ return (_editorRoot$closest = editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.closest('.ak-editor-content-area')) !== null && _editorRoot$closest !== void 0 ? _editorRoot$closest : editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.ownerDocument.body;
425
447
  }
426
448
 
427
449
  /**
@@ -17,6 +17,7 @@ import { extractDiffDescriptors } from '../decorations/decorationKeys';
17
17
  import { extractContributorTags } from '../decorations/extractContributorTags';
18
18
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
19
19
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
20
+ import { getScrollableDecorations } from '../getScrollableDecorations';
20
21
  import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
21
22
  import { diffBySteps } from './diffBySteps';
22
23
  import { groupChangesByBlock } from './groupChangesByBlock';
@@ -732,7 +733,10 @@ const calculateDiffDecorationsInner = ({
732
733
  });
733
734
  const decorationSet = DecorationSet.empty.add(tr.doc, decorations);
734
735
  return {
735
- contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors) : [],
736
+ contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors, activeIndexPos,
737
+ // The stops the step buttons walk, so a stop cannot hold a second tag for one
738
+ // contributor that navigation can never reach.
739
+ getScrollableDecorations(decorationSet, tr.doc, diffType)) : [],
736
740
  decorations: decorationSet,
737
741
  diffDescriptors: extractDiffDescriptors(decorationSet)
738
742
  };
@@ -39,7 +39,24 @@ export const mountContributorTag = ({
39
39
  diffId
40
40
  });
41
41
  controller.mount(host);
42
- return controller;
42
+
43
+ /**
44
+ * Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
45
+ * change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
46
+ * and runs no mount callback, so tearing down here left an active change with no tag
47
+ * (EDITOR-8971).
48
+ *
49
+ * Deferred, because the host is still attached while that update is in flight. A host outside a
50
+ * live editor root is a real removal — including the view being destroyed, which drops the root.
51
+ */
52
+ return {
53
+ destroy: () => queueMicrotask(() => {
54
+ var _mountContext$getEdit;
55
+ if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
56
+ controller.destroy();
57
+ }
58
+ })
59
+ };
43
60
  };
44
61
  export const unmountContributorTag = mount => {
45
62
  mount === null || mount === void 0 ? void 0 : mount.destroy();
@@ -34,12 +34,44 @@ const leadsReplacement = widget => fg('confluence_ncs_step_diffing_version_histo
34
34
  */
35
35
  const isSameBlockChange = (inner, block) => inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
36
36
 
37
+ /**
38
+ * Which of two changes in the same range leads, and so is the one a tag captions. Position first,
39
+ * then `side`, since deleted content shares its `from` with the content that replaced it and paints
40
+ * above; then the narrower range, so a change nested in another captions itself.
41
+ */
42
+ const byLeadingPosition = (a, b) => {
43
+ var _a$decoration$spec$si, _b$decoration$spec$si;
44
+ return a.decoration.from - b.decoration.from || ((_a$decoration$spec$si = a.decoration.spec.side) !== null && _a$decoration$spec$si !== void 0 ? _a$decoration$spec$si : 0) - ((_b$decoration$spec$si = b.decoration.spec.side) !== null && _b$decoration$spec$si !== void 0 ? _b$decoration$spec$si : 0) || a.decoration.to - a.decoration.from - (b.decoration.to - b.decoration.from);
45
+ };
46
+
47
+ /** The targets a navigation range wholly covers, leader first. */
48
+ const containedBy = (targets, {
49
+ from,
50
+ to
51
+ }) => targets.filter(({
52
+ decoration
53
+ }) => from <= decoration.from && decoration.to <= to).sort(byLeadingPosition);
54
+
55
+ /**
56
+ * The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
57
+ * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
58
+ * containment, and the leading change of what it covers.
59
+ */
60
+ const resolveActiveTarget = (surviving, activeIndexPos) => containedBy(surviving, activeIndexPos)[0];
61
+
37
62
  /**
38
63
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
39
64
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
40
65
  * into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
66
+ *
67
+ * `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
68
+ * Absent, each tag keeps the active state its own decoration was drawn with.
69
+ *
70
+ * `stops`, when given, is the navigation stop list the step buttons walk
71
+ * (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
72
+ * leading one would ever be reachable. Absent, every decoration keeps its own tag.
41
73
  */
42
- export const extractContributorTags = (decorations, contributors) => {
74
+ export const extractContributorTags = (decorations, contributors, activeIndexPos, stops) => {
43
75
  if (!contributors) {
44
76
  return [];
45
77
  }
@@ -77,10 +109,15 @@ export const extractContributorTags = (decorations, contributors) => {
77
109
  const linkedDiffIds = new Map();
78
110
  const folded = new Set();
79
111
  const fold = (host, target) => {
80
- var _linkedDiffIds$get;
112
+ var _linkedDiffIds$get, _linkedDiffIds$get2;
81
113
  folded.add(target);
82
114
  const hostDiffId = host.decoration.spec.diffId;
83
- linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), target.decoration.spec.diffId]);
115
+ const targetDiffId = target.decoration.spec.diffId;
116
+ // A target that already hosts folded tags hands them up, so no hover target is orphaned when
117
+ // one host folds into another.
118
+ const inherited = (_linkedDiffIds$get = linkedDiffIds.get(targetDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : [];
119
+ linkedDiffIds.delete(targetDiffId);
120
+ linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get2 = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get2 !== void 0 ? _linkedDiffIds$get2 : []), targetDiffId, ...inherited]);
84
121
  };
85
122
  for (const target of targets) {
86
123
  const {
@@ -120,15 +157,38 @@ export const extractContributorTags = (decorations, contributors) => {
120
157
  fold(host, target);
121
158
  }
122
159
  }
123
- return targets.filter(target => !folded.has(target)).map(({
124
- contributor,
125
- decoration: {
126
- spec
160
+
161
+ // One tag per contributor per navigation stop. The folds above only catch a replacement's two
162
+ // halves and a block's own highlights; two of one contributor's changes that merely touch are a
163
+ // single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
164
+ // drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
165
+ // contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
166
+ // still captions each, rather than crediting one for the other's change.
167
+ for (const stop of stops !== null && stops !== void 0 ? stops : []) {
168
+ const byContributor = new Map();
169
+ for (const target of containedBy(targets.filter(target => !folded.has(target)), stop)) {
170
+ var _target$decoration$sp, _byContributor$get;
171
+ const key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
172
+ byContributor.set(key, [...((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), target]);
127
173
  }
128
- }) => {
174
+ for (const [leader, ...trailing] of byContributor.values()) {
175
+ trailing.forEach(target => fold(leader, target));
176
+ }
177
+ }
178
+ const surviving = targets.filter(target => !folded.has(target));
179
+ const activeTarget = activeIndexPos === undefined ? undefined : resolveActiveTarget(surviving, activeIndexPos);
180
+ return surviving.map(target => {
181
+ const {
182
+ contributor,
183
+ decoration: {
184
+ spec
185
+ }
186
+ } = target;
129
187
  const connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
130
188
  const connectedContributor = connected ? toTagContributor(connected) : undefined;
131
189
  const linked = linkedDiffIds.get(spec.diffId);
190
+ // Only the navigated tag reveals; with no active range, the decoration's own state stands.
191
+ const isActive = activeIndexPos === undefined ? spec.isActive : target === activeTarget;
132
192
  return {
133
193
  contributor: toTagContributor(contributor),
134
194
  diffId: spec.diffId,
@@ -138,8 +198,8 @@ export const extractContributorTags = (decorations, contributors) => {
138
198
  ...(spec.colorScheme ? {
139
199
  colorScheme: spec.colorScheme
140
200
  } : {}),
141
- ...(spec.isActive !== undefined ? {
142
- isActive: spec.isActive
201
+ ...(isActive !== undefined ? {
202
+ isActive
143
203
  } : {}),
144
204
  ...(spec.isInserted !== undefined ? {
145
205
  isInserted: spec.isInserted
@@ -41,6 +41,30 @@ export const CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = 'data-revealed';
41
41
  */
42
42
  export const TAG_EXIT_FALLBACK_MS = 1200;
43
43
 
44
+ /**
45
+ * The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
46
+ * hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
47
+ * `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
48
+ * look every other vanilla tooltip has; keep the two in step.
49
+ */
50
+ export const CONTRIBUTOR_TAG_TOOLTIP_STYLES = {
51
+ boxSizing: 'border-box',
52
+ maxWidth: '240px',
53
+ backgroundColor: "var(--ds-background-neutral-bold, #292A2E)",
54
+ // A `popover` is given one by the UA stylesheet.
55
+ border: 'none',
56
+ borderRadius: "var(--ds-radius-small, 3px)",
57
+ color: "var(--ds-text-inverse, #FFFFFF)",
58
+ font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
59
+ fontFamily: "var(--ds-font-family-body, \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
60
+ overflowWrap: 'break-word',
61
+ paddingBlock: "var(--ds-space-050, 4px)",
62
+ paddingInline: "var(--ds-space-075, 6px)",
63
+ whiteSpace: 'normal',
64
+ // A tooltip is never a hit target — it hangs over the document's own content.
65
+ pointerEvents: 'none'
66
+ };
67
+
44
68
  // Positioned against the host widget its decoration renders on the change's first character:
45
69
  // `bottom: 100%` lifts the tag onto the line above, `inset-inline-start` starts it on that
46
70
  // character.
@@ -3,7 +3,7 @@ import { bind, bindAll } from 'bind-event-listener';
3
3
  import { VanillaTooltip } from '@atlaskit/editor-common/vanilla-tooltip';
4
4
  import { getAccentTokens } from '../../pm-plugins/decorations/colorSchemes/factory';
5
5
  import { colorSchemeRegistry } from '../../pm-plugins/decorations/colorSchemes/schemes';
6
- import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
6
+ import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, CONTRIBUTOR_TAG_TOOLTIP_STYLES, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
7
7
  import { contributorAvatarRenderer } from './contributorAvatarRenderer';
8
8
  import { formatContributorLabel } from './contributorLabel';
9
9
  /**
@@ -357,7 +357,7 @@ export class ContributorTagController {
357
357
  * shape of `syncVanillaDisabledTooltip` in `mentionNodeView`.
358
358
  */
359
359
  syncTooltip() {
360
- var _this$tooltip2, _this$dom$tag$querySe;
360
+ var _this$tooltip2;
361
361
  const content = this.dom ? this.fullLabel : undefined;
362
362
  if (content === this.tooltipContent) {
363
363
  return;
@@ -368,17 +368,37 @@ export class ContributorTagController {
368
368
  if (!content || !this.dom) {
369
369
  return;
370
370
  }
371
- this.tooltip = new VanillaTooltip(this.dom.tag, content, undefined,
372
- // `ak-editor-vanilla-tooltip-default` opts into the shared `VanillaTooltip` look, defined as
373
- // `vanillaTooltipDefaultStyles` in both EditorContentContainer stylesheets — keep in sync when
374
- // renaming. Those styles are scoped under `.ProseMirror`, and the tag renders inside it.
375
- 'ak-editor-vanilla-tooltip-default');
371
+ this.tooltip = new VanillaTooltip(this.dom.tag, content,
372
+ // Generated id.
373
+ undefined,
374
+ // No class: the look is inline, because a hoisted tooltip is outside the `.ProseMirror`
375
+ // scope `VANILLA_TOOLTIP_DEFAULT_CLASS` is keyed on — see `CONTRIBUTOR_TAG_TOOLTIP_STYLES`.
376
+ '',
377
+ // Default delay, no `onShow`, and the default `top` placement.
378
+ undefined, CONTRIBUTOR_TAG_TOOLTIP_STYLES, undefined, undefined, this.resolveTooltipContainer());
376
379
  // `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
377
380
  // second time — the hidden child already announces it, and names the tag with it.
378
381
  this.dom.tag.removeAttribute('aria-describedby');
379
- // The popover is a child of the trigger, so its text would be a third copy of the same
380
- // sentence. It stays visible for sighted users, who need the part of the name the tag clipped.
381
- (_this$dom$tag$querySe = this.dom.tag.querySelector(':scope > [role="tooltip"]')) === null || _this$dom$tag$querySe === void 0 ? void 0 : _this$dom$tag$querySe.setAttribute('aria-hidden', 'true');
382
+ // Out of the tag but still in the document, so its text would otherwise be a third copy of the
383
+ // same sentence. It stays visible for sighted users, who need the part of the name the tag
384
+ // clipped.
385
+ this.tooltip.element.setAttribute('aria-hidden', 'true');
386
+ }
387
+
388
+ /**
389
+ * Where the tooltip is appended, rather than inside the tag.
390
+ *
391
+ * Popper and the browser only agree on a top-layer popover's origin when nothing above it is
392
+ * transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
393
+ * that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
394
+ * (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
395
+ * the tooltip inside the editor it belongs to; the document body covers an appearance that has
396
+ * no content area.
397
+ */
398
+ resolveTooltipContainer() {
399
+ var _editorRoot$closest;
400
+ const editorRoot = this.options.getEditorRoot();
401
+ return (_editorRoot$closest = editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.closest('.ak-editor-content-area')) !== null && _editorRoot$closest !== void 0 ? _editorRoot$closest : editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.ownerDocument.body;
382
402
  }
383
403
 
384
404
  /**
@@ -25,6 +25,7 @@ import { extractDiffDescriptors } from '../decorations/decorationKeys';
25
25
  import { extractContributorTags } from '../decorations/extractContributorTags';
26
26
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
27
27
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
28
+ import { getScrollableDecorations } from '../getScrollableDecorations';
28
29
  import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
29
30
  import { diffBySteps } from './diffBySteps';
30
31
  import { groupChangesByBlock } from './groupChangesByBlock';
@@ -781,7 +782,10 @@ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8
781
782
  });
782
783
  var decorationSet = DecorationSet.empty.add(tr.doc, decorations);
783
784
  return {
784
- contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors) : [],
785
+ contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors, activeIndexPos,
786
+ // The stops the step buttons walk, so a stop cannot hold a second tag for one
787
+ // contributor that navigation can never reach.
788
+ getScrollableDecorations(decorationSet, tr.doc, diffType)) : [],
785
789
  decorations: decorationSet,
786
790
  diffDescriptors: extractDiffDescriptors(decorationSet)
787
791
  };
@@ -42,7 +42,26 @@ export var mountContributorTag = function mountContributorTag(_ref) {
42
42
  diffId: diffId
43
43
  }));
44
44
  controller.mount(host);
45
- return controller;
45
+
46
+ /**
47
+ * Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
48
+ * change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
49
+ * and runs no mount callback, so tearing down here left an active change with no tag
50
+ * (EDITOR-8971).
51
+ *
52
+ * Deferred, because the host is still attached while that update is in flight. A host outside a
53
+ * live editor root is a real removal — including the view being destroyed, which drops the root.
54
+ */
55
+ return {
56
+ destroy: function destroy() {
57
+ return queueMicrotask(function () {
58
+ var _mountContext$getEdit;
59
+ if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
60
+ controller.destroy();
61
+ }
62
+ });
63
+ }
64
+ };
46
65
  };
47
66
  export var unmountContributorTag = function unmountContributorTag(mount) {
48
67
  mount === null || mount === void 0 || mount.destroy();
@@ -1,3 +1,4 @@
1
+ import _toArray from "@babel/runtime/helpers/toArray";
1
2
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
3
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
4
  import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
@@ -50,12 +51,48 @@ var isSameBlockChange = function isSameBlockChange(inner, block) {
50
51
  return inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
51
52
  };
52
53
 
54
+ /**
55
+ * Which of two changes in the same range leads, and so is the one a tag captions. Position first,
56
+ * then `side`, since deleted content shares its `from` with the content that replaced it and paints
57
+ * above; then the narrower range, so a change nested in another captions itself.
58
+ */
59
+ var byLeadingPosition = function byLeadingPosition(a, b) {
60
+ var _a$decoration$spec$si, _b$decoration$spec$si;
61
+ return a.decoration.from - b.decoration.from || ((_a$decoration$spec$si = a.decoration.spec.side) !== null && _a$decoration$spec$si !== void 0 ? _a$decoration$spec$si : 0) - ((_b$decoration$spec$si = b.decoration.spec.side) !== null && _b$decoration$spec$si !== void 0 ? _b$decoration$spec$si : 0) || a.decoration.to - a.decoration.from - (b.decoration.to - b.decoration.from);
62
+ };
63
+
64
+ /** The targets a navigation range wholly covers, leader first. */
65
+ var containedBy = function containedBy(targets, _ref2) {
66
+ var from = _ref2.from,
67
+ to = _ref2.to;
68
+ return targets.filter(function (_ref3) {
69
+ var decoration = _ref3.decoration;
70
+ return from <= decoration.from && decoration.to <= to;
71
+ }).sort(byLeadingPosition);
72
+ };
73
+
74
+ /**
75
+ * The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
76
+ * group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
77
+ * containment, and the leading change of what it covers.
78
+ */
79
+ var resolveActiveTarget = function resolveActiveTarget(surviving, activeIndexPos) {
80
+ return containedBy(surviving, activeIndexPos)[0];
81
+ };
82
+
53
83
  /**
54
84
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
55
85
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
56
86
  * into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
87
+ *
88
+ * `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
89
+ * Absent, each tag keeps the active state its own decoration was drawn with.
90
+ *
91
+ * `stops`, when given, is the navigation stop list the step buttons walk
92
+ * (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
93
+ * leading one would ever be reachable. Absent, every decoration keeps its own tag.
57
94
  */
58
- export var extractContributorTags = function extractContributorTags(decorations, contributors) {
95
+ export var extractContributorTags = function extractContributorTags(decorations, contributors, activeIndexPos, stops) {
59
96
  if (!contributors) {
60
97
  return [];
61
98
  }
@@ -88,25 +125,30 @@ export var extractContributorTags = function extractContributorTags(decorations,
88
125
  } finally {
89
126
  _iterator.f();
90
127
  }
91
- var inlineTargets = targets.filter(function (_ref2) {
92
- var decoration = _ref2.decoration;
128
+ var inlineTargets = targets.filter(function (_ref4) {
129
+ var decoration = _ref4.decoration;
93
130
  return decoration.spec.decorationType === 'inline';
94
131
  });
95
- var blockTargets = targets.filter(function (_ref3) {
96
- var decoration = _ref3.decoration;
132
+ var blockTargets = targets.filter(function (_ref5) {
133
+ var decoration = _ref5.decoration;
97
134
  return decoration.spec.decorationType === 'block';
98
135
  });
99
- var widgetTargets = targets.filter(function (_ref4) {
100
- var decoration = _ref4.decoration;
136
+ var widgetTargets = targets.filter(function (_ref6) {
137
+ var decoration = _ref6.decoration;
101
138
  return decoration.spec.decorationType === 'widget';
102
139
  });
103
140
  var linkedDiffIds = new Map();
104
141
  var folded = new Set();
105
142
  var fold = function fold(host, target) {
106
- var _linkedDiffIds$get;
143
+ var _linkedDiffIds$get, _linkedDiffIds$get2;
107
144
  folded.add(target);
108
145
  var hostDiffId = host.decoration.spec.diffId;
109
- linkedDiffIds.set(hostDiffId, [].concat(_toConsumableArray((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), [target.decoration.spec.diffId]));
146
+ var targetDiffId = target.decoration.spec.diffId;
147
+ // A target that already hosts folded tags hands them up, so no hover target is orphaned when
148
+ // one host folds into another.
149
+ var inherited = (_linkedDiffIds$get = linkedDiffIds.get(targetDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : [];
150
+ linkedDiffIds.delete(targetDiffId);
151
+ linkedDiffIds.set(hostDiffId, [].concat(_toConsumableArray((_linkedDiffIds$get2 = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get2 !== void 0 ? _linkedDiffIds$get2 : []), [targetDiffId], _toConsumableArray(inherited)));
110
152
  };
111
153
  var _loop = function _loop() {
112
154
  var target = _targets[_i];
@@ -116,8 +158,8 @@ export var extractContributorTags = function extractContributorTags(decorations,
116
158
  }
117
159
 
118
160
  // A block outranks both other kinds, so it is tried first.
119
- var block = blockTargets.find(function (_ref5) {
120
- var decoration = _ref5.decoration;
161
+ var block = blockTargets.find(function (_ref7) {
162
+ var decoration = _ref7.decoration;
121
163
  return isSameBlockChange(target.decoration, decoration);
122
164
  });
123
165
  if (block) {
@@ -126,8 +168,8 @@ export var extractContributorTags = function extractContributorTags(decorations,
126
168
  }
127
169
  if (decorationType === 'inline') {
128
170
  // The deleted half renders in front of this one, so that half captions the change.
129
- var leadingWidget = widgetTargets.find(function (_ref6) {
130
- var decoration = _ref6.decoration;
171
+ var leadingWidget = widgetTargets.find(function (_ref8) {
172
+ var decoration = _ref8.decoration;
131
173
  return leadsReplacement(decoration) && isSameChange(decoration, target.decoration);
132
174
  });
133
175
  if (leadingWidget) {
@@ -140,8 +182,8 @@ export var extractContributorTags = function extractContributorTags(decorations,
140
182
  if (leadsReplacement(target.decoration)) {
141
183
  return 0; // continue
142
184
  }
143
- var host = inlineTargets.find(function (_ref7) {
144
- var decoration = _ref7.decoration;
185
+ var host = inlineTargets.find(function (_ref9) {
186
+ var decoration = _ref9.decoration;
145
187
  return isSameChange(target.decoration, decoration);
146
188
  });
147
189
  if (host) {
@@ -153,14 +195,72 @@ export var extractContributorTags = function extractContributorTags(decorations,
153
195
  _ret = _loop();
154
196
  if (_ret === 0) continue;
155
197
  }
156
- return targets.filter(function (target) {
198
+
199
+ // One tag per contributor per navigation stop. The folds above only catch a replacement's two
200
+ // halves and a block's own highlights; two of one contributor's changes that merely touch are a
201
+ // single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
202
+ // drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
203
+ // contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
204
+ // still captions each, rather than crediting one for the other's change.
205
+ var _iterator2 = _createForOfIteratorHelper(stops !== null && stops !== void 0 ? stops : []),
206
+ _step2;
207
+ try {
208
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
209
+ var stop = _step2.value;
210
+ var byContributor = new Map();
211
+ var _iterator3 = _createForOfIteratorHelper(containedBy(targets.filter(function (target) {
212
+ return !folded.has(target);
213
+ }), stop)),
214
+ _step3;
215
+ try {
216
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
217
+ var _target$decoration$sp, _byContributor$get;
218
+ var target = _step3.value;
219
+ var key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
220
+ byContributor.set(key, [].concat(_toConsumableArray((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), [target]));
221
+ }
222
+ } catch (err) {
223
+ _iterator3.e(err);
224
+ } finally {
225
+ _iterator3.f();
226
+ }
227
+ var _iterator4 = _createForOfIteratorHelper(byContributor.values()),
228
+ _step4;
229
+ try {
230
+ var _loop2 = function _loop2() {
231
+ var _step4$value = _toArray(_step4.value),
232
+ leader = _step4$value[0],
233
+ trailing = _arrayLikeToArray(_step4$value).slice(1);
234
+ trailing.forEach(function (target) {
235
+ return fold(leader, target);
236
+ });
237
+ };
238
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
239
+ _loop2();
240
+ }
241
+ } catch (err) {
242
+ _iterator4.e(err);
243
+ } finally {
244
+ _iterator4.f();
245
+ }
246
+ }
247
+ } catch (err) {
248
+ _iterator2.e(err);
249
+ } finally {
250
+ _iterator2.f();
251
+ }
252
+ var surviving = targets.filter(function (target) {
157
253
  return !folded.has(target);
158
- }).map(function (_ref8) {
159
- var contributor = _ref8.contributor,
160
- spec = _ref8.decoration.spec;
254
+ });
255
+ var activeTarget = activeIndexPos === undefined ? undefined : resolveActiveTarget(surviving, activeIndexPos);
256
+ return surviving.map(function (target) {
257
+ var contributor = target.contributor,
258
+ spec = target.decoration.spec;
161
259
  var connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
162
260
  var connectedContributor = connected ? toTagContributor(connected) : undefined;
163
261
  var linked = linkedDiffIds.get(spec.diffId);
262
+ // Only the navigated tag reveals; with no active range, the decoration's own state stands.
263
+ var isActive = activeIndexPos === undefined ? spec.isActive : target === activeTarget;
164
264
  return _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
165
265
  contributor: toTagContributor(contributor),
166
266
  diffId: spec.diffId
@@ -168,8 +268,8 @@ export var extractContributorTags = function extractContributorTags(decorations,
168
268
  connectedContributor: connectedContributor
169
269
  } : {}), spec.colorScheme ? {
170
270
  colorScheme: spec.colorScheme
171
- } : {}), spec.isActive !== undefined ? {
172
- isActive: spec.isActive
271
+ } : {}), isActive !== undefined ? {
272
+ isActive: isActive
173
273
  } : {}), spec.isInserted !== undefined ? {
174
274
  isInserted: spec.isInserted
175
275
  } : {}), linked ? {
@@ -42,6 +42,30 @@ export var CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = 'data-revealed';
42
42
  */
43
43
  export var TAG_EXIT_FALLBACK_MS = 1200;
44
44
 
45
+ /**
46
+ * The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
47
+ * hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
48
+ * `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
49
+ * look every other vanilla tooltip has; keep the two in step.
50
+ */
51
+ export var CONTRIBUTOR_TAG_TOOLTIP_STYLES = {
52
+ boxSizing: 'border-box',
53
+ maxWidth: '240px',
54
+ backgroundColor: "var(--ds-background-neutral-bold, #292A2E)",
55
+ // A `popover` is given one by the UA stylesheet.
56
+ border: 'none',
57
+ borderRadius: "var(--ds-radius-small, 3px)",
58
+ color: "var(--ds-text-inverse, #FFFFFF)",
59
+ font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
60
+ fontFamily: "var(--ds-font-family-body, \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
61
+ overflowWrap: 'break-word',
62
+ paddingBlock: "var(--ds-space-050, 4px)",
63
+ paddingInline: "var(--ds-space-075, 6px)",
64
+ whiteSpace: 'normal',
65
+ // A tooltip is never a hit target — it hangs over the document's own content.
66
+ pointerEvents: 'none'
67
+ };
68
+
45
69
  // Positioned against the host widget its decoration renders on the change's first character:
46
70
  // `bottom: 100%` lifts the tag onto the line above, `inset-inline-start` starts it on that
47
71
  // character.
@@ -6,7 +6,7 @@ import { bind, bindAll } from 'bind-event-listener';
6
6
  import { VanillaTooltip } from '@atlaskit/editor-common/vanilla-tooltip';
7
7
  import { getAccentTokens } from '../../pm-plugins/decorations/colorSchemes/factory';
8
8
  import { colorSchemeRegistry } from '../../pm-plugins/decorations/colorSchemes/schemes';
9
- import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
9
+ import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, CONTRIBUTOR_TAG_TOOLTIP_STYLES, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
10
10
  import { contributorAvatarRenderer } from './contributorAvatarRenderer';
11
11
  import { formatContributorLabel } from './contributorLabel';
12
12
  /**
@@ -393,7 +393,7 @@ export var ContributorTagController = /*#__PURE__*/function () {
393
393
  }, {
394
394
  key: "syncTooltip",
395
395
  value: function syncTooltip() {
396
- var _this$tooltip2, _this$dom$tag$querySe;
396
+ var _this$tooltip2;
397
397
  var content = this.dom ? this.fullLabel : undefined;
398
398
  if (content === this.tooltipContent) {
399
399
  return;
@@ -404,17 +404,39 @@ export var ContributorTagController = /*#__PURE__*/function () {
404
404
  if (!content || !this.dom) {
405
405
  return;
406
406
  }
407
- this.tooltip = new VanillaTooltip(this.dom.tag, content, undefined,
408
- // `ak-editor-vanilla-tooltip-default` opts into the shared `VanillaTooltip` look, defined as
409
- // `vanillaTooltipDefaultStyles` in both EditorContentContainer stylesheets — keep in sync when
410
- // renaming. Those styles are scoped under `.ProseMirror`, and the tag renders inside it.
411
- 'ak-editor-vanilla-tooltip-default');
407
+ this.tooltip = new VanillaTooltip(this.dom.tag, content,
408
+ // Generated id.
409
+ undefined,
410
+ // No class: the look is inline, because a hoisted tooltip is outside the `.ProseMirror`
411
+ // scope `VANILLA_TOOLTIP_DEFAULT_CLASS` is keyed on — see `CONTRIBUTOR_TAG_TOOLTIP_STYLES`.
412
+ '',
413
+ // Default delay, no `onShow`, and the default `top` placement.
414
+ undefined, CONTRIBUTOR_TAG_TOOLTIP_STYLES, undefined, undefined, this.resolveTooltipContainer());
412
415
  // `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
413
416
  // second time — the hidden child already announces it, and names the tag with it.
414
417
  this.dom.tag.removeAttribute('aria-describedby');
415
- // The popover is a child of the trigger, so its text would be a third copy of the same
416
- // sentence. It stays visible for sighted users, who need the part of the name the tag clipped.
417
- (_this$dom$tag$querySe = this.dom.tag.querySelector(':scope > [role="tooltip"]')) === null || _this$dom$tag$querySe === void 0 || _this$dom$tag$querySe.setAttribute('aria-hidden', 'true');
418
+ // Out of the tag but still in the document, so its text would otherwise be a third copy of the
419
+ // same sentence. It stays visible for sighted users, who need the part of the name the tag
420
+ // clipped.
421
+ this.tooltip.element.setAttribute('aria-hidden', 'true');
422
+ }
423
+
424
+ /**
425
+ * Where the tooltip is appended, rather than inside the tag.
426
+ *
427
+ * Popper and the browser only agree on a top-layer popover's origin when nothing above it is
428
+ * transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
429
+ * that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
430
+ * (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
431
+ * the tooltip inside the editor it belongs to; the document body covers an appearance that has
432
+ * no content area.
433
+ */
434
+ }, {
435
+ key: "resolveTooltipContainer",
436
+ value: function resolveTooltipContainer() {
437
+ var _editorRoot$closest;
438
+ var editorRoot = this.options.getEditorRoot();
439
+ return (_editorRoot$closest = editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.closest('.ak-editor-content-area')) !== null && _editorRoot$closest !== void 0 ? _editorRoot$closest : editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.ownerDocument.body;
418
440
  }
419
441
 
420
442
  /**
@@ -5,5 +5,18 @@ import type { ResolvedDiffContributors } from './colorSchemes/attributions';
5
5
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
6
6
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
7
7
  * into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
8
+ *
9
+ * `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
10
+ * Absent, each tag keeps the active state its own decoration was drawn with.
11
+ *
12
+ * `stops`, when given, is the navigation stop list the step buttons walk
13
+ * (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
14
+ * leading one would ever be reachable. Absent, every decoration keeps its own tag.
8
15
  */
9
- export declare const extractContributorTags: (decorations: DecorationSet, contributors: ResolvedDiffContributors | undefined) => ContributorTagModel[];
16
+ export declare const extractContributorTags: (decorations: DecorationSet, contributors: ResolvedDiffContributors | undefined, activeIndexPos?: {
17
+ from: number;
18
+ to: number;
19
+ }, stops?: ReadonlyArray<{
20
+ from: number;
21
+ to: number;
22
+ }>) => ContributorTagModel[];
@@ -34,6 +34,13 @@ export declare const CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = "data-revealed";
34
34
  * number — hence the wide margin. Raise in step with that duration.
35
35
  */
36
36
  export declare const TAG_EXIT_FALLBACK_MS = 1200;
37
+ /**
38
+ * The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
39
+ * hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
40
+ * `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
41
+ * look every other vanilla tooltip has; keep the two in step.
42
+ */
43
+ export declare const CONTRIBUTOR_TAG_TOOLTIP_STYLES: Readonly<Record<string, string>>;
37
44
  export type ContributorTagDom = {
38
45
  avatars: HTMLSpanElement;
39
46
  name: HTMLSpanElement;
@@ -91,6 +91,17 @@ export declare class ContributorTagController {
91
91
  * shape of `syncVanillaDisabledTooltip` in `mentionNodeView`.
92
92
  */
93
93
  private syncTooltip;
94
+ /**
95
+ * Where the tooltip is appended, rather than inside the tag.
96
+ *
97
+ * Popper and the browser only agree on a top-layer popover's origin when nothing above it is
98
+ * transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
99
+ * that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
100
+ * (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
101
+ * the tooltip inside the editor it belongs to; the document body covers an appearance that has
102
+ * no content area.
103
+ */
104
+ private resolveTooltipContainer;
94
105
  /**
95
106
  * Observes the highlight this tag describes, for what CSS here cannot see: its hover state. The
96
107
  * highlight is a ProseMirror decoration in a different DOM subtree, so listeners are bound
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "16.0.2",
3
+ "version": "16.0.3",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "@atlaskit/platform-feature-experiments": "^0.3.0",
33
33
  "@atlaskit/platform-feature-flags": "^2.2.0",
34
34
  "@atlaskit/primitives": "^22.5.0",
35
- "@atlaskit/tmp-editor-statsig": "^189.0.0",
35
+ "@atlaskit/tmp-editor-statsig": "^190.0.0",
36
36
  "@atlaskit/tokens": "^16.12.0",
37
37
  "@babel/runtime": "^7.0.0",
38
38
  "@compiled/react": "^1.0.2",
@@ -46,7 +46,7 @@
46
46
  "@atlaskit/button": "^25.3.0",
47
47
  "@atlaskit/css": "^1.1.0",
48
48
  "@atlaskit/dropdown-menu": "^18.3.0",
49
- "@atlaskit/editor-core": "^228.0.0",
49
+ "@atlaskit/editor-core": "^228.1.0",
50
50
  "@atlaskit/editor-json-transformer": "^9.8.0",
51
51
  "@atlaskit/editor-plugins": "^16.1.0",
52
52
  "@atlaskit/form": "^17.2.0",
@@ -64,7 +64,7 @@
64
64
  "react-intl": "^7.0.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^122.2.0",
67
+ "@atlaskit/editor-common": "^122.3.0",
68
68
  "react": "^18.2.0 || ^19.2.0",
69
69
  "react-dom": "^18.2.0 || ^19.2.0",
70
70
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"