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

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/afm-products/tsconfig.json +3 -0
  3. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +86 -50
  4. package/dist/cjs/pm-plugins/decorations/utils/getAttrChangeRanges.js +19 -8
  5. package/dist/cjs/pm-plugins/decorations/utils/getMarkChangeRanges.js +18 -4
  6. package/dist/cjs/pm-plugins/decorations/utils/mapStepRangeToFinal.js +34 -0
  7. package/dist/cjs/pm-plugins/main.js +18 -0
  8. package/dist/cjs/ui/ContributorTag/buildContributorTagDom.js +26 -4
  9. package/dist/cjs/ui/ContributorTag/contributorLabel.js +55 -0
  10. package/dist/cjs/ui/ContributorTag/contributorTagController.js +14 -39
  11. package/dist/cjs/ui/ContributorTag/messages.js +5 -0
  12. package/dist/cjs/ui/DiffNavigation/announceActiveDiff.js +54 -0
  13. package/dist/cjs/ui/DiffNavigation/messages.js +19 -0
  14. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +43 -12
  15. package/dist/es2019/pm-plugins/decorations/utils/getAttrChangeRanges.js +16 -0
  16. package/dist/es2019/pm-plugins/decorations/utils/getMarkChangeRanges.js +12 -4
  17. package/dist/es2019/pm-plugins/decorations/utils/mapStepRangeToFinal.js +28 -0
  18. package/dist/es2019/pm-plugins/main.js +18 -0
  19. package/dist/es2019/ui/ContributorTag/buildContributorTagDom.js +24 -4
  20. package/dist/es2019/ui/ContributorTag/contributorLabel.js +49 -0
  21. package/dist/es2019/ui/ContributorTag/contributorTagController.js +14 -39
  22. package/dist/es2019/ui/ContributorTag/messages.js +5 -0
  23. package/dist/es2019/ui/DiffNavigation/announceActiveDiff.js +49 -0
  24. package/dist/es2019/ui/DiffNavigation/messages.js +13 -0
  25. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +86 -50
  26. package/dist/esm/pm-plugins/decorations/utils/getAttrChangeRanges.js +19 -8
  27. package/dist/esm/pm-plugins/decorations/utils/getMarkChangeRanges.js +17 -4
  28. package/dist/esm/pm-plugins/decorations/utils/mapStepRangeToFinal.js +28 -0
  29. package/dist/esm/pm-plugins/main.js +18 -0
  30. package/dist/esm/ui/ContributorTag/buildContributorTagDom.js +26 -4
  31. package/dist/esm/ui/ContributorTag/contributorLabel.js +49 -0
  32. package/dist/esm/ui/ContributorTag/contributorTagController.js +14 -39
  33. package/dist/esm/ui/ContributorTag/messages.js +5 -0
  34. package/dist/esm/ui/DiffNavigation/announceActiveDiff.js +48 -0
  35. package/dist/esm/ui/DiffNavigation/messages.js +13 -0
  36. package/dist/types/pm-plugins/decorations/utils/getAttrChangeRanges.d.ts +3 -0
  37. package/dist/types/pm-plugins/decorations/utils/getMarkChangeRanges.d.ts +4 -1
  38. package/dist/types/pm-plugins/decorations/utils/mapStepRangeToFinal.d.ts +8 -0
  39. package/dist/types/showDiffPluginType.d.ts +7 -1
  40. package/dist/types/ui/ContributorTag/buildContributorTagDom.d.ts +5 -2
  41. package/dist/types/ui/ContributorTag/contributorLabel.d.ts +13 -0
  42. package/dist/types/ui/ContributorTag/messages.d.ts +1 -0
  43. package/dist/types/ui/DiffNavigation/announceActiveDiff.d.ts +18 -0
  44. package/dist/types/ui/DiffNavigation/messages.d.ts +5 -0
  45. package/package.json +3 -2
@@ -15,22 +15,7 @@ var _factory = require("../../pm-plugins/decorations/colorSchemes/factory");
15
15
  var _schemes = require("../../pm-plugins/decorations/colorSchemes/schemes");
16
16
  var _buildContributorTagDom = require("./buildContributorTagDom");
17
17
  var _contributorAvatarRenderer = require("./contributorAvatarRenderer");
18
- var _messages = require("./messages");
19
- var getContributorName = function getContributorName(contributor, formatMessage) {
20
- var _contributor$name, _contributor$agentKin;
21
- var name = (_contributor$name = contributor.name) === null || _contributor$name === void 0 ? void 0 : _contributor$name.trim();
22
- if (name) {
23
- return name;
24
- }
25
-
26
- // `agentKind` defaults to `'external'` per the plugin type contract; an unidentified external
27
- // agent falls back to a generic label rather than rendering a nameless tag.
28
- if (contributor.kind === 'agent' && ((_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : 'external') === 'external') {
29
- return formatMessage(_messages.contributorTagMessages.externalAgentName);
30
- }
31
- return '';
32
- };
33
-
18
+ var _contributorLabel = require("./contributorLabel");
34
19
  /**
35
20
  * Resolves the tag's accent from the same scheme value the highlight was drawn from, so the colour
36
21
  * the two carry can never drift — only the tone does, which is `getAccentTokens`'s to pick.
@@ -113,8 +98,8 @@ var fadeTagOut = function fadeTagOut(tag, release) {
113
98
  /** The contributors a tag draws, keyed so a republished model can be recognised as the same pair. */
114
99
  var identityOf = function identityOf(model) {
115
100
  return [model === null || model === void 0 ? void 0 : model.contributor, model === null || model === void 0 ? void 0 : model.connectedContributor].map(function (contributor) {
116
- var _contributor$agentKin2, _contributor$avatarUr;
117
- return contributor ? "".concat(contributor.kind, ":").concat((_contributor$agentKin2 = contributor.agentKind) !== null && _contributor$agentKin2 !== void 0 ? _contributor$agentKin2 : '', ":").concat(contributor.name, ":").concat((_contributor$avatarUr = contributor.avatarUrl) !== null && _contributor$avatarUr !== void 0 ? _contributor$avatarUr : '') : '';
101
+ var _contributor$agentKin, _contributor$avatarUr;
102
+ return contributor ? "".concat(contributor.kind, ":").concat((_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : '', ":").concat(contributor.name, ":").concat((_contributor$avatarUr = contributor.avatarUrl) !== null && _contributor$avatarUr !== void 0 ? _contributor$avatarUr : '') : '';
118
103
  }).join('|');
119
104
  };
120
105
 
@@ -355,24 +340,12 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
355
340
  if (!this.dom) {
356
341
  return;
357
342
  }
358
- var _this$options$getIntl = this.options.getIntl(),
359
- formatMessage = _this$options$getIntl.formatMessage;
360
- var isPrimaryAgent = model.contributor.kind === 'agent';
361
- var primaryName = getContributorName(model.contributor, formatMessage);
362
- var secondaryName = model.connectedContributor ? getContributorName(model.connectedContributor, formatMessage) : '';
363
- var agentName = isPrimaryAgent ? primaryName : secondaryName;
364
- var userName = isPrimaryAgent ? secondaryName : primaryName;
365
-
366
- // A connected pair only ever shows the agent; the pair is spelled out in the label, because the
367
- // tag is capped at `MAX_TAG_WIDTH` and two names rarely fit inside it.
368
- this.dom.name.textContent = model.connectedContributor ? agentName || primaryName : primaryName;
369
- this.fullLabel = model.connectedContributor ? formatMessage(_messages.contributorTagMessages.changedByConnected, {
370
- agentName: agentName,
371
- userName: userName
372
- }) : formatMessage(_messages.contributorTagMessages.changedBy, {
373
- name: primaryName
374
- });
375
- this.dom.srLabel.textContent = this.fullLabel;
343
+ var _formatContributorLab = (0, _contributorLabel.formatContributorLabel)(model, this.options.getIntl().formatMessage),
344
+ fullLabel = _formatContributorLab.fullLabel,
345
+ visibleName = _formatContributorLab.visibleName;
346
+ this.dom.name.textContent = visibleName;
347
+ this.fullLabel = fullLabel;
348
+ this.dom.srLabel.textContent = fullLabel;
376
349
  }
377
350
  }, {
378
351
  key: "accentOf",
@@ -427,7 +400,7 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
427
400
  }, {
428
401
  key: "syncTooltip",
429
402
  value: function syncTooltip() {
430
- var _this$tooltip2;
403
+ var _this$tooltip2, _this$dom$tag$querySe;
431
404
  var content = this.dom ? this.fullLabel : undefined;
432
405
  if (content === this.tooltipContent) {
433
406
  return;
@@ -444,9 +417,11 @@ var ContributorTagController = exports.ContributorTagController = /*#__PURE__*/f
444
417
  // renaming. Those styles are scoped under `.ProseMirror`, and the tag renders inside it.
445
418
  'ak-editor-vanilla-tooltip-default');
446
419
  // `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
447
- // second time — the visually hidden child already announces it, after the change as design
448
- // requires.
420
+ // second time — the hidden child already announces it, and names the tag with it.
449
421
  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');
450
425
  }
451
426
 
452
427
  /**
@@ -11,6 +11,11 @@ var contributorTagMessages = exports.contributorTagMessages = (0, _reactIntl.def
11
11
  defaultMessage: 'Changed by {name}',
12
12
  description: 'Screen-reader label and tooltip for the tag pinned to a highlighted change in the version-history diff, naming the single person or agent that made the change. The name placeholder is the contributor display name.'
13
13
  },
14
+ changedByAgent: {
15
+ id: 'editor-plugin-show-diff.ContributorTag.changedByAgent',
16
+ defaultMessage: 'Changed by {name}, an AI agent',
17
+ description: 'Screen-reader label and tooltip for the tag pinned to a highlighted change in the version-history diff, when a named AI agent made the change rather than a person. The name placeholder is the agent display name.'
18
+ },
14
19
  changedByConnected: {
15
20
  id: 'editor-plugin-show-diff.ContributorTag.changedByConnected',
16
21
  defaultMessage: 'Changed by {agentName} with {userName}',
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.getActiveDiffAnnouncement = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _decorationKeys = require("../../pm-plugins/decorations/decorationKeys");
10
+ var _contributorLabel = require("../ContributorTag/contributorLabel");
11
+ var _messages = require("./messages");
12
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
13
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
14
+ /**
15
+ * The tag that captions the given decoration, if any. `linkedDiffIds` is checked as well as
16
+ * `diffId`, because a replacement's two decorations share one tag and navigation can land on either.
17
+ */
18
+ var findTagFor = function findTagFor(decoration, contributorTags) {
19
+ if (!(contributorTags !== null && contributorTags !== void 0 && contributorTags.length) || !(0, _decorationKeys.isDiffDecoration)(decoration)) {
20
+ return undefined;
21
+ }
22
+ var diffId = decoration.spec.diffId;
23
+ return contributorTags.find(function (tag) {
24
+ var _tag$linkedDiffIds;
25
+ return tag.diffId === diffId || ((_tag$linkedDiffIds = tag.linkedDiffIds) === null || _tag$linkedDiffIds === void 0 ? void 0 : _tag$linkedDiffIds.includes(diffId));
26
+ });
27
+ };
28
+
29
+ /**
30
+ * What a screen reader should say once the reader has stepped onto a change: where they are in the
31
+ * diff, and — when the plugin can credit the change — who made it. Stepping otherwise only scrolls,
32
+ * which is silent.
33
+ *
34
+ * Returns `undefined` for an index that resolves no change, so the caller says nothing at all
35
+ * rather than "Change 4 of 3".
36
+ */
37
+ var getActiveDiffAnnouncement = exports.getActiveDiffAnnouncement = function getActiveDiffAnnouncement(_ref) {
38
+ var activeIndex = _ref.activeIndex,
39
+ contributorTags = _ref.contributorTags,
40
+ decorations = _ref.decorations,
41
+ intl = _ref.intl;
42
+ var activeDecoration = decorations[activeIndex];
43
+ if (!activeDecoration) {
44
+ return undefined;
45
+ }
46
+ var position = {
47
+ index: activeIndex + 1,
48
+ total: decorations.length
49
+ };
50
+ var tag = findTagFor(activeDecoration, contributorTags);
51
+ return tag ? intl.formatMessage(_messages.diffNavigationMessages.activeChangeWithContributor, _objectSpread(_objectSpread({}, position), {}, {
52
+ contributor: (0, _contributorLabel.formatContributorLabel)(tag, intl.formatMessage).fullLabel
53
+ })) : intl.formatMessage(_messages.diffNavigationMessages.activeChange, position);
54
+ };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.diffNavigationMessages = void 0;
7
+ var _reactIntl = require("react-intl");
8
+ var diffNavigationMessages = exports.diffNavigationMessages = (0, _reactIntl.defineMessages)({
9
+ activeChange: {
10
+ id: 'editor-plugin-show-diff.DiffNavigation.activeChange',
11
+ defaultMessage: 'Change {index} of {total}',
12
+ description: 'Announced to screen readers when the reader steps to the next or previous change in the version-history diff. The index placeholder is the position of the change stepped to, counting from 1; total is how many changes the diff has.'
13
+ },
14
+ activeChangeWithContributor: {
15
+ id: 'editor-plugin-show-diff.DiffNavigation.activeChangeWithContributor',
16
+ defaultMessage: 'Change {index} of {total}. {contributor}',
17
+ description: 'Announced to screen readers when the reader steps to the next or previous change in the version-history diff and that change is credited to someone. The index and total placeholders are as in activeChange. The contributor placeholder is an already-translated sentence (changedBy, changedByAgent or changedByConnected), so it must be kept whole and only repositioned.'
18
+ }
19
+ });
@@ -316,11 +316,12 @@ const calculateDiffDecorationsInner = ({
316
316
  }
317
317
  const pendingAttrSteps = [];
318
318
  const stepMaps = [];
319
- for (const step of simplifiedSteps) {
319
+ for (const [stepIndex, step] of simplifiedSteps.entries()) {
320
320
  const result = step.apply(steppedDoc);
321
321
  if (result.failed === null && result.doc) {
322
322
  if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
323
323
  pendingAttrSteps.push({
324
+ attributionKey: getAttributionKey(simplifiedStepAttributions[stepIndex]),
324
325
  afterNode: result.doc.nodeAt(step.pos),
325
326
  beforeNode: steppedDoc.nodeAt(step.pos),
326
327
  mapIndex: stepMaps.length,
@@ -335,6 +336,7 @@ const calculateDiffDecorationsInner = ({
335
336
  // The fallback deliberately preserves the legacy raw-position lookups for rollout safety.
336
337
  const attrStepContexts = fg('platform_editor_reduce_diff_attr_sensitivity') ? pendingAttrSteps.flatMap(({
337
338
  afterNode,
339
+ attributionKey,
338
340
  beforeNode,
339
341
  mapIndex,
340
342
  step
@@ -345,6 +347,7 @@ const calculateDiffDecorationsInner = ({
345
347
  }
346
348
  const originalPosition = new Mapping(stepMaps.slice(0, mapIndex)).invert().mapResult(step.pos);
347
349
  return [{
350
+ attributionKey,
348
351
  afterNode,
349
352
  beforeNode,
350
353
  finalPos: finalPosition.pos,
@@ -354,8 +357,10 @@ const calculateDiffDecorationsInner = ({
354
357
  step
355
358
  }];
356
359
  }) : pendingAttrSteps.map(({
360
+ attributionKey,
357
361
  step
358
362
  }) => ({
363
+ attributionKey,
359
364
  afterNode: tr.doc.nodeAt(step.pos),
360
365
  beforeNode: originalDoc.nodeAt(step.pos),
361
366
  finalPos: step.pos,
@@ -608,31 +613,48 @@ const calculateDiffDecorationsInner = ({
608
613
  // smart path. Only add the legacy step-range decoration when no equivalent changeset range was
609
614
  // produced; otherwise the same content receives two inline decorations (and two navigation
610
615
  // changes), usually one attribution colour and one default purple decoration.
611
- getMarkChangeRanges(steps).filter(({
616
+ getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(({
612
617
  fromB,
613
618
  toB
614
619
  }) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
620
+ var _attributionColors$ge, _attributionColors;
621
+ const changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
622
+ const attributionKey = showContributorTags ? change.attributionKey : undefined;
615
623
  const isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
616
624
  decorations.push(...createInlineChangedDecoration({
625
+ attributionKey,
617
626
  change,
618
- colorScheme,
627
+ colorScheme: changeColorScheme,
628
+ doc: tr.doc,
619
629
  diffType,
620
630
  isActive,
621
631
  isInserted: true,
622
- reveal
632
+ reveal,
633
+ showContributorTags,
634
+ tagMountContext
623
635
  }));
624
636
  });
625
- getAttrChangeRanges(tr.doc, attrStepContexts, originalDoc).forEach(change => {
637
+ getAttrChangeRanges(tr.doc, attrStepContexts, originalDoc).filter(({
638
+ fromB,
639
+ toB
640
+ }) => !fg('confluence_ncs_step_diffing_version_history') || !changes.some(change => change.fromB === fromB && change.toB === toB)).forEach(change => {
641
+ var _attributionColors$ge2, _attributionColors2;
642
+ const changeColorScheme = change.attributionKey ? (_attributionColors$ge2 = (_attributionColors2 = attributionColors) === null || _attributionColors2 === void 0 ? void 0 : _attributionColors2.get(change.attributionKey)) !== null && _attributionColors$ge2 !== void 0 ? _attributionColors$ge2 : colorScheme : colorScheme;
643
+ const attributionKey = showContributorTags ? change.attributionKey : undefined;
626
644
  if (change.isInline) {
627
645
  // Inline nodes (e.g. date, emoji, mention, status) need an inline decoration rather than a block decoration
628
646
  const isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
629
647
  decorations.push(...createInlineChangedDecoration({
648
+ attributionKey,
630
649
  change,
631
- colorScheme,
650
+ colorScheme: changeColorScheme,
651
+ doc: tr.doc,
632
652
  isActive,
633
653
  isInserted: true,
634
654
  isAtomicInlineNode: true,
635
655
  inlineNodeName: change.inlineNodeName,
656
+ showContributorTags,
657
+ tagMountContext,
636
658
  // Suppress the border-bottom underline for atomic inline nodeviews —
637
659
  // it doesn't render on custom nodeview DOM elements and is unnecessary noise.
638
660
  hideAddedDiffsUnderline: true,
@@ -650,12 +672,15 @@ const calculateDiffDecorationsInner = ({
650
672
  deleted: []
651
673
  },
652
674
  doc: originalDoc,
675
+ attributionKey,
653
676
  nodeViewSerializer,
654
- colorScheme,
677
+ colorScheme: changeColorScheme,
655
678
  newDoc: tr.doc,
656
679
  intl,
657
680
  activeIndexPos,
658
- isInserted: false
681
+ isInserted: false,
682
+ showContributorTags,
683
+ tagMountContext
659
684
  }));
660
685
  }
661
686
  } else {
@@ -663,11 +688,14 @@ const calculateDiffDecorationsInner = ({
663
688
  doc: tr.doc,
664
689
  from: change.fromB,
665
690
  to: change.toB,
666
- colorScheme,
691
+ attributionKey,
692
+ colorScheme: changeColorScheme,
667
693
  isInserted: true,
668
694
  activeIndexPos,
669
695
  intl,
670
- showIndicators
696
+ showContributorTags,
697
+ showIndicators,
698
+ tagMountContext
671
699
  }));
672
700
  // If the original node position is known (e.g. a panel type change), also render
673
701
  // the old block node as a "deleted" widget so the reviewer sees the before/after.
@@ -684,7 +712,8 @@ const calculateDiffDecorationsInner = ({
684
712
  },
685
713
  doc: originalDoc,
686
714
  nodeViewSerializer,
687
- colorScheme,
715
+ attributionKey,
716
+ colorScheme: changeColorScheme,
688
717
  newDoc: tr.doc,
689
718
  intl,
690
719
  activeIndexPos,
@@ -694,7 +723,9 @@ const calculateDiffDecorationsInner = ({
694
723
  hideAddedDiffsUnderline,
695
724
  placeBelow
696
725
  }),
697
- showIndicators
726
+ showIndicators,
727
+ showContributorTags,
728
+ tagMountContext
698
729
  }));
699
730
  }
700
731
  }
@@ -49,6 +49,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
49
49
  return attrStepContexts.map(attrStepContext => {
50
50
  const {
51
51
  afterNode,
52
+ attributionKey,
52
53
  beforeNode,
53
54
  finalPos,
54
55
  originalPos,
@@ -74,6 +75,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
74
75
  const watchedAttrs = inlineNodeAttrMap[nodeName];
75
76
  if (attrsToCheck.some(v => watchedAttrs.includes(v))) {
76
77
  return {
78
+ ...(attributionKey ? {
79
+ attributionKey
80
+ } : {}),
77
81
  fromB: finalPos,
78
82
  toB: finalPos + nodeAtPos.nodeSize,
79
83
  isInline: true,
@@ -90,6 +94,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
90
94
  // taskItem node: state attribute change — highlight the taskItem node
91
95
  if (attrsToCheck.some(v => taskItemAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') {
92
96
  return {
97
+ ...(attributionKey ? {
98
+ attributionKey
99
+ } : {}),
93
100
  fromB: finalPos,
94
101
  toB: finalPos + nodeAtPos.nodeSize
95
102
  };
@@ -101,6 +108,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
101
108
  // panel as a "deleted" widget for a before/after comparison.
102
109
  if (attrsToCheck.some(v => panelAttrs.includes(v)) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
103
110
  return {
111
+ ...(attributionKey ? {
112
+ attributionKey
113
+ } : {}),
104
114
  fromB: finalPos,
105
115
  toB: finalPos + nodeAtPos.nodeSize,
106
116
  ...(originalPos !== undefined && originalNodeAtPos && {
@@ -118,6 +128,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
118
128
  }
119
129
  const isInline = nodeAtPos.type.name === 'inlineExtension';
120
130
  return {
131
+ ...(attributionKey ? {
132
+ attributionKey
133
+ } : {}),
121
134
  fromB: finalPos,
122
135
  toB: finalPos + nodeAtPos.nodeSize,
123
136
  isInline
@@ -128,6 +141,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
128
141
  if (attrsToCheck.some(v => mediaAttrs.includes(v)) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) {
129
142
  const startPos = $pos.pos + $pos.parentOffset;
130
143
  return {
144
+ ...(attributionKey ? {
145
+ attributionKey
146
+ } : {}),
131
147
  fromB: startPos,
132
148
  toB: startPos + $pos.parent.nodeSize - 1
133
149
  };
@@ -1,4 +1,6 @@
1
1
  import { AddMarkStep, RemoveMarkStep } from '@atlaskit/editor-prosemirror/transform';
2
+ import { getAttributionKey } from '../colorSchemes/attributions';
3
+ import { mapStepRangeToFinal } from './mapStepRangeToFinal';
2
4
  const extractMarkStep = step => {
3
5
  if (step instanceof AddMarkStep) {
4
6
  return {
@@ -18,10 +20,10 @@ const extractMarkStep = step => {
18
20
  }
19
21
  return undefined;
20
22
  };
21
- export const getMarkChangeRanges = steps => {
23
+ export const getMarkChangeRanges = (steps, stepAttributions = [], stepMaps = []) => {
22
24
  const resultRanges = [];
23
25
  let lastOp;
24
- for (const step of steps) {
26
+ for (const [stepIndex, step] of steps.entries()) {
25
27
  const op = extractMarkStep(step);
26
28
  if (!op) {
27
29
  continue;
@@ -31,9 +33,15 @@ export const getMarkChangeRanges = steps => {
31
33
  if (lastOp && lastOp.from === op.from && lastOp.to === op.to && lastOp.markName === op.markName && lastOp.type !== op.type) {
32
34
  resultRanges.pop();
33
35
  } else {
36
+ const finalRange = mapStepRangeToFinal(op.from, op.to, stepIndex, stepMaps);
37
+ if (!finalRange) {
38
+ lastOp = op;
39
+ continue;
40
+ }
34
41
  resultRanges.push({
35
- fromB: op.from,
36
- toB: op.to
42
+ attributionKey: getAttributionKey(stepAttributions[stepIndex]),
43
+ fromB: finalRange.fromB,
44
+ toB: finalRange.toB
37
45
  });
38
46
  }
39
47
  lastOp = op;
@@ -0,0 +1,28 @@
1
+ import { Mapping } from '@atlaskit/editor-prosemirror/transform';
2
+ // ProseMirror's assoc value chooses which side of an insertion a mapped boundary stays on.
3
+ const FROM_ASSOC = -1;
4
+ const TO_ASSOC = 1;
5
+
6
+ /** Maps a step's range into final-document coordinates when the rollout path supplies step maps. */
7
+ export const mapStepRangeToFinal = (from, to, stepIndex, stepMaps) => {
8
+ if (!stepMaps) {
9
+ return {
10
+ fromB: from,
11
+ toB: to
12
+ };
13
+ }
14
+
15
+ // The range is already in the document immediately after this step, so skip its own map and
16
+ // apply only the maps from subsequent steps.
17
+ const firstSubsequentStepIndex = stepIndex + 1;
18
+ const finalRange = new Mapping(stepMaps.slice(firstSubsequentStepIndex));
19
+ const fromB = finalRange.mapResult(from, FROM_ASSOC);
20
+ const toB = finalRange.mapResult(to, TO_ASSOC);
21
+ if (fromB.deleted && toB.deleted) {
22
+ return undefined;
23
+ }
24
+ return {
25
+ fromB: fromB.pos,
26
+ toB: toB.pos
27
+ };
28
+ };
@@ -4,6 +4,7 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
4
4
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
5
5
  import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform-override';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
+ import { getActiveDiffAnnouncement } from '../ui/DiffNavigation/announceActiveDiff';
7
8
  import { calculateDiffDecorations } from './calculateDiff/calculateDiffDecorations';
8
9
  import { isDiffDecoration, isDiffDecorationSpec } from './decorations/decorationKeys';
9
10
  import { enforceCustomStepRegisters } from './enforceCustomStepRegisters';
@@ -285,6 +286,23 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
285
286
  api === null || api === void 0 ? void 0 : api.core.actions.execute(toggleExpandRange(activeDecoration.from, activeDecoration.to, true));
286
287
  }
287
288
  cancelPendingScrollToDecoration = scrollToDecoration(view, scrollableDecorations, pluginState.activeIndex);
289
+
290
+ // Stepping only scrolls — no focus moves and no content changes — so without this a
291
+ // screen-reader user gets silence. Announced through the live region rather than by
292
+ // focusing the change, since the "next change" control has to stay focused to be
293
+ // pressed again. After the scroll above, so the announcement never precedes it.
294
+ if (isExtendedEnabled(pluginState === null || pluginState === void 0 ? void 0 : pluginState.diffType)) {
295
+ const announcement = getActiveDiffAnnouncement({
296
+ activeIndex: pluginState.activeIndex,
297
+ contributorTags: pluginState.contributorTags,
298
+ decorations: scrollableDecorations,
299
+ intl: getIntl()
300
+ });
301
+ if (announcement) {
302
+ var _api$accessibilityUti;
303
+ api === null || api === void 0 ? void 0 : (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 ? void 0 : _api$accessibilityUti.actions.ariaNotify(announcement);
304
+ }
305
+ }
288
306
  }
289
307
  },
290
308
  destroy() {
@@ -124,6 +124,17 @@ const srLabelStyle = convertToInlineCss({
124
124
  userSelect: 'none',
125
125
  whiteSpace: 'nowrap'
126
126
  });
127
+
128
+ /**
129
+ * Ids for the hidden label that the tag's `aria-labelledby` points at. Namespaced and random because
130
+ * the tag renders inside the document and shares the host page's id space — the same reason
131
+ * `VanillaTooltip` does it this way. The counter is a fallback for non-secure contexts, where
132
+ * `crypto.randomUUID` is unavailable.
133
+ */
134
+ const generateLabelId = (() => {
135
+ let count = 0;
136
+ return () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? `${CONTRIBUTOR_TAG_CLASS}-label-${crypto.randomUUID()}` : `${CONTRIBUTOR_TAG_CLASS}-label-${count += 1}`;
137
+ })();
127
138
  const createSpan = (doc, style, attributes = {}) => {
128
139
  const span = doc.createElement('span');
129
140
  span.setAttribute('style', style);
@@ -139,21 +150,30 @@ const createSpan = (doc, style, attributes = {}) => {
139
150
  * would need a cast per reference below.
140
151
  */
141
152
  export const buildContributorTagDom = doc => {
153
+ const labelId = generateLabelId();
142
154
  const root = createSpan(doc, constraintStyle);
143
155
  const tag = createSpan(doc, tagStyle, {
144
156
  class: CONTRIBUTOR_TAG_CLASS,
145
157
  'data-testid': CONTRIBUTOR_TAG_TESTID,
146
- // Deliberately no `aria-label`: it would be announced ahead of the element's contents, but
147
- // design requires the contributor *after* the change — hence the visually hidden label.
158
+ // Named by the hidden label rather than `aria-label`, which would be a second copy of the same
159
+ // sentence. `role="note"` takes its name from the author, not its contents, so without this
160
+ // the tag is a focus stop with no accessible name.
161
+ 'aria-labelledby': labelId,
148
162
  role: 'note',
149
- // Focusable so keyboard and screen-reader users can reach the tag and its tooltip.
163
+ // Focusable so keyboard and screen-reader users can reach the tag and its tooltip. Its focus
164
+ // ring is drawn inward — see `contributorTagStyles` in editor-core.
150
165
  tabindex: '0'
151
166
  });
152
167
  const avatars = createSpan(doc, avatarsStyle);
168
+ // `aria-hidden`: the hidden label restates this name in a full sentence, so leaving it in the
169
+ // tree announced the contributor twice ("Priya Changed by Priya").
153
170
  const name = createSpan(doc, nameStyle, {
171
+ 'aria-hidden': 'true',
154
172
  'data-testid': CONTRIBUTOR_TAG_NAME_TESTID
155
173
  });
156
- const srLabel = createSpan(doc, srLabelStyle);
174
+ const srLabel = createSpan(doc, srLabelStyle, {
175
+ id: labelId
176
+ });
157
177
  tag.append(avatars, name, srLabel);
158
178
  root.appendChild(tag);
159
179
  return {
@@ -0,0 +1,49 @@
1
+ import { contributorTagMessages } from './messages';
2
+ const getContributorName = (contributor, formatMessage) => {
3
+ var _contributor$name, _contributor$agentKin;
4
+ const name = (_contributor$name = contributor.name) === null || _contributor$name === void 0 ? void 0 : _contributor$name.trim();
5
+ if (name) {
6
+ return name;
7
+ }
8
+
9
+ // `agentKind` defaults to `'external'` per the plugin type contract; an unidentified external
10
+ // agent falls back to a generic label rather than rendering a nameless tag.
11
+ if (contributor.kind === 'agent' && ((_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : 'external') === 'external') {
12
+ return formatMessage(contributorTagMessages.externalAgentName);
13
+ }
14
+ return '';
15
+ };
16
+
17
+ /**
18
+ * Whether the label has to spell out that the contributor is an agent. A sighted user reads that off
19
+ * the `aria-hidden` avatar, so the label is the only other carrier.
20
+ *
21
+ * Excluded: a connected pair, which says it via `changedByConnected`, and an unnamed external agent,
22
+ * whose name `getContributorName` has already resolved to "External agent".
23
+ */
24
+ const isUnaccompaniedNamedAgent = model => {
25
+ var _model$contributor$na;
26
+ return !model.connectedContributor && model.contributor.kind === 'agent' && Boolean((_model$contributor$na = model.contributor.name) === null || _model$contributor$na === void 0 ? void 0 : _model$contributor$na.trim());
27
+ };
28
+ /**
29
+ * The two strings a tag's model resolves to. Shared with `getActiveDiffAnnouncement`, so stepping
30
+ * onto a change says the same sentence as the tag pinned to it rather than a copy free to drift.
31
+ */
32
+ export const formatContributorLabel = (model, formatMessage) => {
33
+ const isPrimaryAgent = model.contributor.kind === 'agent';
34
+ const primaryName = getContributorName(model.contributor, formatMessage);
35
+ const secondaryName = model.connectedContributor ? getContributorName(model.connectedContributor, formatMessage) : '';
36
+ const agentName = isPrimaryAgent ? primaryName : secondaryName;
37
+ const userName = isPrimaryAgent ? secondaryName : primaryName;
38
+ return {
39
+ fullLabel: model.connectedContributor ? formatMessage(contributorTagMessages.changedByConnected, {
40
+ agentName,
41
+ userName
42
+ }) : formatMessage(isUnaccompaniedNamedAgent(model) ? contributorTagMessages.changedByAgent : contributorTagMessages.changedBy, {
43
+ name: primaryName
44
+ }),
45
+ // A connected pair only ever shows the agent; the pair is spelled out in `fullLabel`, because
46
+ // the tag is capped at `MAX_TAG_WIDTH` and two names rarely fit inside it.
47
+ visibleName: model.connectedContributor ? agentName || primaryName : primaryName
48
+ };
49
+ };