@atlaskit/editor-plugin-code-block 14.0.5 → 14.1.0

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,34 @@
1
1
  # @atlaskit/editor-plugin-code-block
2
2
 
3
+ ## 14.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`b483e293da76f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b483e293da76f) -
8
+ Add a gated `isSuggested` attribute to block menu element transformed analytics events so
9
+ suggested and non-suggested transform selections can be distinguished. The event attribute is
10
+ gated behind the `platform_editor_blocks_patch_2` feature gate.
11
+
12
+ ### Patch Changes
13
+
14
+ - [`64a0b50380570`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/64a0b50380570) - -
15
+ Improve TypeScript auto-detection for class fields and typed arrow function params/return types.
16
+ - Avoid unnecessary code block node updates when auto-detection re-detects the same language.
17
+ - Prevent wrap toggles from scrolling long code blocks to the top.
18
+ - Keep the language picker placement stable while it is open.
19
+ - Reset language picker search state on reopen so recent selections are not reported as search
20
+ selections.
21
+ - Prevent language picker close/selection from scrolling partially visible code blocks while
22
+ restoring editor focus.
23
+ - Add auto-detection analytics metadata to distinguish initial detection from re-detection.
24
+ - Updated dependencies
25
+
26
+ ## 14.0.6
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies
31
+
3
32
  ## 14.0.5
4
33
 
5
34
  ### Patch Changes
@@ -483,6 +483,9 @@ var toggleWordWrapStateForCodeBlockNode = exports.toggleWordWrapStateForCodeBloc
483
483
  tr.setNodeMarkup(codeBlock.pos, undefined, _objectSpread(_objectSpread({}, codeBlockNode.attrs), {}, {
484
484
  wrap: updatedToggleState
485
485
  }));
486
+ if ((0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch')) {
487
+ tr.setMeta('scrollIntoView', false);
488
+ }
486
489
  } else {
487
490
  _codeBlock.codeBlockWrappedStates.set(codeBlockNode, updatedToggleState);
488
491
  }
@@ -35,11 +35,19 @@ var CodeBlockLanguagePicker = exports.CodeBlockLanguagePicker = function CodeBlo
35
35
  var _api$analytics;
36
36
  var command = option.value === _languagePickerOptions.DETECT_LANGUAGE_VALUE && (0, _platformFeatureFlags.fg)('platform_editor_code_block_language_detection_flow') ? (0, _editorCommands.detectLanguage)() : (0, _editorCommands.changeLanguage)(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(option.value, selectionSource);
37
37
  var commandSucceeded = command(editorView.state, editorView.dispatch);
38
+ if ((0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch')) {
39
+ requestAnimationFrame(function () {
40
+ // Let PopupSelect/FocusLock finish returning focus to the trigger, then restore the editor.
41
+ api === null || api === void 0 || api.core.actions.focus({
42
+ scrollIntoView: false
43
+ });
44
+ });
45
+ }
38
46
  if (commandSucceeded && option.value !== _languagePickerOptions.DETECT_LANGUAGE_VALUE) {
39
47
  (0, _recentLanguages.saveRecentLanguage)(option.value);
40
48
  setRecentLanguageValues((0, _recentLanguages.getRecentLanguages)());
41
49
  }
42
- }, [api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, editorView]);
50
+ }, [api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, api === null || api === void 0 ? void 0 : api.core.actions, editorView]);
43
51
  return /*#__PURE__*/_react.default.createElement(_LanguagePicker.LanguagePicker, {
44
52
  defaultValue: defaultValue,
45
53
  filterOption: filterOption,
@@ -13,7 +13,8 @@ var _editorToolbar = require("@atlaskit/editor-toolbar");
13
13
  var _angleBrackets = _interopRequireDefault(require("@atlaskit/icon/core/angle-brackets"));
14
14
  var NODE_NAME = 'codeBlock';
15
15
  var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
16
- var api = _ref.api;
16
+ var api = _ref.api,
17
+ isSuggested = _ref.isSuggested;
17
18
  var _useIntl = (0, _reactIntl.useIntl)(),
18
19
  formatMessage = _useIntl.formatMessage;
19
20
  var handleClick = function handleClick(event) {
@@ -24,6 +25,7 @@ var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
24
25
  var tr = _ref2.tr;
25
26
  var command = api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.commands.transformNode(tr.doc.type.schema.nodes.codeBlock, {
26
27
  inputMethod: inputMethod,
28
+ isSuggested: isSuggested,
27
29
  triggeredFrom: triggeredFrom,
28
30
  targetTypeName: NODE_NAME
29
31
  });
@@ -42,8 +44,11 @@ var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
42
44
  };
43
45
  var createCodeBlockMenuItem = exports.createCodeBlockMenuItem = function createCodeBlockMenuItem(api) {
44
46
  return function () {
47
+ var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
48
+ isSuggested = _ref3.isSuggested;
45
49
  return /*#__PURE__*/_react.default.createElement(CodeBlockMenuItem, {
46
- api: api
50
+ api: api,
51
+ isSuggested: isSuggested
47
52
  });
48
53
  };
49
54
  };
@@ -9,12 +9,14 @@ Object.defineProperty(exports, "__esModule", {
9
9
  exports.LanguagePicker = void 0;
10
10
  require("./LanguagePicker.compiled.css");
11
11
  var _runtime = require("@compiled/react/runtime");
12
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
12
13
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
13
14
  var _react = _interopRequireWildcard(require("react"));
14
15
  var _new = _interopRequireDefault(require("@atlaskit/button/new"));
15
16
  var _messages = require("@atlaskit/editor-common/messages");
16
17
  var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
17
18
  var _chevronDown = _interopRequireDefault(require("@atlaskit/icon/core/chevron-down"));
19
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
18
20
  var _compiled = require("@atlaskit/primitives/compiled");
19
21
  var _select = require("@atlaskit/select");
20
22
  var _languagePickerOptions = require("./language-picker-options");
@@ -60,6 +62,11 @@ var menuPopperProps = {
60
62
  enabled: false
61
63
  }]
62
64
  };
65
+ var focusWithoutScrolling = function focusWithoutScrolling(element) {
66
+ element.focus({
67
+ preventScroll: true
68
+ });
69
+ };
63
70
  var getRecentlyUsedLanguages = function getRecentlyUsedLanguages(recentLanguageValues, optionsByValue) {
64
71
  var recentlyUsedLanguages = [];
65
72
  var _iterator = _createForOfIteratorHelper(recentLanguageValues),
@@ -95,6 +102,10 @@ var LanguagePicker = exports.LanguagePicker = function LanguagePicker(_ref) {
95
102
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
96
103
  hasSearchQuery = _useState2[0],
97
104
  setHasSearchQuery = _useState2[1];
105
+ var _useState3 = (0, _react.useState)(),
106
+ _useState4 = (0, _slicedToArray2.default)(_useState3, 2),
107
+ lockedPopperPlacement = _useState4[0],
108
+ setLockedPopperPlacement = _useState4[1];
98
109
  var inputValueRef = (0, _react.useRef)('');
99
110
  var optionsByValue = (0, _react.useMemo)(function () {
100
111
  return new Map(languagePickerOptions.map(function (option) {
@@ -116,6 +127,29 @@ var LanguagePicker = exports.LanguagePicker = function LanguagePicker(_ref) {
116
127
  return group.options;
117
128
  });
118
129
  }, [options]);
130
+ var stableMenuPopperProps = (0, _react.useMemo)(function () {
131
+ return {
132
+ // Allow Popper to choose top on the first open when bottom has no room, then lock the
133
+ // chosen placement so later hover/search renders do not move the open picker.
134
+ placement: lockedPopperPlacement !== null && lockedPopperPlacement !== void 0 ? lockedPopperPlacement : 'bottom-start',
135
+ modifiers: [{
136
+ name: 'offset',
137
+ options: {
138
+ offset: [0, 8]
139
+ }
140
+ }, {
141
+ name: 'preventOverflow',
142
+ enabled: false
143
+ }].concat((0, _toConsumableArray2.default)(lockedPopperPlacement ? [{
144
+ name: 'flip',
145
+ enabled: false
146
+ }] : [])),
147
+ onFirstUpdate: function onFirstUpdate(_ref2) {
148
+ var placement = _ref2.placement;
149
+ setLockedPopperPlacement(placement);
150
+ }
151
+ };
152
+ }, [lockedPopperPlacement]);
119
153
  var handleChange = (0, _react.useCallback)(function (option) {
120
154
  var _option$selectionSour;
121
155
  if (!option) {
@@ -128,21 +162,44 @@ var LanguagePicker = exports.LanguagePicker = function LanguagePicker(_ref) {
128
162
  var handleInputChange = (0, _react.useCallback)(function (newInputValue, actionMeta) {
129
163
  // React-select clears the input as part of selecting a value before onChange fires.
130
164
  // Keep the last user-typed query so handleChange can report search selections correctly.
131
- if (!actionMeta || actionMeta.action === 'input-change') {
165
+ var isInputChange = !actionMeta || actionMeta.action === 'input-change';
166
+ if (isInputChange) {
132
167
  inputValueRef.current = newInputValue;
133
168
  }
134
- setHasSearchQuery(newInputValue.trim().length > 0);
135
- return newInputValue;
169
+ if (isInputChange || !(0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch')) {
170
+ setHasSearchQuery(newInputValue.trim().length > 0);
171
+ return newInputValue;
172
+ }
173
+ return inputValueRef.current;
174
+ }, []);
175
+ var handleMenuOpen = (0, _react.useCallback)(function () {
176
+ if ((0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch')) {
177
+ inputValueRef.current = '';
178
+ setHasSearchQuery(false);
179
+ setLockedPopperPlacement(undefined);
180
+ }
181
+ onMenuOpen === null || onMenuOpen === void 0 || onMenuOpen();
182
+ }, [onMenuOpen]);
183
+ var handleTriggerMouseDown = (0, _react.useCallback)(function (event) {
184
+ if ((0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch')) {
185
+ // PopupSelect's FocusLock returns focus to the element that was focused before the
186
+ // picker opened. If that is the editor/code block, closing the picker can scroll the
187
+ // whole code block into view. Focus the trigger first without scrolling so FocusLock
188
+ // returns to the trigger; see CodeBlockLanguagePicker's handleSelection for restoring
189
+ // editor focus.
190
+ focusWithoutScrolling(event.currentTarget);
191
+ }
136
192
  }, []);
137
- var renderTarget = (0, _react.useCallback)(function (_ref2) {
138
- var isOpen = _ref2.isOpen,
139
- ref = _ref2.ref,
140
- onKeyDown = _ref2.onKeyDown,
141
- ariaControls = _ref2['aria-controls'];
193
+ var renderTarget = (0, _react.useCallback)(function (_ref3) {
194
+ var isOpen = _ref3.isOpen,
195
+ ref = _ref3.ref,
196
+ onKeyDown = _ref3.onKeyDown,
197
+ ariaControls = _ref3['aria-controls'];
142
198
  return /*#__PURE__*/_react.default.createElement("div", {
143
199
  className: (0, _runtime.ax)([styles.trigger])
144
200
  }, /*#__PURE__*/_react.default.createElement(_new.default, {
145
201
  shouldFitContainer: true,
202
+ onMouseDown: handleTriggerMouseDown,
146
203
  onKeyDown: onKeyDown,
147
204
  ref: ref,
148
205
  iconAfter: _chevronDown.default,
@@ -151,7 +208,7 @@ var LanguagePicker = exports.LanguagePicker = function LanguagePicker(_ref) {
151
208
  "aria-controls": ariaControls,
152
209
  testId: "code-block-language-picker-trigger"
153
210
  }, label));
154
- }, [label]);
211
+ }, [handleTriggerMouseDown, label]);
155
212
  return /*#__PURE__*/_react.default.createElement(_select.PopupSelect, {
156
213
  components: popupSelectComponents,
157
214
  filterOption: filterOption,
@@ -161,9 +218,9 @@ var LanguagePicker = exports.LanguagePicker = function LanguagePicker(_ref) {
161
218
  menuPlacement: "auto",
162
219
  onChange: handleChange,
163
220
  onInputChange: handleInputChange,
164
- onMenuOpen: onMenuOpen,
221
+ onMenuOpen: handleMenuOpen,
165
222
  options: hasSearchQuery ? searchOptions : options,
166
- popperProps: menuPopperProps,
223
+ popperProps: (0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch') ? stableMenuPopperProps : menuPopperProps,
167
224
  searchThreshold: -1,
168
225
  target: renderTarget,
169
226
  testId: "code-block-language-picker",
@@ -8,6 +8,7 @@ exports.syncPendingDetectionTimers = void 0;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
9
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
10
  var _analytics = require("@atlaskit/editor-common/analytics");
11
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
11
12
  var _actions = require("../pm-plugins/actions");
12
13
  var _autoDetectState = require("../pm-plugins/auto-detect-state");
13
14
  var _autoDetectState2 = require("./auto-detect-state");
@@ -43,6 +44,7 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
43
44
  }
44
45
  var detectedLanguage = (0, _languageDetect.detectLanguage)(found.node.textContent);
45
46
  var detectionResult = detectedLanguage ? 'detected' : 'noneDetected';
47
+ var detectionPhase = entry.detectionResult ? 'redetection' : 'initial';
46
48
  // Keep a previous auto-detected language when the latest snippet is too weak to classify.
47
49
  var shouldPreserveAutoDetectedLanguage = !detectedLanguage && Boolean(entry.autoDetectedLanguage) && found.node.attrs.language === entry.autoDetectedLanguage;
48
50
  var nextEntry = _objectSpread(_objectSpread({}, (0, _autoDetectState2.createAutoDetectEntry)(found.node, found.pos, false, entry)), {}, {
@@ -52,10 +54,12 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
52
54
 
53
55
  // If there is no confident detection, record the result without clearing user-visible language.
54
56
  var shouldOnlyUpdateDetectionState = !detectedLanguage && (!found.node.attrs.language || shouldPreserveAutoDetectedLanguage);
57
+ var hasDetectedLanguageChange = Boolean(detectedLanguage) && found.node.attrs.language !== detectedLanguage;
58
+ var shouldClearStaleLanguage = !detectedLanguage && Boolean(found.node.attrs.language) && !shouldPreserveAutoDetectedLanguage;
59
+ var shouldUpdateNodeLanguage = (0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch') ? hasDetectedLanguageChange || shouldClearStaleLanguage : !shouldOnlyUpdateDetectionState;
55
60
  api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref) {
56
- var _api$analytics;
57
61
  var tr = _ref.tr;
58
- if (!shouldOnlyUpdateDetectionState) {
62
+ if (shouldUpdateNodeLanguage) {
59
63
  tr.setNodeMarkup(found.pos, undefined, _objectSpread(_objectSpread({}, found.node.attrs), {}, {
60
64
  language: detectedLanguage
61
65
  }), found.node.marks);
@@ -67,15 +71,21 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
67
71
  entry: nextEntry
68
72
  }
69
73
  });
70
- api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({
71
- action: _analytics.ACTION.LANGUAGE_AUTO_DETECTED,
72
- actionSubject: _analytics.ACTION_SUBJECT.CODE_BLOCK,
73
- attributes: {
74
- language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
75
- detectionResult: detectionResult
76
- },
77
- eventType: _analytics.EVENT_TYPE.TRACK
78
- })(tr);
74
+ // When platform_editor_code_block_dogfooding_patch is cleaned up, merge this with the previous shouldUpdateNodeLanguage check.
75
+ if (!(0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch') || shouldUpdateNodeLanguage) {
76
+ var _api$analytics;
77
+ api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({
78
+ action: _analytics.ACTION.LANGUAGE_AUTO_DETECTED,
79
+ actionSubject: _analytics.ACTION_SUBJECT.CODE_BLOCK,
80
+ attributes: _objectSpread({
81
+ language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
82
+ detectionResult: detectionResult
83
+ }, (0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch') ? {
84
+ detectionPhase: detectionPhase
85
+ } : {}),
86
+ eventType: _analytics.EVENT_TYPE.TRACK
87
+ })(tr);
88
+ }
79
89
  // Language detection runs in the background and should not move the viewport.
80
90
  return tr.setMeta('scrollIntoView', false);
81
91
  });
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.detectLanguage = void 0;
8
8
  var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
10
11
  /* eslint-disable require-unicode-regexp */
11
12
 
12
13
  // Conservative weighted-regex heuristic for common high-confidence snippets, not a full classifier.
@@ -28,6 +29,9 @@ var scorePatterns = function scorePatterns(code, patterns) {
28
29
  return score + (hasPattern(code, pattern) ? value : 0);
29
30
  }, 0);
30
31
  };
32
+ var scoreEnhancedTypeScriptPatterns = function scoreEnhancedTypeScriptPatterns(code) {
33
+ return (0, _platformFeatureFlags.fg)('platform_editor_code_block_dogfooding_patch') ? scorePatterns(code, [[/\b(?:private|protected|public|readonly)\s+[\w$]+\s*=/, 5], [/\([^)]{0,500}\b[\w$]+\??:\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200}[^)]{0,500}\)\s*(?::\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200})?\s*=>/, 5]]) : 0;
34
+ };
31
35
  var looksLikeHtmlTagPair = function looksLikeHtmlTagPair(code) {
32
36
  var openTags = new Set();
33
37
  var tagPattern = /<\/?([a-z][a-z0-9-]*)\b[^<>]{0,500}>/gi;
@@ -77,7 +81,7 @@ var getLanguageScores = function getLanguageScores(code) {
77
81
  score: scorePatterns(code, [[/\bSELECT\b[\s\S]+\bFROM\b/i, 5], [/\b(?:INSERT\s+INTO|UPDATE\b[\s\S]+\bSET\b|DELETE\s+FROM|CREATE\s+TABLE|ALTER\s+TABLE)\b/i, 5], [/\b(?:JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|LIMIT)\b/i, 2]])
78
82
  }, {
79
83
  language: 'typescript',
80
- score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]])
84
+ score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]]) + scoreEnhancedTypeScriptPatterns(code)
81
85
  }, {
82
86
  language: 'javascript',
83
87
  score: scorePatterns(code, [[/\b(?:const|let|var)\s+[\w$]+\s*=/, 3], [/\bfunction\s*[\w$]*\s*\([^)]*\)\s*\{/, 3], [/=>\s*(?:\{|[\w$'"`(])/, 3], [/\b(?:import|export)\s+(?:[\w${}*,\s]+\s+from\s+)?['"][^'"]+['"]/, 3], [/\b(?:console\.log|document\.|window\.|require\()|\bmodule\.exports\b/, 3]])
@@ -492,6 +492,9 @@ export const toggleWordWrapStateForCodeBlockNode = editorAnalyticsAPI => (state,
492
492
  ...codeBlockNode.attrs,
493
493
  wrap: updatedToggleState
494
494
  });
495
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
496
+ tr.setMeta('scrollIntoView', false);
497
+ }
495
498
  } else {
496
499
  codeBlockWrappedStates.set(codeBlockNode, updatedToggleState);
497
500
  }
@@ -21,11 +21,19 @@ export const CodeBlockLanguagePicker = ({
21
21
  var _api$analytics;
22
22
  const command = option.value === DETECT_LANGUAGE_VALUE && fg('platform_editor_code_block_language_detection_flow') ? detectLanguage() : changeLanguage(api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(option.value, selectionSource);
23
23
  const commandSucceeded = command(editorView.state, editorView.dispatch);
24
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
25
+ requestAnimationFrame(() => {
26
+ // Let PopupSelect/FocusLock finish returning focus to the trigger, then restore the editor.
27
+ api === null || api === void 0 ? void 0 : api.core.actions.focus({
28
+ scrollIntoView: false
29
+ });
30
+ });
31
+ }
24
32
  if (commandSucceeded && option.value !== DETECT_LANGUAGE_VALUE) {
25
33
  saveRecentLanguage(option.value);
26
34
  setRecentLanguageValues(getRecentLanguages());
27
35
  }
28
- }, [api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, editorView]);
36
+ }, [api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, api === null || api === void 0 ? void 0 : api.core.actions, editorView]);
29
37
  return /*#__PURE__*/React.createElement(LanguagePicker, {
30
38
  defaultValue: defaultValue,
31
39
  filterOption: filterOption,
@@ -6,7 +6,8 @@ import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
6
6
  import AngleBracketsIcon from '@atlaskit/icon/core/angle-brackets';
7
7
  const NODE_NAME = 'codeBlock';
8
8
  const CodeBlockMenuItem = ({
9
- api
9
+ api,
10
+ isSuggested
10
11
  }) => {
11
12
  const {
12
13
  formatMessage
@@ -20,6 +21,7 @@ const CodeBlockMenuItem = ({
20
21
  var _api$blockMenu;
21
22
  const command = api === null || api === void 0 ? void 0 : (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.commands.transformNode(tr.doc.type.schema.nodes.codeBlock, {
22
23
  inputMethod,
24
+ isSuggested,
23
25
  triggeredFrom,
24
26
  targetTypeName: NODE_NAME
25
27
  });
@@ -37,7 +39,10 @@ const CodeBlockMenuItem = ({
37
39
  }, formatMessage(blockMenuMessages.codeBlock));
38
40
  };
39
41
  export const createCodeBlockMenuItem = api => {
40
- return () => /*#__PURE__*/React.createElement(CodeBlockMenuItem, {
41
- api: api
42
+ return ({
43
+ isSuggested
44
+ } = {}) => /*#__PURE__*/React.createElement(CodeBlockMenuItem, {
45
+ api: api,
46
+ isSuggested: isSuggested
42
47
  });
43
48
  };
@@ -6,6 +6,7 @@ import Button from '@atlaskit/button/new';
6
6
  import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
7
7
  import { akEditorLineHeight } from '@atlaskit/editor-shared-styles';
8
8
  import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
9
+ import { fg } from '@atlaskit/platform-feature-flags';
9
10
  import { Box } from '@atlaskit/primitives/compiled';
10
11
  import { PopupSelect, components } from '@atlaskit/select';
11
12
  import { createGroupedLanguageOptions } from './language-picker-options';
@@ -47,6 +48,11 @@ const menuPopperProps = {
47
48
  enabled: false
48
49
  }]
49
50
  };
51
+ const focusWithoutScrolling = element => {
52
+ element.focus({
53
+ preventScroll: true
54
+ });
55
+ };
50
56
  const getRecentlyUsedLanguages = (recentLanguageValues, optionsByValue) => {
51
57
  const recentlyUsedLanguages = [];
52
58
  for (const recentLanguageValue of recentLanguageValues) {
@@ -70,6 +76,7 @@ export const LanguagePicker = ({
70
76
  const label = (_defaultValue$label = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.label) !== null && _defaultValue$label !== void 0 ? _defaultValue$label : formatMessage(codeBlockButtonMessages.selectLanguage);
71
77
  const selectLanguageLabel = formatMessage(codeBlockButtonMessages.selectLanguage);
72
78
  const [hasSearchQuery, setHasSearchQuery] = useState(false);
79
+ const [lockedPopperPlacement, setLockedPopperPlacement] = useState();
73
80
  const inputValueRef = useRef('');
74
81
  const optionsByValue = useMemo(() => new Map(languagePickerOptions.map(option => [option.value, option])), [languagePickerOptions]);
75
82
  const recentlyUsedLanguages = useMemo(() => getRecentlyUsedLanguages(recentLanguageValues, optionsByValue), [recentLanguageValues, optionsByValue]);
@@ -79,6 +86,28 @@ export const LanguagePicker = ({
79
86
  recentlyUsedLanguages
80
87
  }), [formatMessage, languagePickerOptions, recentlyUsedLanguages]);
81
88
  const searchOptions = useMemo(() => options.flatMap(group => group.options), [options]);
89
+ const stableMenuPopperProps = useMemo(() => ({
90
+ // Allow Popper to choose top on the first open when bottom has no room, then lock the
91
+ // chosen placement so later hover/search renders do not move the open picker.
92
+ placement: lockedPopperPlacement !== null && lockedPopperPlacement !== void 0 ? lockedPopperPlacement : 'bottom-start',
93
+ modifiers: [{
94
+ name: 'offset',
95
+ options: {
96
+ offset: [0, 8]
97
+ }
98
+ }, {
99
+ name: 'preventOverflow',
100
+ enabled: false
101
+ }, ...(lockedPopperPlacement ? [{
102
+ name: 'flip',
103
+ enabled: false
104
+ }] : [])],
105
+ onFirstUpdate: ({
106
+ placement
107
+ }) => {
108
+ setLockedPopperPlacement(placement);
109
+ }
110
+ }), [lockedPopperPlacement]);
82
111
  const handleChange = useCallback(option => {
83
112
  var _option$selectionSour;
84
113
  if (!option) {
@@ -91,11 +120,33 @@ export const LanguagePicker = ({
91
120
  const handleInputChange = useCallback((newInputValue, actionMeta) => {
92
121
  // React-select clears the input as part of selecting a value before onChange fires.
93
122
  // Keep the last user-typed query so handleChange can report search selections correctly.
94
- if (!actionMeta || actionMeta.action === 'input-change') {
123
+ const isInputChange = !actionMeta || actionMeta.action === 'input-change';
124
+ if (isInputChange) {
95
125
  inputValueRef.current = newInputValue;
96
126
  }
97
- setHasSearchQuery(newInputValue.trim().length > 0);
98
- return newInputValue;
127
+ if (isInputChange || !fg('platform_editor_code_block_dogfooding_patch')) {
128
+ setHasSearchQuery(newInputValue.trim().length > 0);
129
+ return newInputValue;
130
+ }
131
+ return inputValueRef.current;
132
+ }, []);
133
+ const handleMenuOpen = useCallback(() => {
134
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
135
+ inputValueRef.current = '';
136
+ setHasSearchQuery(false);
137
+ setLockedPopperPlacement(undefined);
138
+ }
139
+ onMenuOpen === null || onMenuOpen === void 0 ? void 0 : onMenuOpen();
140
+ }, [onMenuOpen]);
141
+ const handleTriggerMouseDown = useCallback(event => {
142
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
143
+ // PopupSelect's FocusLock returns focus to the element that was focused before the
144
+ // picker opened. If that is the editor/code block, closing the picker can scroll the
145
+ // whole code block into view. Focus the trigger first without scrolling so FocusLock
146
+ // returns to the trigger; see CodeBlockLanguagePicker's handleSelection for restoring
147
+ // editor focus.
148
+ focusWithoutScrolling(event.currentTarget);
149
+ }
99
150
  }, []);
100
151
  const renderTarget = useCallback(({
101
152
  isOpen,
@@ -106,6 +157,7 @@ export const LanguagePicker = ({
106
157
  className: ax([styles.trigger])
107
158
  }, /*#__PURE__*/React.createElement(Button, {
108
159
  shouldFitContainer: true,
160
+ onMouseDown: handleTriggerMouseDown,
109
161
  onKeyDown: onKeyDown,
110
162
  ref: ref,
111
163
  iconAfter: ChevronDownIcon,
@@ -113,7 +165,7 @@ export const LanguagePicker = ({
113
165
  isSelected: isOpen,
114
166
  "aria-controls": ariaControls,
115
167
  testId: "code-block-language-picker-trigger"
116
- }, label)), [label]);
168
+ }, label)), [handleTriggerMouseDown, label]);
117
169
  return /*#__PURE__*/React.createElement(PopupSelect, {
118
170
  components: popupSelectComponents,
119
171
  filterOption: filterOption,
@@ -123,9 +175,9 @@ export const LanguagePicker = ({
123
175
  menuPlacement: "auto",
124
176
  onChange: handleChange,
125
177
  onInputChange: handleInputChange,
126
- onMenuOpen: onMenuOpen,
178
+ onMenuOpen: handleMenuOpen,
127
179
  options: hasSearchQuery ? searchOptions : options,
128
- popperProps: menuPopperProps,
180
+ popperProps: fg('platform_editor_code_block_dogfooding_patch') ? stableMenuPopperProps : menuPopperProps,
129
181
  searchThreshold: -1,
130
182
  target: renderTarget,
131
183
  testId: "code-block-language-picker",
@@ -1,4 +1,5 @@
1
1
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
2
3
  import { ACTIONS } from '../pm-plugins/actions';
3
4
  import { autoDetectPluginKey } from '../pm-plugins/auto-detect-state';
4
5
  import { createAutoDetectEntry } from './auto-detect-state';
@@ -32,6 +33,7 @@ const runPendingDetection = (view, localId, api) => {
32
33
  }
33
34
  const detectedLanguage = detectLanguage(found.node.textContent);
34
35
  const detectionResult = detectedLanguage ? 'detected' : 'noneDetected';
36
+ const detectionPhase = entry.detectionResult ? 'redetection' : 'initial';
35
37
  // Keep a previous auto-detected language when the latest snippet is too weak to classify.
36
38
  const shouldPreserveAutoDetectedLanguage = !detectedLanguage && Boolean(entry.autoDetectedLanguage) && found.node.attrs.language === entry.autoDetectedLanguage;
37
39
  const nextEntry = {
@@ -42,11 +44,13 @@ const runPendingDetection = (view, localId, api) => {
42
44
 
43
45
  // If there is no confident detection, record the result without clearing user-visible language.
44
46
  const shouldOnlyUpdateDetectionState = !detectedLanguage && (!found.node.attrs.language || shouldPreserveAutoDetectedLanguage);
47
+ const hasDetectedLanguageChange = Boolean(detectedLanguage) && found.node.attrs.language !== detectedLanguage;
48
+ const shouldClearStaleLanguage = !detectedLanguage && Boolean(found.node.attrs.language) && !shouldPreserveAutoDetectedLanguage;
49
+ const shouldUpdateNodeLanguage = fg('platform_editor_code_block_dogfooding_patch') ? hasDetectedLanguageChange || shouldClearStaleLanguage : !shouldOnlyUpdateDetectionState;
45
50
  api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
46
51
  tr
47
52
  }) => {
48
- var _api$analytics;
49
- if (!shouldOnlyUpdateDetectionState) {
53
+ if (shouldUpdateNodeLanguage) {
50
54
  tr.setNodeMarkup(found.pos, undefined, {
51
55
  ...found.node.attrs,
52
56
  language: detectedLanguage
@@ -59,15 +63,22 @@ const runPendingDetection = (view, localId, api) => {
59
63
  entry: nextEntry
60
64
  }
61
65
  });
62
- api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
63
- action: ACTION.LANGUAGE_AUTO_DETECTED,
64
- actionSubject: ACTION_SUBJECT.CODE_BLOCK,
65
- attributes: {
66
- language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
67
- detectionResult
68
- },
69
- eventType: EVENT_TYPE.TRACK
70
- })(tr);
66
+ // When platform_editor_code_block_dogfooding_patch is cleaned up, merge this with the previous shouldUpdateNodeLanguage check.
67
+ if (!fg('platform_editor_code_block_dogfooding_patch') || shouldUpdateNodeLanguage) {
68
+ var _api$analytics;
69
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
70
+ action: ACTION.LANGUAGE_AUTO_DETECTED,
71
+ actionSubject: ACTION_SUBJECT.CODE_BLOCK,
72
+ attributes: {
73
+ language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
74
+ detectionResult,
75
+ ...(fg('platform_editor_code_block_dogfooding_patch') ? {
76
+ detectionPhase
77
+ } : {})
78
+ },
79
+ eventType: EVENT_TYPE.TRACK
80
+ })(tr);
81
+ }
71
82
  // Language detection runs in the background and should not move the viewport.
72
83
  return tr.setMeta('scrollIntoView', false);
73
84
  });
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
-
2
+ import { fg } from '@atlaskit/platform-feature-flags';
3
3
  // Conservative weighted-regex heuristic for common high-confidence snippets, not a full classifier.
4
4
  // Ambiguous snippets intentionally return null so users can select the language manually.
5
5
  const MAX_DETECTION_CHARS = 10_000;
@@ -12,6 +12,7 @@ const hasPattern = (code, pattern) => {
12
12
  return pattern.test(code);
13
13
  };
14
14
  const scorePatterns = (code, patterns) => patterns.reduce((score, [pattern, value]) => score + (hasPattern(code, pattern) ? value : 0), 0);
15
+ const scoreEnhancedTypeScriptPatterns = code => fg('platform_editor_code_block_dogfooding_patch') ? scorePatterns(code, [[/\b(?:private|protected|public|readonly)\s+[\w$]+\s*=/, 5], [/\([^)]{0,500}\b[\w$]+\??:\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200}[^)]{0,500}\)\s*(?::\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200})?\s*=>/, 5]]) : 0;
15
16
  const looksLikeHtmlTagPair = code => {
16
17
  const openTags = new Set();
17
18
  const tagPattern = /<\/?([a-z][a-z0-9-]*)\b[^<>]{0,500}>/gi;
@@ -57,7 +58,7 @@ const getLanguageScores = code => [{
57
58
  score: scorePatterns(code, [[/\bSELECT\b[\s\S]+\bFROM\b/i, 5], [/\b(?:INSERT\s+INTO|UPDATE\b[\s\S]+\bSET\b|DELETE\s+FROM|CREATE\s+TABLE|ALTER\s+TABLE)\b/i, 5], [/\b(?:JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|LIMIT)\b/i, 2]])
58
59
  }, {
59
60
  language: 'typescript',
60
- score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]])
61
+ score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]]) + scoreEnhancedTypeScriptPatterns(code)
61
62
  }, {
62
63
  language: 'javascript',
63
64
  score: scorePatterns(code, [[/\b(?:const|let|var)\s+[\w$]+\s*=/, 3], [/\bfunction\s*[\w$]*\s*\([^)]*\)\s*\{/, 3], [/=>\s*(?:\{|[\w$'"`(])/, 3], [/\b(?:import|export)\s+(?:[\w${}*,\s]+\s+from\s+)?['"][^'"]+['"]/, 3], [/\b(?:console\.log|document\.|window\.|require\()|\bmodule\.exports\b/, 3]])
@@ -472,6 +472,9 @@ export var toggleWordWrapStateForCodeBlockNode = function toggleWordWrapStateFor
472
472
  tr.setNodeMarkup(codeBlock.pos, undefined, _objectSpread(_objectSpread({}, codeBlockNode.attrs), {}, {
473
473
  wrap: updatedToggleState
474
474
  }));
475
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
476
+ tr.setMeta('scrollIntoView', false);
477
+ }
475
478
  } else {
476
479
  codeBlockWrappedStates.set(codeBlockNode, updatedToggleState);
477
480
  }
@@ -26,11 +26,19 @@ export var CodeBlockLanguagePicker = function CodeBlockLanguagePicker(_ref) {
26
26
  var _api$analytics;
27
27
  var command = option.value === DETECT_LANGUAGE_VALUE && fg('platform_editor_code_block_language_detection_flow') ? detectLanguage() : changeLanguage(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(option.value, selectionSource);
28
28
  var commandSucceeded = command(editorView.state, editorView.dispatch);
29
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
30
+ requestAnimationFrame(function () {
31
+ // Let PopupSelect/FocusLock finish returning focus to the trigger, then restore the editor.
32
+ api === null || api === void 0 || api.core.actions.focus({
33
+ scrollIntoView: false
34
+ });
35
+ });
36
+ }
29
37
  if (commandSucceeded && option.value !== DETECT_LANGUAGE_VALUE) {
30
38
  saveRecentLanguage(option.value);
31
39
  setRecentLanguageValues(getRecentLanguages());
32
40
  }
33
- }, [api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, editorView]);
41
+ }, [api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, api === null || api === void 0 ? void 0 : api.core.actions, editorView]);
34
42
  return /*#__PURE__*/React.createElement(LanguagePicker, {
35
43
  defaultValue: defaultValue,
36
44
  filterOption: filterOption,
@@ -6,7 +6,8 @@ import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
6
6
  import AngleBracketsIcon from '@atlaskit/icon/core/angle-brackets';
7
7
  var NODE_NAME = 'codeBlock';
8
8
  var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
9
- var api = _ref.api;
9
+ var api = _ref.api,
10
+ isSuggested = _ref.isSuggested;
10
11
  var _useIntl = useIntl(),
11
12
  formatMessage = _useIntl.formatMessage;
12
13
  var handleClick = function handleClick(event) {
@@ -17,6 +18,7 @@ var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
17
18
  var tr = _ref2.tr;
18
19
  var command = api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.commands.transformNode(tr.doc.type.schema.nodes.codeBlock, {
19
20
  inputMethod: inputMethod,
21
+ isSuggested: isSuggested,
20
22
  triggeredFrom: triggeredFrom,
21
23
  targetTypeName: NODE_NAME
22
24
  });
@@ -35,8 +37,11 @@ var CodeBlockMenuItem = function CodeBlockMenuItem(_ref) {
35
37
  };
36
38
  export var createCodeBlockMenuItem = function createCodeBlockMenuItem(api) {
37
39
  return function () {
40
+ var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
41
+ isSuggested = _ref3.isSuggested;
38
42
  return /*#__PURE__*/React.createElement(CodeBlockMenuItem, {
39
- api: api
43
+ api: api,
44
+ isSuggested: isSuggested
40
45
  });
41
46
  };
42
47
  };
@@ -1,4 +1,5 @@
1
1
  /* LanguagePicker.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import "./LanguagePicker.compiled.css";
4
5
  import { ax, ix } from "@compiled/react/runtime";
@@ -10,6 +11,7 @@ import Button from '@atlaskit/button/new';
10
11
  import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
11
12
  import { akEditorLineHeight } from '@atlaskit/editor-shared-styles';
12
13
  import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
14
+ import { fg } from '@atlaskit/platform-feature-flags';
13
15
  import { Box } from '@atlaskit/primitives/compiled';
14
16
  import { PopupSelect, components } from '@atlaskit/select';
15
17
  import { createGroupedLanguageOptions } from './language-picker-options';
@@ -51,6 +53,11 @@ var menuPopperProps = {
51
53
  enabled: false
52
54
  }]
53
55
  };
56
+ var focusWithoutScrolling = function focusWithoutScrolling(element) {
57
+ element.focus({
58
+ preventScroll: true
59
+ });
60
+ };
54
61
  var getRecentlyUsedLanguages = function getRecentlyUsedLanguages(recentLanguageValues, optionsByValue) {
55
62
  var recentlyUsedLanguages = [];
56
63
  var _iterator = _createForOfIteratorHelper(recentLanguageValues),
@@ -86,6 +93,10 @@ export var LanguagePicker = function LanguagePicker(_ref) {
86
93
  _useState2 = _slicedToArray(_useState, 2),
87
94
  hasSearchQuery = _useState2[0],
88
95
  setHasSearchQuery = _useState2[1];
96
+ var _useState3 = useState(),
97
+ _useState4 = _slicedToArray(_useState3, 2),
98
+ lockedPopperPlacement = _useState4[0],
99
+ setLockedPopperPlacement = _useState4[1];
89
100
  var inputValueRef = useRef('');
90
101
  var optionsByValue = useMemo(function () {
91
102
  return new Map(languagePickerOptions.map(function (option) {
@@ -107,6 +118,29 @@ export var LanguagePicker = function LanguagePicker(_ref) {
107
118
  return group.options;
108
119
  });
109
120
  }, [options]);
121
+ var stableMenuPopperProps = useMemo(function () {
122
+ return {
123
+ // Allow Popper to choose top on the first open when bottom has no room, then lock the
124
+ // chosen placement so later hover/search renders do not move the open picker.
125
+ placement: lockedPopperPlacement !== null && lockedPopperPlacement !== void 0 ? lockedPopperPlacement : 'bottom-start',
126
+ modifiers: [{
127
+ name: 'offset',
128
+ options: {
129
+ offset: [0, 8]
130
+ }
131
+ }, {
132
+ name: 'preventOverflow',
133
+ enabled: false
134
+ }].concat(_toConsumableArray(lockedPopperPlacement ? [{
135
+ name: 'flip',
136
+ enabled: false
137
+ }] : [])),
138
+ onFirstUpdate: function onFirstUpdate(_ref2) {
139
+ var placement = _ref2.placement;
140
+ setLockedPopperPlacement(placement);
141
+ }
142
+ };
143
+ }, [lockedPopperPlacement]);
110
144
  var handleChange = useCallback(function (option) {
111
145
  var _option$selectionSour;
112
146
  if (!option) {
@@ -119,21 +153,44 @@ export var LanguagePicker = function LanguagePicker(_ref) {
119
153
  var handleInputChange = useCallback(function (newInputValue, actionMeta) {
120
154
  // React-select clears the input as part of selecting a value before onChange fires.
121
155
  // Keep the last user-typed query so handleChange can report search selections correctly.
122
- if (!actionMeta || actionMeta.action === 'input-change') {
156
+ var isInputChange = !actionMeta || actionMeta.action === 'input-change';
157
+ if (isInputChange) {
123
158
  inputValueRef.current = newInputValue;
124
159
  }
125
- setHasSearchQuery(newInputValue.trim().length > 0);
126
- return newInputValue;
160
+ if (isInputChange || !fg('platform_editor_code_block_dogfooding_patch')) {
161
+ setHasSearchQuery(newInputValue.trim().length > 0);
162
+ return newInputValue;
163
+ }
164
+ return inputValueRef.current;
165
+ }, []);
166
+ var handleMenuOpen = useCallback(function () {
167
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
168
+ inputValueRef.current = '';
169
+ setHasSearchQuery(false);
170
+ setLockedPopperPlacement(undefined);
171
+ }
172
+ onMenuOpen === null || onMenuOpen === void 0 || onMenuOpen();
173
+ }, [onMenuOpen]);
174
+ var handleTriggerMouseDown = useCallback(function (event) {
175
+ if (fg('platform_editor_code_block_dogfooding_patch')) {
176
+ // PopupSelect's FocusLock returns focus to the element that was focused before the
177
+ // picker opened. If that is the editor/code block, closing the picker can scroll the
178
+ // whole code block into view. Focus the trigger first without scrolling so FocusLock
179
+ // returns to the trigger; see CodeBlockLanguagePicker's handleSelection for restoring
180
+ // editor focus.
181
+ focusWithoutScrolling(event.currentTarget);
182
+ }
127
183
  }, []);
128
- var renderTarget = useCallback(function (_ref2) {
129
- var isOpen = _ref2.isOpen,
130
- ref = _ref2.ref,
131
- onKeyDown = _ref2.onKeyDown,
132
- ariaControls = _ref2['aria-controls'];
184
+ var renderTarget = useCallback(function (_ref3) {
185
+ var isOpen = _ref3.isOpen,
186
+ ref = _ref3.ref,
187
+ onKeyDown = _ref3.onKeyDown,
188
+ ariaControls = _ref3['aria-controls'];
133
189
  return /*#__PURE__*/React.createElement("div", {
134
190
  className: ax([styles.trigger])
135
191
  }, /*#__PURE__*/React.createElement(Button, {
136
192
  shouldFitContainer: true,
193
+ onMouseDown: handleTriggerMouseDown,
137
194
  onKeyDown: onKeyDown,
138
195
  ref: ref,
139
196
  iconAfter: ChevronDownIcon,
@@ -142,7 +199,7 @@ export var LanguagePicker = function LanguagePicker(_ref) {
142
199
  "aria-controls": ariaControls,
143
200
  testId: "code-block-language-picker-trigger"
144
201
  }, label));
145
- }, [label]);
202
+ }, [handleTriggerMouseDown, label]);
146
203
  return /*#__PURE__*/React.createElement(PopupSelect, {
147
204
  components: popupSelectComponents,
148
205
  filterOption: filterOption,
@@ -152,9 +209,9 @@ export var LanguagePicker = function LanguagePicker(_ref) {
152
209
  menuPlacement: "auto",
153
210
  onChange: handleChange,
154
211
  onInputChange: handleInputChange,
155
- onMenuOpen: onMenuOpen,
212
+ onMenuOpen: handleMenuOpen,
156
213
  options: hasSearchQuery ? searchOptions : options,
157
- popperProps: menuPopperProps,
214
+ popperProps: fg('platform_editor_code_block_dogfooding_patch') ? stableMenuPopperProps : menuPopperProps,
158
215
  searchThreshold: -1,
159
216
  target: renderTarget,
160
217
  testId: "code-block-language-picker",
@@ -3,6 +3,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
3
  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; }
4
4
  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; }
5
5
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
6
7
  import { ACTIONS } from '../pm-plugins/actions';
7
8
  import { autoDetectPluginKey } from '../pm-plugins/auto-detect-state';
8
9
  import { createAutoDetectEntry } from './auto-detect-state';
@@ -36,6 +37,7 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
36
37
  }
37
38
  var detectedLanguage = detectLanguage(found.node.textContent);
38
39
  var detectionResult = detectedLanguage ? 'detected' : 'noneDetected';
40
+ var detectionPhase = entry.detectionResult ? 'redetection' : 'initial';
39
41
  // Keep a previous auto-detected language when the latest snippet is too weak to classify.
40
42
  var shouldPreserveAutoDetectedLanguage = !detectedLanguage && Boolean(entry.autoDetectedLanguage) && found.node.attrs.language === entry.autoDetectedLanguage;
41
43
  var nextEntry = _objectSpread(_objectSpread({}, createAutoDetectEntry(found.node, found.pos, false, entry)), {}, {
@@ -45,10 +47,12 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
45
47
 
46
48
  // If there is no confident detection, record the result without clearing user-visible language.
47
49
  var shouldOnlyUpdateDetectionState = !detectedLanguage && (!found.node.attrs.language || shouldPreserveAutoDetectedLanguage);
50
+ var hasDetectedLanguageChange = Boolean(detectedLanguage) && found.node.attrs.language !== detectedLanguage;
51
+ var shouldClearStaleLanguage = !detectedLanguage && Boolean(found.node.attrs.language) && !shouldPreserveAutoDetectedLanguage;
52
+ var shouldUpdateNodeLanguage = fg('platform_editor_code_block_dogfooding_patch') ? hasDetectedLanguageChange || shouldClearStaleLanguage : !shouldOnlyUpdateDetectionState;
48
53
  api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref) {
49
- var _api$analytics;
50
54
  var tr = _ref.tr;
51
- if (!shouldOnlyUpdateDetectionState) {
55
+ if (shouldUpdateNodeLanguage) {
52
56
  tr.setNodeMarkup(found.pos, undefined, _objectSpread(_objectSpread({}, found.node.attrs), {}, {
53
57
  language: detectedLanguage
54
58
  }), found.node.marks);
@@ -60,15 +64,21 @@ var runPendingDetection = function runPendingDetection(view, localId, api) {
60
64
  entry: nextEntry
61
65
  }
62
66
  });
63
- api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({
64
- action: ACTION.LANGUAGE_AUTO_DETECTED,
65
- actionSubject: ACTION_SUBJECT.CODE_BLOCK,
66
- attributes: {
67
- language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
68
- detectionResult: detectionResult
69
- },
70
- eventType: EVENT_TYPE.TRACK
71
- })(tr);
67
+ // When platform_editor_code_block_dogfooding_patch is cleaned up, merge this with the previous shouldUpdateNodeLanguage check.
68
+ if (!fg('platform_editor_code_block_dogfooding_patch') || shouldUpdateNodeLanguage) {
69
+ var _api$analytics;
70
+ api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({
71
+ action: ACTION.LANGUAGE_AUTO_DETECTED,
72
+ actionSubject: ACTION_SUBJECT.CODE_BLOCK,
73
+ attributes: _objectSpread({
74
+ language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
75
+ detectionResult: detectionResult
76
+ }, fg('platform_editor_code_block_dogfooding_patch') ? {
77
+ detectionPhase: detectionPhase
78
+ } : {}),
79
+ eventType: EVENT_TYPE.TRACK
80
+ })(tr);
81
+ }
72
82
  // Language detection runs in the background and should not move the viewport.
73
83
  return tr.setMeta('scrollIntoView', false);
74
84
  });
@@ -1,7 +1,7 @@
1
1
  import _typeof from "@babel/runtime/helpers/typeof";
2
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
3
  /* eslint-disable require-unicode-regexp */
4
-
4
+ import { fg } from '@atlaskit/platform-feature-flags';
5
5
  // Conservative weighted-regex heuristic for common high-confidence snippets, not a full classifier.
6
6
  // Ambiguous snippets intentionally return null so users can select the language manually.
7
7
  var MAX_DETECTION_CHARS = 10000;
@@ -21,6 +21,9 @@ var scorePatterns = function scorePatterns(code, patterns) {
21
21
  return score + (hasPattern(code, pattern) ? value : 0);
22
22
  }, 0);
23
23
  };
24
+ var scoreEnhancedTypeScriptPatterns = function scoreEnhancedTypeScriptPatterns(code) {
25
+ return fg('platform_editor_code_block_dogfooding_patch') ? scorePatterns(code, [[/\b(?:private|protected|public|readonly)\s+[\w$]+\s*=/, 5], [/\([^)]{0,500}\b[\w$]+\??:\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200}[^)]{0,500}\)\s*(?::\s*[A-Za-z_$][\w$<>{}\[\]|&,\s.?]{0,200})?\s*=>/, 5]]) : 0;
26
+ };
24
27
  var looksLikeHtmlTagPair = function looksLikeHtmlTagPair(code) {
25
28
  var openTags = new Set();
26
29
  var tagPattern = /<\/?([a-z][a-z0-9-]*)\b[^<>]{0,500}>/gi;
@@ -70,7 +73,7 @@ var getLanguageScores = function getLanguageScores(code) {
70
73
  score: scorePatterns(code, [[/\bSELECT\b[\s\S]+\bFROM\b/i, 5], [/\b(?:INSERT\s+INTO|UPDATE\b[\s\S]+\bSET\b|DELETE\s+FROM|CREATE\s+TABLE|ALTER\s+TABLE)\b/i, 5], [/\b(?:JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|LIMIT)\b/i, 2]])
71
74
  }, {
72
75
  language: 'typescript',
73
- score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]])
76
+ score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]]) + scoreEnhancedTypeScriptPatterns(code)
74
77
  }, {
75
78
  language: 'javascript',
76
79
  score: scorePatterns(code, [[/\b(?:const|let|var)\s+[\w$]+\s*=/, 3], [/\bfunction\s*[\w$]*\s*\([^)]*\)\s*\{/, 3], [/=>\s*(?:\{|[\w$'"`(])/, 3], [/\b(?:import|export)\s+(?:[\w${}*,\s]+\s+from\s+)?['"][^'"]+['"]/, 3], [/\b(?:console\.log|document\.|window\.|require\()|\bmodule\.exports\b/, 3]])
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
3
+ import type { BlockMenuItemComponentProps } from '@atlaskit/editor-plugin-block-menu/blockMenuPluginType';
3
4
  import type { CodeBlockPlugin } from '../codeBlockPluginType';
4
- export declare const createCodeBlockMenuItem: (api: ExtractInjectionAPI<CodeBlockPlugin> | undefined) => () => React.JSX.Element;
5
+ export declare const createCodeBlockMenuItem: (api: ExtractInjectionAPI<CodeBlockPlugin> | undefined) => ({ isSuggested }?: BlockMenuItemComponentProps) => React.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-code-block",
3
- "version": "14.0.5",
3
+ "version": "14.1.0",
4
4
  "description": "Code block plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -24,12 +24,12 @@
24
24
  "@atlaskit/code": "^18.0.0",
25
25
  "@atlaskit/css": "^1.0.0",
26
26
  "@atlaskit/editor-plugin-analytics": "^12.0.0",
27
- "@atlaskit/editor-plugin-block-menu": "^11.0.0",
27
+ "@atlaskit/editor-plugin-block-menu": "^11.1.0",
28
28
  "@atlaskit/editor-plugin-composition": "^11.0.0",
29
29
  "@atlaskit/editor-plugin-decorations": "^12.0.0",
30
30
  "@atlaskit/editor-plugin-editor-disabled": "^12.0.0",
31
31
  "@atlaskit/editor-plugin-editor-viewmode": "^14.0.0",
32
- "@atlaskit/editor-plugin-interaction": "^21.0.0",
32
+ "@atlaskit/editor-plugin-interaction": "^22.0.0",
33
33
  "@atlaskit/editor-plugin-selection": "^12.0.0",
34
34
  "@atlaskit/editor-plugin-toolbar": "^9.0.0",
35
35
  "@atlaskit/editor-prosemirror": "^8.0.0",
@@ -41,14 +41,14 @@
41
41
  "@atlaskit/platform-feature-flags": "^2.0.0",
42
42
  "@atlaskit/primitives": "^20.0.0",
43
43
  "@atlaskit/prosemirror-input-rules": "^4.0.0",
44
- "@atlaskit/select": "^22.0.0",
45
- "@atlaskit/tmp-editor-statsig": "^108.3.0",
46
- "@atlaskit/tokens": "^14.0.0",
44
+ "@atlaskit/select": "^22.1.0",
45
+ "@atlaskit/tmp-editor-statsig": "^110.0.0",
46
+ "@atlaskit/tokens": "^15.0.0",
47
47
  "@babel/runtime": "^7.0.0",
48
48
  "@compiled/react": "^0.20.0"
49
49
  },
50
50
  "peerDependencies": {
51
- "@atlaskit/editor-common": "^116.4.0",
51
+ "@atlaskit/editor-common": "^116.11.0",
52
52
  "react": "^18.2.0",
53
53
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
54
54
  },
@@ -103,12 +103,15 @@
103
103
  "platform_editor_code_block_add_line_number_button": {
104
104
  "type": "boolean"
105
105
  },
106
+ "platform_editor_code_block_dogfooding_patch": {
107
+ "type": "boolean"
108
+ },
106
109
  "platform_editor_code_block_language_detection_flow": {
107
110
  "type": "boolean"
108
111
  }
109
112
  },
110
113
  "devDependencies": {
111
114
  "react": "^18.2.0",
112
- "react-intl": "^6.6.2"
115
+ "react-intl": "^7.0.0"
113
116
  }
114
117
  }