@manuscripts/body-editor 3.9.10 → 3.9.12

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 (105) hide show
  1. package/dist/cjs/commands.js +5 -2
  2. package/dist/cjs/components/keywords/AddKeywordInline.js +12 -1
  3. package/dist/cjs/components/views/FigureDropdown.js +65 -6
  4. package/dist/cjs/configs/ManuscriptsEditor.js +1 -1
  5. package/dist/cjs/index.js +1 -0
  6. package/dist/cjs/keys/misc.js +2 -1
  7. package/dist/cjs/keys/title.js +0 -38
  8. package/dist/cjs/lib/comments.js +1 -0
  9. package/dist/cjs/lib/context-menu.js +74 -8
  10. package/dist/cjs/lib/media.js +27 -3
  11. package/dist/cjs/lib/navigation-utils.js +132 -0
  12. package/dist/cjs/lib/popper.js +25 -1
  13. package/dist/cjs/lib/position-menu.js +3 -0
  14. package/dist/cjs/lib/utils.js +7 -2
  15. package/dist/cjs/plugins/accessibility_element.js +10 -2
  16. package/dist/cjs/plugins/add-subtitle.js +8 -2
  17. package/dist/cjs/plugins/alt-titles.js +6 -1
  18. package/dist/cjs/plugins/comments.js +27 -15
  19. package/dist/cjs/plugins/persistent-cursor.js +4 -6
  20. package/dist/cjs/plugins/section_category.js +42 -9
  21. package/dist/cjs/plugins/translations.js +49 -13
  22. package/dist/cjs/versions.js +1 -1
  23. package/dist/cjs/views/accessibility_element.js +30 -0
  24. package/dist/cjs/views/alt_title.js +29 -0
  25. package/dist/cjs/views/alt_titles_section.js +9 -1
  26. package/dist/cjs/views/attachment.js +1 -1
  27. package/dist/cjs/views/bibliography_element.js +39 -17
  28. package/dist/cjs/views/citation.js +1 -0
  29. package/dist/cjs/views/citation_editable.js +4 -2
  30. package/dist/cjs/views/contributors.js +23 -2
  31. package/dist/cjs/views/cross_reference.js +3 -0
  32. package/dist/cjs/views/editable_block.js +37 -3
  33. package/dist/cjs/views/embed.js +3 -3
  34. package/dist/cjs/views/figure_editable.js +1 -1
  35. package/dist/cjs/views/figure_element.js +3 -0
  36. package/dist/cjs/views/footnote.js +3 -0
  37. package/dist/cjs/views/hero_image.js +4 -1
  38. package/dist/cjs/views/image_element.js +15 -7
  39. package/dist/cjs/views/inline_footnote.js +3 -0
  40. package/dist/cjs/views/keyword.js +15 -0
  41. package/dist/cjs/views/keyword_group.js +38 -0
  42. package/dist/cjs/views/quote_image_editable.js +1 -0
  43. package/dist/cjs/views/supplements.js +4 -1
  44. package/dist/es/commands.js +5 -2
  45. package/dist/es/components/keywords/AddKeywordInline.js +12 -1
  46. package/dist/es/components/views/FigureDropdown.js +66 -7
  47. package/dist/es/configs/ManuscriptsEditor.js +1 -1
  48. package/dist/es/index.js +1 -0
  49. package/dist/es/keys/misc.js +2 -1
  50. package/dist/es/keys/title.js +1 -39
  51. package/dist/es/lib/comments.js +1 -0
  52. package/dist/es/lib/context-menu.js +74 -8
  53. package/dist/es/lib/media.js +27 -3
  54. package/dist/es/lib/navigation-utils.js +122 -0
  55. package/dist/es/lib/popper.js +25 -1
  56. package/dist/es/lib/position-menu.js +3 -0
  57. package/dist/es/lib/utils.js +7 -2
  58. package/dist/es/plugins/accessibility_element.js +10 -2
  59. package/dist/es/plugins/add-subtitle.js +8 -2
  60. package/dist/es/plugins/alt-titles.js +6 -1
  61. package/dist/es/plugins/comments.js +27 -15
  62. package/dist/es/plugins/persistent-cursor.js +4 -6
  63. package/dist/es/plugins/section_category.js +42 -9
  64. package/dist/es/plugins/translations.js +49 -13
  65. package/dist/es/versions.js +1 -1
  66. package/dist/es/views/accessibility_element.js +30 -0
  67. package/dist/es/views/alt_title.js +29 -0
  68. package/dist/es/views/alt_titles_section.js +9 -1
  69. package/dist/es/views/attachment.js +1 -1
  70. package/dist/es/views/bibliography_element.js +39 -17
  71. package/dist/es/views/citation.js +1 -0
  72. package/dist/es/views/citation_editable.js +4 -2
  73. package/dist/es/views/contributors.js +23 -2
  74. package/dist/es/views/cross_reference.js +3 -0
  75. package/dist/es/views/editable_block.js +37 -3
  76. package/dist/es/views/embed.js +3 -3
  77. package/dist/es/views/figure_editable.js +1 -1
  78. package/dist/es/views/figure_element.js +3 -0
  79. package/dist/es/views/footnote.js +3 -0
  80. package/dist/es/views/hero_image.js +4 -1
  81. package/dist/es/views/image_element.js +15 -7
  82. package/dist/es/views/inline_footnote.js +3 -0
  83. package/dist/es/views/keyword.js +15 -0
  84. package/dist/es/views/keyword_group.js +38 -0
  85. package/dist/es/views/quote_image_editable.js +1 -0
  86. package/dist/es/views/supplements.js +4 -1
  87. package/dist/types/configs/ManuscriptsEditor.d.ts +1 -1
  88. package/dist/types/index.d.ts +1 -0
  89. package/dist/types/lib/context-menu.d.ts +1 -0
  90. package/dist/types/lib/media.d.ts +1 -1
  91. package/dist/types/lib/navigation-utils.d.ts +45 -0
  92. package/dist/types/lib/popper.d.ts +3 -0
  93. package/dist/types/lib/utils.d.ts +1 -1
  94. package/dist/types/versions.d.ts +1 -1
  95. package/dist/types/views/accessibility_element.d.ts +2 -0
  96. package/dist/types/views/alt_title.d.ts +2 -0
  97. package/dist/types/views/bibliography_element.d.ts +3 -0
  98. package/dist/types/views/citation_editable.d.ts +1 -1
  99. package/dist/types/views/contributors.d.ts +3 -1
  100. package/dist/types/views/keyword.d.ts +1 -0
  101. package/dist/types/views/keyword_group.d.ts +4 -0
  102. package/package.json +4 -4
  103. package/styles/AdvancedEditor.css +116 -10
  104. package/styles/Editor.css +61 -6
  105. package/styles/popper.css +3 -1
@@ -38,6 +38,7 @@ const comments_2 = require("./plugins/comments");
38
38
  const editor_props_1 = require("./plugins/editor-props");
39
39
  const search_replace_1 = require("./plugins/search-replace");
40
40
  const autocompletion_1 = require("./plugins/section_title/autocompletion");
41
+ const persistent_cursor_1 = require("./plugins/persistent-cursor");
41
42
  const addToStart = (state, dispatch) => {
42
43
  const { selection } = state;
43
44
  const props = (0, editor_props_1.getEditorProps)(state);
@@ -1358,10 +1359,12 @@ const ignoreEnterInSubtitles = (state) => {
1358
1359
  return false;
1359
1360
  };
1360
1361
  exports.ignoreEnterInSubtitles = ignoreEnterInSubtitles;
1361
- const exitEditorToContainer = () => {
1362
+ const exitEditorToContainer = (state, dispatch, view) => {
1362
1363
  const editorContainer = document.getElementById('editor');
1363
- if (editorContainer) {
1364
+ if (editorContainer && dispatch && view) {
1364
1365
  editorContainer.focus();
1366
+ const tr = view.state.tr.setMeta(persistent_cursor_1.persistentCursor, { on: true });
1367
+ view.dispatch(tr);
1365
1368
  return true;
1366
1369
  }
1367
1370
  return false;
@@ -67,6 +67,12 @@ const NewKeywordButton = styled_components_1.default.button `
67
67
  margin: 0;
68
68
  padding: 0;
69
69
  cursor: pointer;
70
+
71
+ &:focus-visible {
72
+ outline: 4px solid ${(props) => props.theme.colors.outline.focus};
73
+ outline-offset: 2px;
74
+ border-radius: 3px;
75
+ }
70
76
  `;
71
77
  const CreateKeywordButtonWrapper = styled_components_1.default.div `
72
78
  position: absolute;
@@ -196,8 +202,13 @@ const AddKeywordInline = ({ viewProps, getUpdatedNode }) => {
196
202
  },
197
203
  };
198
204
  return (react_1.default.createElement(AddNewKeyword, { ref: nodeRef },
199
- !isAddingNewKeyword && (react_1.default.createElement(NewKeywordButton, { onClick: () => {
205
+ !isAddingNewKeyword && (react_1.default.createElement(NewKeywordButton, { tabIndex: -1, className: "keyword-add", onClick: () => {
200
206
  setIsAddingNewKeyword(true);
207
+ }, onKeyDown: (e) => {
208
+ if (e.key === 'Enter') {
209
+ e.preventDefault();
210
+ setIsAddingNewKeyword(true);
211
+ }
201
212
  } }, "New keyword...")),
202
213
  isAddingNewKeyword && react_1.default.createElement(KeywordInput, null),
203
214
  isAddingNewKeyword && isValidNewKeyword() && (react_1.default.createElement(CreateKeywordButtonElement, null)),
@@ -42,6 +42,40 @@ const react_1 = __importStar(require("react"));
42
42
  const styled_components_1 = __importDefault(require("styled-components"));
43
43
  const files_1 = require("../../lib/files");
44
44
  const get_media_type_1 = require("../../lib/get-media-type");
45
+ const navigation_utils_1 = require("../../lib/navigation-utils");
46
+ function useDropdownKeyboardNav(isOpen, containerRef, onEscape, onArrowLeft) {
47
+ (0, react_1.useEffect)(() => {
48
+ const container = containerRef.current;
49
+ if (!isOpen || !container) {
50
+ return;
51
+ }
52
+ const buttons = Array.from(container.querySelectorAll('button:not([disabled])'));
53
+ if (buttons.length === 0) {
54
+ return;
55
+ }
56
+ const removeKeydownListener = (0, navigation_utils_1.createKeyboardInteraction)({
57
+ container: container,
58
+ navigation: {
59
+ getItems: () => Array.from(container.querySelectorAll('button:not([disabled])')),
60
+ arrowKeys: {
61
+ forward: 'ArrowDown',
62
+ backward: 'ArrowUp',
63
+ },
64
+ },
65
+ additionalKeys: {
66
+ Enter: (e) => e.target.click(),
67
+ Escape: () => onEscape(),
68
+ ...(onArrowLeft ? { ArrowLeft: () => onArrowLeft() } : {}),
69
+ },
70
+ });
71
+ window.requestAnimationFrame(() => {
72
+ buttons[0]?.focus();
73
+ });
74
+ return () => {
75
+ removeKeydownListener();
76
+ };
77
+ }, [isOpen, containerRef, onEscape, onArrowLeft]);
78
+ }
45
79
  function getSupplements(getFiles, getDoc, groupFiles, isEmbed) {
46
80
  return groupFiles(getDoc(), getFiles())
47
81
  .supplements.map((s) => s.file)
@@ -60,6 +94,7 @@ function getOtherFiles(getFiles, getDoc, groupFiles, isEmbed) {
60
94
  }
61
95
  const FigureOptions = ({ can, getDoc, getFiles, onDownload, onUpload, onDetach, onReplace, onReplaceEmbed, onDelete, isEmbed, hasSiblings, container, }) => {
62
96
  const { isOpen, toggleOpen, wrapperRef } = (0, style_guide_1.useDropdown)();
97
+ const dropdownRef = (0, react_1.useRef)(null);
63
98
  const showDownload = onDownload && can.downloadFiles;
64
99
  const showUpload = onUpload && can.uploadFile;
65
100
  const showDetach = onDetach && can.detachFile;
@@ -83,12 +118,18 @@ const FigureOptions = ({ can, getDoc, getFiles, onDownload, onUpload, onDetach,
83
118
  container.classList.remove(activeClass);
84
119
  }
85
120
  }, [isOpen, container.classList]);
121
+ useDropdownKeyboardNav(isOpen, dropdownRef, toggleOpen);
86
122
  const isEmbedMode = !!onReplaceEmbed;
87
123
  const groupFiles = (0, files_1.memoGroupFiles)();
88
124
  return (react_1.default.createElement(DropdownWrapper, { ref: wrapperRef },
89
- react_1.default.createElement(OptionsButton, { className: 'options-button', onClick: toggleOpen },
125
+ react_1.default.createElement(OptionsButton, { className: 'options-button', onClick: toggleOpen, onKeyDown: (e) => {
126
+ if (e.key === 'Enter') {
127
+ e.preventDefault();
128
+ toggleOpen();
129
+ }
130
+ } },
90
131
  react_1.default.createElement(style_guide_1.DotsIcon, null)),
91
- isOpen && (react_1.default.createElement(OptionsDropdownList, { direction: 'right', width: 128, top: 5 },
132
+ isOpen && (react_1.default.createElement(OptionsDropdownList, { direction: 'right', width: 128, top: 5, ref: dropdownRef },
92
133
  showReplace && isEmbedMode && (react_1.default.createElement(ListItemButton, { onClick: () => onReplaceEmbed && onReplaceEmbed() }, "Edit Link")),
93
134
  showReplace && !isEmbedMode && (react_1.default.createElement(NestedDropdown, { disabled: !showReplace, parentToggleOpen: toggleOpen, buttonText: replaceBtnText, moveLeft: true, list: react_1.default.createElement(react_1.default.Fragment, null,
94
135
  getSupplements(getFiles, getDoc, groupFiles, isEmbed).map((file, index) => (react_1.default.createElement(ListItemButton, { key: file.id, id: index.toString(), onClick: () => onReplace && onReplace(file, true) },
@@ -107,11 +148,23 @@ const FigureOptions = ({ can, getDoc, getFiles, onDownload, onUpload, onDetach,
107
148
  exports.FigureOptions = FigureOptions;
108
149
  const NestedDropdown = ({ parentToggleOpen, buttonText, disabled, list, moveLeft }) => {
109
150
  const { isOpen, toggleOpen, wrapperRef } = (0, style_guide_1.useDropdown)();
151
+ const nestedListRef = (0, react_1.useRef)(null);
152
+ const handleArrowLeft = (0, react_1.useCallback)(() => {
153
+ toggleOpen();
154
+ const parentButton = wrapperRef.current?.querySelector('.nested-list-button');
155
+ parentButton?.focus();
156
+ }, [toggleOpen, wrapperRef]);
157
+ useDropdownKeyboardNav(isOpen, nestedListRef, handleArrowLeft, handleArrowLeft);
110
158
  return (react_1.default.createElement(DropdownWrapper, { ref: wrapperRef },
111
- react_1.default.createElement(NestedListButton, { onClick: toggleOpen, disabled: disabled },
159
+ react_1.default.createElement(NestedListButton, { className: "nested-list-button", onClick: toggleOpen, disabled: disabled, onKeyDown: (e) => {
160
+ if (e.key === 'ArrowRight') {
161
+ e.preventDefault();
162
+ toggleOpen();
163
+ }
164
+ } },
112
165
  react_1.default.createElement("div", null, buttonText),
113
166
  react_1.default.createElement(style_guide_1.TriangleCollapsedIcon, null)),
114
- isOpen && (react_1.default.createElement(NestedListDropdownList, { direction: 'right', moveLeft: moveLeft, width: 192, onClick: (e) => {
167
+ isOpen && (react_1.default.createElement(NestedListDropdownList, { direction: 'right', moveLeft: moveLeft, width: 192, ref: nestedListRef, onClick: (e) => {
115
168
  toggleOpen();
116
169
  parentToggleOpen(e);
117
170
  } }, list))));
@@ -154,7 +207,8 @@ const ListItemButton = (0, styled_components_1.default)(style_guide_1.IconTextBu
154
207
  justify-content: space-between;
155
208
  align-items: center;
156
209
 
157
- :hover {
210
+ :hover,
211
+ :focus-visible {
158
212
  background: #f2fbfc;
159
213
  }
160
214
 
@@ -173,7 +227,8 @@ const ListItemText = styled_components_1.default.div `
173
227
  const NestedListButton = (0, styled_components_1.default)(ListItemButton) `
174
228
  width: 100%;
175
229
  &:active,
176
- &:focus {
230
+ &:focus,
231
+ &:focus-visible {
177
232
  background: #f2fbfc;
178
233
  }
179
234
  svg {
@@ -188,4 +243,8 @@ const UploadButton = (0, styled_components_1.default)(style_guide_1.IconTextButt
188
243
  border-top: 1px solid #f2f2f2;
189
244
  padding: ${(props) => props.theme.grid.unit * 4}px;
190
245
  justify-content: flex-start;
246
+
247
+ &:focus-visible {
248
+ background: #f2fbfc;
249
+ }
191
250
  `;
@@ -56,7 +56,7 @@ const createEditorView = (props, root, state, dispatch) => {
56
56
  handleScrollToSelection: helpers_1.handleScrollToSelectedTarget,
57
57
  transformCopied: copy_1.transformCopied,
58
58
  handleClickOn: (view, pos, node, nodePos, event) => {
59
- props.onEditorClick(pos, node, nodePos, event);
59
+ props.onEditorClick(event);
60
60
  if (event?.target &&
61
61
  event.target.classList.contains('table-context-menu-button')) {
62
62
  return true;
package/dist/cjs/index.js CHANGED
@@ -51,6 +51,7 @@ var popper_1 = require("./lib/popper");
51
51
  Object.defineProperty(exports, "PopperManager", { enumerable: true, get: function () { return popper_1.PopperManager; } });
52
52
  __exportStar(require("./toolbar"), exports);
53
53
  __exportStar(require("./lib/capabilities"), exports);
54
+ __exportStar(require("./lib/navigation-utils"), exports);
54
55
  __exportStar(require("./lib/comments"), exports);
55
56
  __exportStar(require("./lib/files"), exports);
56
57
  __exportStar(require("./lib/footnotes"), exports);
@@ -22,10 +22,11 @@ const prosemirror_inputrules_1 = require("prosemirror-inputrules");
22
22
  const prosemirror_tables_1 = require("prosemirror-tables");
23
23
  const commands_1 = require("../commands");
24
24
  const list_1 = require("./list");
25
+ const navigation_utils_1 = require("../lib/navigation-utils");
25
26
  const customKeymap = {
26
27
  Backspace: (0, prosemirror_commands_1.chainCommands)(prosemirror_inputrules_1.undoInputRule, commands_1.ignoreAtomBlockNodeBackward, commands_1.ignoreMetaNodeBackspaceCommand, (0, list_1.skipCommandTracking)(prosemirror_commands_1.joinBackward)),
27
28
  Delete: commands_1.ignoreAtomBlockNodeForward,
28
- Tab: (0, prosemirror_tables_1.goToNextCell)(1),
29
+ Tab: (0, prosemirror_commands_1.chainCommands)((0, prosemirror_tables_1.goToNextCell)(1), navigation_utils_1.focusNearestElement),
29
30
  Escape: commands_1.exitEditorToContainer,
30
31
  'Mod-z': prosemirror_history_1.undo,
31
32
  'Mod-y': prosemirror_history_1.redo,
@@ -34,42 +34,6 @@ const insertParagraph = (dispatch, state, $anchor) => {
34
34
  tr.setSelection(prosemirror_state_1.TextSelection.create(tr.doc, pos + 1)).scrollIntoView();
35
35
  dispatch(tr);
36
36
  };
37
- const enterNextBlock = (dispatch, state, $anchor, create) => {
38
- const { schema: { nodes }, tr, } = state;
39
- const pos = $anchor.after($anchor.depth - 1);
40
- let selection = prosemirror_state_1.Selection.findFrom(tr.doc.resolve(pos), 1, true);
41
- if (!selection && create) {
42
- tr.insert(pos, nodes.paragraph.create());
43
- selection = prosemirror_state_1.Selection.findFrom(tr.doc.resolve(pos), 1, true);
44
- }
45
- if (!selection) {
46
- return false;
47
- }
48
- tr.setSelection(selection).scrollIntoView();
49
- dispatch(tr);
50
- return true;
51
- };
52
- const enterPreviousBlock = (dispatch, state, $anchor) => {
53
- const { tr } = state;
54
- const offset = $anchor.nodeBefore ? $anchor.nodeBefore.nodeSize : 0;
55
- const $pos = tr.doc.resolve($anchor.pos - offset - 1);
56
- const previous = prosemirror_state_1.Selection.findFrom($pos, -1, true);
57
- if (!previous) {
58
- return false;
59
- }
60
- tr.setSelection(prosemirror_state_1.TextSelection.create(tr.doc, previous.from)).scrollIntoView();
61
- dispatch(tr);
62
- return true;
63
- };
64
- const exitBlock = (direction) => (state, dispatch) => {
65
- const { selection: { $anchor }, } = state;
66
- if (dispatch) {
67
- return direction === 1
68
- ? enterNextBlock(dispatch, state, $anchor)
69
- : enterPreviousBlock(dispatch, state, $anchor);
70
- }
71
- return true;
72
- };
73
37
  const leaveTitle = (state, dispatch, view) => {
74
38
  const { selection } = state;
75
39
  if (!(0, commands_1.isTextSelection)(selection)) {
@@ -154,8 +118,6 @@ const keepCaption = (state) => {
154
118
  const titleKeymap = {
155
119
  Backspace: (0, prosemirror_commands_1.chainCommands)(protectTitles, exports.protectReferencesTitle, protectCaption),
156
120
  Enter: (0, prosemirror_commands_1.chainCommands)(commands_1.autoComplete, leaveTitle, leaveFigcaption),
157
- Tab: exitBlock(1),
158
121
  Delete: (0, prosemirror_commands_1.chainCommands)(keepCaption, exports.protectReferencesTitle),
159
- 'Shift-Tab': exitBlock(-1),
160
122
  };
161
123
  exports.default = titleKeymap;
@@ -35,6 +35,7 @@ const createCommentMarker = (tagName, key, count) => {
35
35
  element.id = getMarkerID(key);
36
36
  element.dataset.key = key;
37
37
  element.classList.add('comment-marker');
38
+ element.tabIndex = 0;
38
39
  if (count && count > 1) {
39
40
  element.dataset.count = String(count);
40
41
  }
@@ -19,6 +19,7 @@ exports.ContextMenu = exports.contextMenuBtnClass = exports.sectionLevel = void
19
19
  const style_guide_1 = require("@manuscripts/style-guide");
20
20
  const transform_1 = require("@manuscripts/transform");
21
21
  const prosemirror_state_1 = require("prosemirror-state");
22
+ const navigation_utils_1 = require("./navigation-utils");
22
23
  const prosemirror_utils_1 = require("prosemirror-utils");
23
24
  const react_1 = require("react");
24
25
  const server_1 = require("react-dom/server");
@@ -52,7 +53,9 @@ exports.contextMenuBtnClass = 'btn-context-menu';
52
53
  const contextSubmenuBtnClass = 'context-submenu-trigger';
53
54
  class ContextMenu {
54
55
  constructor(node, view, getPos) {
56
+ this.menuItems = [];
55
57
  this.showAddMenu = (target) => {
58
+ this.menuItems = [];
56
59
  const menu = document.createElement('div');
57
60
  menu.className = 'menu';
58
61
  const $pos = this.resolvePos();
@@ -150,6 +153,7 @@ class ContextMenu {
150
153
  this.addPopperEventListeners();
151
154
  };
152
155
  this.showEditMenu = (target) => {
156
+ this.menuItems = [];
153
157
  const menu = document.createElement('div');
154
158
  menu.className = 'menu';
155
159
  const $pos = this.resolvePos();
@@ -160,7 +164,7 @@ class ContextMenu {
160
164
  const figure = (0, utils_1.getMatchingChild)(this.node, (node) => node.type === transform_1.schema.nodes.figure);
161
165
  if (figure) {
162
166
  const attrType = figure.attrs.type;
163
- const submenuOptions = (0, position_menu_1.createPositionOptions)(transform_1.schema.nodes.figure, figure, attrType, this.view);
167
+ const submenuOptions = (0, position_menu_1.createPositionOptions)(transform_1.schema.nodes.figure, figure, attrType, this.view, () => popper.destroy());
164
168
  const submenuLabel = 'Position';
165
169
  const submenu = this.createSubmenu(submenuLabel, submenuOptions);
166
170
  menu.appendChild(submenu);
@@ -278,16 +282,28 @@ class ContextMenu {
278
282
  this.createSubmenuTrigger = (contents) => {
279
283
  const item = document.createElement('div');
280
284
  item.className = 'menu-item';
285
+ item.tabIndex = 0;
281
286
  const textNode = document.createTextNode(contents);
282
287
  item.innerHTML = (0, server_1.renderToStaticMarkup)((0, react_1.createElement)(style_guide_1.TriangleCollapsedIcon));
283
288
  item.prepend(textNode);
284
289
  item.classList.add(contextSubmenuBtnClass);
285
290
  item.addEventListener('mousedown', this.toggleSubmenu);
291
+ item.addEventListener('keydown', (0, navigation_utils_1.handleEnterKey)((e) => {
292
+ const target = e.target;
293
+ this.toggleSubmenu(e);
294
+ const submenuContent = target.nextElementSibling;
295
+ if (submenuContent?.classList.contains('show')) {
296
+ const firstItem = submenuContent.querySelector('.menu-item');
297
+ firstItem?.focus();
298
+ }
299
+ }));
300
+ this.menuItems.push(item);
286
301
  return item;
287
302
  };
288
303
  this.createMenuItem = (contents, handler, IconComponent = null, selected = false) => {
289
304
  const item = document.createElement('div');
290
305
  item.className = 'menu-item';
306
+ item.setAttribute('tabindex', '0');
291
307
  selected && item.classList.add('selected');
292
308
  if (IconComponent) {
293
309
  if (typeof IconComponent === 'string') {
@@ -303,6 +319,8 @@ class ContextMenu {
303
319
  event.preventDefault();
304
320
  handler(event);
305
321
  });
322
+ item.addEventListener('keydown', (0, navigation_utils_1.handleEnterKey)(handler));
323
+ this.menuItems.push(item);
306
324
  return item;
307
325
  };
308
326
  this.createMenuSection = (createMenuItems, isSubmenu = false) => {
@@ -399,14 +417,62 @@ class ContextMenu {
399
417
  popper.destroy();
400
418
  });
401
419
  };
402
- const keyListener = (event) => {
403
- if (event.key === 'Escape') {
404
- window.removeEventListener('keydown', keyListener);
405
- popper.destroy();
406
- }
407
- };
408
420
  window.addEventListener('mousedown', mouseListener);
409
- window.addEventListener('keydown', keyListener);
421
+ window.requestAnimationFrame(() => {
422
+ const popperContainer = popper.getContainer();
423
+ if (popperContainer) {
424
+ (0, navigation_utils_1.createKeyboardInteraction)({
425
+ container: popperContainer,
426
+ navigation: {
427
+ getItems: () => {
428
+ const activeElement = document.activeElement;
429
+ const openSubmenu = activeElement?.closest('.menu-section.menu.show');
430
+ if (openSubmenu) {
431
+ return this.menuItems.filter((item) => openSubmenu.contains(item));
432
+ }
433
+ return this.menuItems.filter((item) => {
434
+ const menuSection = item.closest('.menu-section');
435
+ if (menuSection && menuSection.classList.contains('menu')) {
436
+ return menuSection.classList.contains('show');
437
+ }
438
+ return true;
439
+ });
440
+ },
441
+ arrowKeys: {
442
+ forward: 'ArrowDown',
443
+ backward: 'ArrowUp',
444
+ },
445
+ getCurrentElement: () => document.activeElement,
446
+ },
447
+ additionalKeys: {
448
+ ArrowRight: (event) => {
449
+ const target = event.target;
450
+ if (target.classList.contains(contextSubmenuBtnClass)) {
451
+ this.toggleSubmenu(event);
452
+ const submenuContent = target.nextElementSibling;
453
+ if (submenuContent?.classList.contains('show')) {
454
+ const firstItem = submenuContent.querySelector('.menu-item');
455
+ firstItem?.focus();
456
+ }
457
+ }
458
+ },
459
+ ArrowLeft: (event) => {
460
+ const target = event.target;
461
+ const submenu = target.closest('.context-submenu');
462
+ if (submenu) {
463
+ const trigger = submenu.querySelector(`.${contextSubmenuBtnClass}`);
464
+ if (trigger) {
465
+ const submenuContent = trigger.nextElementSibling;
466
+ submenuContent?.classList.toggle('show');
467
+ trigger.focus();
468
+ }
469
+ }
470
+ },
471
+ },
472
+ });
473
+ }
474
+ this.menuItems[0]?.focus();
475
+ });
410
476
  };
411
477
  this.trimTitle = (title, max) => {
412
478
  return title.length > max ? title.substring(0, max) + '…' : title;
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.addInteractionHandlers = exports.createFileUploader = exports.createReactTools = exports.createFileHandlers = exports.createMediaPlaceholder = exports.MediaType = exports.createUnsupportedFormat = void 0;
22
22
  const transform_1 = require("@manuscripts/transform");
23
23
  const InsertEmbedDialog_1 = require("../components/toolbar/InsertEmbedDialog");
24
+ const navigation_utils_1 = require("./navigation-utils");
24
25
  const FigureDropdown_1 = require("../components/views/FigureDropdown");
25
26
  const icons_1 = require("../icons");
26
27
  const ReactSubView_1 = __importDefault(require("../views/ReactSubView"));
@@ -57,9 +58,10 @@ const MediaLabels = {
57
58
  [MediaType.Figure]: 'figure',
58
59
  [MediaType.ExternalLink]: 'a file to link',
59
60
  };
60
- const createMediaPlaceholder = (mediaType = MediaType.Media, view, getPos) => {
61
+ const createMediaPlaceholder = (mediaType = MediaType.Media, view, getPos, props) => {
61
62
  const element = document.createElement('div');
62
63
  element.classList.add('figure', 'placeholder');
64
+ element.tabIndex = 0;
63
65
  const instructions = document.createElement('div');
64
66
  instructions.classList.add('instructions');
65
67
  instructions.innerHTML = `
@@ -79,14 +81,29 @@ const createMediaPlaceholder = (mediaType = MediaType.Media, view, getPos) => {
79
81
  `;
80
82
  if (mediaType === MediaType.Media && view && getPos) {
81
83
  const embedLink = instructions.querySelector("[data-action='add-external-link']");
84
+ embedLink.tabIndex = 0;
82
85
  if (embedLink) {
83
86
  embedLink.addEventListener('click', (e) => {
84
87
  e.stopPropagation();
85
88
  e.preventDefault();
86
89
  (0, InsertEmbedDialog_1.openEmbedDialog)(view, getPos());
87
90
  });
91
+ embedLink.addEventListener('keydown', (0, navigation_utils_1.handleEnterKey)((e) => {
92
+ e.stopPropagation();
93
+ e.preventDefault();
94
+ (0, InsertEmbedDialog_1.openEmbedDialog)(view, getPos());
95
+ }));
88
96
  }
89
97
  }
98
+ const links = instructions.querySelectorAll(`[data-action='open-other-files'], [data-action='open-supplement-files'] `);
99
+ links.forEach((link) => {
100
+ link.tabIndex = 0;
101
+ link.addEventListener('keydown', (0, navigation_utils_1.handleEnterKey)((event) => {
102
+ event.preventDefault();
103
+ event.stopPropagation();
104
+ props?.onEditorClick(event);
105
+ }));
106
+ });
90
107
  element.appendChild(instructions);
91
108
  return element;
92
109
  };
@@ -165,8 +182,7 @@ const createFileUploader = (handler, accept = '*/*') => {
165
182
  };
166
183
  exports.createFileUploader = createFileUploader;
167
184
  const addInteractionHandlers = (element, uploadFn, accept = '*/*') => {
168
- const handlePlaceholderClick = (event) => {
169
- const target = event.target;
185
+ const handlePlaceholderInteraction = (target) => {
170
186
  if (target.dataset && target.dataset.action) {
171
187
  return;
172
188
  }
@@ -181,7 +197,15 @@ const addInteractionHandlers = (element, uploadFn, accept = '*/*') => {
181
197
  });
182
198
  input.click();
183
199
  };
200
+ const handlePlaceholderClick = (event) => {
201
+ const target = event.target;
202
+ handlePlaceholderInteraction(target);
203
+ };
184
204
  element.addEventListener('click', handlePlaceholderClick);
205
+ element.addEventListener('keydown', (0, navigation_utils_1.handleEnterKey)(() => {
206
+ const target = document.activeElement;
207
+ handlePlaceholderInteraction(target);
208
+ }));
185
209
  element.addEventListener('mouseenter', () => {
186
210
  element.classList.toggle('over', true);
187
211
  });
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /*!
3
+ * © 2025 Atypon Systems LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.focusNearestElement = void 0;
19
+ exports.focusNextElement = focusNextElement;
20
+ exports.handleEnterKey = handleEnterKey;
21
+ exports.handleArrowNavigation = handleArrowNavigation;
22
+ exports.createKeyboardInteraction = createKeyboardInteraction;
23
+ exports.getCursorContainer = getCursorContainer;
24
+ exports.findNearestTabbable = findNearestTabbable;
25
+ const utils_1 = require("./utils");
26
+ function focusNextElement(elements, currentIndex, direction) {
27
+ const nextIndex = direction === 'forward'
28
+ ? (currentIndex + 1) % elements.length
29
+ : (currentIndex - 1 + elements.length) % elements.length;
30
+ const nextElement = elements[nextIndex];
31
+ nextElement?.focus();
32
+ }
33
+ function handleEnterKey(action) {
34
+ return (event) => {
35
+ if (event.key === 'Enter') {
36
+ event.preventDefault();
37
+ action(event);
38
+ }
39
+ };
40
+ }
41
+ function handleArrowNavigation(event, elements, currentElement, keys) {
42
+ const { forward, backward } = keys;
43
+ if (event.key !== forward && event.key !== backward) {
44
+ return;
45
+ }
46
+ event.preventDefault();
47
+ if (elements.length === 0) {
48
+ return;
49
+ }
50
+ const currentIndex = elements.indexOf(currentElement);
51
+ if (currentIndex === -1) {
52
+ elements[0]?.focus();
53
+ return;
54
+ }
55
+ const direction = event.key === forward ? 'forward' : 'backward';
56
+ focusNextElement(elements, currentIndex, direction);
57
+ }
58
+ function createKeyboardInteraction(options) {
59
+ const { container, additionalKeys, navigation } = options;
60
+ const handleKeydown = (event) => {
61
+ const e = event;
62
+ const key = e.key;
63
+ const handler = additionalKeys?.[key];
64
+ if (handler) {
65
+ e.preventDefault();
66
+ handler(e);
67
+ return;
68
+ }
69
+ if (!navigation) {
70
+ return;
71
+ }
72
+ const { getItems, arrowKeys, getCurrentElement } = navigation;
73
+ const currentElement = getCurrentElement
74
+ ? getCurrentElement(e)
75
+ : e.target;
76
+ if (!currentElement) {
77
+ return;
78
+ }
79
+ const list = getItems();
80
+ if (!list.length) {
81
+ return;
82
+ }
83
+ handleArrowNavigation(e, list, currentElement, arrowKeys);
84
+ };
85
+ container.addEventListener('keydown', handleKeydown);
86
+ return () => {
87
+ container.removeEventListener('keydown', handleKeydown);
88
+ };
89
+ }
90
+ const focusNearestElement = (state, dispatch, view) => {
91
+ if (!view) {
92
+ return false;
93
+ }
94
+ const active = document.activeElement;
95
+ if (!active || !active.classList.contains('manuscript-editor')) {
96
+ return false;
97
+ }
98
+ const { from } = view.state.selection;
99
+ const coords = view.coordsAtPos(from);
100
+ const container = getCursorContainer(view);
101
+ const target = findNearestTabbable(container, coords.top);
102
+ if (!target) {
103
+ return false;
104
+ }
105
+ target.focus();
106
+ return true;
107
+ };
108
+ exports.focusNearestElement = focusNearestElement;
109
+ function getCursorContainer(view) {
110
+ const scoped = (0, utils_1.findParentNodeWithIdValue)(view.state.selection);
111
+ if (scoped) {
112
+ const dom = view.nodeDOM(scoped.pos);
113
+ if (dom instanceof HTMLElement) {
114
+ return dom;
115
+ }
116
+ }
117
+ return view.dom;
118
+ }
119
+ function findNearestTabbable(container, verticalPosition) {
120
+ const tabbables = container.querySelectorAll('a[href], button, [tabindex]:not([tabindex="-1"])');
121
+ let target = null;
122
+ let minDistance = null;
123
+ tabbables.forEach((el) => {
124
+ const rect = el.getBoundingClientRect();
125
+ const distance = Math.abs(rect.top - verticalPosition);
126
+ if (minDistance === null || distance < minDistance) {
127
+ minDistance = distance;
128
+ target = el;
129
+ }
130
+ });
131
+ return target;
132
+ }