@atlaskit/editor-common 85.2.0 → 86.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/cjs/lazy-node-view/index.js +217 -0
  3. package/dist/cjs/link/ConfigureLinkOverlay/Dropdown.js +19 -4
  4. package/dist/cjs/link/ConfigureLinkOverlay/index.js +2 -1
  5. package/dist/cjs/link/constants.js +2 -4
  6. package/dist/cjs/link/index.js +0 -13
  7. package/dist/cjs/link/utils.js +1 -12
  8. package/dist/cjs/messages/help-dialog.js +5 -0
  9. package/dist/cjs/monitoring/error.js +1 -1
  10. package/dist/cjs/ui/DropList/index.js +1 -1
  11. package/dist/cjs/ui-menu/DropdownMenu/index.js +4 -4
  12. package/dist/cjs/ui-menu/ToolbarButton/index.js +7 -1
  13. package/dist/es2019/lazy-node-view/index.js +212 -0
  14. package/dist/es2019/link/ConfigureLinkOverlay/Dropdown.js +17 -4
  15. package/dist/es2019/link/ConfigureLinkOverlay/index.js +2 -1
  16. package/dist/es2019/link/constants.js +1 -3
  17. package/dist/es2019/link/index.js +0 -1
  18. package/dist/es2019/link/utils.js +2 -13
  19. package/dist/es2019/messages/help-dialog.js +5 -0
  20. package/dist/es2019/monitoring/error.js +1 -1
  21. package/dist/es2019/ui/DropList/index.js +1 -1
  22. package/dist/es2019/ui-menu/DropdownMenu/index.js +4 -4
  23. package/dist/es2019/ui-menu/ToolbarButton/index.js +7 -1
  24. package/dist/esm/lazy-node-view/index.js +211 -0
  25. package/dist/esm/link/ConfigureLinkOverlay/Dropdown.js +19 -4
  26. package/dist/esm/link/ConfigureLinkOverlay/index.js +2 -1
  27. package/dist/esm/link/constants.js +1 -3
  28. package/dist/esm/link/index.js +0 -1
  29. package/dist/esm/link/utils.js +2 -13
  30. package/dist/esm/messages/help-dialog.js +5 -0
  31. package/dist/esm/monitoring/error.js +1 -1
  32. package/dist/esm/ui/DropList/index.js +1 -1
  33. package/dist/esm/ui-menu/DropdownMenu/index.js +4 -4
  34. package/dist/esm/ui-menu/ToolbarButton/index.js +7 -1
  35. package/dist/types/lazy-node-view/index.d.ts +109 -0
  36. package/dist/types/link/ConfigureLinkOverlay/Dropdown.d.ts +2 -0
  37. package/dist/types/link/constants.d.ts +0 -2
  38. package/dist/types/link/index.d.ts +0 -1
  39. package/dist/types/messages/help-dialog.d.ts +5 -0
  40. package/dist/types/ui-menu/ToolbarButton/index.d.ts +3 -0
  41. package/dist/types-ts4.5/lazy-node-view/index.d.ts +109 -0
  42. package/dist/types-ts4.5/link/ConfigureLinkOverlay/Dropdown.d.ts +2 -0
  43. package/dist/types-ts4.5/link/constants.d.ts +0 -2
  44. package/dist/types-ts4.5/link/index.d.ts +0 -1
  45. package/dist/types-ts4.5/messages/help-dialog.d.ts +5 -0
  46. package/dist/types-ts4.5/ui-menu/ToolbarButton/index.d.ts +3 -0
  47. package/lazy-node-view/package.json +15 -0
  48. package/package.json +5 -7
@@ -0,0 +1,212 @@
1
+ import debounce from 'lodash/debounce';
2
+ import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
3
+
4
+ /**
5
+ * 📢 Public Type
6
+ *
7
+ * @see {withLazyLoading}
8
+ */
9
+
10
+ /**
11
+ * 📢 Public Type
12
+ *
13
+ * @see {withLazyLoading}
14
+ */
15
+
16
+ /**
17
+ * 🧱 Internal: Editor FE Platform
18
+ */
19
+
20
+ /**
21
+ * 🧱 Internal: Editor FE Platform
22
+ */
23
+
24
+ /**
25
+ * 🧱 Internal: Editor FE Platform
26
+ */
27
+
28
+ /**
29
+ * 🧱 Internal: Editor FE Platform
30
+ *
31
+ * A cache to store loaded React NodeViews for each EditorView.
32
+ *
33
+ * This cache will help us to avoid any race condition
34
+ * when multiple Editors exist in the same page.
35
+ *
36
+ * @type {CacheType}
37
+ */
38
+ const cachePerEditorView = new WeakMap();
39
+
40
+ /**
41
+ * 🧱 Internal: Editor FE Platform
42
+ */
43
+ const isFirefox = /gecko\/\d/i.test(navigator.userAgent);
44
+
45
+ /**
46
+ * 🧱 Internal: Editor FE Platform
47
+ *
48
+ * A NodeView that serves as a placeholder until the actual NodeView is loaded.
49
+ */
50
+ class LazyNodeView {
51
+ constructor(node, view, getPos) {
52
+ var _node$type, _node$type$spec;
53
+ if (typeof ((_node$type = node.type) === null || _node$type === void 0 ? void 0 : (_node$type$spec = _node$type.spec) === null || _node$type$spec === void 0 ? void 0 : _node$type$spec.toDOM) !== 'function') {
54
+ this.dom = document.createElement('div');
55
+ return;
56
+ }
57
+ const fallback = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node));
58
+ this.dom = fallback.dom;
59
+ this.contentDOM = fallback.contentDOM;
60
+ if (this.dom instanceof HTMLElement) {
61
+ // This attribute is mostly used for debugging purposed
62
+ // It will help us to found out when the node was replaced
63
+ this.dom.setAttribute('data-lazy-node-view', node.type.name);
64
+ // This is used on Libra tests
65
+ // We are using this to make sure all lazy noded were replaced
66
+ // before the test started
67
+ this.dom.setAttribute('data-lazy-node-view-fallback', 'true');
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 🧱 Internal: Editor FE Platform
74
+ *
75
+ * Debounces and replaces the node views in a ProseMirror editor with lazy-loaded node views.
76
+ *
77
+ * This function is used to update the `nodeViews` property of the `EditorView` after lazy-loaded
78
+ * node views have been loaded. It uses a debounced approach to ensure that the replacement does
79
+ * not happen too frequently, which can be performance-intensive.
80
+ *
81
+ * The function checks if there are any loaded node views in the cache associated with the given
82
+ * `EditorView`. If there are, it replaces the current `nodeViews` in the `EditorView` with the
83
+ * loaded node views. The replacement is scheduled using `requestIdleCallback` or
84
+ * `requestAnimationFrame` to avoid blocking the main thread, especially in Firefox where
85
+ * `requestIdleCallback` may not be supported.
86
+ *
87
+ * @param {WeakMap<EditorView, Record<string, NodeViewConstructor>>} cache - A WeakMap that stores
88
+ * the loaded node views for each `EditorView`. The key is the `EditorView`, and the value
89
+ * is a record of node type names to their corresponding `NodeViewConstructor`.
90
+ * @param {EditorView} view - The ProseMirror `EditorView` instance whose `nodeViews` property
91
+ * needs to be updated.
92
+ *
93
+ * @example
94
+ * const cache = new WeakMap();
95
+ * const view = new EditorView(...);
96
+ *
97
+ * // Assume some node views have been loaded and stored in the cache
98
+ * cache.set(view, {
99
+ * 'table': TableViewConstructor,
100
+ * 'tableCell': TableCellViewConstructor,
101
+ * });
102
+ *
103
+ * debouncedReplaceNodeviews(cache, view);
104
+ */
105
+ export const debouncedReplaceNodeviews = debounce((cache, view) => {
106
+ const loadedNodeviews = cache.get(view);
107
+ if (!loadedNodeviews) {
108
+ return;
109
+ }
110
+ cache.delete(view);
111
+
112
+ // eslint-disable-next-line compat/compat
113
+ const idle = window.requestIdleCallback;
114
+
115
+ /*
116
+ * For reasons that goes beyond my knowledge
117
+ * some Firefox versions aren't calling the requestIdleCallback.
118
+ *
119
+ * So, we need this check to make sure we use the requestAnimationFrame instead
120
+ */
121
+ const later = isFirefox || typeof idle !== 'function' ? window.requestAnimationFrame : idle;
122
+ later(() => {
123
+ const currentNodeViews = view.props.nodeViews;
124
+ const nextNodeViews = {
125
+ ...currentNodeViews,
126
+ ...loadedNodeviews
127
+ };
128
+ view.setProps({
129
+ nodeViews: nextNodeViews
130
+ });
131
+ });
132
+ });
133
+
134
+ /**
135
+ * 📢 Public: Any EditorPlugin can use this function
136
+ *
137
+ * Wraps a NodeView constructor with laziness, allowing the NodeView to be loaded only when required.
138
+ *
139
+ * This higher-order function is designed to optimize the loading and rendering performance
140
+ * of ProseMirror editor nodes by deferring the loading of their associated NodeViews until they are actually needed.
141
+ * This is particularly useful for complex or heavy NodeViews, such as tables, table cells, rows, and headers within
142
+ * the ProseMirror editor. By using dynamic imports (with promises), the initial load time of the editor can be significantly
143
+ * reduced, leading to a smoother and faster user experience.
144
+ *
145
+ * The function accepts configuration parameters including the node name, a loader function that dynamically imports
146
+ * the NodeView, and a function to retrieve NodeView options. It returns a NodeViewConstructor that ProseMirror
147
+ * can use when rendering nodes of the specified type.
148
+ *
149
+ * @template NodeViewOptions - The type parameter that describes the shape of the options object for the NodeView.
150
+ * @param {LazyLoadingProps<NodeViewOptions>} params - Configuration parameters for lazy loading.
151
+ * @param {string} params.nodeName - The name of the node (e.g., 'table', 'tableCell', 'tableHeader', 'tableRow') for which the lazy-loaded NodeView is intended.
152
+ * @param {() => Promise<CreateReactNodeViewProps<NodeViewOptions>>} params.loader - A function that, when called, returns a promise that resolves to the actual NodeView constructor. This function typically uses dynamic `import()` to load the NodeView code.
153
+ * @param {() => NodeViewOptions} params.getNodeViewOptions - A function that returns the options to be passed to the NodeView constructor. These options can include dependencies like `portalProviderAPI`, `eventDispatcher`, and others, which are necessary for the NodeView's operation.
154
+ * @param {DispatchAnalyticsEvent} [params.dispatchAnalyticsEvent] - An optional function for dispatching analytics events, which can be used to monitor the performance and usage of the lazy-loaded NodeViews.
155
+ * @returns {NodeViewConstructor} A constructor function for creating a NodeView that ProseMirror can instantiate when it encounters a node of the specified type. This constructor is a lightweight placeholder until the actual NodeView is loaded.
156
+ *
157
+ * @example
158
+ * // Lazy load a table NodeView with specific options
159
+ * const lazyTableView = withLazyLoading({
160
+ * nodeName: 'table',
161
+ * loader: () => import('./table').then(module => module.createTableView),
162
+ * getNodeViewOptions: () => ({
163
+ * portalProviderAPI,
164
+ * eventDispatcher,
165
+ * getEditorContainerWidth,
166
+ * getEditorFeatureFlags,
167
+ * dispatchAnalyticsEvent,
168
+ * pluginInjectionApi,
169
+ * }),
170
+ * });
171
+ *
172
+ * // Then, use `lazyTableView` in ProseMirror editor setup to enhance 'table' nodes with lazy loading
173
+ */
174
+ export const withLazyLoading = ({
175
+ nodeName,
176
+ loader,
177
+ getNodeViewOptions,
178
+ dispatchAnalyticsEvent
179
+ }) => {
180
+ const createLazyNodeView = (node, view, getPos, decorations) => {
181
+ var _node$type2, _node$type2$spec;
182
+ let cachedMap = cachePerEditorView.get(view);
183
+ if (!cachedMap) {
184
+ cachedMap = {};
185
+ cachePerEditorView.set(view, cachedMap);
186
+ }
187
+ const wasAlreadyRequested = cachedMap.hasOwnProperty(nodeName);
188
+ if (wasAlreadyRequested) {
189
+ return new LazyNodeView(node, view, getPos);
190
+ }
191
+ loader().then(nodeViewFuncModule => {
192
+ cachedMap[nodeName] = (node, view, getPos, decorations) => {
193
+ return nodeViewFuncModule(node, view, getPos, decorations, getNodeViewOptions);
194
+ };
195
+ debouncedReplaceNodeviews(cachePerEditorView, view);
196
+ });
197
+ if (typeof ((_node$type2 = node.type) === null || _node$type2 === void 0 ? void 0 : (_node$type2$spec = _node$type2.spec) === null || _node$type2$spec === void 0 ? void 0 : _node$type2$spec.toDOM) !== 'function') {
198
+ // TODO: Analytics ED-23982
199
+ // dispatchAnalyticsEvent({
200
+ // action: ACTION.LAZY_NODE_VIEW_ERROR,
201
+ // actionSubject: ACTION_SUBJECT.LAZY_NODE_VIEW,
202
+ // eventType: EVENT_TYPE.OPERATIONAL,
203
+ // attributes: {
204
+ // nodeName,
205
+ // error: 'No spec found',
206
+ // },
207
+ // });
208
+ }
209
+ return new LazyNodeView(node, view, getPos);
210
+ };
211
+ return createLazyNodeView;
212
+ };
@@ -17,6 +17,7 @@ const SMALL_LINK_TOOLBAR_ANALYTICS_SOURCE = 'smallLinkToolbar';
17
17
  const Dropdown = ({
18
18
  onConfigureClick: onConfigureClickCallback,
19
19
  onDropdownChange,
20
+ editorView,
20
21
  testId
21
22
  }) => {
22
23
  const {
@@ -29,21 +30,33 @@ const Dropdown = ({
29
30
  fireLinkClickEvent,
30
31
  fireToolbarViewEvent
31
32
  } = useLinkOverlayAnalyticsEvents();
33
+ const focusEditor = useCallback(() => {
34
+ // Fix dropdown giving focus back to the trigger async which is then unmounted and losing focus
35
+ // this is happening deep within atlaskit dropdown as a result of this code: https://github.com/focus-trap/focus-trap/blob/master/index.js#L987
36
+ // use setTimeout to run this async after that call
37
+ setTimeout(() => editorView.focus(), 0);
38
+ }, [editorView]);
32
39
  const onOpenChange = useCallback(({
33
- isOpen
40
+ isOpen,
41
+ event
34
42
  }) => {
35
43
  onDropdownChange === null || onDropdownChange === void 0 ? void 0 : onDropdownChange(isOpen);
36
44
  if (isOpen) {
37
45
  fireToolbarViewEvent();
38
46
  }
39
- }, [fireToolbarViewEvent, onDropdownChange]);
47
+ if (!isOpen && event instanceof KeyboardEvent) {
48
+ focusEditor();
49
+ }
50
+ }, [fireToolbarViewEvent, focusEditor, onDropdownChange]);
40
51
  const onGoToLinkClick = useCallback(() => {
41
52
  fireActionClickEvent('goToLink');
42
- }, [fireActionClickEvent]);
53
+ focusEditor();
54
+ }, [fireActionClickEvent, focusEditor]);
43
55
  const onConfigureClick = useCallback(() => {
44
56
  fireActionClickEvent('configureLink');
45
57
  onConfigureClickCallback === null || onConfigureClickCallback === void 0 ? void 0 : onConfigureClickCallback();
46
- }, [fireActionClickEvent, onConfigureClickCallback]);
58
+ focusEditor();
59
+ }, [fireActionClickEvent, focusEditor, onConfigureClickCallback]);
47
60
  return jsx(DropdownMenu, {
48
61
  trigger: ({
49
62
  onClick,
@@ -86,7 +86,8 @@ export const OverlayButton = withAnalyticsContext()(({
86
86
  }, showDropdown ? jsx(Dropdown, {
87
87
  testId: testId,
88
88
  onConfigureClick: handleConfigureClick,
89
- onDropdownChange: onDropdownChange
89
+ onDropdownChange: onDropdownChange,
90
+ editorView: editorView
90
91
  }) : jsx(Tooltip, {
91
92
  content: configureLinkLabel,
92
93
  hideTooltipOnClick: true,
@@ -1,3 +1 @@
1
- export const linkPreferencesPath = '/manage-profile/link-preferences';
2
- export const stagingLinkPreferencesUrl = 'https://id.stg.internal.atlassian.com/manage-profile/link-preferences';
3
- export const productionLinkPreferencesUrl = 'https://id.atlassian.com/manage-profile/link-preferences';
1
+ export const linkPreferencesPath = '/manage-profile/link-preferences';
@@ -9,5 +9,4 @@ export { HyperlinkAddToolbar } from './LinkPicker/HyperlinkAddToolbar';
9
9
  export { default as HyperlinkLinkAddToolbar, HyperlinkLinkAddToolbarWithIntl, RECENT_SEARCH_LIST_SIZE } from './LinkPicker/HyperlinkAddToolbar/HyperlinkAddToolbar';
10
10
  export { sha1 } from './LinkPicker/HyperlinkAddToolbar/utils';
11
11
  export { isLinkAtPos, isTextAtPos, getLinkPreferencesURLFromENV } from './utils';
12
- export { stagingLinkPreferencesUrl, productionLinkPreferencesUrl } from './constants';
13
12
  export { OverlayButton } from './ConfigureLinkOverlay';
@@ -1,6 +1,5 @@
1
1
  import { getATLContextUrl } from '@atlaskit/atlassian-context';
2
- import { getBooleanFF } from '@atlaskit/platform-feature-flags';
3
- import { linkPreferencesPath, productionLinkPreferencesUrl, stagingLinkPreferencesUrl } from './constants';
2
+ import { linkPreferencesPath } from './constants';
4
3
  export function isTextAtPos(pos) {
5
4
  return state => {
6
5
  const node = state.doc.nodeAt(pos);
@@ -14,15 +13,5 @@ export function isLinkAtPos(pos) {
14
13
  };
15
14
  }
16
15
  export const getLinkPreferencesURLFromENV = () => {
17
- if (getBooleanFF('platform.editor.linking-preferences-url-atlassian-context')) {
18
- return getATLContextUrl('id') + linkPreferencesPath;
19
- }
20
- if (process.env.NODE_ENV === 'production' && process.env.CLOUD_ENV === 'staging') {
21
- // only a production CLOUD_ENV staging environment has a different link preferences URL
22
- return stagingLinkPreferencesUrl;
23
- } else if (process.env.NODE_ENV === 'production') {
24
- return productionLinkPreferencesUrl;
25
- } else {
26
- return stagingLinkPreferencesUrl;
27
- }
16
+ return getATLContextUrl('id') + linkPreferencesPath;
28
17
  };
@@ -70,6 +70,11 @@ export const helpDialogMessages = defineMessages({
70
70
  defaultMessage: 'Decrease table or media size',
71
71
  description: 'The text is shown as an shortcut description in help dialog modal, when the user uses the described shortcut, he is able to decrease the width of the selected element. Optimal characters less than 21.'
72
72
  },
73
+ openCellOptions: {
74
+ id: 'fabric.editor.openCellOptions',
75
+ defaultMessage: 'Open cell options',
76
+ description: 'Keyboard shortcut to open cell options.'
77
+ },
73
78
  focusTableResizeHandle: {
74
79
  id: 'fabric.editor.focusTableResizeHandle',
75
80
  defaultMessage: 'Focus table resize handle',
@@ -1,7 +1,7 @@
1
1
  import { isFedRamp } from './environment';
2
2
  const SENTRY_DSN = 'https://0b10c8e02fb44d8796c047b102c9bee8@o55978.ingest.sentry.io/4505129224110080';
3
3
  const packageName = 'editor-common'; // Sentry doesn't accept '/' in its releases https://docs.sentry.io/platforms/javascript/configuration/releases/
4
- const packageVersion = "85.2.0";
4
+ const packageVersion = "86.1.0";
5
5
  const sanitiseSentryEvents = (data, _hint) => {
6
6
  // Remove URL as it has UGC
7
7
  // TODO: Sanitise the URL instead of just removing it
@@ -9,7 +9,7 @@ import { createAndFireEvent, withAnalyticsContext, withAnalyticsEvents } from '@
9
9
  import { N0, N50A, N60A, N900 } from '@atlaskit/theme/colors';
10
10
  import Layer from '../Layer';
11
11
  const packageName = "@atlaskit/editor-common";
12
- const packageVersion = "85.2.0";
12
+ const packageVersion = "86.1.0";
13
13
  const halfFocusRing = 1;
14
14
  const dropOffset = '0, 8';
15
15
  class DropList extends Component {
@@ -106,7 +106,7 @@ export default class DropdownMenuWrapper extends PureComponent {
106
106
  _defineProperty(this, "handleCloseAndFocus", event => {
107
107
  var _this$state$target, _this$state$target$qu;
108
108
  (_this$state$target = this.state.target) === null || _this$state$target === void 0 ? void 0 : (_this$state$target$qu = _this$state$target.querySelector('button')) === null || _this$state$target$qu === void 0 ? void 0 : _this$state$target$qu.focus();
109
- if (fg('platform.editor.a11y-table-context-menu_y4c9c')) {
109
+ if (fg('platform_editor_a11y_table_context_menu')) {
110
110
  this.handleClose(event);
111
111
  } else {
112
112
  this.handleClose();
@@ -117,7 +117,7 @@ export default class DropdownMenuWrapper extends PureComponent {
117
117
  onOpenChange
118
118
  } = this.props;
119
119
  if (onOpenChange) {
120
- if (fg('platform.editor.a11y-table-context-menu_y4c9c')) {
120
+ if (fg('platform_editor_a11y_table_context_menu')) {
121
121
  onOpenChange({
122
122
  isOpen: false,
123
123
  event: event
@@ -195,7 +195,7 @@ export default class DropdownMenuWrapper extends PureComponent {
195
195
  handleClickOutside: this.handleClose,
196
196
  handleEscapeKeydown: fg('platform-editor-a11y-image-border-options-dropdown') ? handleEscapeKeydown || this.handleCloseAndFocus : this.handleCloseAndFocus,
197
197
  handleEnterKeydown: e => {
198
- if (fg('platform.editor.a11y-table-context-menu_y4c9c') || fg('platform-editor-a11y-image-border-options-dropdown')) {
198
+ if (fg('platform_editor_a11y_table_context_menu') || fg('platform-editor-a11y-image-border-options-dropdown')) {
199
199
  if (!allowEnterDefaultBehavior) {
200
200
  e.preventDefault();
201
201
  }
@@ -353,7 +353,7 @@ export function DropdownMenuItem({
353
353
  onMouseLeave: () => onMouseLeave && onMouseLeave({
354
354
  item
355
355
  }),
356
- "aria-expanded": fg('platform.editor.a11y-table-context-menu_y4c9c') ? item['aria-expanded'] : undefined
356
+ "aria-expanded": fg('platform_editor_a11y_table_context_menu') ? item['aria-expanded'] : undefined
357
357
  }, item.content));
358
358
  if (item.tooltipDescription) {
359
359
  var _item$key3;
@@ -6,8 +6,10 @@ import React, { useCallback } from 'react';
6
6
  // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
7
7
  import { css, jsx } from '@emotion/react';
8
8
  import { FabricChannel } from '@atlaskit/analytics-listeners';
9
+ import { fg } from '@atlaskit/platform-feature-flags';
9
10
  import Tooltip from '@atlaskit/tooltip';
10
11
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE, TOOLBAR_ACTION_SUBJECT_ID } from '../../analytics';
12
+ import { ToolTipContent } from '../../keymaps';
11
13
  import Button from './styles';
12
14
  export const TOOLBAR_BUTTON = TOOLBAR_ACTION_SUBJECT_ID;
13
15
  const buttonWrapper = css({
@@ -29,6 +31,7 @@ const ToolbarButton = /*#__PURE__*/React.forwardRef((props, ref) => {
29
31
  children,
30
32
  hideTooltip,
31
33
  title,
34
+ keymap,
32
35
  titlePosition = 'top',
33
36
  item,
34
37
  'aria-label': ariaLabel,
@@ -88,7 +91,10 @@ const ToolbarButton = /*#__PURE__*/React.forwardRef((props, ref) => {
88
91
  if (!title) {
89
92
  return button;
90
93
  }
91
- const tooltipContent = !hideTooltip ? title : null;
94
+ const tooltipContent = hideTooltip ? null : fg('platform_editor_a11y_table_context_menu') ? jsx(ToolTipContent, {
95
+ description: title,
96
+ keymap: keymap
97
+ }) : title;
92
98
  return jsx(Tooltip, {
93
99
  content: tooltipContent,
94
100
  hideTooltipOnClick: true,
@@ -0,0 +1,211 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
4
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
+ import debounce from 'lodash/debounce';
7
+ import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
8
+
9
+ /**
10
+ * 📢 Public Type
11
+ *
12
+ * @see {withLazyLoading}
13
+ */
14
+
15
+ /**
16
+ * 📢 Public Type
17
+ *
18
+ * @see {withLazyLoading}
19
+ */
20
+
21
+ /**
22
+ * 🧱 Internal: Editor FE Platform
23
+ */
24
+
25
+ /**
26
+ * 🧱 Internal: Editor FE Platform
27
+ */
28
+
29
+ /**
30
+ * 🧱 Internal: Editor FE Platform
31
+ */
32
+
33
+ /**
34
+ * 🧱 Internal: Editor FE Platform
35
+ *
36
+ * A cache to store loaded React NodeViews for each EditorView.
37
+ *
38
+ * This cache will help us to avoid any race condition
39
+ * when multiple Editors exist in the same page.
40
+ *
41
+ * @type {CacheType}
42
+ */
43
+ var cachePerEditorView = new WeakMap();
44
+
45
+ /**
46
+ * 🧱 Internal: Editor FE Platform
47
+ */
48
+ var isFirefox = /gecko\/\d/i.test(navigator.userAgent);
49
+
50
+ /**
51
+ * 🧱 Internal: Editor FE Platform
52
+ *
53
+ * A NodeView that serves as a placeholder until the actual NodeView is loaded.
54
+ */
55
+ var LazyNodeView = /*#__PURE__*/_createClass(function LazyNodeView(node, view, getPos) {
56
+ var _node$type;
57
+ _classCallCheck(this, LazyNodeView);
58
+ if (typeof ((_node$type = node.type) === null || _node$type === void 0 || (_node$type = _node$type.spec) === null || _node$type === void 0 ? void 0 : _node$type.toDOM) !== 'function') {
59
+ this.dom = document.createElement('div');
60
+ return;
61
+ }
62
+ var fallback = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node));
63
+ this.dom = fallback.dom;
64
+ this.contentDOM = fallback.contentDOM;
65
+ if (this.dom instanceof HTMLElement) {
66
+ // This attribute is mostly used for debugging purposed
67
+ // It will help us to found out when the node was replaced
68
+ this.dom.setAttribute('data-lazy-node-view', node.type.name);
69
+ // This is used on Libra tests
70
+ // We are using this to make sure all lazy noded were replaced
71
+ // before the test started
72
+ this.dom.setAttribute('data-lazy-node-view-fallback', 'true');
73
+ }
74
+ });
75
+ /**
76
+ * 🧱 Internal: Editor FE Platform
77
+ *
78
+ * Debounces and replaces the node views in a ProseMirror editor with lazy-loaded node views.
79
+ *
80
+ * This function is used to update the `nodeViews` property of the `EditorView` after lazy-loaded
81
+ * node views have been loaded. It uses a debounced approach to ensure that the replacement does
82
+ * not happen too frequently, which can be performance-intensive.
83
+ *
84
+ * The function checks if there are any loaded node views in the cache associated with the given
85
+ * `EditorView`. If there are, it replaces the current `nodeViews` in the `EditorView` with the
86
+ * loaded node views. The replacement is scheduled using `requestIdleCallback` or
87
+ * `requestAnimationFrame` to avoid blocking the main thread, especially in Firefox where
88
+ * `requestIdleCallback` may not be supported.
89
+ *
90
+ * @param {WeakMap<EditorView, Record<string, NodeViewConstructor>>} cache - A WeakMap that stores
91
+ * the loaded node views for each `EditorView`. The key is the `EditorView`, and the value
92
+ * is a record of node type names to their corresponding `NodeViewConstructor`.
93
+ * @param {EditorView} view - The ProseMirror `EditorView` instance whose `nodeViews` property
94
+ * needs to be updated.
95
+ *
96
+ * @example
97
+ * const cache = new WeakMap();
98
+ * const view = new EditorView(...);
99
+ *
100
+ * // Assume some node views have been loaded and stored in the cache
101
+ * cache.set(view, {
102
+ * 'table': TableViewConstructor,
103
+ * 'tableCell': TableCellViewConstructor,
104
+ * });
105
+ *
106
+ * debouncedReplaceNodeviews(cache, view);
107
+ */
108
+ export var debouncedReplaceNodeviews = debounce(function (cache, view) {
109
+ var loadedNodeviews = cache.get(view);
110
+ if (!loadedNodeviews) {
111
+ return;
112
+ }
113
+ cache.delete(view);
114
+
115
+ // eslint-disable-next-line compat/compat
116
+ var idle = window.requestIdleCallback;
117
+
118
+ /*
119
+ * For reasons that goes beyond my knowledge
120
+ * some Firefox versions aren't calling the requestIdleCallback.
121
+ *
122
+ * So, we need this check to make sure we use the requestAnimationFrame instead
123
+ */
124
+ var later = isFirefox || typeof idle !== 'function' ? window.requestAnimationFrame : idle;
125
+ later(function () {
126
+ var currentNodeViews = view.props.nodeViews;
127
+ var nextNodeViews = _objectSpread(_objectSpread({}, currentNodeViews), loadedNodeviews);
128
+ view.setProps({
129
+ nodeViews: nextNodeViews
130
+ });
131
+ });
132
+ });
133
+
134
+ /**
135
+ * 📢 Public: Any EditorPlugin can use this function
136
+ *
137
+ * Wraps a NodeView constructor with laziness, allowing the NodeView to be loaded only when required.
138
+ *
139
+ * This higher-order function is designed to optimize the loading and rendering performance
140
+ * of ProseMirror editor nodes by deferring the loading of their associated NodeViews until they are actually needed.
141
+ * This is particularly useful for complex or heavy NodeViews, such as tables, table cells, rows, and headers within
142
+ * the ProseMirror editor. By using dynamic imports (with promises), the initial load time of the editor can be significantly
143
+ * reduced, leading to a smoother and faster user experience.
144
+ *
145
+ * The function accepts configuration parameters including the node name, a loader function that dynamically imports
146
+ * the NodeView, and a function to retrieve NodeView options. It returns a NodeViewConstructor that ProseMirror
147
+ * can use when rendering nodes of the specified type.
148
+ *
149
+ * @template NodeViewOptions - The type parameter that describes the shape of the options object for the NodeView.
150
+ * @param {LazyLoadingProps<NodeViewOptions>} params - Configuration parameters for lazy loading.
151
+ * @param {string} params.nodeName - The name of the node (e.g., 'table', 'tableCell', 'tableHeader', 'tableRow') for which the lazy-loaded NodeView is intended.
152
+ * @param {() => Promise<CreateReactNodeViewProps<NodeViewOptions>>} params.loader - A function that, when called, returns a promise that resolves to the actual NodeView constructor. This function typically uses dynamic `import()` to load the NodeView code.
153
+ * @param {() => NodeViewOptions} params.getNodeViewOptions - A function that returns the options to be passed to the NodeView constructor. These options can include dependencies like `portalProviderAPI`, `eventDispatcher`, and others, which are necessary for the NodeView's operation.
154
+ * @param {DispatchAnalyticsEvent} [params.dispatchAnalyticsEvent] - An optional function for dispatching analytics events, which can be used to monitor the performance and usage of the lazy-loaded NodeViews.
155
+ * @returns {NodeViewConstructor} A constructor function for creating a NodeView that ProseMirror can instantiate when it encounters a node of the specified type. This constructor is a lightweight placeholder until the actual NodeView is loaded.
156
+ *
157
+ * @example
158
+ * // Lazy load a table NodeView with specific options
159
+ * const lazyTableView = withLazyLoading({
160
+ * nodeName: 'table',
161
+ * loader: () => import('./table').then(module => module.createTableView),
162
+ * getNodeViewOptions: () => ({
163
+ * portalProviderAPI,
164
+ * eventDispatcher,
165
+ * getEditorContainerWidth,
166
+ * getEditorFeatureFlags,
167
+ * dispatchAnalyticsEvent,
168
+ * pluginInjectionApi,
169
+ * }),
170
+ * });
171
+ *
172
+ * // Then, use `lazyTableView` in ProseMirror editor setup to enhance 'table' nodes with lazy loading
173
+ */
174
+ export var withLazyLoading = function withLazyLoading(_ref) {
175
+ var nodeName = _ref.nodeName,
176
+ loader = _ref.loader,
177
+ getNodeViewOptions = _ref.getNodeViewOptions,
178
+ dispatchAnalyticsEvent = _ref.dispatchAnalyticsEvent;
179
+ var createLazyNodeView = function createLazyNodeView(node, view, getPos, decorations) {
180
+ var _node$type2;
181
+ var cachedMap = cachePerEditorView.get(view);
182
+ if (!cachedMap) {
183
+ cachedMap = {};
184
+ cachePerEditorView.set(view, cachedMap);
185
+ }
186
+ var wasAlreadyRequested = cachedMap.hasOwnProperty(nodeName);
187
+ if (wasAlreadyRequested) {
188
+ return new LazyNodeView(node, view, getPos);
189
+ }
190
+ loader().then(function (nodeViewFuncModule) {
191
+ cachedMap[nodeName] = function (node, view, getPos, decorations) {
192
+ return nodeViewFuncModule(node, view, getPos, decorations, getNodeViewOptions);
193
+ };
194
+ debouncedReplaceNodeviews(cachePerEditorView, view);
195
+ });
196
+ if (typeof ((_node$type2 = node.type) === null || _node$type2 === void 0 || (_node$type2 = _node$type2.spec) === null || _node$type2 === void 0 ? void 0 : _node$type2.toDOM) !== 'function') {
197
+ // TODO: Analytics ED-23982
198
+ // dispatchAnalyticsEvent({
199
+ // action: ACTION.LAZY_NODE_VIEW_ERROR,
200
+ // actionSubject: ACTION_SUBJECT.LAZY_NODE_VIEW,
201
+ // eventType: EVENT_TYPE.OPERATIONAL,
202
+ // attributes: {
203
+ // nodeName,
204
+ // error: 'No spec found',
205
+ // },
206
+ // });
207
+ }
208
+ return new LazyNodeView(node, view, getPos);
209
+ };
210
+ return createLazyNodeView;
211
+ };
@@ -19,6 +19,7 @@ var SMALL_LINK_TOOLBAR_ANALYTICS_SOURCE = 'smallLinkToolbar';
19
19
  var Dropdown = function Dropdown(_ref) {
20
20
  var onConfigureClickCallback = _ref.onConfigureClick,
21
21
  onDropdownChange = _ref.onDropdownChange,
22
+ editorView = _ref.editorView,
22
23
  testId = _ref.testId;
23
24
  var _useIntl = useIntl(),
24
25
  formatMessage = _useIntl.formatMessage;
@@ -28,20 +29,34 @@ var Dropdown = function Dropdown(_ref) {
28
29
  fireActionClickEvent = _useLinkOverlayAnalyt.fireActionClickEvent,
29
30
  fireLinkClickEvent = _useLinkOverlayAnalyt.fireLinkClickEvent,
30
31
  fireToolbarViewEvent = _useLinkOverlayAnalyt.fireToolbarViewEvent;
32
+ var focusEditor = useCallback(function () {
33
+ // Fix dropdown giving focus back to the trigger async which is then unmounted and losing focus
34
+ // this is happening deep within atlaskit dropdown as a result of this code: https://github.com/focus-trap/focus-trap/blob/master/index.js#L987
35
+ // use setTimeout to run this async after that call
36
+ setTimeout(function () {
37
+ return editorView.focus();
38
+ }, 0);
39
+ }, [editorView]);
31
40
  var onOpenChange = useCallback(function (_ref2) {
32
- var isOpen = _ref2.isOpen;
41
+ var isOpen = _ref2.isOpen,
42
+ event = _ref2.event;
33
43
  onDropdownChange === null || onDropdownChange === void 0 || onDropdownChange(isOpen);
34
44
  if (isOpen) {
35
45
  fireToolbarViewEvent();
36
46
  }
37
- }, [fireToolbarViewEvent, onDropdownChange]);
47
+ if (!isOpen && event instanceof KeyboardEvent) {
48
+ focusEditor();
49
+ }
50
+ }, [fireToolbarViewEvent, focusEditor, onDropdownChange]);
38
51
  var onGoToLinkClick = useCallback(function () {
39
52
  fireActionClickEvent('goToLink');
40
- }, [fireActionClickEvent]);
53
+ focusEditor();
54
+ }, [fireActionClickEvent, focusEditor]);
41
55
  var onConfigureClick = useCallback(function () {
42
56
  fireActionClickEvent('configureLink');
43
57
  onConfigureClickCallback === null || onConfigureClickCallback === void 0 || onConfigureClickCallback();
44
- }, [fireActionClickEvent, onConfigureClickCallback]);
58
+ focusEditor();
59
+ }, [fireActionClickEvent, focusEditor, onConfigureClickCallback]);
45
60
  return jsx(DropdownMenu, {
46
61
  trigger: function trigger(_ref3) {
47
62
  var _onClick = _ref3.onClick,