@atlaskit/editor-plugin-block-menu 4.0.2 → 4.0.4

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,24 @@
1
1
  # @atlaskit/editor-plugin-block-menu
2
2
 
3
+ ## 4.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`1eda79686167c`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1eda79686167c) -
8
+ ED-29418: Fix empty code block convert to lists
9
+ - [`0778701e62192`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0778701e62192) -
10
+ [ux] ED-29424 Focus first menu item when block menu is opened and remove decorations when delete
11
+ button unmounts
12
+ - Updated dependencies
13
+
14
+ ## 4.0.3
15
+
16
+ ### Patch Changes
17
+
18
+ - [`c158b1ba4f0fd`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/c158b1ba4f0fd) -
19
+ ED-29388: fix converting empty list
20
+ - Updated dependencies
21
+
3
22
  ## 4.0.2
4
23
 
5
24
  ### Patch Changes
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-dev-agents/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-dev-agents/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-dev-agents/tsconfig.json"
28
31
  },
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-jira/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-jira/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-jira/tsconfig.json"
28
31
  },
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-passionfruit/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-passionfruit/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-passionfruit/tsconfig.json"
28
31
  },
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-post-office/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-post-office/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-post-office/tsconfig.json"
28
31
  },
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-rovo-extension/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-rovo-extension/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-rovo-extension/tsconfig.json"
28
31
  },
@@ -23,6 +23,9 @@
23
23
  {
24
24
  "path": "../../../design-system/dropdown-menu/afm-townsquare/tsconfig.json"
25
25
  },
26
+ {
27
+ "path": "../../editor-plugin-analytics/afm-townsquare/tsconfig.json"
28
+ },
26
29
  {
27
30
  "path": "../../editor-plugin-block-controls/afm-townsquare/tsconfig.json"
28
31
  },
@@ -3,14 +3,16 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.formatNode = void 0;
6
+ exports.formatNodeSelectEmptyList = exports.formatNode = void 0;
7
7
  var _analytics = require("@atlaskit/editor-common/analytics");
8
+ var _state = require("@atlaskit/editor-prosemirror/state");
8
9
  var _utils = require("@atlaskit/editor-prosemirror/utils");
9
10
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
10
11
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
11
12
  var _selection = require("./selection");
12
13
  var _layoutTransforms = require("./transforms/layout-transforms");
13
14
  var _transformNodeToTargetType = require("./transforms/transformNodeToTargetType");
15
+ var _utils2 = require("./transforms/utils");
14
16
  /**
15
17
  * Handles formatting when selection is empty by inserting a new target node
16
18
  */
@@ -50,6 +52,42 @@ var formatNodeWhenSelectionEmpty = function formatNodeWhenSelectionEmpty(tr, tar
50
52
  return tr;
51
53
  };
52
54
 
55
+ /**
56
+ * Handles formatting when an empty list is selected
57
+ * Converting an empty list to a target node, will remove the list and replace with an empty target node
58
+ */
59
+ var formatNodeSelectEmptyList = exports.formatNodeSelectEmptyList = function formatNodeSelectEmptyList(tr, targetType, listNode, schema) {
60
+ var nodes = schema.nodes;
61
+ var headingLevel = 1;
62
+ var finalTargetType = targetType;
63
+ if (targetType.startsWith('heading')) {
64
+ var levelString = targetType.slice(-1);
65
+ var level = parseInt(levelString, 10);
66
+ if (!isNaN(level) && level >= 1 && level <= 6) {
67
+ headingLevel = level;
68
+ finalTargetType = 'heading';
69
+ }
70
+ }
71
+ var replaceNode = null;
72
+ if (finalTargetType === 'layoutSection') {
73
+ var emptyPara = nodes.paragraph.createAndFill();
74
+ if (emptyPara) {
75
+ replaceNode = (0, _layoutTransforms.createDefaultLayoutSection)(schema, emptyPara);
76
+ }
77
+ } else if (finalTargetType === 'heading') {
78
+ replaceNode = nodes.heading.createAndFill({
79
+ level: headingLevel
80
+ });
81
+ } else {
82
+ replaceNode = nodes[finalTargetType].createAndFill();
83
+ }
84
+ if (replaceNode) {
85
+ tr.replaceWith(listNode.pos, listNode.pos + listNode.node.nodeSize, replaceNode);
86
+ tr.setSelection(new _state.TextSelection(tr.doc.resolve(listNode.pos)));
87
+ }
88
+ return tr;
89
+ };
90
+
53
91
  /**
54
92
  * Formats the current node or selection to the specified target type
55
93
  * @param api - The editor API injection that provides access to analytics and other plugin actions
@@ -69,7 +107,26 @@ var formatNode = exports.formatNode = function formatNode(api) {
69
107
 
70
108
  // when selection is empty, we insert a empty target node
71
109
  if (selection.empty && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_empty_line', 'isEnabled', true)) {
72
- return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
110
+ var listNodes = [];
111
+ // need to find if there is any list node in the current selection
112
+ // As when select a empty list, selection is empty, but we want to convert the list instead of inserting a target node
113
+ // findSelectedNodeOfType does not work when selection is empty, so we use nodesBetween
114
+ tr.doc.nodesBetween(selection.from, selection.to, function (node, pos) {
115
+ if ((0, _utils2.isListNodeType)(node.type)) {
116
+ listNodes.push({
117
+ node: node,
118
+ pos: pos
119
+ });
120
+ }
121
+ });
122
+ // get the first list node as when click on drag handle if there are list node
123
+ // can only select one list at a time, so we just need to find the first one
124
+ if (listNodes.length > 0 && (0, _platformFeatureFlags.fg)('platform_editor_block_menu_patch_2')) {
125
+ var firstChild = listNodes[0];
126
+ return formatNodeSelectEmptyList(tr, targetType, firstChild, schema);
127
+ } else {
128
+ return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
129
+ }
73
130
  }
74
131
 
75
132
  // Try to find the current node from selection
@@ -8,6 +8,7 @@ exports.unwrapAndConvertToList = exports.unwrapAndConvertToBlockType = exports.t
8
8
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
9
9
  var _model = require("@atlaskit/editor-prosemirror/model");
10
10
  var _utils = require("@atlaskit/editor-prosemirror/utils");
11
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
11
12
  var _inlineNodeTransforms = require("./inline-node-transforms");
12
13
  var _utils2 = require("./utils");
13
14
  var convertInvalidNodeToValidNodeType = function convertInvalidNodeToValidNodeType(sourceContent, sourceNodeType, validNodeType, withMarks) {
@@ -203,7 +204,11 @@ var unwrapAndConvertToList = exports.unwrapAndConvertToList = function unwrapAnd
203
204
  heading = _schema$nodes2.heading;
204
205
  var isTargetTaskList = targetNodeType === taskList;
205
206
  var createListItemFromInline = function createListItemFromInline(content) {
206
- return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
207
+ if (!content && (0, _platformFeatureFlags.fg)('platform_editor_block_menu_patch_2')) {
208
+ return isTargetTaskList ? taskItem.create() : listItem.create(null, paragraph.create());
209
+ } else {
210
+ return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
211
+ }
207
212
  };
208
213
  var getInlineContent = function getInlineContent(textblock) {
209
214
  var inlineContent = [];
@@ -233,6 +238,15 @@ var unwrapAndConvertToList = exports.unwrapAndConvertToList = function unwrapAnd
233
238
  };
234
239
  if (sourceNode.type.name === 'codeBlock') {
235
240
  var codeText = sourceNode.textContent;
241
+ // check if code block only contains newline characters
242
+ // eslint-disable-next-line require-unicode-regexp
243
+ var isOnlyNewLines = function isOnlyNewLines(codeText) {
244
+ return codeText.replace(/\n/g, '').trim() === '';
245
+ };
246
+ if ((!codeText || isOnlyNewLines(codeText)) && (0, _platformFeatureFlags.fg)('platform_editor_block_menu_patch_2')) {
247
+ // Empty code block - create an empty list item
248
+ currentListItems.push(createListItemFromInline());
249
+ }
236
250
  if (codeText) {
237
251
  var lines = codeText.split('\n');
238
252
  // Remove empty lines
@@ -9,6 +9,7 @@ exports.BlockMenuRenderer = void 0;
9
9
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
10
  var _react = _interopRequireWildcard(require("react"));
11
11
  var _uiMenu = require("@atlaskit/editor-common/ui-menu");
12
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
12
13
  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); }
13
14
  var NoOp = function NoOp(props) {
14
15
  return null;
@@ -50,48 +51,51 @@ var BlockMenuRenderer = exports.BlockMenuRenderer = function BlockMenuRenderer(_
50
51
  var menuSections = getSortedNonNestedSections(components);
51
52
  var menuItems = components.filter(isMenuItem);
52
53
  var nestedMenus = components.filter(isNestedMenu);
53
- return /*#__PURE__*/_react.default.createElement(_uiMenu.ArrowKeyNavigationProvider, {
54
- type: _uiMenu.ArrowKeyNavigationType.MENU
55
- }, /*#__PURE__*/_react.default.createElement(_react.Fragment, null, menuSections.map(function (section) {
56
- // Get all items for the current section, including nested menus, and sort them by rank
57
- var currentSectionItemsSorted = getSortedChildren([].concat((0, _toConsumableArray2.default)(menuItems), (0, _toConsumableArray2.default)(nestedMenus)), section.key);
58
- if (currentSectionItemsSorted.length === 0) {
59
- return null;
60
- }
54
+ var renderMenu = function renderMenu() {
55
+ return /*#__PURE__*/_react.default.createElement(_react.Fragment, null, menuSections.map(function (section) {
56
+ // Get all items for the current section, including nested menus, and sort them by rank
57
+ var currentSectionItemsSorted = getSortedChildren([].concat((0, _toConsumableArray2.default)(menuItems), (0, _toConsumableArray2.default)(nestedMenus)), section.key);
58
+ if (currentSectionItemsSorted.length === 0) {
59
+ return null;
60
+ }
61
61
 
62
- // iterate over the current section items, if it is nested menu, get their children, sort them
63
- // if they are menu items, just render as they are sorted above
64
- var getChildrenWithNestedItems = function getChildrenWithNestedItems(items) {
65
- return items.map(function (item) {
66
- if (isNestedMenu(item)) {
67
- var sortedNestedSections = getSortedNestedSections(components, item.key);
68
- return sortedNestedSections.map(function (section) {
69
- var sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
70
- var NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
71
- var NestedSection = section.component || fallbacks.section || NoOp;
72
- return /*#__PURE__*/_react.default.createElement(NestedMenuComponent, {
62
+ // iterate over the current section items, if it is nested menu, get their children, sort them
63
+ // if they are menu items, just render as they are sorted above
64
+ var getChildrenWithNestedItems = function getChildrenWithNestedItems(items) {
65
+ return items.map(function (item) {
66
+ if (isNestedMenu(item)) {
67
+ var sortedNestedSections = getSortedNestedSections(components, item.key);
68
+ return sortedNestedSections.map(function (section) {
69
+ var sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
70
+ var NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
71
+ var NestedSection = section.component || fallbacks.section || NoOp;
72
+ return /*#__PURE__*/_react.default.createElement(NestedMenuComponent, {
73
+ key: item.key
74
+ }, /*#__PURE__*/_react.default.createElement(NestedSection, {
75
+ key: section.key
76
+ }, sortedNestedMenuItems.map(function (nestedItem) {
77
+ var NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
78
+ return /*#__PURE__*/_react.default.createElement(NestedMenuItemComponent, {
79
+ key: nestedItem.key
80
+ });
81
+ })));
82
+ });
83
+ } else {
84
+ var ItemComponent = item.component || fallbacks.item || NoOp;
85
+ return /*#__PURE__*/_react.default.createElement(ItemComponent, {
73
86
  key: item.key
74
- }, /*#__PURE__*/_react.default.createElement(NestedSection, {
75
- key: section.key
76
- }, sortedNestedMenuItems.map(function (nestedItem) {
77
- var NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
78
- return /*#__PURE__*/_react.default.createElement(NestedMenuItemComponent, {
79
- key: nestedItem.key
80
- });
81
- })));
82
- });
83
- } else {
84
- var ItemComponent = item.component || fallbacks.item || NoOp;
85
- return /*#__PURE__*/_react.default.createElement(ItemComponent, {
86
- key: item.key
87
- });
88
- }
89
- });
90
- };
91
- var children = getChildrenWithNestedItems(currentSectionItemsSorted);
92
- var SectionComponent = section.component || fallbacks.section || NoOp;
93
- return /*#__PURE__*/_react.default.createElement(SectionComponent, {
94
- key: section.key
95
- }, children);
96
- })));
87
+ });
88
+ }
89
+ });
90
+ };
91
+ var children = getChildrenWithNestedItems(currentSectionItemsSorted);
92
+ var SectionComponent = section.component || fallbacks.section || NoOp;
93
+ return /*#__PURE__*/_react.default.createElement(SectionComponent, {
94
+ key: section.key
95
+ }, children);
96
+ }));
97
+ };
98
+ return (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? /*#__PURE__*/_react.default.createElement(_uiMenu.ArrowKeyNavigationProvider, {
99
+ type: _uiMenu.ArrowKeyNavigationType.MENU
100
+ }, renderMenu()) : renderMenu();
97
101
  };
@@ -64,18 +64,20 @@ var BlockMenu = function BlockMenu(_ref2) {
64
64
  boundariesElement = _ref2.boundariesElement,
65
65
  scrollableElement = _ref2.scrollableElement;
66
66
  var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['blockControls', 'userIntent'], function (states) {
67
- var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta;
67
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta, _states$blockControls4;
68
68
  return {
69
69
  menuTriggerBy: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.menuTriggerBy,
70
70
  isSelectedViaDragHandle: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.isSelectedViaDragHandle,
71
71
  isMenuOpen: (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.isMenuOpen,
72
- currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
72
+ currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent,
73
+ openedViaKeyboard: (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 || (_states$blockControls4 = _states$blockControls4.blockMenuOptions) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.openedViaKeyboard
73
74
  };
74
75
  }),
75
76
  menuTriggerBy = _useSharedPluginState.menuTriggerBy,
76
77
  isSelectedViaDragHandle = _useSharedPluginState.isSelectedViaDragHandle,
77
78
  isMenuOpen = _useSharedPluginState.isMenuOpen,
78
- currentUserIntent = _useSharedPluginState.currentUserIntent;
79
+ currentUserIntent = _useSharedPluginState.currentUserIntent,
80
+ openedViaKeyboard = _useSharedPluginState.openedViaKeyboard;
79
81
  var _useBlockMenu = (0, _blockMenuProvider.useBlockMenu)(),
80
82
  onDropdownOpenChanged = _useBlockMenu.onDropdownOpenChanged,
81
83
  fireAnalyticsEvent = _useBlockMenu.fireAnalyticsEvent;
@@ -86,7 +88,7 @@ var BlockMenu = function BlockMenu(_ref2) {
86
88
  // hasSelection true, always show block menu
87
89
  // hasSelection false, only show block menu when empty line experiment is enabled
88
90
  var shouldShowBlockMenuForEmptyLine = hasSelection || !hasSelection && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_empty_line', 'isEnabled', true);
89
- var selectedByShortcutORDragHandle = isSelectedViaDragHandle;
91
+ var selectedByShortcutORDragHandle = isSelectedViaDragHandle || openedViaKeyboard && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true);
90
92
  (0, _react.useEffect)(function () {
91
93
  var _api$userIntent;
92
94
  if (!isMenuOpen || !menuTriggerBy || !selectedByShortcutORDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
@@ -149,7 +151,9 @@ var BlockMenu = function BlockMenu(_ref2) {
149
151
  preventOverflow: true // disables forced horizontal placement when forcePlacement is on, so fitWidth controls flipping
150
152
  ,
151
153
  stick: true,
152
- focusTrap: true,
154
+ focusTrap: (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? openedViaKeyboard ? {
155
+ initialFocus: undefined
156
+ } : true : undefined,
153
157
  offset: [_styles.DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, 0]
154
158
  }, /*#__PURE__*/_react.default.createElement(BlockMenuContent, {
155
159
  api: api
@@ -78,10 +78,22 @@ var DeleteDropdownItemContent = function DeleteDropdownItemContent(_ref) {
78
78
  return tr;
79
79
  });
80
80
  }, [api, nodeTypes]);
81
- var onRemoveHoverDecoration = function onRemoveHoverDecoration() {
81
+ var onRemoveHoverDecoration = (0, _react.useCallback)(function () {
82
82
  var _api$decorations2, _api$decorations2$rem;
83
83
  api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$decorations2 = api.decorations) === null || _api$decorations2 === void 0 || (_api$decorations2 = _api$decorations2.commands) === null || _api$decorations2 === void 0 || (_api$decorations2$rem = _api$decorations2.removeDecoration) === null || _api$decorations2$rem === void 0 ? void 0 : _api$decorations2$rem.call(_api$decorations2));
84
- };
84
+ }, [api]);
85
+ (0, _react.useEffect)(function () {
86
+ if (!(0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
87
+ return;
88
+ }
89
+ return function () {
90
+ if (!(0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
91
+ return;
92
+ }
93
+ // clean up hover decoration when unmounting
94
+ onRemoveHoverDecoration();
95
+ };
96
+ }, [onRemoveHoverDecoration]);
85
97
  var text = (0, _platformFeatureFlags.fg)('platform_editor_block_menu_patch_1') ? formatMessage(_messages.blockMenuMessages.deleteBlock) : formatMessage(_blockMenu.messages.deleteBlock);
86
98
  return /*#__PURE__*/_react.default.createElement(_box.Box, {
87
99
  onMouseEnter: onShowHoverDecoration,
@@ -1,10 +1,13 @@
1
1
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
2
+ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
2
3
  import { findParentNodeOfType, findSelectedNodeOfType, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils';
3
4
  import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
5
6
  import { setSelectionAfterTransform } from './selection';
6
7
  import { createDefaultLayoutSection } from './transforms/layout-transforms';
7
8
  import { transformNodeToTargetType } from './transforms/transformNodeToTargetType';
9
+ import { isListNodeType } from './transforms/utils';
10
+
8
11
  /**
9
12
  * Handles formatting when selection is empty by inserting a new target node
10
13
  */
@@ -48,6 +51,44 @@ const formatNodeWhenSelectionEmpty = (tr, targetType, nodePos, schema) => {
48
51
  return tr;
49
52
  };
50
53
 
54
+ /**
55
+ * Handles formatting when an empty list is selected
56
+ * Converting an empty list to a target node, will remove the list and replace with an empty target node
57
+ */
58
+ export const formatNodeSelectEmptyList = (tr, targetType, listNode, schema) => {
59
+ const {
60
+ nodes
61
+ } = schema;
62
+ let headingLevel = 1;
63
+ let finalTargetType = targetType;
64
+ if (targetType.startsWith('heading')) {
65
+ const levelString = targetType.slice(-1);
66
+ const level = parseInt(levelString, 10);
67
+ if (!isNaN(level) && level >= 1 && level <= 6) {
68
+ headingLevel = level;
69
+ finalTargetType = 'heading';
70
+ }
71
+ }
72
+ let replaceNode = null;
73
+ if (finalTargetType === 'layoutSection') {
74
+ const emptyPara = nodes.paragraph.createAndFill();
75
+ if (emptyPara) {
76
+ replaceNode = createDefaultLayoutSection(schema, emptyPara);
77
+ }
78
+ } else if (finalTargetType === 'heading') {
79
+ replaceNode = nodes.heading.createAndFill({
80
+ level: headingLevel
81
+ });
82
+ } else {
83
+ replaceNode = nodes[finalTargetType].createAndFill();
84
+ }
85
+ if (replaceNode) {
86
+ tr.replaceWith(listNode.pos, listNode.pos + listNode.node.nodeSize, replaceNode);
87
+ tr.setSelection(new TextSelection(tr.doc.resolve(listNode.pos)));
88
+ }
89
+ return tr;
90
+ };
91
+
51
92
  /**
52
93
  * Formats the current node or selection to the specified target type
53
94
  * @param api - The editor API injection that provides access to analytics and other plugin actions
@@ -71,7 +112,26 @@ export const formatNode = api => targetType => {
71
112
 
72
113
  // when selection is empty, we insert a empty target node
73
114
  if (selection.empty && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true)) {
74
- return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
115
+ const listNodes = [];
116
+ // need to find if there is any list node in the current selection
117
+ // As when select a empty list, selection is empty, but we want to convert the list instead of inserting a target node
118
+ // findSelectedNodeOfType does not work when selection is empty, so we use nodesBetween
119
+ tr.doc.nodesBetween(selection.from, selection.to, (node, pos) => {
120
+ if (isListNodeType(node.type)) {
121
+ listNodes.push({
122
+ node,
123
+ pos
124
+ });
125
+ }
126
+ });
127
+ // get the first list node as when click on drag handle if there are list node
128
+ // can only select one list at a time, so we just need to find the first one
129
+ if (listNodes.length > 0 && fg('platform_editor_block_menu_patch_2')) {
130
+ const firstChild = listNodes[0];
131
+ return formatNodeSelectEmptyList(tr, targetType, firstChild, schema);
132
+ } else {
133
+ return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
134
+ }
75
135
  }
76
136
 
77
137
  // Try to find the current node from selection
@@ -1,5 +1,6 @@
1
1
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
2
2
  import { findChildrenByType } from '@atlaskit/editor-prosemirror/utils';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
3
4
  import { getInlineNodeTextContent } from './inline-node-transforms';
4
5
  import { isBlockNodeType, isListNodeType, isContainerNodeType, isBlockNodeForExtraction, convertNodeToInlineContent, getContentSupportChecker, convertCodeBlockContentToParagraphs, filterMarksForTargetNodeType, getMarksWithBreakout } from './utils';
5
6
  const convertInvalidNodeToValidNodeType = (sourceContent, sourceNodeType, validNodeType, withMarks) => {
@@ -204,7 +205,11 @@ export const unwrapAndConvertToList = ({
204
205
  } = schema.nodes;
205
206
  const isTargetTaskList = targetNodeType === taskList;
206
207
  const createListItemFromInline = content => {
207
- return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
208
+ if (!content && fg('platform_editor_block_menu_patch_2')) {
209
+ return isTargetTaskList ? taskItem.create() : listItem.create(null, paragraph.create());
210
+ } else {
211
+ return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
212
+ }
208
213
  };
209
214
  const getInlineContent = textblock => {
210
215
  const inlineContent = [];
@@ -234,6 +239,13 @@ export const unwrapAndConvertToList = ({
234
239
  };
235
240
  if (sourceNode.type.name === 'codeBlock') {
236
241
  const codeText = sourceNode.textContent;
242
+ // check if code block only contains newline characters
243
+ // eslint-disable-next-line require-unicode-regexp
244
+ const isOnlyNewLines = codeText => codeText.replace(/\n/g, '').trim() === '';
245
+ if ((!codeText || isOnlyNewLines(codeText)) && fg('platform_editor_block_menu_patch_2')) {
246
+ // Empty code block - create an empty list item
247
+ currentListItems.push(createListItemFromInline());
248
+ }
237
249
  if (codeText) {
238
250
  const lines = codeText.split('\n');
239
251
  // Remove empty lines
@@ -1,5 +1,6 @@
1
1
  import React, { Fragment } from 'react';
2
2
  import { ArrowKeyNavigationProvider, ArrowKeyNavigationType } from '@atlaskit/editor-common/ui-menu';
3
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
4
  const NoOp = props => null;
4
5
  const isNonNestedMenuSection = component => {
5
6
  return component.type === 'block-menu-section' && !('parent' in component);
@@ -29,48 +30,51 @@ export const BlockMenuRenderer = ({
29
30
  const menuSections = getSortedNonNestedSections(components);
30
31
  const menuItems = components.filter(isMenuItem);
31
32
  const nestedMenus = components.filter(isNestedMenu);
32
- return /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, {
33
- type: ArrowKeyNavigationType.MENU
34
- }, /*#__PURE__*/React.createElement(Fragment, null, menuSections.map(section => {
35
- // Get all items for the current section, including nested menus, and sort them by rank
36
- const currentSectionItemsSorted = getSortedChildren([...menuItems, ...nestedMenus], section.key);
37
- if (currentSectionItemsSorted.length === 0) {
38
- return null;
39
- }
33
+ const renderMenu = () => {
34
+ return /*#__PURE__*/React.createElement(Fragment, null, menuSections.map(section => {
35
+ // Get all items for the current section, including nested menus, and sort them by rank
36
+ const currentSectionItemsSorted = getSortedChildren([...menuItems, ...nestedMenus], section.key);
37
+ if (currentSectionItemsSorted.length === 0) {
38
+ return null;
39
+ }
40
40
 
41
- // iterate over the current section items, if it is nested menu, get their children, sort them
42
- // if they are menu items, just render as they are sorted above
43
- const getChildrenWithNestedItems = items => {
44
- return items.map(item => {
45
- if (isNestedMenu(item)) {
46
- const sortedNestedSections = getSortedNestedSections(components, item.key);
47
- return sortedNestedSections.map(section => {
48
- const sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
49
- const NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
50
- const NestedSection = section.component || fallbacks.section || NoOp;
51
- return /*#__PURE__*/React.createElement(NestedMenuComponent, {
41
+ // iterate over the current section items, if it is nested menu, get their children, sort them
42
+ // if they are menu items, just render as they are sorted above
43
+ const getChildrenWithNestedItems = items => {
44
+ return items.map(item => {
45
+ if (isNestedMenu(item)) {
46
+ const sortedNestedSections = getSortedNestedSections(components, item.key);
47
+ return sortedNestedSections.map(section => {
48
+ const sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
49
+ const NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
50
+ const NestedSection = section.component || fallbacks.section || NoOp;
51
+ return /*#__PURE__*/React.createElement(NestedMenuComponent, {
52
+ key: item.key
53
+ }, /*#__PURE__*/React.createElement(NestedSection, {
54
+ key: section.key
55
+ }, sortedNestedMenuItems.map(nestedItem => {
56
+ const NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
57
+ return /*#__PURE__*/React.createElement(NestedMenuItemComponent, {
58
+ key: nestedItem.key
59
+ });
60
+ })));
61
+ });
62
+ } else {
63
+ const ItemComponent = item.component || fallbacks.item || NoOp;
64
+ return /*#__PURE__*/React.createElement(ItemComponent, {
52
65
  key: item.key
53
- }, /*#__PURE__*/React.createElement(NestedSection, {
54
- key: section.key
55
- }, sortedNestedMenuItems.map(nestedItem => {
56
- const NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
57
- return /*#__PURE__*/React.createElement(NestedMenuItemComponent, {
58
- key: nestedItem.key
59
- });
60
- })));
61
- });
62
- } else {
63
- const ItemComponent = item.component || fallbacks.item || NoOp;
64
- return /*#__PURE__*/React.createElement(ItemComponent, {
65
- key: item.key
66
- });
67
- }
68
- });
69
- };
70
- const children = getChildrenWithNestedItems(currentSectionItemsSorted);
71
- const SectionComponent = section.component || fallbacks.section || NoOp;
72
- return /*#__PURE__*/React.createElement(SectionComponent, {
73
- key: section.key
74
- }, children);
75
- })));
66
+ });
67
+ }
68
+ });
69
+ };
70
+ const children = getChildrenWithNestedItems(currentSectionItemsSorted);
71
+ const SectionComponent = section.component || fallbacks.section || NoOp;
72
+ return /*#__PURE__*/React.createElement(SectionComponent, {
73
+ key: section.key
74
+ }, children);
75
+ }));
76
+ };
77
+ return expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, {
78
+ type: ArrowKeyNavigationType.MENU
79
+ }, renderMenu()) : renderMenu();
76
80
  };
@@ -55,14 +55,16 @@ const BlockMenu = ({
55
55
  menuTriggerBy,
56
56
  isSelectedViaDragHandle,
57
57
  isMenuOpen,
58
- currentUserIntent
58
+ currentUserIntent,
59
+ openedViaKeyboard
59
60
  } = useSharedPluginStateWithSelector(api, ['blockControls', 'userIntent'], states => {
60
- var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta;
61
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta, _states$blockControls4, _states$blockControls5;
61
62
  return {
62
63
  menuTriggerBy: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.menuTriggerBy,
63
64
  isSelectedViaDragHandle: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.isSelectedViaDragHandle,
64
65
  isMenuOpen: (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.isMenuOpen,
65
- currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
66
+ currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent,
67
+ openedViaKeyboard: (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 ? void 0 : (_states$blockControls5 = _states$blockControls4.blockMenuOptions) === null || _states$blockControls5 === void 0 ? void 0 : _states$blockControls5.openedViaKeyboard
66
68
  };
67
69
  });
68
70
  const {
@@ -76,7 +78,7 @@ const BlockMenu = ({
76
78
  // hasSelection true, always show block menu
77
79
  // hasSelection false, only show block menu when empty line experiment is enabled
78
80
  const shouldShowBlockMenuForEmptyLine = hasSelection || !hasSelection && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
79
- const selectedByShortcutORDragHandle = isSelectedViaDragHandle;
81
+ const selectedByShortcutORDragHandle = isSelectedViaDragHandle || openedViaKeyboard && expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true);
80
82
  useEffect(() => {
81
83
  var _api$userIntent;
82
84
  if (!isMenuOpen || !menuTriggerBy || !selectedByShortcutORDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
@@ -140,7 +142,9 @@ const BlockMenu = ({
140
142
  preventOverflow: true // disables forced horizontal placement when forcePlacement is on, so fitWidth controls flipping
141
143
  ,
142
144
  stick: true,
143
- focusTrap: true,
145
+ focusTrap: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? openedViaKeyboard ? {
146
+ initialFocus: undefined
147
+ } : true : undefined,
144
148
  offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, 0]
145
149
  }, /*#__PURE__*/React.createElement(BlockMenuContent, {
146
150
  api: api
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from 'react';
1
+ import React, { useCallback, useEffect } from 'react';
2
2
  import { useIntl, injectIntl } from 'react-intl-next';
3
3
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
4
4
  import { messages } from '@atlaskit/editor-common/block-menu';
@@ -74,10 +74,22 @@ const DeleteDropdownItemContent = ({
74
74
  return tr;
75
75
  });
76
76
  }, [api, nodeTypes]);
77
- const onRemoveHoverDecoration = () => {
77
+ const onRemoveHoverDecoration = useCallback(() => {
78
78
  var _api$decorations2, _api$decorations2$com, _api$decorations2$com2;
79
79
  api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : (_api$decorations2 = api.decorations) === null || _api$decorations2 === void 0 ? void 0 : (_api$decorations2$com = _api$decorations2.commands) === null || _api$decorations2$com === void 0 ? void 0 : (_api$decorations2$com2 = _api$decorations2$com.removeDecoration) === null || _api$decorations2$com2 === void 0 ? void 0 : _api$decorations2$com2.call(_api$decorations2$com));
80
- };
80
+ }, [api]);
81
+ useEffect(() => {
82
+ if (!expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
83
+ return;
84
+ }
85
+ return () => {
86
+ if (!expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
87
+ return;
88
+ }
89
+ // clean up hover decoration when unmounting
90
+ onRemoveHoverDecoration();
91
+ };
92
+ }, [onRemoveHoverDecoration]);
81
93
  const text = fg('platform_editor_block_menu_patch_1') ? formatMessage(blockMenuMessages.deleteBlock) : formatMessage(messages.deleteBlock);
82
94
  return /*#__PURE__*/React.createElement(Box, {
83
95
  onMouseEnter: onShowHoverDecoration,
@@ -1,10 +1,13 @@
1
1
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
2
+ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
2
3
  import { findParentNodeOfType, findSelectedNodeOfType, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils';
3
4
  import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
5
6
  import { setSelectionAfterTransform } from './selection';
6
7
  import { createDefaultLayoutSection } from './transforms/layout-transforms';
7
8
  import { transformNodeToTargetType } from './transforms/transformNodeToTargetType';
9
+ import { isListNodeType } from './transforms/utils';
10
+
8
11
  /**
9
12
  * Handles formatting when selection is empty by inserting a new target node
10
13
  */
@@ -44,6 +47,42 @@ var formatNodeWhenSelectionEmpty = function formatNodeWhenSelectionEmpty(tr, tar
44
47
  return tr;
45
48
  };
46
49
 
50
+ /**
51
+ * Handles formatting when an empty list is selected
52
+ * Converting an empty list to a target node, will remove the list and replace with an empty target node
53
+ */
54
+ export var formatNodeSelectEmptyList = function formatNodeSelectEmptyList(tr, targetType, listNode, schema) {
55
+ var nodes = schema.nodes;
56
+ var headingLevel = 1;
57
+ var finalTargetType = targetType;
58
+ if (targetType.startsWith('heading')) {
59
+ var levelString = targetType.slice(-1);
60
+ var level = parseInt(levelString, 10);
61
+ if (!isNaN(level) && level >= 1 && level <= 6) {
62
+ headingLevel = level;
63
+ finalTargetType = 'heading';
64
+ }
65
+ }
66
+ var replaceNode = null;
67
+ if (finalTargetType === 'layoutSection') {
68
+ var emptyPara = nodes.paragraph.createAndFill();
69
+ if (emptyPara) {
70
+ replaceNode = createDefaultLayoutSection(schema, emptyPara);
71
+ }
72
+ } else if (finalTargetType === 'heading') {
73
+ replaceNode = nodes.heading.createAndFill({
74
+ level: headingLevel
75
+ });
76
+ } else {
77
+ replaceNode = nodes[finalTargetType].createAndFill();
78
+ }
79
+ if (replaceNode) {
80
+ tr.replaceWith(listNode.pos, listNode.pos + listNode.node.nodeSize, replaceNode);
81
+ tr.setSelection(new TextSelection(tr.doc.resolve(listNode.pos)));
82
+ }
83
+ return tr;
84
+ };
85
+
47
86
  /**
48
87
  * Formats the current node or selection to the specified target type
49
88
  * @param api - The editor API injection that provides access to analytics and other plugin actions
@@ -63,7 +102,26 @@ export var formatNode = function formatNode(api) {
63
102
 
64
103
  // when selection is empty, we insert a empty target node
65
104
  if (selection.empty && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true)) {
66
- return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
105
+ var listNodes = [];
106
+ // need to find if there is any list node in the current selection
107
+ // As when select a empty list, selection is empty, but we want to convert the list instead of inserting a target node
108
+ // findSelectedNodeOfType does not work when selection is empty, so we use nodesBetween
109
+ tr.doc.nodesBetween(selection.from, selection.to, function (node, pos) {
110
+ if (isListNodeType(node.type)) {
111
+ listNodes.push({
112
+ node: node,
113
+ pos: pos
114
+ });
115
+ }
116
+ });
117
+ // get the first list node as when click on drag handle if there are list node
118
+ // can only select one list at a time, so we just need to find the first one
119
+ if (listNodes.length > 0 && fg('platform_editor_block_menu_patch_2')) {
120
+ var firstChild = listNodes[0];
121
+ return formatNodeSelectEmptyList(tr, targetType, firstChild, schema);
122
+ } else {
123
+ return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
124
+ }
67
125
  }
68
126
 
69
127
  // Try to find the current node from selection
@@ -1,6 +1,7 @@
1
1
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
2
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
3
3
  import { findChildrenByType } from '@atlaskit/editor-prosemirror/utils';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
4
5
  import { getInlineNodeTextContent } from './inline-node-transforms';
5
6
  import { isBlockNodeType, isListNodeType, isContainerNodeType, isBlockNodeForExtraction, convertNodeToInlineContent, getContentSupportChecker, convertCodeBlockContentToParagraphs, filterMarksForTargetNodeType, getMarksWithBreakout } from './utils';
6
7
  var convertInvalidNodeToValidNodeType = function convertInvalidNodeToValidNodeType(sourceContent, sourceNodeType, validNodeType, withMarks) {
@@ -196,7 +197,11 @@ export var unwrapAndConvertToList = function unwrapAndConvertToList(_ref3) {
196
197
  heading = _schema$nodes2.heading;
197
198
  var isTargetTaskList = targetNodeType === taskList;
198
199
  var createListItemFromInline = function createListItemFromInline(content) {
199
- return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
200
+ if (!content && fg('platform_editor_block_menu_patch_2')) {
201
+ return isTargetTaskList ? taskItem.create() : listItem.create(null, paragraph.create());
202
+ } else {
203
+ return isTargetTaskList ? taskItem.create(null, content) : listItem.create(null, paragraph.create(null, content));
204
+ }
200
205
  };
201
206
  var getInlineContent = function getInlineContent(textblock) {
202
207
  var inlineContent = [];
@@ -226,6 +231,15 @@ export var unwrapAndConvertToList = function unwrapAndConvertToList(_ref3) {
226
231
  };
227
232
  if (sourceNode.type.name === 'codeBlock') {
228
233
  var codeText = sourceNode.textContent;
234
+ // check if code block only contains newline characters
235
+ // eslint-disable-next-line require-unicode-regexp
236
+ var isOnlyNewLines = function isOnlyNewLines(codeText) {
237
+ return codeText.replace(/\n/g, '').trim() === '';
238
+ };
239
+ if ((!codeText || isOnlyNewLines(codeText)) && fg('platform_editor_block_menu_patch_2')) {
240
+ // Empty code block - create an empty list item
241
+ currentListItems.push(createListItemFromInline());
242
+ }
229
243
  if (codeText) {
230
244
  var lines = codeText.split('\n');
231
245
  // Remove empty lines
@@ -1,6 +1,7 @@
1
1
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
2
  import React, { Fragment } from 'react';
3
3
  import { ArrowKeyNavigationProvider, ArrowKeyNavigationType } from '@atlaskit/editor-common/ui-menu';
4
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
4
5
  var NoOp = function NoOp(props) {
5
6
  return null;
6
7
  };
@@ -41,48 +42,51 @@ export var BlockMenuRenderer = function BlockMenuRenderer(_ref) {
41
42
  var menuSections = getSortedNonNestedSections(components);
42
43
  var menuItems = components.filter(isMenuItem);
43
44
  var nestedMenus = components.filter(isNestedMenu);
44
- return /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, {
45
- type: ArrowKeyNavigationType.MENU
46
- }, /*#__PURE__*/React.createElement(Fragment, null, menuSections.map(function (section) {
47
- // Get all items for the current section, including nested menus, and sort them by rank
48
- var currentSectionItemsSorted = getSortedChildren([].concat(_toConsumableArray(menuItems), _toConsumableArray(nestedMenus)), section.key);
49
- if (currentSectionItemsSorted.length === 0) {
50
- return null;
51
- }
45
+ var renderMenu = function renderMenu() {
46
+ return /*#__PURE__*/React.createElement(Fragment, null, menuSections.map(function (section) {
47
+ // Get all items for the current section, including nested menus, and sort them by rank
48
+ var currentSectionItemsSorted = getSortedChildren([].concat(_toConsumableArray(menuItems), _toConsumableArray(nestedMenus)), section.key);
49
+ if (currentSectionItemsSorted.length === 0) {
50
+ return null;
51
+ }
52
52
 
53
- // iterate over the current section items, if it is nested menu, get their children, sort them
54
- // if they are menu items, just render as they are sorted above
55
- var getChildrenWithNestedItems = function getChildrenWithNestedItems(items) {
56
- return items.map(function (item) {
57
- if (isNestedMenu(item)) {
58
- var sortedNestedSections = getSortedNestedSections(components, item.key);
59
- return sortedNestedSections.map(function (section) {
60
- var sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
61
- var NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
62
- var NestedSection = section.component || fallbacks.section || NoOp;
63
- return /*#__PURE__*/React.createElement(NestedMenuComponent, {
53
+ // iterate over the current section items, if it is nested menu, get their children, sort them
54
+ // if they are menu items, just render as they are sorted above
55
+ var getChildrenWithNestedItems = function getChildrenWithNestedItems(items) {
56
+ return items.map(function (item) {
57
+ if (isNestedMenu(item)) {
58
+ var sortedNestedSections = getSortedNestedSections(components, item.key);
59
+ return sortedNestedSections.map(function (section) {
60
+ var sortedNestedMenuItems = getSortedChildren(menuItems, section.key);
61
+ var NestedMenuComponent = item.component || fallbacks.nestedMenu || NoOp;
62
+ var NestedSection = section.component || fallbacks.section || NoOp;
63
+ return /*#__PURE__*/React.createElement(NestedMenuComponent, {
64
+ key: item.key
65
+ }, /*#__PURE__*/React.createElement(NestedSection, {
66
+ key: section.key
67
+ }, sortedNestedMenuItems.map(function (nestedItem) {
68
+ var NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
69
+ return /*#__PURE__*/React.createElement(NestedMenuItemComponent, {
70
+ key: nestedItem.key
71
+ });
72
+ })));
73
+ });
74
+ } else {
75
+ var ItemComponent = item.component || fallbacks.item || NoOp;
76
+ return /*#__PURE__*/React.createElement(ItemComponent, {
64
77
  key: item.key
65
- }, /*#__PURE__*/React.createElement(NestedSection, {
66
- key: section.key
67
- }, sortedNestedMenuItems.map(function (nestedItem) {
68
- var NestedMenuItemComponent = nestedItem.component || fallbacks.item || NoOp;
69
- return /*#__PURE__*/React.createElement(NestedMenuItemComponent, {
70
- key: nestedItem.key
71
- });
72
- })));
73
- });
74
- } else {
75
- var ItemComponent = item.component || fallbacks.item || NoOp;
76
- return /*#__PURE__*/React.createElement(ItemComponent, {
77
- key: item.key
78
- });
79
- }
80
- });
81
- };
82
- var children = getChildrenWithNestedItems(currentSectionItemsSorted);
83
- var SectionComponent = section.component || fallbacks.section || NoOp;
84
- return /*#__PURE__*/React.createElement(SectionComponent, {
85
- key: section.key
86
- }, children);
87
- })));
78
+ });
79
+ }
80
+ });
81
+ };
82
+ var children = getChildrenWithNestedItems(currentSectionItemsSorted);
83
+ var SectionComponent = section.component || fallbacks.section || NoOp;
84
+ return /*#__PURE__*/React.createElement(SectionComponent, {
85
+ key: section.key
86
+ }, children);
87
+ }));
88
+ };
89
+ return expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, {
90
+ type: ArrowKeyNavigationType.MENU
91
+ }, renderMenu()) : renderMenu();
88
92
  };
@@ -56,18 +56,20 @@ var BlockMenu = function BlockMenu(_ref2) {
56
56
  boundariesElement = _ref2.boundariesElement,
57
57
  scrollableElement = _ref2.scrollableElement;
58
58
  var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls', 'userIntent'], function (states) {
59
- var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta;
59
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$userIntentSta, _states$blockControls4;
60
60
  return {
61
61
  menuTriggerBy: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.menuTriggerBy,
62
62
  isSelectedViaDragHandle: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.isSelectedViaDragHandle,
63
63
  isMenuOpen: (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.isMenuOpen,
64
- currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
64
+ currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent,
65
+ openedViaKeyboard: (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 || (_states$blockControls4 = _states$blockControls4.blockMenuOptions) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.openedViaKeyboard
65
66
  };
66
67
  }),
67
68
  menuTriggerBy = _useSharedPluginState.menuTriggerBy,
68
69
  isSelectedViaDragHandle = _useSharedPluginState.isSelectedViaDragHandle,
69
70
  isMenuOpen = _useSharedPluginState.isMenuOpen,
70
- currentUserIntent = _useSharedPluginState.currentUserIntent;
71
+ currentUserIntent = _useSharedPluginState.currentUserIntent,
72
+ openedViaKeyboard = _useSharedPluginState.openedViaKeyboard;
71
73
  var _useBlockMenu = useBlockMenu(),
72
74
  onDropdownOpenChanged = _useBlockMenu.onDropdownOpenChanged,
73
75
  fireAnalyticsEvent = _useBlockMenu.fireAnalyticsEvent;
@@ -78,7 +80,7 @@ var BlockMenu = function BlockMenu(_ref2) {
78
80
  // hasSelection true, always show block menu
79
81
  // hasSelection false, only show block menu when empty line experiment is enabled
80
82
  var shouldShowBlockMenuForEmptyLine = hasSelection || !hasSelection && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
81
- var selectedByShortcutORDragHandle = isSelectedViaDragHandle;
83
+ var selectedByShortcutORDragHandle = isSelectedViaDragHandle || openedViaKeyboard && expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true);
82
84
  useEffect(function () {
83
85
  var _api$userIntent;
84
86
  if (!isMenuOpen || !menuTriggerBy || !selectedByShortcutORDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
@@ -141,7 +143,9 @@ var BlockMenu = function BlockMenu(_ref2) {
141
143
  preventOverflow: true // disables forced horizontal placement when forcePlacement is on, so fitWidth controls flipping
142
144
  ,
143
145
  stick: true,
144
- focusTrap: true,
146
+ focusTrap: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? openedViaKeyboard ? {
147
+ initialFocus: undefined
148
+ } : true : undefined,
145
149
  offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, 0]
146
150
  }, /*#__PURE__*/React.createElement(BlockMenuContent, {
147
151
  api: api
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from 'react';
1
+ import React, { useCallback, useEffect } from 'react';
2
2
  import { useIntl, injectIntl } from 'react-intl-next';
3
3
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
4
4
  import { messages } from '@atlaskit/editor-common/block-menu';
@@ -69,10 +69,22 @@ var DeleteDropdownItemContent = function DeleteDropdownItemContent(_ref) {
69
69
  return tr;
70
70
  });
71
71
  }, [api, nodeTypes]);
72
- var onRemoveHoverDecoration = function onRemoveHoverDecoration() {
72
+ var onRemoveHoverDecoration = useCallback(function () {
73
73
  var _api$decorations2, _api$decorations2$rem;
74
74
  api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$decorations2 = api.decorations) === null || _api$decorations2 === void 0 || (_api$decorations2 = _api$decorations2.commands) === null || _api$decorations2 === void 0 || (_api$decorations2$rem = _api$decorations2.removeDecoration) === null || _api$decorations2$rem === void 0 ? void 0 : _api$decorations2$rem.call(_api$decorations2));
75
- };
75
+ }, [api]);
76
+ useEffect(function () {
77
+ if (!expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
78
+ return;
79
+ }
80
+ return function () {
81
+ if (!expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true)) {
82
+ return;
83
+ }
84
+ // clean up hover decoration when unmounting
85
+ onRemoveHoverDecoration();
86
+ };
87
+ }, [onRemoveHoverDecoration]);
76
88
  var text = fg('platform_editor_block_menu_patch_1') ? formatMessage(blockMenuMessages.deleteBlock) : formatMessage(messages.deleteBlock);
77
89
  return /*#__PURE__*/React.createElement(Box, {
78
90
  onMouseEnter: onShowHoverDecoration,
@@ -1,6 +1,16 @@
1
1
  import type { EditorCommand, ExtractInjectionAPI } from '@atlaskit/editor-common/types';
2
+ import { type Schema, type Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
+ import { type Transaction } from '@atlaskit/editor-prosemirror/state';
2
4
  import type { BlockMenuPlugin } from '../blockMenuPluginType';
3
5
  import type { FormatNodeTargetType } from './transforms/types';
6
+ /**
7
+ * Handles formatting when an empty list is selected
8
+ * Converting an empty list to a target node, will remove the list and replace with an empty target node
9
+ */
10
+ export declare const formatNodeSelectEmptyList: (tr: Transaction, targetType: FormatNodeTargetType, listNode: {
11
+ node: PMNode;
12
+ pos: number;
13
+ }, schema: Schema) => Transaction;
4
14
  /**
5
15
  * Formats the current node or selection to the specified target type
6
16
  * @param api - The editor API injection that provides access to analytics and other plugin actions
@@ -1,6 +1,16 @@
1
1
  import type { EditorCommand, ExtractInjectionAPI } from '@atlaskit/editor-common/types';
2
+ import { type Schema, type Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
+ import { type Transaction } from '@atlaskit/editor-prosemirror/state';
2
4
  import type { BlockMenuPlugin } from '../blockMenuPluginType';
3
5
  import type { FormatNodeTargetType } from './transforms/types';
6
+ /**
7
+ * Handles formatting when an empty list is selected
8
+ * Converting an empty list to a target node, will remove the list and replace with an empty target node
9
+ */
10
+ export declare const formatNodeSelectEmptyList: (tr: Transaction, targetType: FormatNodeTargetType, listNode: {
11
+ node: PMNode;
12
+ pos: number;
13
+ }, schema: Schema) => Transaction;
4
14
  /**
5
15
  * Formats the current node or selection to the specified target type
6
16
  * @param api - The editor API injection that provides access to analytics and other plugin actions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-menu",
3
- "version": "4.0.2",
3
+ "version": "4.0.4",
4
4
  "description": "BlockMenu plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/css": "^0.14.0",
32
32
  "@atlaskit/dropdown-menu": "^16.3.0",
33
- "@atlaskit/editor-plugin-analytics": "^6.0.0",
34
- "@atlaskit/editor-plugin-block-controls": "^7.1.0",
33
+ "@atlaskit/editor-plugin-analytics": "^6.1.0",
34
+ "@atlaskit/editor-plugin-block-controls": "^7.2.0",
35
35
  "@atlaskit/editor-plugin-decorations": "^6.1.0",
36
36
  "@atlaskit/editor-plugin-selection": "^6.0.0",
37
37
  "@atlaskit/editor-plugin-user-intent": "^4.0.0",
@@ -40,7 +40,7 @@
40
40
  "@atlaskit/editor-tables": "^2.9.0",
41
41
  "@atlaskit/editor-toolbar": "^0.10.0",
42
42
  "@atlaskit/icon": "^28.3.0",
43
- "@atlaskit/icon-lab": "^5.7.0",
43
+ "@atlaskit/icon-lab": "^5.8.0",
44
44
  "@atlaskit/platform-feature-flags": "^1.1.0",
45
45
  "@atlaskit/primitives": "^14.15.0",
46
46
  "@atlaskit/tmp-editor-statsig": "^12.32.0",
@@ -48,7 +48,7 @@
48
48
  "@babel/runtime": "^7.0.0"
49
49
  },
50
50
  "peerDependencies": {
51
- "@atlaskit/editor-common": "^110.2.0",
51
+ "@atlaskit/editor-common": "^110.4.0",
52
52
  "react": "^18.2.0",
53
53
  "react-intl-next": "npm:react-intl@^5.18.1"
54
54
  },