diff --git a/docs/content/docs/2.components/editor-emoji-menu.md b/docs/content/docs/2.components/editor-emoji-menu.md
index 143ad86808..c186dfa548 100644
--- a/docs/content/docs/2.components/editor-emoji-menu.md
+++ b/docs/content/docs/2.components/editor-emoji-menu.md
@@ -73,6 +73,26 @@ Use the `char` prop to change the trigger character. Defaults to `:`{lang="ts-ty
```
+### Suggestion :badge{label="Soon" class="align-text-top"}
+
+Use the `suggestion` prop to customize TipTap's [Suggestion matching behavior](https://tiptap.dev/docs/editor/api/utilities/suggestion#settings).
+
+This is useful when the trigger character should open directly after other characters instead of requiring the default whitespace prefix.
+
+```vue
+
+
+
+
+
+```
+
### Options
Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options).
diff --git a/docs/content/docs/2.components/editor-mention-menu.md b/docs/content/docs/2.components/editor-mention-menu.md
index 57317fdbc6..46035a0818 100644
--- a/docs/content/docs/2.components/editor-mention-menu.md
+++ b/docs/content/docs/2.components/editor-mention-menu.md
@@ -81,6 +81,27 @@ You can use multiple `EditorMentionMenu` components on the same editor with diff
```
::
+### Suggestion :badge{label="Soon" class="align-text-top"}
+
+Use the `suggestion` prop to customize TipTap's [Suggestion matching behavior](https://tiptap.dev/docs/editor/api/utilities/suggestion#settings).
+
+This is useful when the trigger character should open directly after other characters instead of requiring the default whitespace prefix.
+
+```vue
+
+
+
+
+
+```
+
### Options
Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options).
diff --git a/docs/content/docs/2.components/editor-suggestion-menu.md b/docs/content/docs/2.components/editor-suggestion-menu.md
index 0d5bb426c4..84d85abbf7 100644
--- a/docs/content/docs/2.components/editor-suggestion-menu.md
+++ b/docs/content/docs/2.components/editor-suggestion-menu.md
@@ -69,6 +69,27 @@ Use the `char` prop to change the trigger character. Defaults to `/`{lang="ts-ty
```
+### Suggestion :badge{label="Soon" class="align-text-top"}
+
+Use the `suggestion` prop to customize TipTap's [Suggestion matching behavior](https://tiptap.dev/docs/editor/api/utilities/suggestion#settings).
+
+This is useful when the trigger character should open directly after other characters instead of requiring the default whitespace prefix.
+
+```vue
+
+
+
+
+
+```
+
### Options
Use the `options` prop to customize the positioning behavior using [Floating UI options](https://floating-ui.com/docs/computeposition#options).
diff --git a/src/runtime/components/EditorEmojiMenu.vue b/src/runtime/components/EditorEmojiMenu.vue
index cb3ab986eb..187eb8d91c 100644
--- a/src/runtime/components/EditorEmojiMenu.vue
+++ b/src/runtime/components/EditorEmojiMenu.vue
@@ -16,7 +16,7 @@ export interface EditorEmojiMenuItem {
[key: string]: any
}
-export interface EditorEmojiMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'appendTo'>> {
+export interface EditorEmojiMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'suggestion' | 'appendTo'>> {
/**
* @defaultValue 'md'
*/
@@ -64,6 +64,7 @@ onMounted(async () => {
filterFields: props.filterFields,
limit: props.limit,
options: props.options,
+ suggestion: props.suggestion,
appendTo: props.appendTo,
ui,
onSelect: (editor, range, item) => {
diff --git a/src/runtime/components/EditorMentionMenu.vue b/src/runtime/components/EditorMentionMenu.vue
index f776b6c8f3..96ac3acd9a 100644
--- a/src/runtime/components/EditorMentionMenu.vue
+++ b/src/runtime/components/EditorMentionMenu.vue
@@ -20,7 +20,7 @@ export interface EditorMentionMenuItem {
[key: string]: any
}
-export interface EditorMentionMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'appendTo' | 'ignoreFilter'>> {
+export interface EditorMentionMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'suggestion' | 'appendTo' | 'ignoreFilter'>> {
/**
* @defaultValue 'md'
*/
@@ -72,6 +72,7 @@ onMounted(async () => {
ignoreFilter: props.ignoreFilter,
limit: props.limit,
options: props.options,
+ suggestion: props.suggestion,
appendTo: props.appendTo,
searchTerm,
ui,
diff --git a/src/runtime/components/EditorSuggestionMenu.vue b/src/runtime/components/EditorSuggestionMenu.vue
index d4bce9a317..0d2f65e30f 100644
--- a/src/runtime/components/EditorSuggestionMenu.vue
+++ b/src/runtime/components/EditorSuggestionMenu.vue
@@ -39,7 +39,7 @@ export type EditorSuggestionMenuItem
-export interface EditorSuggestionMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'appendTo'>> {
+export interface EditorSuggestionMenuProps extends Partial, 'editor' | 'char' | 'pluginKey' | 'filterFields' | 'limit' | 'options' | 'suggestion' | 'appendTo'>> {
/**
* @defaultValue 'md'
*/
@@ -90,6 +90,7 @@ onMounted(async () => {
filterFields: props.filterFields,
limit: props.limit,
options: props.options,
+ suggestion: props.suggestion,
appendTo: props.appendTo,
ui,
onSelect: (editor, range, item) => {
diff --git a/src/runtime/composables/useEditorMenu.ts b/src/runtime/composables/useEditorMenu.ts
index d62845ce2a..049686b816 100644
--- a/src/runtime/composables/useEditorMenu.ts
+++ b/src/runtime/composables/useEditorMenu.ts
@@ -6,7 +6,7 @@ import { computePosition } from '@floating-ui/dom'
import type { Strategy, Placement } from '@floating-ui/dom'
import type { Editor } from '@tiptap/vue-3'
import { VueRenderer } from '@tiptap/vue-3'
-import type { SuggestionProps } from '@tiptap/suggestion'
+import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'
import Suggestion from '@tiptap/suggestion'
import { PluginKey } from '@tiptap/pm/state'
import type { FloatingUIOptions } from '../types/editor'
@@ -65,6 +65,10 @@ export interface EditorMenuOptions {
* @see https://floating-ui.com/docs/computePosition#options
*/
options?: FloatingUIOptions
+ /**
+ * Optional TipTap Suggestion matching options.
+ */
+ suggestion?: Omit, 'pluginKey' | 'editor' | 'char' | 'items' | 'command' | 'render'>
/**
* The DOM element to append the menu to. Default is the editor's parent element.
*
@@ -333,7 +337,8 @@ export function useEditorMenu(options: EditorMenuOptions) {
element.addEventListener('mousedown', handleMouseDown)
const appendToElement = typeof options.appendTo === 'function' ? options.appendTo() : options.appendTo
- ;(appendToElement ?? options.editor.view.dom.parentElement)?.appendChild(element)
+ const container = appendToElement ?? options.editor.view.dom.parentElement
+ container?.appendChild(element)
if (renderer.element) {
element.appendChild(renderer.element)
}
@@ -490,6 +495,7 @@ export function useEditorMenu(options: EditorMenuOptions) {
// Create the suggestion plugin
const plugin = Suggestion({
+ ...(options.suggestion || {}),
pluginKey: pluginKeyInstance,
editor: options.editor,
char: options.char,
diff --git a/test/composables/useEditorMenu.spec.ts b/test/composables/useEditorMenu.spec.ts
new file mode 100644
index 0000000000..dfc8b793ea
--- /dev/null
+++ b/test/composables/useEditorMenu.spec.ts
@@ -0,0 +1,149 @@
+import { computed } from 'vue'
+import { beforeEach, describe, expect, it, vi, expectTypeOf } from 'vitest'
+import type { SuggestionOptions } from '@tiptap/suggestion'
+import { PluginKey } from '@tiptap/pm/state'
+import { useEditorMenu } from '../../src/runtime/composables/useEditorMenu'
+import type { EditorMenuOptions } from '../../src/runtime/composables/useEditorMenu'
+import type { EditorSuggestionMenuProps } from '../../src/runtime/components/EditorSuggestionMenu.vue'
+import type { EditorMentionMenuProps } from '../../src/runtime/components/EditorMentionMenu.vue'
+import type { EditorEmojiMenuProps } from '../../src/runtime/components/EditorEmojiMenu.vue'
+
+const { suggestionMock } = vi.hoisted(() => ({
+ suggestionMock: vi.fn((config: any) => config)
+}))
+
+vi.mock('@tiptap/suggestion', () => ({
+ default: suggestionMock
+}))
+
+function createEditor() {
+ const dom = document.createElement('div')
+ const parent = document.createElement('div')
+ parent.appendChild(dom)
+
+ return {
+ isDestroyed: false,
+ view: {
+ dom,
+ state: {
+ tr: {
+ setMeta: vi.fn(() => ({}))
+ }
+ },
+ dispatch: vi.fn()
+ }
+ } as any
+}
+
+function createOptions(overrides: Partial> = {}): EditorMenuOptions<{ label: string }> {
+ return {
+ editor: createEditor(),
+ char: ':',
+ pluginKey: 'suggestion-menu',
+ items: [{ label: 'Alpha' }, { label: 'Beta' }],
+ onSelect: vi.fn(),
+ renderItem: vi.fn(() => []),
+ ui: computed(() => ({
+ content: () => '',
+ viewport: () => '',
+ group: () => '',
+ label: () => '',
+ separator: () => '',
+ item: () => '',
+ itemLeadingIcon: () => '',
+ itemWrapper: () => '',
+ itemLabel: () => '',
+ itemDescription: () => ''
+ })),
+ ...overrides
+ }
+}
+
+function getSuggestionConfig() {
+ if (suggestionMock.mock.calls.length !== 1) {
+ throw new Error(`Suggestion should be called exactly once, but was called ${suggestionMock.mock.calls.length} times`)
+ }
+
+ return suggestionMock.mock.calls[0]![0]
+}
+
+describe('useEditorMenu', () => {
+ beforeEach(() => {
+ suggestionMock.mockClear()
+ })
+
+ it('forwards suggestion matching options', () => {
+ useEditorMenu(createOptions({
+ suggestion: {
+ allowedPrefixes: null,
+ allowSpaces: true,
+ startOfLine: true
+ }
+ }))
+
+ const config = getSuggestionConfig()
+
+ expect(config.allowedPrefixes).toBeNull()
+ expect(config.allowSpaces).toBe(true)
+ expect(config.startOfLine).toBe(true)
+ expect(config.char).toBe(':')
+ })
+
+ it('keeps existing defaults when suggestion is omitted', () => {
+ useEditorMenu(createOptions())
+
+ const config = getSuggestionConfig()
+ const items = config.items({ query: 'al' })
+
+ expect(config).not.toHaveProperty('allowedPrefixes')
+ expect(items).toEqual([{ label: 'Alpha' }])
+ })
+
+ it('keeps plugin identity fields authoritative over suggestion overrides', () => {
+ const wrongPluginKey = new PluginKey('wrong-plugin-key')
+ const wrongEditor = createEditor()
+
+ useEditorMenu(createOptions({
+ suggestion: {
+ pluginKey: wrongPluginKey,
+ editor: wrongEditor,
+ char: '@'
+ } as Partial
+ }))
+
+ const config = getSuggestionConfig()
+
+ expect(config.pluginKey).not.toBe(wrongPluginKey)
+ expect(config.editor).not.toBe(wrongEditor)
+ expect(config.char).toBe(':')
+ })
+
+ it('keeps menu callbacks authoritative over suggestion overrides', () => {
+ const suggestionItems = vi.fn(() => [])
+ const suggestionCommand = vi.fn()
+ const suggestionRender = vi.fn()
+
+ useEditorMenu(createOptions({
+ suggestion: {
+ items: suggestionItems,
+ command: suggestionCommand,
+ render: suggestionRender
+ } as Partial
+ }))
+
+ const config = getSuggestionConfig()
+
+ expect(config.items).not.toBe(suggestionItems)
+ expect(config.command).not.toBe(suggestionCommand)
+ expect(config.render).not.toBe(suggestionRender)
+ })
+
+ it('types suggestion options on the composable and component props', () => {
+ type ExpectedType = Omit, 'pluginKey' | 'editor' | 'char' | 'items' | 'command' | 'render'> | undefined
+
+ expectTypeOf['suggestion']>().toEqualTypeOf()
+ expectTypeOf().toEqualTypeOf()
+ expectTypeOf().toEqualTypeOf()
+ expectTypeOf().toEqualTypeOf()
+ })
+})