@atlaskit/editor-plugin-layout 11.0.3 → 11.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 (54) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/layoutPlugin.js +22 -6
  3. package/dist/cjs/nodeviews/index.js +49 -1
  4. package/dist/cjs/pm-plugins/actions.js +30 -10
  5. package/dist/cjs/pm-plugins/keymap.js +31 -0
  6. package/dist/cjs/pm-plugins/main.js +90 -21
  7. package/dist/cjs/pm-plugins/resizing.js +29 -4
  8. package/dist/cjs/pm-plugins/utils/layout-column-selection.js +36 -12
  9. package/dist/cjs/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +25 -5
  10. package/dist/cjs/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +7 -1
  11. package/dist/cjs/ui/LayoutColumnMenu/index.js +51 -4
  12. package/dist/cjs/ui/LayoutSSRReactContextsProvider.js +34 -0
  13. package/dist/cjs/ui/global-styles.js +11 -1
  14. package/dist/es2019/layoutPlugin.js +21 -7
  15. package/dist/es2019/nodeviews/index.js +49 -1
  16. package/dist/es2019/pm-plugins/actions.js +26 -11
  17. package/dist/es2019/pm-plugins/keymap.js +26 -0
  18. package/dist/es2019/pm-plugins/main.js +97 -25
  19. package/dist/es2019/pm-plugins/resizing.js +37 -7
  20. package/dist/es2019/pm-plugins/utils/layout-column-selection.js +33 -8
  21. package/dist/es2019/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +26 -6
  22. package/dist/es2019/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +8 -2
  23. package/dist/es2019/ui/LayoutColumnMenu/index.js +52 -5
  24. package/dist/es2019/ui/LayoutSSRReactContextsProvider.js +28 -0
  25. package/dist/es2019/ui/global-styles.js +9 -1
  26. package/dist/esm/layoutPlugin.js +23 -7
  27. package/dist/esm/nodeviews/index.js +49 -1
  28. package/dist/esm/pm-plugins/actions.js +30 -10
  29. package/dist/esm/pm-plugins/keymap.js +25 -0
  30. package/dist/esm/pm-plugins/main.js +90 -21
  31. package/dist/esm/pm-plugins/resizing.js +29 -4
  32. package/dist/esm/pm-plugins/utils/layout-column-selection.js +35 -11
  33. package/dist/esm/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +26 -6
  34. package/dist/esm/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +8 -2
  35. package/dist/esm/ui/LayoutColumnMenu/index.js +52 -5
  36. package/dist/esm/ui/LayoutSSRReactContextsProvider.js +27 -0
  37. package/dist/esm/ui/global-styles.js +11 -1
  38. package/dist/types/layoutPluginType.d.ts +5 -7
  39. package/dist/types/nodeviews/index.d.ts +5 -0
  40. package/dist/types/pm-plugins/actions.d.ts +8 -4
  41. package/dist/types/pm-plugins/keymap.d.ts +10 -0
  42. package/dist/types/pm-plugins/resizing.d.ts +2 -1
  43. package/dist/types/pm-plugins/types.d.ts +2 -0
  44. package/dist/types/pm-plugins/utils/layout-column-selection.d.ts +1 -0
  45. package/dist/types/ui/LayoutSSRReactContextsProvider.d.ts +19 -0
  46. package/dist/types-ts4.5/layoutPluginType.d.ts +5 -7
  47. package/dist/types-ts4.5/nodeviews/index.d.ts +5 -0
  48. package/dist/types-ts4.5/pm-plugins/actions.d.ts +8 -4
  49. package/dist/types-ts4.5/pm-plugins/keymap.d.ts +10 -0
  50. package/dist/types-ts4.5/pm-plugins/resizing.d.ts +2 -1
  51. package/dist/types-ts4.5/pm-plugins/types.d.ts +2 -0
  52. package/dist/types-ts4.5/pm-plugins/utils/layout-column-selection.d.ts +1 -0
  53. package/dist/types-ts4.5/ui/LayoutSSRReactContextsProvider.d.ts +19 -0
  54. package/package.json +5 -5
@@ -53,6 +53,18 @@ const moveCursorToNextColumn = (state, dispatch) => {
53
53
  const getNodeDecoration = (pos, node) => [Decoration.node(pos, pos + node.nodeSize, {
54
54
  class: 'selected'
55
55
  })];
56
+ const getDangerPreviewDecorations = (state, positions) => {
57
+ var _positions$flatMap;
58
+ return (_positions$flatMap = positions === null || positions === void 0 ? void 0 : positions.flatMap(pos => {
59
+ const node = state.doc.nodeAt(pos);
60
+ if (!node) {
61
+ return [];
62
+ }
63
+ return [Decoration.node(pos, pos + node.nodeSize, {
64
+ class: 'layout-column-danger-preview'
65
+ })];
66
+ })) !== null && _positions$flatMap !== void 0 ? _positions$flatMap : [];
67
+ };
56
68
  const getInitialPluginState = (options, state) => {
57
69
  const maybeLayoutSection = getMaybeLayoutSection(state);
58
70
  const allowBreakout = options.allowBreakout || false;
@@ -68,9 +80,58 @@ const getInitialPluginState = (options, state) => {
68
80
  allowSingleColumnLayout,
69
81
  isResizing: false,
70
82
  isLayoutColumnMenuOpen: false,
71
- layoutColumnMenuAnchorPos: undefined
83
+ layoutColumnMenuOpenedViaKeyboard: false,
84
+ layoutColumnMenuAnchorPos: undefined,
85
+ dangerPreviewLayoutColumnPositions: undefined
72
86
  };
73
87
  };
88
+ const reduceLayoutColumnMenuState = (pluginState, action) => {
89
+ switch (action.type) {
90
+ case 'toggleLayoutColumnMenu':
91
+ {
92
+ const {
93
+ anchorPos,
94
+ isOpen,
95
+ openedViaKeyboard
96
+ } = action.meta;
97
+ const nextIsOpen = isOpen !== null && isOpen !== void 0 ? isOpen : !pluginState.isLayoutColumnMenuOpen;
98
+ return {
99
+ ...pluginState,
100
+ isLayoutColumnMenuOpen: nextIsOpen,
101
+ layoutColumnMenuOpenedViaKeyboard: nextIsOpen ? openedViaKeyboard !== null && openedViaKeyboard !== void 0 ? openedViaKeyboard : false : false,
102
+ layoutColumnMenuAnchorPos: nextIsOpen ? anchorPos : undefined,
103
+ dangerPreviewLayoutColumnPositions: nextIsOpen ? pluginState.dangerPreviewLayoutColumnPositions : undefined
104
+ };
105
+ }
106
+ case 'setDangerPreview':
107
+ return {
108
+ ...pluginState,
109
+ dangerPreviewLayoutColumnPositions: action.positions
110
+ };
111
+ case 'clearDangerPreview':
112
+ return {
113
+ ...pluginState,
114
+ dangerPreviewLayoutColumnPositions: undefined
115
+ };
116
+ case 'setResizeState':
117
+ return {
118
+ ...pluginState,
119
+ isResizing: action.isResizing
120
+ };
121
+ case 'syncSelectionState':
122
+ {
123
+ const maybeLayoutSection = getMaybeLayoutSection(action.state);
124
+ return {
125
+ ...pluginState,
126
+ pos: maybeLayoutSection ? maybeLayoutSection.pos : null,
127
+ selectedLayout: getSelectedLayout(maybeLayoutSection && maybeLayoutSection.node,
128
+ // Ignored via go/ees005
129
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130
+ pluginState.selectedLayout)
131
+ };
132
+ }
133
+ }
134
+ };
74
135
 
75
136
  // To prevent a single-column layout,
76
137
  // if a user attempts to delete a layout column and
@@ -115,31 +176,39 @@ export default ((options, editorAnalyticsAPI) => {
115
176
  state: {
116
177
  init: (_, state) => getInitialPluginState(options, state),
117
178
  apply: (tr, pluginState, oldState, newState) => {
118
- var _columnMenuMeta$isOpe, _tr$getMeta, _pluginKey$getState;
179
+ var _tr$getMeta, _pluginKey$getState;
180
+ let nextPluginState = pluginState;
119
181
  const columnMenuMeta = tr.getMeta('toggleLayoutColumnMenu');
120
- const nextPluginState = columnMenuMeta ? {
121
- ...pluginState,
122
- isLayoutColumnMenuOpen: (_columnMenuMeta$isOpe = columnMenuMeta.isOpen) !== null && _columnMenuMeta$isOpe !== void 0 ? _columnMenuMeta$isOpe : !pluginState.isLayoutColumnMenuOpen,
123
- layoutColumnMenuAnchorPos: columnMenuMeta.isOpen === false ? undefined : columnMenuMeta.anchorPos
124
- } : pluginState;
182
+ const dangerPreviewMeta = tr.getMeta('layoutColumnDangerPreview');
183
+ if (columnMenuMeta) {
184
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
185
+ meta: columnMenuMeta,
186
+ type: 'toggleLayoutColumnMenu'
187
+ });
188
+ }
189
+ if (tr.getMeta('layoutColumnDangerPreview') !== undefined) {
190
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
191
+ positions: dangerPreviewMeta !== null && dangerPreviewMeta !== void 0 ? dangerPreviewMeta : undefined,
192
+ type: 'setDangerPreview'
193
+ });
194
+ }
195
+ if (tr.docChanged) {
196
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
197
+ type: 'clearDangerPreview'
198
+ });
199
+ }
125
200
  const isResizing = editorExperiment('single_column_layouts', true) ? (_tr$getMeta = tr.getMeta('is-resizer-resizing')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : (_pluginKey$getState = pluginKey.getState(oldState)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.isResizing : false;
201
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
202
+ isResizing,
203
+ type: 'setResizeState'
204
+ });
126
205
  if (tr.docChanged || tr.selectionSet) {
127
- const maybeLayoutSection = getMaybeLayoutSection(newState);
128
- const newPluginState = {
129
- ...nextPluginState,
130
- pos: maybeLayoutSection ? maybeLayoutSection.pos : null,
131
- isResizing,
132
- selectedLayout: getSelectedLayout(maybeLayoutSection && maybeLayoutSection.node,
133
- // Ignored via go/ees005
134
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
135
- pluginState.selectedLayout)
136
- };
137
- return newPluginState;
206
+ return reduceLayoutColumnMenuState(nextPluginState, {
207
+ state: newState,
208
+ type: 'syncSelectionState'
209
+ });
138
210
  }
139
- return {
140
- ...nextPluginState,
141
- isResizing
142
- };
211
+ return nextPluginState;
143
212
  }
144
213
  },
145
214
  props: {
@@ -149,14 +218,17 @@ export default ((options, editorAnalyticsAPI) => {
149
218
  if (editorExperiment('advanced_layouts', true) && editorExperiment('platform_editor_layout_column_resize_handle', true) && isLayoutResizingPluginAvailable) {
150
219
  const dividerDecorations = getColumnDividerDecorations(state, editorViewRef, editorAnalyticsAPI);
151
220
  const selectedDecorations = layoutState.pos !== null ? getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)) : [];
152
- const allDecorations = [...selectedDecorations, ...dividerDecorations];
221
+ const dangerPreviewDecorations = getDangerPreviewDecorations(state, layoutState.dangerPreviewLayoutColumnPositions);
222
+ const allDecorations = [...selectedDecorations, ...dividerDecorations, ...dangerPreviewDecorations];
153
223
  if (allDecorations.length > 0) {
154
224
  return DecorationSet.create(state.doc, allDecorations);
155
225
  }
156
226
  return undefined;
157
227
  }
158
- if (layoutState.pos !== null) {
159
- return DecorationSet.create(state.doc, getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)));
228
+ const dangerPreviewDecorations = getDangerPreviewDecorations(state, layoutState.dangerPreviewLayoutColumnPositions);
229
+ if (layoutState.pos !== null || dangerPreviewDecorations.length > 0) {
230
+ const selectedDecorations = layoutState.pos !== null ? getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)) : [];
231
+ return DecorationSet.create(state.doc, [...selectedDecorations, ...dangerPreviewDecorations]);
160
232
  }
161
233
  return undefined;
162
234
  },
@@ -1,3 +1,4 @@
1
+ import { isSSR, isSSRStreaming } from '@atlaskit/editor-common/core-utils';
1
2
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
3
  import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
3
4
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
@@ -14,6 +15,28 @@ export const pluginKey = new PluginKey('layoutResizingPlugin');
14
15
  * (e.g. setting flex-basis to give real-time visual feedback without dispatching
15
16
  * PM transactions) are not "corrected" back by ProseMirror's DOM reconciliation.
16
17
  */
18
+ const isLayoutElementLike = element => {
19
+ if (isSSR() && isSSRStreaming()) {
20
+ // In SSR environments, `HTMLElement` is undefined globally so a plain
21
+ // `instanceof HTMLElement` check is always `false`. That makes the
22
+ // `DOMSerializer.renderSpec(...)` result get rejected by the guard below and
23
+ // the NodeView falls back to a bare `<div>`, losing every schema-defined
24
+ // attribute (`data-layout-column`, `style="flex-basis:..."`,
25
+ // `data-column-width`, plus the inner `<div data-layout-content="true">`
26
+ // wrapper) and breaking the layout's flex sizing in SSR output.
27
+ //
28
+ // To unblock SSR streaming without changing CSR semantics, we gate the check:
29
+ // - In SSR (and only when `platform_editor_editor_ssr_streaming` is enabled),
30
+ // use a duck-typed check that mirrors `safe-plugin`'s `isHTMLElement`.
31
+ // - Everywhere else, keep the original `instanceof HTMLElement` check exactly
32
+ // as it was so we don't accidentally widen acceptance in CSR.
33
+ if (element === null || element === undefined) {
34
+ return false;
35
+ }
36
+ return typeof element === 'object' && 'innerHTML' in element && 'style' in element && 'classList' in element;
37
+ }
38
+ return element instanceof HTMLElement;
39
+ };
17
40
  class LayoutColumnView {
18
41
  constructor(node, view, getPos) {
19
42
  // Use the NodeSpec's own toDOM to produce the correct DOM structure and attributes.
@@ -31,7 +54,7 @@ class LayoutColumnView {
31
54
  dom,
32
55
  contentDOM
33
56
  } = DOMSerializer.renderSpec(document, nodeType.spec.toDOM(node));
34
- if (!(dom instanceof HTMLElement) || !(contentDOM instanceof HTMLElement)) {
57
+ if (!isLayoutElementLike(dom) || !isLayoutElementLike(contentDOM)) {
35
58
  const fallbackDiv = document.createElement('div');
36
59
  this.dom = fallbackDiv;
37
60
  this.contentDOM = fallbackDiv;
@@ -56,7 +79,7 @@ class LayoutColumnView {
56
79
  return mutation.type === 'attributes' && mutation.attributeName === 'style';
57
80
  }
58
81
  }
59
- export default ((options, pluginInjectionApi, portalProviderAPI, eventDispatcher) => new SafePlugin({
82
+ export default ((options, pluginInjectionApi, portalProviderAPI, eventDispatcher, intl) => new SafePlugin({
60
83
  key: pluginKey,
61
84
  props: {
62
85
  nodeViews: {
@@ -68,13 +91,20 @@ export default ((options, pluginInjectionApi, portalProviderAPI, eventDispatcher
68
91
  portalProviderAPI,
69
92
  eventDispatcher,
70
93
  pluginInjectionApi,
71
- options
94
+ options,
95
+ intl
72
96
  }).init();
73
97
  },
74
- // Only register the column node view when the resize handle experiment is on.
75
- // It exists solely to suppress style-attribute MutationObserver callbacks
76
- // during drag, allowing direct flex-basis writes without PM interference.
77
- ...(editorExperiment('platform_editor_layout_column_resize_handle', true) ? {
98
+ // Register the column node view when EITHER:
99
+ // 1. The resize handle experiment is on (its original purpose:
100
+ // suppress style-attribute MutationObserver callbacks during
101
+ // drag, allowing direct flex-basis writes without PM
102
+ // interference).
103
+ // 2. SSR streaming is enabled — the column node view stamps
104
+ // `container-type: inline-size` inline on each column dom so
105
+ // that the SSR-rendered table inside the column constrains
106
+ // its width to the column (see comment in the constructor).
107
+ ...(editorExperiment('platform_editor_layout_column_resize_handle', true) || isSSR() && isSSRStreaming() ? {
78
108
  layoutColumn: (node, view, getPos) => new LayoutColumnView(node, view, getPos)
79
109
  } : {})
80
110
  }
@@ -22,7 +22,7 @@ const findLayoutColumnsFromLayoutSection = (layoutSectionNode, layoutSectionPos
22
22
  pos: pos + layoutSectionPos + 1
23
23
  }));
24
24
  };
25
- export const getSelectedLayoutColumnsFromSelection = selection => {
25
+ const getSelectedLayoutColumns = (selection, isColumnSelected) => {
26
26
  const layoutSection = findLayoutSectionFromSelection(selection);
27
27
  if (!layoutSection) {
28
28
  return undefined;
@@ -37,18 +37,15 @@ export const getSelectedLayoutColumnsFromSelection = selection => {
37
37
  }
38
38
  let startIndex = -1;
39
39
  let endIndex = -1;
40
- const selectedLayoutColumns = allLayoutColumns.filter(({
41
- node,
42
- pos
43
- }, index) => {
44
- const isSelected = selection.from <= pos && selection.to >= pos + node.nodeSize;
45
- if (isSelected) {
40
+ const selectedLayoutColumns = allLayoutColumns.filter((column, index) => {
41
+ if (isColumnSelected(column, index)) {
46
42
  if (startIndex === -1) {
47
43
  startIndex = index;
48
44
  }
49
45
  endIndex = index;
46
+ return true;
50
47
  }
51
- return isSelected;
48
+ return false;
52
49
  });
53
50
  return {
54
51
  layoutSectionNode,
@@ -58,6 +55,34 @@ export const getSelectedLayoutColumnsFromSelection = selection => {
58
55
  endIndex
59
56
  };
60
57
  };
58
+ export const getSelectedLayoutColumnsFromSelection = selection => {
59
+ return getSelectedLayoutColumns(selection, ({
60
+ node,
61
+ pos
62
+ }) => {
63
+ // NodeSelection on a layout column is clearly selected.
64
+ if (selection instanceof NodeSelection && selection.node === node) {
65
+ return true;
66
+ }
67
+
68
+ // For TextSelection, only count columns that are fully contained within the selection
69
+ // (not partial text selections inside a column).
70
+ const nodeEndPos = pos + node.nodeSize;
71
+ return !selection.empty && selection.from <= pos && selection.to >= nodeEndPos;
72
+ });
73
+ };
74
+ export const getLayoutColumnsFromContentSelection = selection => {
75
+ return getSelectedLayoutColumns(selection, ({
76
+ node,
77
+ pos
78
+ }) => {
79
+ if (selection instanceof NodeSelection && selection.node === node) {
80
+ return true;
81
+ }
82
+ const nodeEndPos = pos + node.nodeSize;
83
+ return selection.empty ? selection.from > pos && selection.from < nodeEndPos : selection.from < nodeEndPos && selection.to > pos;
84
+ });
85
+ };
61
86
  export const getAllLayoutColumnsFromSelection = selection => {
62
87
  const layoutSection = findLayoutSectionFromSelection(selection);
63
88
  if (!layoutSection) {
@@ -1,25 +1,37 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
+ import { deleteColumn, getAriaKeyshortcuts, tooltip } from '@atlaskit/editor-common/keymaps';
3
4
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
- import { DeleteIcon, ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
+ import { DeleteIcon, ToolbarDropdownItem, ToolbarKeyboardShortcutHint } from '@atlaskit/editor-toolbar';
5
6
  import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
6
7
  const DeleteColumnDropdownItem = ({
7
8
  api
8
9
  }) => {
10
+ var _tooltip;
9
11
  const {
10
12
  formatMessage
11
13
  } = useIntl();
12
14
  const selectedLayoutColumns = useSelectedLayoutColumns(api);
15
+ const setDangerPreview = useCallback(show => {
16
+ var _api$core, _api$layout;
17
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.setLayoutColumnDangerPreview(show));
18
+ }, [api]);
19
+ const showDangerPreview = useCallback(() => {
20
+ setDangerPreview(true);
21
+ }, [setDangerPreview]);
22
+ const hideDangerPreview = useCallback(() => {
23
+ setDangerPreview(false);
24
+ }, [setDangerPreview]);
13
25
  const onClick = useCallback(() => {
14
- var _api$layout, _api$core;
15
- const deleteCommand = api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.deleteLayoutColumn;
16
- api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(props => {
17
- var _api$layout2;
26
+ var _api$layout2, _api$core2;
27
+ const deleteCommand = api === null || api === void 0 ? void 0 : (_api$layout2 = api.layout) === null || _api$layout2 === void 0 ? void 0 : _api$layout2.commands.deleteLayoutColumn();
28
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(props => {
29
+ var _api$layout3;
18
30
  const tr = deleteCommand === null || deleteCommand === void 0 ? void 0 : deleteCommand(props);
19
31
  if (!tr) {
20
32
  return tr !== null && tr !== void 0 ? tr : null;
21
33
  }
22
- api === null || api === void 0 ? void 0 : (_api$layout2 = api.layout) === null || _api$layout2 === void 0 ? void 0 : _api$layout2.commands.toggleLayoutColumnMenu({
34
+ api === null || api === void 0 ? void 0 : (_api$layout3 = api.layout) === null || _api$layout3 === void 0 ? void 0 : _api$layout3.commands.toggleLayoutColumnMenu({
23
35
  isOpen: false
24
36
  })({
25
37
  tr
@@ -32,11 +44,19 @@ const DeleteColumnDropdownItem = ({
32
44
  }
33
45
  const selectedColumnCount = selectedLayoutColumns.selectedLayoutColumns.length;
34
46
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
47
+ ariaKeyshortcuts: getAriaKeyshortcuts(deleteColumn),
35
48
  onClick: onClick,
49
+ onFocus: showDangerPreview,
50
+ onMouseEnter: showDangerPreview,
51
+ onBlur: hideDangerPreview,
52
+ onMouseLeave: hideDangerPreview,
36
53
  elemBefore: /*#__PURE__*/React.createElement(DeleteIcon, {
37
54
  color: "currentColor",
38
55
  label: "",
39
56
  size: "small"
57
+ }),
58
+ elemAfter: /*#__PURE__*/React.createElement(ToolbarKeyboardShortcutHint, {
59
+ shortcut: (_tooltip = tooltip(deleteColumn)) !== null && _tooltip !== void 0 ? _tooltip : ''
40
60
  })
41
61
  }, formatMessage(layoutMessages.deleteColumn, {
42
62
  count: selectedColumnCount
@@ -1,7 +1,8 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
+ import { addColumnAfter, addColumnBefore, getAriaKeyshortcuts, tooltip } from '@atlaskit/editor-common/keymaps';
3
4
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
- import { TableColumnAddLeftIcon, TableColumnAddRightIcon, ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
+ import { TableColumnAddLeftIcon, TableColumnAddRightIcon, ToolbarDropdownItem, ToolbarKeyboardShortcutHint } from '@atlaskit/editor-toolbar';
5
6
  import { getEffectiveMaxLayoutColumns } from '../../pm-plugins/actions';
6
7
  import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
7
8
  const INSERT_COLUMN_OPTIONS = {
@@ -20,7 +21,7 @@ export const InsertColumnDropdownItem = ({
20
21
  api,
21
22
  side
22
23
  }) => {
23
- var _selectedLayoutColumn, _selectedLayoutColumn2;
24
+ var _selectedLayoutColumn, _selectedLayoutColumn2, _tooltip;
24
25
  const {
25
26
  formatMessage
26
27
  } = useIntl();
@@ -28,6 +29,7 @@ export const InsertColumnDropdownItem = ({
28
29
  Icon,
29
30
  label
30
31
  } = INSERT_COLUMN_OPTIONS[side];
32
+ const shortcut = side === 'left' ? addColumnBefore : addColumnAfter;
31
33
  const selectedLayoutColumns = useSelectedLayoutColumns(api);
32
34
  const columnCount = (_selectedLayoutColumn = selectedLayoutColumns === null || selectedLayoutColumns === void 0 ? void 0 : (_selectedLayoutColumn2 = selectedLayoutColumns.layoutSectionNode) === null || _selectedLayoutColumn2 === void 0 ? void 0 : _selectedLayoutColumn2.childCount) !== null && _selectedLayoutColumn !== void 0 ? _selectedLayoutColumn : 0;
33
35
  const maxColumnCount = getEffectiveMaxLayoutColumns();
@@ -53,11 +55,15 @@ export const InsertColumnDropdownItem = ({
53
55
  return null;
54
56
  }
55
57
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
58
+ ariaKeyshortcuts: getAriaKeyshortcuts(shortcut),
56
59
  elemBefore: /*#__PURE__*/React.createElement(Icon, {
57
60
  color: "currentColor",
58
61
  label: "",
59
62
  size: "small"
60
63
  }),
64
+ elemAfter: /*#__PURE__*/React.createElement(ToolbarKeyboardShortcutHint, {
65
+ shortcut: (_tooltip = tooltip(shortcut)) !== null && _tooltip !== void 0 ? _tooltip : ''
66
+ }),
61
67
  onClick: onClick
62
68
  }, formatMessage(label));
63
69
  };
@@ -1,7 +1,9 @@
1
- import React, { useCallback, useEffect, useMemo } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { bind } from 'bind-event-listener';
2
3
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
4
  import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
4
5
  import { Popup } from '@atlaskit/editor-common/ui';
6
+ import { ArrowKeyNavigationProvider, ArrowKeyNavigationType } from '@atlaskit/editor-common/ui-menu';
5
7
  import { withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react';
6
8
  import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent';
7
9
  import { akEditorFloatingOverlapPanelZIndex } from '@atlaskit/editor-shared-styles';
@@ -13,6 +15,7 @@ import { LAYOUT_COLUMN_MENU } from './keys';
13
15
  const PopupWithListeners = withReactEditorViewOuterListeners(Popup);
14
16
  const LAYOUT_COLUMN_MENU_POPUP_OFFSET = [0, 4];
15
17
  const TOOLBAR_MENU_SELECTOR = '[data-toolbar-component="menu"]';
18
+ const NESTED_DROPDOWN_MENU_SELECTOR = '[data-toolbar-nested-dropdown-menu]';
16
19
 
17
20
  /**
18
21
  * Returns the drag handle button for the selected layout column.
@@ -30,6 +33,9 @@ const getLayoutColumnMenuTarget = (editorView, selection, anchorPosFromHandle) =
30
33
  const dragHandleContainer = (_columnDomRef$parentE = columnDomRef.parentElement) === null || _columnDomRef$parentE === void 0 ? void 0 : _columnDomRef$parentE.querySelector(':scope > [data-blocks-drag-handle-container]');
31
34
  return dragHandleContainer === null || dragHandleContainer === void 0 ? void 0 : dragHandleContainer.querySelector(DRAG_HANDLE_SELECTOR);
32
35
  };
36
+ const focusTrap = {
37
+ initialFocus: undefined
38
+ };
33
39
  export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMenu({
34
40
  api,
35
41
  editorView,
@@ -41,12 +47,14 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
41
47
  const {
42
48
  isLayoutColumnMenuOpen,
43
49
  layoutColumnMenuAnchorPos,
50
+ openedViaKeyboard,
44
51
  selection
45
52
  } = useSharedPluginStateWithSelector(api, ['layout', 'selection'], states => {
46
- var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$selectionStat;
53
+ var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$layoutState$l, _states$layoutState3, _states$selectionStat;
47
54
  return {
48
55
  isLayoutColumnMenuOpen: (_states$layoutState$i = (_states$layoutState = states.layoutState) === null || _states$layoutState === void 0 ? void 0 : _states$layoutState.isLayoutColumnMenuOpen) !== null && _states$layoutState$i !== void 0 ? _states$layoutState$i : false,
49
56
  layoutColumnMenuAnchorPos: (_states$layoutState2 = states.layoutState) === null || _states$layoutState2 === void 0 ? void 0 : _states$layoutState2.layoutColumnMenuAnchorPos,
57
+ openedViaKeyboard: (_states$layoutState$l = (_states$layoutState3 = states.layoutState) === null || _states$layoutState3 === void 0 ? void 0 : _states$layoutState3.layoutColumnMenuOpenedViaKeyboard) !== null && _states$layoutState$l !== void 0 ? _states$layoutState$l : false,
50
58
  selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection
51
59
  };
52
60
  });
@@ -57,7 +65,7 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
57
65
  }));
58
66
  }, [api]);
59
67
  const handleClickOutside = useCallback(event => {
60
- if (event.target instanceof Element && (event.target.closest(TOOLBAR_MENU_SELECTOR) || event.target.closest('[data-toolbar-nested-dropdown-menu]'))) {
68
+ if (event.target instanceof Element && (event.target.closest(TOOLBAR_MENU_SELECTOR) || event.target.closest(NESTED_DROPDOWN_MENU_SELECTOR))) {
61
69
  return;
62
70
  }
63
71
 
@@ -74,6 +82,38 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
74
82
  closeLayoutColumnMenu();
75
83
  }
76
84
  }, [closeLayoutColumnMenu]);
85
+ const handleArrowKeyNavigationClose = useCallback(event => {
86
+ event.preventDefault();
87
+ closeLayoutColumnMenu();
88
+ }, [closeLayoutColumnMenu]);
89
+ const shouldDisableArrowKeyNavigation = useCallback(event => {
90
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
91
+ return false;
92
+ }
93
+ const target = event.target;
94
+ if (!(target instanceof HTMLElement)) {
95
+ return false;
96
+ }
97
+ return target.closest(NESTED_DROPDOWN_MENU_SELECTOR) !== null;
98
+ }, []);
99
+ const menuWrapperRef = useRef(null);
100
+ const handleMenuKeyDown = useCallback(event => {
101
+ // Keep menu keyboard events scoped to the menu while preserving Escape and
102
+ // ArrowUp/ArrowDown handling from Popup and ArrowKeyNavigationProvider.
103
+ if (event.key !== 'Escape' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
104
+ event.stopPropagation();
105
+ }
106
+ }, []);
107
+ useEffect(() => {
108
+ const menuWrapper = menuWrapperRef.current;
109
+ if (!isLayoutColumnMenuOpen || !menuWrapper) {
110
+ return;
111
+ }
112
+ return bind(menuWrapper, {
113
+ type: 'keydown',
114
+ listener: handleMenuKeyDown
115
+ });
116
+ }, [handleMenuKeyDown, isLayoutColumnMenuOpen]);
77
117
  const components = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(LAYOUT_COLUMN_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
78
118
  const target = useMemo(() => isLayoutColumnMenuOpen ? getLayoutColumnMenuTarget(editorView, selection, layoutColumnMenuAnchorPos) : null, [editorView, isLayoutColumnMenuOpen, layoutColumnMenuAnchorPos, selection]);
79
119
  const hasValidTarget = target instanceof HTMLElement;
@@ -96,16 +136,23 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
96
136
  stick: true,
97
137
  offset: LAYOUT_COLUMN_MENU_POPUP_OFFSET,
98
138
  handleClickOutside: handleClickOutside,
99
- handleEscapeKeydown: closeLayoutColumnMenu
139
+ handleEscapeKeydown: closeLayoutColumnMenu,
140
+ focusTrap: openedViaKeyboard ? focusTrap : undefined
141
+ }, /*#__PURE__*/React.createElement("div", {
142
+ ref: menuWrapperRef
100
143
  }, /*#__PURE__*/React.createElement(UserIntentPopupWrapper, {
101
144
  api: api,
102
145
  userIntent: "layoutColumnMenuPopupOpen"
103
146
  }, /*#__PURE__*/React.createElement(ToolbarDropdownMenuProvider, {
104
147
  isOpen: isLayoutColumnMenuOpen,
105
148
  setIsOpen: handleSetIsOpen
149
+ }, /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, {
150
+ type: ArrowKeyNavigationType.MENU,
151
+ handleClose: handleArrowKeyNavigationClose,
152
+ disableArrowKeyNavigation: shouldDisableArrowKeyNavigation
106
153
  }, /*#__PURE__*/React.createElement(SurfaceRenderer, {
107
154
  components: components,
108
155
  fallbacks: LAYOUT_COLUMN_MENU_FALLBACKS,
109
156
  surface: LAYOUT_COLUMN_MENU
110
- }))));
157
+ }))))));
111
158
  });
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { RawIntlProvider } from 'react-intl';
3
+ import { isSSR, isSSRStreaming } from '@atlaskit/editor-common/core-utils';
4
+ /**
5
+ * Wraps the layout section nodeview children with the editor's actual
6
+ * IntlProvider during SSR streaming (renderToStaticMarkup). This ensures any
7
+ * descendants that call `useIntl()` (e.g. `BreakoutResizer`'s ARIA labels)
8
+ * have a valid intl context and do not throw during the static render pass.
9
+ *
10
+ * Outside of SSR streaming this is a no-op passthrough.
11
+ *
12
+ * Follows the same pattern as `MediaSSRReactContextsProvider` and
13
+ * `SyncBlockSSRReactContextsProvider`.
14
+ */
15
+ export function LayoutSSRReactContextsProvider({
16
+ children,
17
+ intl
18
+ }) {
19
+ if (!isSSRStreaming() || !isSSR()) {
20
+ return children;
21
+ }
22
+ if (!intl) {
23
+ return children;
24
+ }
25
+ return /*#__PURE__*/React.createElement(RawIntlProvider, {
26
+ value: intl
27
+ }, children);
28
+ }
@@ -10,6 +10,13 @@ import { useIntl } from 'react-intl';
10
10
  import { layoutMessages as messages } from '@atlaskit/editor-common/messages';
11
11
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
12
12
  const PLACEHOLDER_SELECTOR = '.ProseMirror-focused .layoutSectionView-content-wrap.selected [data-layout-column] > [data-layout-content] > p:only-child:has(.ProseMirror-trailingBreak:only-child)';
13
+ const layoutColumnDangerPreviewStyle = css({
14
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766
15
+ '.ProseMirror [data-layout-column].layout-column-danger-preview': {
16
+ backgroundColor: "var(--ds-background-danger, #FFECEB)",
17
+ boxShadow: `inset 0 0 0 2px ${"var(--ds-border-danger, #E2483D)"}`
18
+ }
19
+ });
13
20
  const getPlaceholderStyle = message => {
14
21
  if (editorExperiment('platform_editor_controls', 'variant1')) {
15
22
  return css({
@@ -52,7 +59,8 @@ export const GlobalStylesWrapper = () => {
52
59
  const placeholderText = editorExperiment('platform_editor_controls', 'variant1') ? messages.controlslayoutPlaceholder : messages.layoutPlaceholder;
53
60
  return getPlaceholderStyle(formatMessage(placeholderText));
54
61
  }, [formatMessage]);
62
+ const globalStyles = useMemo(() => [placeholderStyle, layoutColumnDangerPreviewStyle], [placeholderStyle]);
55
63
  return jsx(Global, {
56
- styles: placeholderStyle
64
+ styles: globalStyles
57
65
  });
58
66
  };
@@ -11,7 +11,8 @@ import { fg } from '@atlaskit/platform-feature-flags';
11
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
12
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
13
13
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
14
- import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn, distributeLayoutColumns as _distributeLayoutColumns, insertLayoutColumn as _insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnValign as _setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
14
+ import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn as _deleteLayoutColumn, distributeLayoutColumns as _distributeLayoutColumns, insertLayoutColumn as _insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnDangerPreview, setLayoutColumnValign as _setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
15
+ import { default as createLayoutKeymapPlugin } from './pm-plugins/keymap';
15
16
  import { default as createLayoutPlugin } from './pm-plugins/main';
16
17
  import { pluginKey } from './pm-plugins/plugin-key';
17
18
  import { default as createLayoutResizingPlugin } from './pm-plugins/resizing';
@@ -50,7 +51,7 @@ export var selectIntoLayoutSection = function selectIntoLayoutSection(tr) {
50
51
  return tr;
51
52
  };
52
53
  export var layoutPlugin = function layoutPlugin(_ref) {
53
- var _api$analytics2, _api$analytics5;
54
+ var _api$analytics2;
54
55
  var _ref$config = _ref.config,
55
56
  options = _ref$config === void 0 ? {} : _ref$config,
56
57
  api = _ref.api;
@@ -102,13 +103,24 @@ export var layoutPlugin = function layoutPlugin(_ref) {
102
103
  return createLayoutPlugin(options, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
103
104
  }
104
105
  }];
106
+ if (expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
107
+ plugins.push({
108
+ name: 'layoutKeymap',
109
+ plugin: function plugin() {
110
+ return createLayoutKeymapPlugin({
111
+ api: api
112
+ });
113
+ }
114
+ });
115
+ }
105
116
  if ((options.editorAppearance === 'full-page' || options.editorAppearance === 'full-width' || options.editorAppearance === 'max' && editorExperiment('platform_editor_layout_column_resize_handle', true)) && api && editorExperiment('advanced_layouts', true)) {
106
117
  plugins.push({
107
118
  name: 'layoutResizing',
108
119
  plugin: function plugin(_ref2) {
109
120
  var portalProviderAPI = _ref2.portalProviderAPI,
110
- eventDispatcher = _ref2.eventDispatcher;
111
- return createLayoutResizingPlugin(options, api, portalProviderAPI, eventDispatcher);
121
+ eventDispatcher = _ref2.eventDispatcher,
122
+ getIntl = _ref2.getIntl;
123
+ return createLayoutResizingPlugin(options, api, portalProviderAPI, eventDispatcher, getIntl());
112
124
  }
113
125
  });
114
126
  }
@@ -371,15 +383,19 @@ export var layoutPlugin = function layoutPlugin(_ref) {
371
383
  return pluginKey.getState(editorState);
372
384
  },
373
385
  commands: {
374
- deleteLayoutColumn: deleteLayoutColumn(api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, api),
386
+ deleteLayoutColumn: function deleteLayoutColumn(inputMethod) {
387
+ var _api$analytics5;
388
+ return _deleteLayoutColumn(api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, api, inputMethod);
389
+ },
375
390
  distributeLayoutColumns: function distributeLayoutColumns(options) {
376
391
  var _api$analytics6;
377
392
  return _distributeLayoutColumns(api === null || api === void 0 || (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : _api$analytics6.actions, api)(options);
378
393
  },
379
- insertLayoutColumn: function insertLayoutColumn(side) {
394
+ insertLayoutColumn: function insertLayoutColumn(side, inputMethod) {
380
395
  var _api$analytics7;
381
- return _insertLayoutColumn(side, api === null || api === void 0 || (_api$analytics7 = api.analytics) === null || _api$analytics7 === void 0 ? void 0 : _api$analytics7.actions, api);
396
+ return _insertLayoutColumn(side, api === null || api === void 0 || (_api$analytics7 = api.analytics) === null || _api$analytics7 === void 0 ? void 0 : _api$analytics7.actions, api, inputMethod);
382
397
  },
398
+ setLayoutColumnDangerPreview: setLayoutColumnDangerPreview,
383
399
  setLayoutColumnValign: function setLayoutColumnValign(valign) {
384
400
  var _api$analytics8;
385
401
  return _setLayoutColumnValign(valign, api === null || api === void 0 || (_api$analytics8 = api.analytics) === null || _api$analytics8 === void 0 ? void 0 : _api$analytics8.actions, api);