@atlaskit/editor-plugin-status 13.0.25 → 13.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-plugin-status
2
2
 
3
+ ## 13.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 13.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`f4f1a7be76c2c`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f4f1a7be76c2c) -
14
+ Add reusable status suggestions to the editor status picker
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+
3
20
  ## 13.0.25
4
21
 
5
22
  ### Patch Changes
@@ -6,13 +6,19 @@ Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
8
  exports.ContentComponent = ContentComponent;
9
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
10
  var _react = _interopRequireWildcard(require("react"));
10
11
  var _hooks = require("@atlaskit/editor-common/hooks");
11
12
  var _utils = require("@atlaskit/editor-prosemirror/utils");
12
13
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
14
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
13
15
  var _actions = require("../pm-plugins/actions");
16
+ var _getSuggestedStatuses = require("./getSuggestedStatuses");
14
17
  var _statusPicker = _interopRequireDefault(require("./statusPicker"));
15
18
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
19
+ 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; }
20
+ 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; }
21
+ /** Renders the status picker popup for the currently selected status node. */
16
22
  function ContentComponent(_ref) {
17
23
  var api = _ref.api,
18
24
  popupsMountPoint = _ref.popupsMountPoint,
@@ -33,7 +39,9 @@ function ContentComponent(_ref) {
33
39
  }, [showStatusPickerAt, domAtPos]);
34
40
  var statusNode = (0, _react.useMemo)(function () {
35
41
  return showStatusPickerAt ? editorView.state.doc.nodeAt(showStatusPickerAt) : undefined;
36
- }, [showStatusPickerAt, editorView]);
42
+ },
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- preserve original recompute cadence for the ungated path
44
+ [showStatusPickerAt, editorView]);
37
45
  var onSelect = (0, _react.useCallback)(function (status) {
38
46
  (0, _actions.updateStatus)(status)(editorView.state, editorView.dispatch);
39
47
  }, [editorView]);
@@ -46,6 +54,50 @@ function ContentComponent(_ref) {
46
54
  var onEnter = (0, _react.useCallback)(function () {
47
55
  (0, _actions.commitStatusPicker)()(editorView);
48
56
  }, [editorView]);
57
+ var onSuggestedStatusClick = (0, _react.useCallback)(function (status) {
58
+ var _editorView$state$doc;
59
+ var currentLocalId = typeof showStatusPickerAt === 'number' ? (_editorView$state$doc = editorView.state.doc.nodeAt(showStatusPickerAt)) === null || _editorView$state$doc === void 0 ? void 0 : _editorView$state$doc.attrs.localId : undefined;
60
+ var suggestedStatus = _objectSpread(_objectSpread(_objectSpread({
61
+ color: status.color
62
+ }, currentLocalId ? {
63
+ localId: currentLocalId
64
+ } : {}), status.style ? {
65
+ style: status.style
66
+ } : {}), {}, {
67
+ text: status.text
68
+ });
69
+ var didUpdateStatus = (0, _actions.updateStatus)(suggestedStatus)(editorView.state, editorView.dispatch);
70
+ if (didUpdateStatus) {
71
+ (0, _actions.commitStatusPicker)()(editorView);
72
+ }
73
+ }, [editorView, showStatusPickerAt]);
74
+ var suggestedStatuses = (0, _react.useMemo)(function () {
75
+ if (typeof showStatusPickerAt !== 'number' || !(0, _expValEquals.expValEquals)('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
76
+ return [];
77
+ }
78
+
79
+ // Read the node directly from the current doc so suggestions reflect the latest
80
+ // document state (the memoized `statusNode` intentionally preserves the original
81
+ // recompute cadence for the ungated path).
82
+ var currentNode = editorView.state.doc.nodeAt(showStatusPickerAt);
83
+ if (!currentNode || currentNode.type.name !== 'status') {
84
+ return [];
85
+ }
86
+ var _currentNode$attrs = currentNode.attrs,
87
+ color = _currentNode$attrs.color,
88
+ localId = _currentNode$attrs.localId,
89
+ text = _currentNode$attrs.text;
90
+ return (0, _getSuggestedStatuses.getSuggestedStatuses)({
91
+ currentPos: showStatusPickerAt,
92
+ currentStatus: {
93
+ color: color,
94
+ localId: localId,
95
+ text: text
96
+ },
97
+ doc: editorView.state.doc,
98
+ shouldUppercaseText: (0, _platformFeatureFlags.fg)('platform-dst-lozenge-tag-badge-visual-uplifts')
99
+ });
100
+ }, [showStatusPickerAt, editorView.state.doc]);
49
101
  if (typeof showStatusPickerAt !== 'number') {
50
102
  return null;
51
103
  }
@@ -72,7 +124,9 @@ function ContentComponent(_ref) {
72
124
  onTextChanged: onTextChanged,
73
125
  closeStatusPicker: closeStatusPicker,
74
126
  onEnter: onEnter,
127
+ onSuggestedStatusClick: onSuggestedStatusClick,
75
128
  editorView: editorView,
76
- api: api
129
+ api: api,
130
+ suggestedStatuses: suggestedStatuses
77
131
  });
78
132
  }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.getSuggestedStatuses = exports.MAX_SUGGESTED_STATUSES = void 0;
8
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
9
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
+ var _excluded = ["distance"];
11
+ 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; }
12
+ 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; }
13
+ var MAX_SUGGESTED_STATUSES = exports.MAX_SUGGESTED_STATUSES = 7;
14
+ var getStatusKey = function getStatusKey(_ref) {
15
+ var color = _ref.color,
16
+ text = _ref.text;
17
+ return "".concat(color, ":").concat(text.trim().toLowerCase());
18
+ };
19
+ var getDisplayText = function getDisplayText(status, shouldUppercaseText) {
20
+ return shouldUppercaseText && status.style !== 'mixedCase' ? status.text.toUpperCase() : status.text;
21
+ };
22
+ var getSuggestedStatuses = exports.getSuggestedStatuses = function getSuggestedStatuses(_ref2) {
23
+ var currentPos = _ref2.currentPos,
24
+ currentStatus = _ref2.currentStatus,
25
+ doc = _ref2.doc,
26
+ _ref2$limit = _ref2.limit,
27
+ limit = _ref2$limit === void 0 ? MAX_SUGGESTED_STATUSES : _ref2$limit,
28
+ _ref2$shouldUppercase = _ref2.shouldUppercaseText,
29
+ shouldUppercaseText = _ref2$shouldUppercase === void 0 ? false : _ref2$shouldUppercase;
30
+ var closestStatusesByKey = new Map();
31
+ var currentStatusKey = getStatusKey(currentStatus);
32
+ doc.descendants(function (node, pos) {
33
+ if (node.type.name !== 'status' || pos === currentPos) {
34
+ return;
35
+ }
36
+ var text = typeof node.attrs.text === 'string' ? node.attrs.text.trim() : '';
37
+ if (!text) {
38
+ return;
39
+ }
40
+ var status = {
41
+ color: node.attrs.color,
42
+ localId: node.attrs.localId,
43
+ style: node.attrs.style,
44
+ text: text
45
+ };
46
+ var statusKey = getStatusKey(status);
47
+ if (status.localId === currentStatus.localId || statusKey === currentStatusKey) {
48
+ return;
49
+ }
50
+ var existingStatus = closestStatusesByKey.get(statusKey);
51
+ var nextStatus = _objectSpread(_objectSpread({}, status), {}, {
52
+ distance: Math.abs(pos - currentPos),
53
+ displayText: getDisplayText(status, shouldUppercaseText)
54
+ });
55
+ if (!existingStatus || nextStatus.distance < existingStatus.distance) {
56
+ closestStatusesByKey.set(statusKey, nextStatus);
57
+ }
58
+ });
59
+ return Array.from(closestStatusesByKey.values()).sort(function (statusA, statusB) {
60
+ if (statusA.distance !== statusB.distance) {
61
+ return statusA.distance - statusB.distance;
62
+ }
63
+ if (statusA.displayText !== statusB.displayText) {
64
+ return statusA.displayText.localeCompare(statusB.displayText);
65
+ }
66
+ return statusA.color.localeCompare(statusB.color);
67
+ }).slice(0, limit).map(function (_ref3) {
68
+ var _distance = _ref3.distance,
69
+ status = (0, _objectWithoutProperties2.default)(_ref3, _excluded);
70
+ return status;
71
+ });
72
+ };
@@ -22,7 +22,10 @@ var _uiReact = require("@atlaskit/editor-common/ui-react");
22
22
  var _userIntent = require("@atlaskit/editor-common/user-intent");
23
23
  var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
24
24
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
25
+ var _element = require("@atlaskit/status/element");
25
26
  var _picker = require("@atlaskit/status/picker");
27
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
28
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
26
29
  var _visuallyHidden = _interopRequireDefault(require("@atlaskit/visually-hidden"));
27
30
  var _actions = require("../pm-plugins/actions");
28
31
  var _analytics = require("./analytics");
@@ -68,6 +71,27 @@ var pickerContainerStylesTeam26 = (0, _react2.css)({
68
71
  outline: 'none'
69
72
  }
70
73
  });
74
+ var suggestedStatusesContainerStyles = (0, _react2.css)({
75
+ display: 'flex',
76
+ flexDirection: 'column',
77
+ gap: "var(--ds-space-100, 8px)",
78
+ margin: 0,
79
+ padding: "var(--ds-space-100, 8px)".concat(" ", "var(--ds-space-150, 12px)", " ", "var(--ds-space-050, 4px)")
80
+ });
81
+ var suggestedStatusButtonStyles = (0, _react2.css)({
82
+ background: 'transparent',
83
+ border: 0,
84
+ borderRadius: "var(--ds-radius-small, 3px)",
85
+ cursor: 'pointer',
86
+ display: 'flex',
87
+ justifyContent: 'flex-start',
88
+ padding: 0,
89
+ width: '100%',
90
+ '&:focus-visible': {
91
+ outline: "2px solid ".concat("var(--ds-border-focused, #0C66E4)"),
92
+ outlineOffset: "var(--ds-space-025, 2px)"
93
+ }
94
+ });
71
95
 
72
96
  // eslint-disable-next-line @repo/internal/react/no-class-components
73
97
  var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
@@ -89,18 +113,59 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
89
113
  _this.props.onEnter(_this.state);
90
114
  });
91
115
  (0, _defineProperty2.default)(_this, "handleTabPress", function (event) {
92
- var _getDocument;
93
- var colorButtons = event.currentTarget.querySelectorAll('button');
116
+ var _getDocument2;
117
+ if (!(0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
118
+ var _getDocument;
119
+ /* original tab-navigation behaviour: cycle between color buttons and the input field */
120
+ var _colorButtons = event.currentTarget.querySelectorAll('button');
121
+ var _inputField = event.currentTarget.querySelector('input');
122
+ var _activeElement = (_getDocument = (0, _browserApis.getDocument)()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
123
+ var _isInputFocussed = _activeElement === _inputField;
124
+ var _isButtonFocussed = Array.from(_colorButtons).some(function (buttonElement) {
125
+ return _activeElement === buttonElement;
126
+ });
127
+ if (event !== null && event !== void 0 && event.shiftKey) {
128
+ /* shift + tab */
129
+ if (_isInputFocussed) {
130
+ _colorButtons[0].focus();
131
+ event.preventDefault();
132
+ }
133
+ /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
134
+ To correctly set focus to the input field instead of the first color button we need to set focus manually
135
+ */
136
+ if (_isButtonFocussed) {
137
+ _inputField === null || _inputField === void 0 || _inputField.focus();
138
+ event.preventDefault();
139
+ }
140
+ } else {
141
+ /* tab */
142
+ if (_isButtonFocussed) {
143
+ _inputField === null || _inputField === void 0 || _inputField.focus();
144
+ event.preventDefault();
145
+ }
146
+ }
147
+ return;
148
+ }
149
+ var colorButtons = event.currentTarget.querySelectorAll('button:not([data-suggested-status-button])');
150
+ var suggestedStatusButtons = event.currentTarget.querySelectorAll('button[data-suggested-status-button]');
94
151
  var inputField = event.currentTarget.querySelector('input');
95
- var activeElement = (_getDocument = (0, _browserApis.getDocument)()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
152
+ var activeElement = (_getDocument2 = (0, _browserApis.getDocument)()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
96
153
  var isInputFocussed = activeElement === inputField;
97
154
  var isButtonFocussed = Array.from(colorButtons).some(function (buttonElement) {
98
155
  return activeElement === buttonElement;
99
156
  });
157
+ var isSuggestedStatusButtonFocussed = Array.from(suggestedStatusButtons).some(function (buttonElement) {
158
+ return activeElement === buttonElement;
159
+ });
100
160
  if (event !== null && event !== void 0 && event.shiftKey) {
101
161
  /* shift + tab */
102
162
  if (isInputFocussed) {
103
- colorButtons[0].focus();
163
+ if (suggestedStatusButtons.length > 0) {
164
+ suggestedStatusButtons[suggestedStatusButtons.length - 1].focus();
165
+ } else {
166
+ var _colorButtons$;
167
+ (_colorButtons$ = colorButtons[0]) === null || _colorButtons$ === void 0 || _colorButtons$.focus();
168
+ }
104
169
  event.preventDefault();
105
170
  }
106
171
  /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
@@ -110,17 +175,30 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
110
175
  inputField === null || inputField === void 0 || inputField.focus();
111
176
  event.preventDefault();
112
177
  }
178
+ if (isSuggestedStatusButtonFocussed) {
179
+ var _colorButtons2;
180
+ (_colorButtons2 = colorButtons[colorButtons.length - 1]) === null || _colorButtons2 === void 0 || _colorButtons2.focus();
181
+ event.preventDefault();
182
+ }
113
183
  } else {
114
184
  /* tab */
115
185
  if (isButtonFocussed) {
186
+ if (suggestedStatusButtons.length > 0) {
187
+ suggestedStatusButtons[0].focus();
188
+ } else {
189
+ inputField === null || inputField === void 0 || inputField.focus();
190
+ }
191
+ event.preventDefault();
192
+ }
193
+ if (isSuggestedStatusButtonFocussed) {
116
194
  inputField === null || inputField === void 0 || inputField.focus();
117
195
  event.preventDefault();
118
196
  }
119
197
  }
120
198
  });
121
199
  (0, _defineProperty2.default)(_this, "handleArrow", function (event, closingMethod) {
122
- var _getDocument2;
123
- var activeElement = (_getDocument2 = (0, _browserApis.getDocument)()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
200
+ var _getDocument3;
201
+ var activeElement = (_getDocument3 = (0, _browserApis.getDocument)()) === null || _getDocument3 === void 0 ? void 0 : _getDocument3.activeElement;
124
202
  if (activeElement === _this.popupBodyWrapper.current) {
125
203
  var _this$popupBodyWrappe;
126
204
  event.preventDefault();
@@ -194,6 +272,19 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
194
272
  _this.inputMethod = InputMethod.enterKey;
195
273
  _this.props.onEnter(_this.state);
196
274
  });
275
+ (0, _defineProperty2.default)(_this, "onSuggestedStatusClick", function (status, position) {
276
+ _this.fireSuggestedStatusClickedAnalytics(status, position);
277
+ _this.props.onSuggestedStatusClick(status);
278
+ });
279
+ (0, _defineProperty2.default)(_this, "handleSuggestedStatusKeyDown", function (event) {
280
+ // Selection (Enter/Space) is handled natively via the button's onClick.
281
+ // Keydown only drives ArrowUp/ArrowDown navigation between suggestions.
282
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
283
+ event.preventDefault();
284
+ event.stopPropagation();
285
+ _this.focusAdjacentSuggestedStatus(event.currentTarget, event.key === 'ArrowDown' ? 1 : -1);
286
+ }
287
+ });
197
288
  // cancel bubbling to fix clickOutside logic:
198
289
  // popup re-renders its content before the click event bubbles up to the document
199
290
  // therefore click target element would be different from the popup content
@@ -302,10 +393,12 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
302
393
  }, {
303
394
  key: "renderWithSetOutsideClickTargetRef",
304
395
  value: function renderWithSetOutsideClickTargetRef(setOutsideClickTargetRef) {
396
+ var _this3 = this;
305
397
  var _this$props = this.props,
306
398
  isNew = _this$props.isNew,
307
399
  focusStatusInput = _this$props.focusStatusInput,
308
- api = _this$props.api;
400
+ api = _this$props.api,
401
+ suggestedStatuses = _this$props.suggestedStatuses;
309
402
  var _this$state3 = this.state,
310
403
  color = _this$state3.color,
311
404
  text = _this$state3.text;
@@ -326,7 +419,26 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
326
419
  onColorHover: this.onColorHover,
327
420
  onTextChanged: this.onTextChanged,
328
421
  onEnter: this.onEnter
329
- })));
422
+ }), suggestedStatuses !== null && suggestedStatuses !== void 0 && suggestedStatuses.length && (0, _expValEquals.expValEquals)('platform_editor_status_popup_suggestions', 'isEnabled', true) ? (0, _react2.jsx)("div", {
423
+ css: suggestedStatusesContainerStyles,
424
+ "data-suggested-status-list": true
425
+ }, suggestedStatuses.map(function (suggestedStatus, index) {
426
+ return (0, _react2.jsx)("div", {
427
+ key: "".concat(suggestedStatus.color, ":").concat(suggestedStatus.text)
428
+ }, (0, _react2.jsx)("button", {
429
+ type: "button",
430
+ css: suggestedStatusButtonStyles,
431
+ onClick: function onClick() {
432
+ return _this3.onSuggestedStatusClick(suggestedStatus, index + 1);
433
+ },
434
+ onKeyDown: _this3.handleSuggestedStatusKeyDown,
435
+ "aria-label": suggestedStatus.displayText,
436
+ "data-suggested-status-button": true
437
+ }, (0, _react2.jsx)(_element.Status, {
438
+ color: suggestedStatus.color,
439
+ text: suggestedStatus.displayText
440
+ })));
441
+ })) : null));
330
442
  }
331
443
  }, {
332
444
  key: "render",
@@ -360,6 +472,38 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
360
472
  role: "alert"
361
473
  }, intl.formatMessage(_messages.statusMessages.statusPickerOpenedAlert)), (0, _react2.jsx)(_uiReact.OutsideClickTargetRefContext.Consumer, null, this.renderWithSetOutsideClickTargetRef.bind(this)));
362
474
  }
475
+ }, {
476
+ key: "fireSuggestedStatusClickedAnalytics",
477
+ value: function fireSuggestedStatusClickedAnalytics(status, position) {
478
+ var _this$props$suggested, _this$props$suggested2;
479
+ this.createStatusAnalyticsAndFireFunc({
480
+ action: 'clicked',
481
+ actionSubject: 'statusSuggestion',
482
+ attributes: {
483
+ color: status.color,
484
+ localId: this.state.localId,
485
+ position: position,
486
+ state: (0, _analytics.analyticsState)(this.props.isNew),
487
+ textLength: status.text ? status.text.length : 0,
488
+ totalSuggestions: (_this$props$suggested = (_this$props$suggested2 = this.props.suggestedStatuses) === null || _this$props$suggested2 === void 0 ? void 0 : _this$props$suggested2.length) !== null && _this$props$suggested !== void 0 ? _this$props$suggested : 0
489
+ }
490
+ });
491
+ }
492
+ }, {
493
+ key: "focusAdjacentSuggestedStatus",
494
+ value: function focusAdjacentSuggestedStatus(currentButton, direction) {
495
+ var _this$popupBodyWrappe2, _suggestedStatusButto;
496
+ var suggestedStatusButtons = (_this$popupBodyWrappe2 = this.popupBodyWrapper.current) === null || _this$popupBodyWrappe2 === void 0 ? void 0 : _this$popupBodyWrappe2.querySelectorAll('button[data-suggested-status-button]');
497
+ if (!(suggestedStatusButtons !== null && suggestedStatusButtons !== void 0 && suggestedStatusButtons.length)) {
498
+ return;
499
+ }
500
+ var currentIndex = Array.from(suggestedStatusButtons).indexOf(currentButton);
501
+ if (currentIndex < 0) {
502
+ return;
503
+ }
504
+ var nextIndex = (currentIndex + direction + suggestedStatusButtons.length) % suggestedStatusButtons.length;
505
+ (_suggestedStatusButto = suggestedStatusButtons[nextIndex]) === null || _suggestedStatusButto === void 0 || _suggestedStatusButto.focus();
506
+ }
363
507
  }]);
364
508
  }(_react.default.Component); // eslint-disable-next-line @typescript-eslint/ban-types
365
509
  var StatusPickerWithoutAnalytcs = exports.StatusPickerWithoutAnalytcs = (0, _reactIntl.injectIntl)(StatusPickerWithIntl);
@@ -2,8 +2,11 @@ import React, { useCallback, useMemo } from 'react';
2
2
  import { useSharedPluginState } from '@atlaskit/editor-common/hooks';
3
3
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
6
  import { commitStatusPicker, updateStatus } from '../pm-plugins/actions';
7
+ import { getSuggestedStatuses } from './getSuggestedStatuses';
6
8
  import StatusPicker from './statusPicker';
9
+ /** Renders the status picker popup for the currently selected status node. */
7
10
  export function ContentComponent({
8
11
  api,
9
12
  popupsMountPoint,
@@ -22,7 +25,9 @@ export function ContentComponent({
22
25
  // Ignored via go/ees005
23
26
  // eslint-disable-next-line @atlaskit/editor/no-as-casting
24
27
  showStatusPickerAt ? findDomRefAtPos(showStatusPickerAt, domAtPos) : null, [showStatusPickerAt, domAtPos]);
25
- const statusNode = useMemo(() => showStatusPickerAt ? editorView.state.doc.nodeAt(showStatusPickerAt) : undefined, [showStatusPickerAt, editorView]);
28
+ const statusNode = useMemo(() => showStatusPickerAt ? editorView.state.doc.nodeAt(showStatusPickerAt) : undefined,
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- preserve original recompute cadence for the ungated path
30
+ [showStatusPickerAt, editorView]);
26
31
  const onSelect = useCallback(status => {
27
32
  updateStatus(status)(editorView.state, editorView.dispatch);
28
33
  }, [editorView]);
@@ -35,6 +40,52 @@ export function ContentComponent({
35
40
  const onEnter = useCallback(() => {
36
41
  commitStatusPicker()(editorView);
37
42
  }, [editorView]);
43
+ const onSuggestedStatusClick = useCallback(status => {
44
+ var _editorView$state$doc;
45
+ const currentLocalId = typeof showStatusPickerAt === 'number' ? (_editorView$state$doc = editorView.state.doc.nodeAt(showStatusPickerAt)) === null || _editorView$state$doc === void 0 ? void 0 : _editorView$state$doc.attrs.localId : undefined;
46
+ const suggestedStatus = {
47
+ color: status.color,
48
+ ...(currentLocalId ? {
49
+ localId: currentLocalId
50
+ } : {}),
51
+ ...(status.style ? {
52
+ style: status.style
53
+ } : {}),
54
+ text: status.text
55
+ };
56
+ const didUpdateStatus = updateStatus(suggestedStatus)(editorView.state, editorView.dispatch);
57
+ if (didUpdateStatus) {
58
+ commitStatusPicker()(editorView);
59
+ }
60
+ }, [editorView, showStatusPickerAt]);
61
+ const suggestedStatuses = useMemo(() => {
62
+ if (typeof showStatusPickerAt !== 'number' || !expValEquals('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
63
+ return [];
64
+ }
65
+
66
+ // Read the node directly from the current doc so suggestions reflect the latest
67
+ // document state (the memoized `statusNode` intentionally preserves the original
68
+ // recompute cadence for the ungated path).
69
+ const currentNode = editorView.state.doc.nodeAt(showStatusPickerAt);
70
+ if (!currentNode || currentNode.type.name !== 'status') {
71
+ return [];
72
+ }
73
+ const {
74
+ color,
75
+ localId,
76
+ text
77
+ } = currentNode.attrs;
78
+ return getSuggestedStatuses({
79
+ currentPos: showStatusPickerAt,
80
+ currentStatus: {
81
+ color,
82
+ localId,
83
+ text
84
+ },
85
+ doc: editorView.state.doc,
86
+ shouldUppercaseText: fg('platform-dst-lozenge-tag-badge-visual-uplifts')
87
+ });
88
+ }, [showStatusPickerAt, editorView.state.doc]);
38
89
  if (typeof showStatusPickerAt !== 'number') {
39
90
  return null;
40
91
  }
@@ -62,7 +113,9 @@ export function ContentComponent({
62
113
  onTextChanged: onTextChanged,
63
114
  closeStatusPicker: closeStatusPicker,
64
115
  onEnter: onEnter,
116
+ onSuggestedStatusClick: onSuggestedStatusClick,
65
117
  editorView: editorView,
66
- api: api
118
+ api: api,
119
+ suggestedStatuses: suggestedStatuses
67
120
  });
68
121
  }
@@ -0,0 +1,56 @@
1
+ export const MAX_SUGGESTED_STATUSES = 7;
2
+ const getStatusKey = ({
3
+ color,
4
+ text
5
+ }) => `${color}:${text.trim().toLowerCase()}`;
6
+ const getDisplayText = (status, shouldUppercaseText) => shouldUppercaseText && status.style !== 'mixedCase' ? status.text.toUpperCase() : status.text;
7
+ export const getSuggestedStatuses = ({
8
+ currentPos,
9
+ currentStatus,
10
+ doc,
11
+ limit = MAX_SUGGESTED_STATUSES,
12
+ shouldUppercaseText = false
13
+ }) => {
14
+ const closestStatusesByKey = new Map();
15
+ const currentStatusKey = getStatusKey(currentStatus);
16
+ doc.descendants((node, pos) => {
17
+ if (node.type.name !== 'status' || pos === currentPos) {
18
+ return;
19
+ }
20
+ const text = typeof node.attrs.text === 'string' ? node.attrs.text.trim() : '';
21
+ if (!text) {
22
+ return;
23
+ }
24
+ const status = {
25
+ color: node.attrs.color,
26
+ localId: node.attrs.localId,
27
+ style: node.attrs.style,
28
+ text
29
+ };
30
+ const statusKey = getStatusKey(status);
31
+ if (status.localId === currentStatus.localId || statusKey === currentStatusKey) {
32
+ return;
33
+ }
34
+ const existingStatus = closestStatusesByKey.get(statusKey);
35
+ const nextStatus = {
36
+ ...status,
37
+ distance: Math.abs(pos - currentPos),
38
+ displayText: getDisplayText(status, shouldUppercaseText)
39
+ };
40
+ if (!existingStatus || nextStatus.distance < existingStatus.distance) {
41
+ closestStatusesByKey.set(statusKey, nextStatus);
42
+ }
43
+ });
44
+ return Array.from(closestStatusesByKey.values()).sort((statusA, statusB) => {
45
+ if (statusA.distance !== statusB.distance) {
46
+ return statusA.distance - statusB.distance;
47
+ }
48
+ if (statusA.displayText !== statusB.displayText) {
49
+ return statusA.displayText.localeCompare(statusB.displayText);
50
+ }
51
+ return statusA.color.localeCompare(statusB.color);
52
+ }).slice(0, limit).map(({
53
+ distance: _distance,
54
+ ...status
55
+ }) => status);
56
+ };
@@ -17,7 +17,10 @@ import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners as with
17
17
  import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent';
18
18
  import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
19
19
  import { fg } from '@atlaskit/platform-feature-flags';
20
+ import { Status } from '@atlaskit/status/element';
20
21
  import { StatusPicker as AkStatusPicker } from '@atlaskit/status/picker';
22
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
23
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
21
24
  import VisuallyHidden from '@atlaskit/visually-hidden';
22
25
  import { DEFAULT_STATUS } from '../pm-plugins/actions';
23
26
  import { analyticsState, createStatusAnalyticsAndFire } from './analytics';
@@ -58,6 +61,27 @@ const pickerContainerStylesTeam26 = css({
58
61
  outline: 'none'
59
62
  }
60
63
  });
64
+ const suggestedStatusesContainerStyles = css({
65
+ display: 'flex',
66
+ flexDirection: 'column',
67
+ gap: "var(--ds-space-100, 8px)",
68
+ margin: 0,
69
+ padding: `${"var(--ds-space-100, 8px)"} ${"var(--ds-space-150, 12px)"} ${"var(--ds-space-050, 4px)"}`
70
+ });
71
+ const suggestedStatusButtonStyles = css({
72
+ background: 'transparent',
73
+ border: 0,
74
+ borderRadius: "var(--ds-radius-small, 3px)",
75
+ cursor: 'pointer',
76
+ display: 'flex',
77
+ justifyContent: 'flex-start',
78
+ padding: 0,
79
+ width: '100%',
80
+ '&:focus-visible': {
81
+ outline: `2px solid ${"var(--ds-border-focused, #0C66E4)"}`,
82
+ outlineOffset: "var(--ds-space-025, 2px)"
83
+ }
84
+ });
61
85
 
62
86
  // eslint-disable-next-line @repo/internal/react/no-class-components
63
87
  class StatusPickerWithIntl extends React.Component {
@@ -77,18 +101,59 @@ class StatusPickerWithIntl extends React.Component {
77
101
  this.props.onEnter(this.state);
78
102
  });
79
103
  _defineProperty(this, "handleTabPress", event => {
80
- var _getDocument;
81
- const colorButtons = event.currentTarget.querySelectorAll('button');
104
+ var _getDocument2;
105
+ if (!expValEqualsNoExposure('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
106
+ var _getDocument;
107
+ /* original tab-navigation behaviour: cycle between color buttons and the input field */
108
+ const colorButtons = event.currentTarget.querySelectorAll('button');
109
+ const inputField = event.currentTarget.querySelector('input');
110
+ const activeElement = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
111
+ const isInputFocussed = activeElement === inputField;
112
+ const isButtonFocussed = Array.from(colorButtons).some(buttonElement => {
113
+ return activeElement === buttonElement;
114
+ });
115
+ if (event !== null && event !== void 0 && event.shiftKey) {
116
+ /* shift + tab */
117
+ if (isInputFocussed) {
118
+ colorButtons[0].focus();
119
+ event.preventDefault();
120
+ }
121
+ /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
122
+ To correctly set focus to the input field instead of the first color button we need to set focus manually
123
+ */
124
+ if (isButtonFocussed) {
125
+ inputField === null || inputField === void 0 ? void 0 : inputField.focus();
126
+ event.preventDefault();
127
+ }
128
+ } else {
129
+ /* tab */
130
+ if (isButtonFocussed) {
131
+ inputField === null || inputField === void 0 ? void 0 : inputField.focus();
132
+ event.preventDefault();
133
+ }
134
+ }
135
+ return;
136
+ }
137
+ const colorButtons = event.currentTarget.querySelectorAll('button:not([data-suggested-status-button])');
138
+ const suggestedStatusButtons = event.currentTarget.querySelectorAll('button[data-suggested-status-button]');
82
139
  const inputField = event.currentTarget.querySelector('input');
83
- const activeElement = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
140
+ const activeElement = (_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
84
141
  const isInputFocussed = activeElement === inputField;
85
142
  const isButtonFocussed = Array.from(colorButtons).some(buttonElement => {
86
143
  return activeElement === buttonElement;
87
144
  });
145
+ const isSuggestedStatusButtonFocussed = Array.from(suggestedStatusButtons).some(buttonElement => {
146
+ return activeElement === buttonElement;
147
+ });
88
148
  if (event !== null && event !== void 0 && event.shiftKey) {
89
149
  /* shift + tab */
90
150
  if (isInputFocussed) {
91
- colorButtons[0].focus();
151
+ if (suggestedStatusButtons.length > 0) {
152
+ suggestedStatusButtons[suggestedStatusButtons.length - 1].focus();
153
+ } else {
154
+ var _colorButtons$;
155
+ (_colorButtons$ = colorButtons[0]) === null || _colorButtons$ === void 0 ? void 0 : _colorButtons$.focus();
156
+ }
92
157
  event.preventDefault();
93
158
  }
94
159
  /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
@@ -98,17 +163,30 @@ class StatusPickerWithIntl extends React.Component {
98
163
  inputField === null || inputField === void 0 ? void 0 : inputField.focus();
99
164
  event.preventDefault();
100
165
  }
166
+ if (isSuggestedStatusButtonFocussed) {
167
+ var _colorButtons;
168
+ (_colorButtons = colorButtons[colorButtons.length - 1]) === null || _colorButtons === void 0 ? void 0 : _colorButtons.focus();
169
+ event.preventDefault();
170
+ }
101
171
  } else {
102
172
  /* tab */
103
173
  if (isButtonFocussed) {
174
+ if (suggestedStatusButtons.length > 0) {
175
+ suggestedStatusButtons[0].focus();
176
+ } else {
177
+ inputField === null || inputField === void 0 ? void 0 : inputField.focus();
178
+ }
179
+ event.preventDefault();
180
+ }
181
+ if (isSuggestedStatusButtonFocussed) {
104
182
  inputField === null || inputField === void 0 ? void 0 : inputField.focus();
105
183
  event.preventDefault();
106
184
  }
107
185
  }
108
186
  });
109
187
  _defineProperty(this, "handleArrow", (event, closingMethod) => {
110
- var _getDocument2;
111
- const activeElement = (_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
188
+ var _getDocument3;
189
+ const activeElement = (_getDocument3 = getDocument()) === null || _getDocument3 === void 0 ? void 0 : _getDocument3.activeElement;
112
190
  if (activeElement === this.popupBodyWrapper.current) {
113
191
  var _this$popupBodyWrappe, _this$popupBodyWrappe2;
114
192
  event.preventDefault();
@@ -184,6 +262,19 @@ class StatusPickerWithIntl extends React.Component {
184
262
  this.inputMethod = InputMethod.enterKey;
185
263
  this.props.onEnter(this.state);
186
264
  });
265
+ _defineProperty(this, "onSuggestedStatusClick", (status, position) => {
266
+ this.fireSuggestedStatusClickedAnalytics(status, position);
267
+ this.props.onSuggestedStatusClick(status);
268
+ });
269
+ _defineProperty(this, "handleSuggestedStatusKeyDown", event => {
270
+ // Selection (Enter/Space) is handled natively via the button's onClick.
271
+ // Keydown only drives ArrowUp/ArrowDown navigation between suggestions.
272
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+ this.focusAdjacentSuggestedStatus(event.currentTarget, event.key === 'ArrowDown' ? 1 : -1);
276
+ }
277
+ });
187
278
  // cancel bubbling to fix clickOutside logic:
188
279
  // popup re-renders its content before the click event bubbles up to the document
189
280
  // therefore click target element would be different from the popup content
@@ -278,7 +369,8 @@ class StatusPickerWithIntl extends React.Component {
278
369
  const {
279
370
  isNew,
280
371
  focusStatusInput,
281
- api
372
+ api,
373
+ suggestedStatuses
282
374
  } = this.props;
283
375
  const {
284
376
  color,
@@ -301,7 +393,22 @@ class StatusPickerWithIntl extends React.Component {
301
393
  onColorHover: this.onColorHover,
302
394
  onTextChanged: this.onTextChanged,
303
395
  onEnter: this.onEnter
304
- })));
396
+ }), suggestedStatuses !== null && suggestedStatuses !== void 0 && suggestedStatuses.length && expValEquals('platform_editor_status_popup_suggestions', 'isEnabled', true) ? jsx("div", {
397
+ css: suggestedStatusesContainerStyles,
398
+ "data-suggested-status-list": true
399
+ }, suggestedStatuses.map((suggestedStatus, index) => jsx("div", {
400
+ key: `${suggestedStatus.color}:${suggestedStatus.text}`
401
+ }, jsx("button", {
402
+ type: "button",
403
+ css: suggestedStatusButtonStyles,
404
+ onClick: () => this.onSuggestedStatusClick(suggestedStatus, index + 1),
405
+ onKeyDown: this.handleSuggestedStatusKeyDown,
406
+ "aria-label": suggestedStatus.displayText,
407
+ "data-suggested-status-button": true
408
+ }, jsx(Status, {
409
+ color: suggestedStatus.color,
410
+ text: suggestedStatus.displayText
411
+ }))))) : null));
305
412
  }
306
413
  render() {
307
414
  const {
@@ -334,6 +441,34 @@ class StatusPickerWithIntl extends React.Component {
334
441
  role: "alert"
335
442
  }, intl.formatMessage(messages.statusPickerOpenedAlert)), jsx(OutsideClickTargetRefContext.Consumer, null, this.renderWithSetOutsideClickTargetRef.bind(this)));
336
443
  }
444
+ fireSuggestedStatusClickedAnalytics(status, position) {
445
+ var _this$props$suggested, _this$props$suggested2;
446
+ this.createStatusAnalyticsAndFireFunc({
447
+ action: 'clicked',
448
+ actionSubject: 'statusSuggestion',
449
+ attributes: {
450
+ color: status.color,
451
+ localId: this.state.localId,
452
+ position,
453
+ state: analyticsState(this.props.isNew),
454
+ textLength: status.text ? status.text.length : 0,
455
+ totalSuggestions: (_this$props$suggested = (_this$props$suggested2 = this.props.suggestedStatuses) === null || _this$props$suggested2 === void 0 ? void 0 : _this$props$suggested2.length) !== null && _this$props$suggested !== void 0 ? _this$props$suggested : 0
456
+ }
457
+ });
458
+ }
459
+ focusAdjacentSuggestedStatus(currentButton, direction) {
460
+ var _this$popupBodyWrappe3, _suggestedStatusButto;
461
+ const suggestedStatusButtons = (_this$popupBodyWrappe3 = this.popupBodyWrapper.current) === null || _this$popupBodyWrappe3 === void 0 ? void 0 : _this$popupBodyWrappe3.querySelectorAll('button[data-suggested-status-button]');
462
+ if (!(suggestedStatusButtons !== null && suggestedStatusButtons !== void 0 && suggestedStatusButtons.length)) {
463
+ return;
464
+ }
465
+ const currentIndex = Array.from(suggestedStatusButtons).indexOf(currentButton);
466
+ if (currentIndex < 0) {
467
+ return;
468
+ }
469
+ const nextIndex = (currentIndex + direction + suggestedStatusButtons.length) % suggestedStatusButtons.length;
470
+ (_suggestedStatusButto = suggestedStatusButtons[nextIndex]) === null || _suggestedStatusButto === void 0 ? void 0 : _suggestedStatusButto.focus();
471
+ }
337
472
  }
338
473
 
339
474
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -1,9 +1,15 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ 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; }
3
+ 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) { _defineProperty(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; }
1
4
  import React, { useCallback, useMemo } from 'react';
2
5
  import { useSharedPluginState } from '@atlaskit/editor-common/hooks';
3
6
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
4
7
  import { fg } from '@atlaskit/platform-feature-flags';
8
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
9
  import { commitStatusPicker, updateStatus } from '../pm-plugins/actions';
10
+ import { getSuggestedStatuses } from './getSuggestedStatuses';
6
11
  import StatusPicker from './statusPicker';
12
+ /** Renders the status picker popup for the currently selected status node. */
7
13
  export function ContentComponent(_ref) {
8
14
  var api = _ref.api,
9
15
  popupsMountPoint = _ref.popupsMountPoint,
@@ -24,7 +30,9 @@ export function ContentComponent(_ref) {
24
30
  }, [showStatusPickerAt, domAtPos]);
25
31
  var statusNode = useMemo(function () {
26
32
  return showStatusPickerAt ? editorView.state.doc.nodeAt(showStatusPickerAt) : undefined;
27
- }, [showStatusPickerAt, editorView]);
33
+ },
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- preserve original recompute cadence for the ungated path
35
+ [showStatusPickerAt, editorView]);
28
36
  var onSelect = useCallback(function (status) {
29
37
  updateStatus(status)(editorView.state, editorView.dispatch);
30
38
  }, [editorView]);
@@ -37,6 +45,50 @@ export function ContentComponent(_ref) {
37
45
  var onEnter = useCallback(function () {
38
46
  commitStatusPicker()(editorView);
39
47
  }, [editorView]);
48
+ var onSuggestedStatusClick = useCallback(function (status) {
49
+ var _editorView$state$doc;
50
+ var currentLocalId = typeof showStatusPickerAt === 'number' ? (_editorView$state$doc = editorView.state.doc.nodeAt(showStatusPickerAt)) === null || _editorView$state$doc === void 0 ? void 0 : _editorView$state$doc.attrs.localId : undefined;
51
+ var suggestedStatus = _objectSpread(_objectSpread(_objectSpread({
52
+ color: status.color
53
+ }, currentLocalId ? {
54
+ localId: currentLocalId
55
+ } : {}), status.style ? {
56
+ style: status.style
57
+ } : {}), {}, {
58
+ text: status.text
59
+ });
60
+ var didUpdateStatus = updateStatus(suggestedStatus)(editorView.state, editorView.dispatch);
61
+ if (didUpdateStatus) {
62
+ commitStatusPicker()(editorView);
63
+ }
64
+ }, [editorView, showStatusPickerAt]);
65
+ var suggestedStatuses = useMemo(function () {
66
+ if (typeof showStatusPickerAt !== 'number' || !expValEquals('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
67
+ return [];
68
+ }
69
+
70
+ // Read the node directly from the current doc so suggestions reflect the latest
71
+ // document state (the memoized `statusNode` intentionally preserves the original
72
+ // recompute cadence for the ungated path).
73
+ var currentNode = editorView.state.doc.nodeAt(showStatusPickerAt);
74
+ if (!currentNode || currentNode.type.name !== 'status') {
75
+ return [];
76
+ }
77
+ var _currentNode$attrs = currentNode.attrs,
78
+ color = _currentNode$attrs.color,
79
+ localId = _currentNode$attrs.localId,
80
+ text = _currentNode$attrs.text;
81
+ return getSuggestedStatuses({
82
+ currentPos: showStatusPickerAt,
83
+ currentStatus: {
84
+ color: color,
85
+ localId: localId,
86
+ text: text
87
+ },
88
+ doc: editorView.state.doc,
89
+ shouldUppercaseText: fg('platform-dst-lozenge-tag-badge-visual-uplifts')
90
+ });
91
+ }, [showStatusPickerAt, editorView.state.doc]);
40
92
  if (typeof showStatusPickerAt !== 'number') {
41
93
  return null;
42
94
  }
@@ -63,7 +115,9 @@ export function ContentComponent(_ref) {
63
115
  onTextChanged: onTextChanged,
64
116
  closeStatusPicker: closeStatusPicker,
65
117
  onEnter: onEnter,
118
+ onSuggestedStatusClick: onSuggestedStatusClick,
66
119
  editorView: editorView,
67
- api: api
120
+ api: api,
121
+ suggestedStatuses: suggestedStatuses
68
122
  });
69
123
  }
@@ -0,0 +1,65 @@
1
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ var _excluded = ["distance"];
4
+ 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; }
5
+ 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) { _defineProperty(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; }
6
+ export var MAX_SUGGESTED_STATUSES = 7;
7
+ var getStatusKey = function getStatusKey(_ref) {
8
+ var color = _ref.color,
9
+ text = _ref.text;
10
+ return "".concat(color, ":").concat(text.trim().toLowerCase());
11
+ };
12
+ var getDisplayText = function getDisplayText(status, shouldUppercaseText) {
13
+ return shouldUppercaseText && status.style !== 'mixedCase' ? status.text.toUpperCase() : status.text;
14
+ };
15
+ export var getSuggestedStatuses = function getSuggestedStatuses(_ref2) {
16
+ var currentPos = _ref2.currentPos,
17
+ currentStatus = _ref2.currentStatus,
18
+ doc = _ref2.doc,
19
+ _ref2$limit = _ref2.limit,
20
+ limit = _ref2$limit === void 0 ? MAX_SUGGESTED_STATUSES : _ref2$limit,
21
+ _ref2$shouldUppercase = _ref2.shouldUppercaseText,
22
+ shouldUppercaseText = _ref2$shouldUppercase === void 0 ? false : _ref2$shouldUppercase;
23
+ var closestStatusesByKey = new Map();
24
+ var currentStatusKey = getStatusKey(currentStatus);
25
+ doc.descendants(function (node, pos) {
26
+ if (node.type.name !== 'status' || pos === currentPos) {
27
+ return;
28
+ }
29
+ var text = typeof node.attrs.text === 'string' ? node.attrs.text.trim() : '';
30
+ if (!text) {
31
+ return;
32
+ }
33
+ var status = {
34
+ color: node.attrs.color,
35
+ localId: node.attrs.localId,
36
+ style: node.attrs.style,
37
+ text: text
38
+ };
39
+ var statusKey = getStatusKey(status);
40
+ if (status.localId === currentStatus.localId || statusKey === currentStatusKey) {
41
+ return;
42
+ }
43
+ var existingStatus = closestStatusesByKey.get(statusKey);
44
+ var nextStatus = _objectSpread(_objectSpread({}, status), {}, {
45
+ distance: Math.abs(pos - currentPos),
46
+ displayText: getDisplayText(status, shouldUppercaseText)
47
+ });
48
+ if (!existingStatus || nextStatus.distance < existingStatus.distance) {
49
+ closestStatusesByKey.set(statusKey, nextStatus);
50
+ }
51
+ });
52
+ return Array.from(closestStatusesByKey.values()).sort(function (statusA, statusB) {
53
+ if (statusA.distance !== statusB.distance) {
54
+ return statusA.distance - statusB.distance;
55
+ }
56
+ if (statusA.displayText !== statusB.displayText) {
57
+ return statusA.displayText.localeCompare(statusB.displayText);
58
+ }
59
+ return statusA.color.localeCompare(statusB.color);
60
+ }).slice(0, limit).map(function (_ref3) {
61
+ var _distance = _ref3.distance,
62
+ status = _objectWithoutProperties(_ref3, _excluded);
63
+ return status;
64
+ });
65
+ };
@@ -24,7 +24,10 @@ import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners as with
24
24
  import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent';
25
25
  import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
26
26
  import { fg } from '@atlaskit/platform-feature-flags';
27
+ import { Status } from '@atlaskit/status/element';
27
28
  import { StatusPicker as AkStatusPicker } from '@atlaskit/status/picker';
29
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
30
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
28
31
  import VisuallyHidden from '@atlaskit/visually-hidden';
29
32
  import { DEFAULT_STATUS } from '../pm-plugins/actions';
30
33
  import { analyticsState, createStatusAnalyticsAndFire } from './analytics';
@@ -65,6 +68,27 @@ var pickerContainerStylesTeam26 = css({
65
68
  outline: 'none'
66
69
  }
67
70
  });
71
+ var suggestedStatusesContainerStyles = css({
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ gap: "var(--ds-space-100, 8px)",
75
+ margin: 0,
76
+ padding: "var(--ds-space-100, 8px)".concat(" ", "var(--ds-space-150, 12px)", " ", "var(--ds-space-050, 4px)")
77
+ });
78
+ var suggestedStatusButtonStyles = css({
79
+ background: 'transparent',
80
+ border: 0,
81
+ borderRadius: "var(--ds-radius-small, 3px)",
82
+ cursor: 'pointer',
83
+ display: 'flex',
84
+ justifyContent: 'flex-start',
85
+ padding: 0,
86
+ width: '100%',
87
+ '&:focus-visible': {
88
+ outline: "2px solid ".concat("var(--ds-border-focused, #0C66E4)"),
89
+ outlineOffset: "var(--ds-space-025, 2px)"
90
+ }
91
+ });
68
92
 
69
93
  // eslint-disable-next-line @repo/internal/react/no-class-components
70
94
  var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
@@ -86,18 +110,59 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
86
110
  _this.props.onEnter(_this.state);
87
111
  });
88
112
  _defineProperty(_this, "handleTabPress", function (event) {
89
- var _getDocument;
90
- var colorButtons = event.currentTarget.querySelectorAll('button');
113
+ var _getDocument2;
114
+ if (!expValEqualsNoExposure('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
115
+ var _getDocument;
116
+ /* original tab-navigation behaviour: cycle between color buttons and the input field */
117
+ var _colorButtons = event.currentTarget.querySelectorAll('button');
118
+ var _inputField = event.currentTarget.querySelector('input');
119
+ var _activeElement = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
120
+ var _isInputFocussed = _activeElement === _inputField;
121
+ var _isButtonFocussed = Array.from(_colorButtons).some(function (buttonElement) {
122
+ return _activeElement === buttonElement;
123
+ });
124
+ if (event !== null && event !== void 0 && event.shiftKey) {
125
+ /* shift + tab */
126
+ if (_isInputFocussed) {
127
+ _colorButtons[0].focus();
128
+ event.preventDefault();
129
+ }
130
+ /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
131
+ To correctly set focus to the input field instead of the first color button we need to set focus manually
132
+ */
133
+ if (_isButtonFocussed) {
134
+ _inputField === null || _inputField === void 0 || _inputField.focus();
135
+ event.preventDefault();
136
+ }
137
+ } else {
138
+ /* tab */
139
+ if (_isButtonFocussed) {
140
+ _inputField === null || _inputField === void 0 || _inputField.focus();
141
+ event.preventDefault();
142
+ }
143
+ }
144
+ return;
145
+ }
146
+ var colorButtons = event.currentTarget.querySelectorAll('button:not([data-suggested-status-button])');
147
+ var suggestedStatusButtons = event.currentTarget.querySelectorAll('button[data-suggested-status-button]');
91
148
  var inputField = event.currentTarget.querySelector('input');
92
- var activeElement = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
149
+ var activeElement = (_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
93
150
  var isInputFocussed = activeElement === inputField;
94
151
  var isButtonFocussed = Array.from(colorButtons).some(function (buttonElement) {
95
152
  return activeElement === buttonElement;
96
153
  });
154
+ var isSuggestedStatusButtonFocussed = Array.from(suggestedStatusButtons).some(function (buttonElement) {
155
+ return activeElement === buttonElement;
156
+ });
97
157
  if (event !== null && event !== void 0 && event.shiftKey) {
98
158
  /* shift + tab */
99
159
  if (isInputFocussed) {
100
- colorButtons[0].focus();
160
+ if (suggestedStatusButtons.length > 0) {
161
+ suggestedStatusButtons[suggestedStatusButtons.length - 1].focus();
162
+ } else {
163
+ var _colorButtons$;
164
+ (_colorButtons$ = colorButtons[0]) === null || _colorButtons$ === void 0 || _colorButtons$.focus();
165
+ }
101
166
  event.preventDefault();
102
167
  }
103
168
  /* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
@@ -107,17 +172,30 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
107
172
  inputField === null || inputField === void 0 || inputField.focus();
108
173
  event.preventDefault();
109
174
  }
175
+ if (isSuggestedStatusButtonFocussed) {
176
+ var _colorButtons2;
177
+ (_colorButtons2 = colorButtons[colorButtons.length - 1]) === null || _colorButtons2 === void 0 || _colorButtons2.focus();
178
+ event.preventDefault();
179
+ }
110
180
  } else {
111
181
  /* tab */
112
182
  if (isButtonFocussed) {
183
+ if (suggestedStatusButtons.length > 0) {
184
+ suggestedStatusButtons[0].focus();
185
+ } else {
186
+ inputField === null || inputField === void 0 || inputField.focus();
187
+ }
188
+ event.preventDefault();
189
+ }
190
+ if (isSuggestedStatusButtonFocussed) {
113
191
  inputField === null || inputField === void 0 || inputField.focus();
114
192
  event.preventDefault();
115
193
  }
116
194
  }
117
195
  });
118
196
  _defineProperty(_this, "handleArrow", function (event, closingMethod) {
119
- var _getDocument2;
120
- var activeElement = (_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
197
+ var _getDocument3;
198
+ var activeElement = (_getDocument3 = getDocument()) === null || _getDocument3 === void 0 ? void 0 : _getDocument3.activeElement;
121
199
  if (activeElement === _this.popupBodyWrapper.current) {
122
200
  var _this$popupBodyWrappe;
123
201
  event.preventDefault();
@@ -191,6 +269,19 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
191
269
  _this.inputMethod = InputMethod.enterKey;
192
270
  _this.props.onEnter(_this.state);
193
271
  });
272
+ _defineProperty(_this, "onSuggestedStatusClick", function (status, position) {
273
+ _this.fireSuggestedStatusClickedAnalytics(status, position);
274
+ _this.props.onSuggestedStatusClick(status);
275
+ });
276
+ _defineProperty(_this, "handleSuggestedStatusKeyDown", function (event) {
277
+ // Selection (Enter/Space) is handled natively via the button's onClick.
278
+ // Keydown only drives ArrowUp/ArrowDown navigation between suggestions.
279
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
280
+ event.preventDefault();
281
+ event.stopPropagation();
282
+ _this.focusAdjacentSuggestedStatus(event.currentTarget, event.key === 'ArrowDown' ? 1 : -1);
283
+ }
284
+ });
194
285
  // cancel bubbling to fix clickOutside logic:
195
286
  // popup re-renders its content before the click event bubbles up to the document
196
287
  // therefore click target element would be different from the popup content
@@ -299,10 +390,12 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
299
390
  }, {
300
391
  key: "renderWithSetOutsideClickTargetRef",
301
392
  value: function renderWithSetOutsideClickTargetRef(setOutsideClickTargetRef) {
393
+ var _this3 = this;
302
394
  var _this$props = this.props,
303
395
  isNew = _this$props.isNew,
304
396
  focusStatusInput = _this$props.focusStatusInput,
305
- api = _this$props.api;
397
+ api = _this$props.api,
398
+ suggestedStatuses = _this$props.suggestedStatuses;
306
399
  var _this$state3 = this.state,
307
400
  color = _this$state3.color,
308
401
  text = _this$state3.text;
@@ -323,7 +416,26 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
323
416
  onColorHover: this.onColorHover,
324
417
  onTextChanged: this.onTextChanged,
325
418
  onEnter: this.onEnter
326
- })));
419
+ }), suggestedStatuses !== null && suggestedStatuses !== void 0 && suggestedStatuses.length && expValEquals('platform_editor_status_popup_suggestions', 'isEnabled', true) ? jsx("div", {
420
+ css: suggestedStatusesContainerStyles,
421
+ "data-suggested-status-list": true
422
+ }, suggestedStatuses.map(function (suggestedStatus, index) {
423
+ return jsx("div", {
424
+ key: "".concat(suggestedStatus.color, ":").concat(suggestedStatus.text)
425
+ }, jsx("button", {
426
+ type: "button",
427
+ css: suggestedStatusButtonStyles,
428
+ onClick: function onClick() {
429
+ return _this3.onSuggestedStatusClick(suggestedStatus, index + 1);
430
+ },
431
+ onKeyDown: _this3.handleSuggestedStatusKeyDown,
432
+ "aria-label": suggestedStatus.displayText,
433
+ "data-suggested-status-button": true
434
+ }, jsx(Status, {
435
+ color: suggestedStatus.color,
436
+ text: suggestedStatus.displayText
437
+ })));
438
+ })) : null));
327
439
  }
328
440
  }, {
329
441
  key: "render",
@@ -357,6 +469,38 @@ var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
357
469
  role: "alert"
358
470
  }, intl.formatMessage(messages.statusPickerOpenedAlert)), jsx(OutsideClickTargetRefContext.Consumer, null, this.renderWithSetOutsideClickTargetRef.bind(this)));
359
471
  }
472
+ }, {
473
+ key: "fireSuggestedStatusClickedAnalytics",
474
+ value: function fireSuggestedStatusClickedAnalytics(status, position) {
475
+ var _this$props$suggested, _this$props$suggested2;
476
+ this.createStatusAnalyticsAndFireFunc({
477
+ action: 'clicked',
478
+ actionSubject: 'statusSuggestion',
479
+ attributes: {
480
+ color: status.color,
481
+ localId: this.state.localId,
482
+ position: position,
483
+ state: analyticsState(this.props.isNew),
484
+ textLength: status.text ? status.text.length : 0,
485
+ totalSuggestions: (_this$props$suggested = (_this$props$suggested2 = this.props.suggestedStatuses) === null || _this$props$suggested2 === void 0 ? void 0 : _this$props$suggested2.length) !== null && _this$props$suggested !== void 0 ? _this$props$suggested : 0
486
+ }
487
+ });
488
+ }
489
+ }, {
490
+ key: "focusAdjacentSuggestedStatus",
491
+ value: function focusAdjacentSuggestedStatus(currentButton, direction) {
492
+ var _this$popupBodyWrappe2, _suggestedStatusButto;
493
+ var suggestedStatusButtons = (_this$popupBodyWrappe2 = this.popupBodyWrapper.current) === null || _this$popupBodyWrappe2 === void 0 ? void 0 : _this$popupBodyWrappe2.querySelectorAll('button[data-suggested-status-button]');
494
+ if (!(suggestedStatusButtons !== null && suggestedStatusButtons !== void 0 && suggestedStatusButtons.length)) {
495
+ return;
496
+ }
497
+ var currentIndex = Array.from(suggestedStatusButtons).indexOf(currentButton);
498
+ if (currentIndex < 0) {
499
+ return;
500
+ }
501
+ var nextIndex = (currentIndex + direction + suggestedStatusButtons.length) % suggestedStatusButtons.length;
502
+ (_suggestedStatusButto = suggestedStatusButtons[nextIndex]) === null || _suggestedStatusButto === void 0 || _suggestedStatusButto.focus();
503
+ }
360
504
  }]);
361
505
  }(React.Component); // eslint-disable-next-line @typescript-eslint/ban-types
362
506
  export var StatusPickerWithoutAnalytcs = injectIntl(StatusPickerWithIntl);
@@ -11,5 +11,6 @@ interface ContentComponentProps {
11
11
  popupsMountPoint?: HTMLElement;
12
12
  popupsScrollableElement?: HTMLElement;
13
13
  }
14
+ /** Renders the status picker popup for the currently selected status node. */
14
15
  export declare function ContentComponent({ api, popupsMountPoint, popupsBoundariesElement, popupsScrollableElement, editorView, domAtPos, }: ContentComponentProps): React.JSX.Element | null;
15
16
  export {};
@@ -0,0 +1,15 @@
1
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
+ import type { StatusType } from '../types';
3
+ export type SuggestedStatus = StatusType & {
4
+ displayText: string;
5
+ };
6
+ type GetSuggestedStatusesArgs = {
7
+ currentPos: number;
8
+ currentStatus: Pick<StatusType, 'color' | 'localId' | 'text'>;
9
+ doc: PMNode;
10
+ limit?: number;
11
+ shouldUppercaseText?: boolean;
12
+ };
13
+ export declare const MAX_SUGGESTED_STATUSES = 7;
14
+ export declare const getSuggestedStatuses: ({ currentPos, currentStatus, doc, limit, shouldUppercaseText, }: GetSuggestedStatusesArgs) => SuggestedStatus[];
15
+ export {};
@@ -6,6 +6,7 @@ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
6
6
  import type { ColorType as Color } from '@atlaskit/status/picker';
7
7
  import type { StatusPlugin } from '../statusPluginType';
8
8
  import type { ClosingPayload, StatusType } from '../types';
9
+ import type { SuggestedStatus } from './getSuggestedStatuses';
9
10
  export declare enum InputMethod {
10
11
  blur = "blur",
11
12
  escKey = "escKey",
@@ -30,8 +31,10 @@ export interface Props {
30
31
  mountTo?: HTMLElement;
31
32
  onEnter: (status: StatusType) => void;
32
33
  onSelect: (status: StatusType) => void;
34
+ onSuggestedStatusClick: (status: StatusType) => void;
33
35
  onTextChanged: (status: StatusType, isNew: boolean) => void;
34
36
  scrollableElement?: HTMLElement;
37
+ suggestedStatuses?: SuggestedStatus[];
35
38
  target: HTMLElement | null;
36
39
  }
37
40
  export interface State {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-status",
3
- "version": "13.0.25",
3
+ "version": "13.1.1",
4
4
  "description": "Status plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -30,14 +30,14 @@
30
30
  "@atlaskit/icon": "^37.0.0",
31
31
  "@atlaskit/platform-feature-flags": "^2.0.0",
32
32
  "@atlaskit/status": "^5.4.0",
33
- "@atlaskit/tmp-editor-statsig": "^126.1.0",
33
+ "@atlaskit/tmp-editor-statsig": "^127.0.0",
34
34
  "@atlaskit/tokens": "^16.0.0",
35
- "@atlaskit/visually-hidden": "^4.1.0",
35
+ "@atlaskit/visually-hidden": "^4.2.0",
36
36
  "@babel/runtime": "^7.0.0",
37
37
  "@emotion/react": "^11.7.1"
38
38
  },
39
39
  "peerDependencies": {
40
- "@atlaskit/editor-common": "^116.32.0",
40
+ "@atlaskit/editor-common": "^116.33.0",
41
41
  "react": "^18.2.0",
42
42
  "react-dom": "^18.2.0",
43
43
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"