Skip to content

Commit 0f00522

Browse files
authored
fix(types): type contextMenu.customItems as ContextMenuSection[] (SD-2514) (#2777)
* fix(types): type contextMenu.customItems as ContextMenuSection[] (SD-2514) contextMenu.customItems was typed as bare {Array} which resolved to any[] in TypeScript. Define ContextMenuItem, ContextMenuSection, and ContextMenuConfig typedefs with proper shapes derived from runtime usage in the context menu component. Also types menuProvider callback signature. * fix(types): widen context menu callback types to match runtime (SD-2514) action receives (editor, context), not just (editor). Extract ContextMenuContext typedef with the full runtime shape (isInTable, documentMode, activeMarks, etc.) and use it across showWhen, render, action, and menuProvider. Also simplify AugmentedChainedCommands to reuse Chainified<KnownCommandRecord> instead of restating the mapped type. * fix(types): allow menuProvider to return null/undefined (SD-2514) Runtime falls back to original sections when menuProvider returns falsy (menuProvider(...) || allSections), so the type should allow it.
1 parent 2d93b10 commit 0f00522

File tree

4 files changed

+108
-11
lines changed

4 files changed

+108
-11
lines changed

packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,10 @@ type Chainified<T> = {
7878
* but consumers who augment ExtensionCommandMap get their custom commands
7979
* on chain() for free.
8080
*/
81-
type AugmentedChainedCommands = {
82-
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => ChainableCommandObject;
83-
};
81+
type AugmentedChainedCommands = Chainified<KnownCommandRecord>;
8482

85-
/** Same as AugmentedChainedCommands but with original return types for can(). */
86-
type AugmentedCanCommands = {
87-
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => CommandResult<K>;
88-
};
83+
/** Same but with original return types for can(). */
84+
type AugmentedCanCommands = KnownCommandRecord;
8985

9086
/**
9187
* A chainable version of an editor command keyed by command name.

packages/superdoc/src/core/types/index.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,63 @@
8686
* @typedef {(ctx: LinkPopoverContext) => LinkPopoverResolution | null | undefined} LinkPopoverResolver
8787
*/
8888

89+
// ---------------------------------------------------------------------------
90+
// Context menu types
91+
// ---------------------------------------------------------------------------
92+
93+
/**
94+
* Context object passed to context menu callbacks (showWhen, render, action, menuProvider).
95+
* @typedef {Object} ContextMenuContext
96+
* @property {Editor} editor The editor instance
97+
* @property {string} selectedText Currently selected text (empty string if no selection)
98+
* @property {boolean} hasSelection Whether there is an expanded selection
99+
* @property {number} selectionStart ProseMirror start position of the selection
100+
* @property {number} selectionEnd ProseMirror end position of the selection
101+
* @property {'click' | 'slash'} trigger How the menu was opened
102+
* @property {boolean} isInTable Whether the cursor is inside a table
103+
* @property {boolean} isInList Whether the cursor is inside a list
104+
* @property {boolean} isInSectionNode Whether the cursor is inside a document section
105+
* @property {boolean} isCellSelection Whether a table cell selection is active
106+
* @property {string | null} tableSelectionKind Kind of table selection (row, column, etc.)
107+
* @property {string | null} currentNodeType ProseMirror node type name at the cursor
108+
* @property {string[]} activeMarks Names of marks active at the cursor
109+
* @property {boolean} isTrackedChange Whether the cursor is on a tracked change
110+
* @property {string | null} trackedChangeId ID of the tracked change at the cursor
111+
* @property {string} documentMode Current document mode (editing, viewing, suggesting)
112+
* @property {boolean} canUndo Whether undo is available
113+
* @property {boolean} canRedo Whether redo is available
114+
* @property {boolean} isEditable Whether the editor is editable
115+
* @property {{ x: number, y: number } | null} cursorPosition Screen coordinates of the cursor
116+
*/
117+
118+
/**
119+
* A single item inside a context menu section.
120+
* @typedef {Object} ContextMenuItem
121+
* @property {string} id Unique identifier for the menu item
122+
* @property {string} label Display text
123+
* @property {string} [icon] Icon identifier
124+
* @property {unknown} [component] Custom Vue component to render this item
125+
* @property {(editor: Editor, context: ContextMenuContext) => void} [action] Callback invoked when the item is clicked
126+
* @property {(context: ContextMenuContext) => boolean} [showWhen] Predicate controlling visibility
127+
* @property {(context: ContextMenuContext) => HTMLElement} [render] Custom renderer returning an HTML element
128+
* @property {string} [shortcut] Keyboard shortcut label displayed beside the item
129+
*/
130+
131+
/**
132+
* A section (group) of items in the context menu.
133+
* @typedef {Object} ContextMenuSection
134+
* @property {string} id Unique identifier for the section
135+
* @property {ContextMenuItem[]} items Menu items in this section
136+
*/
137+
138+
/**
139+
* Configuration for the context menu module.
140+
* @typedef {Object} ContextMenuConfig
141+
* @property {ContextMenuSection[]} [customItems] Custom menu sections appended (or merged by id) to the default menu
142+
* @property {(context: ContextMenuContext, sections: ContextMenuSection[]) => ContextMenuSection[] | null | undefined} [menuProvider] Advanced: transform the final section list before render. Return null/undefined to keep the original sections.
143+
* @property {boolean} [includeDefaultItems] Whether to include default menu items (default: true)
144+
*/
145+
89146
// ---------------------------------------------------------------------------
90147
// Surface system types
91148
// ---------------------------------------------------------------------------
@@ -469,10 +526,7 @@
469526
* @property {Object} [toolbar] Toolbar module configuration
470527
* @property {Object} [links] Link click popover configuration
471528
* @property {LinkPopoverResolver} [links.popoverResolver] Custom resolver for the link click popover.
472-
* @property {Object} [contextMenu] Context menu module configuration
473-
* @property {Array} [contextMenu.customItems] Array of custom menu sections with items
474-
* @property {Function} [contextMenu.menuProvider] Function to customize menu items
475-
* @property {boolean} [contextMenu.includeDefaultItems] Whether to include default menu items
529+
* @property {ContextMenuConfig} [contextMenu] Context menu module configuration
476530
* @property {Object} [slashMenu] @deprecated Use contextMenu instead
477531
* @property {SurfacesModuleConfig} [surfaces] Surface system configuration
478532
*/

packages/superdoc/src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ import { getSchemaIntrospection } from './helpers/schema-introspection.js';
162162
* @typedef {import('@superdoc/super-editor').ProofingStatus} ProofingStatus
163163
* @typedef {import('@superdoc/super-editor').ProofingError} ProofingError
164164
* @typedef {import('@superdoc/super-editor').PageStyles} PageStyles
165+
*
166+
* @typedef {import('./core/types/index.js').ContextMenuContext} ContextMenuContext
167+
* @typedef {import('./core/types/index.js').ContextMenuItem} ContextMenuItem
168+
* @typedef {import('./core/types/index.js').ContextMenuSection} ContextMenuSection
169+
* @typedef {import('./core/types/index.js').ContextMenuConfig} ContextMenuConfig
165170
*/
166171

167172
// Public exports

tests/consumer-typecheck/src/customer-scenario.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ import type {
132132
ProofingStatus,
133133
ProofingError,
134134

135+
// Context menu
136+
ContextMenuContext,
137+
ContextMenuItem,
138+
ContextMenuSection,
139+
ContextMenuConfig,
140+
135141
// Other
136142
UnsupportedContentItem,
137143
PageStyles,
@@ -583,6 +589,41 @@ function testEditorContentMethods(editor: Editor) {
583589
editor.destroy();
584590
}
585591

592+
// ============================================
593+
// SECTION 12b: Context menu types (SD-2514)
594+
// ============================================
595+
596+
function testContextMenuTypes() {
597+
// action receives (editor, context) — both args
598+
const item: ContextMenuItem = {
599+
id: 'copy-text',
600+
label: 'Copy',
601+
icon: 'copy',
602+
action: (editor, context) => {
603+
editor.commands.selectAll();
604+
const text: string = context.selectedText;
605+
},
606+
showWhen: (ctx) => ctx.hasSelection && !ctx.isInTable,
607+
};
608+
609+
const section: ContextMenuSection = {
610+
id: 'custom-actions',
611+
items: [item],
612+
};
613+
614+
const config: ContextMenuConfig = {
615+
customItems: [section],
616+
includeDefaultItems: true,
617+
menuProvider: (ctx, sections) => sections.filter((s) => s.id !== 'clipboard'),
618+
};
619+
620+
// ContextMenuContext has the full runtime shape
621+
const ctx: ContextMenuContext = {} as ContextMenuContext;
622+
const _trigger: 'click' | 'slash' = ctx.trigger;
623+
const _mode: string = ctx.documentMode;
624+
const _marks: string[] = ctx.activeMarks;
625+
}
626+
586627
// ============================================
587628
// SECTION 13: Extensions, SuperDoc, and utilities
588629
// ============================================
@@ -679,6 +720,7 @@ export {
679720
testProofingProvider,
680721
testEditorStaticMethods,
681722
testEditorContentMethods,
723+
testContextMenuTypes,
682724
testExtensions,
683725
testSuperDoc,
684726
testUtilities,

0 commit comments

Comments
 (0)