diff --git a/apps/docs/core/superdoc/configuration.mdx b/apps/docs/core/superdoc/configuration.mdx index ec90f4406..5b83d3464 100644 --- a/apps/docs/core/superdoc/configuration.mdx +++ b/apps/docs/core/superdoc/configuration.mdx @@ -245,6 +245,7 @@ new SuperDoc({ Use `'web'` for mobile devices and WCAG AA reflow compliance (Success Criterion 1.4.10). When set to `'web'`, the layout engine is automatically disabled. + Web layout also forces `disableContextMenu: true` to prevent long-press context menus on mobile. ```javascript viewOptions: { layout: 'web' } diff --git a/apps/docs/modules/context-menu.mdx b/apps/docs/modules/context-menu.mdx index 26675f0ba..ebf5c2745 100644 --- a/apps/docs/modules/context-menu.mdx +++ b/apps/docs/modules/context-menu.mdx @@ -7,7 +7,8 @@ A contextual command menu triggered by right-clicking. Shows relevant actions ba ## Quick start -The context menu is **enabled by default**. Right-click anywhere in the document to open it. +The context menu is **enabled by default** in print layout. In web layout (`viewOptions.layout = 'web'`), the custom +context menu is disabled to avoid long-press menus on mobile. To disable it: @@ -35,6 +36,7 @@ new SuperDoc({ Top-level option to disable the context menu entirely + Web layout (`viewOptions.layout = 'web'`) forces this to `true`. diff --git a/packages/super-editor/src/core/Editor.ts b/packages/super-editor/src/core/Editor.ts index 89e2e80e6..2fdf0d904 100644 --- a/packages/super-editor/src/core/Editor.ts +++ b/packages/super-editor/src/core/Editor.ts @@ -253,6 +253,7 @@ export class Editor extends EventEmitter { * Guard flag to prevent double-tracking document open */ #documentOpenTracked = false; + #disableContextMenuBeforeWebLayout: boolean | undefined = undefined; options: EditorOptions = { element: null, @@ -1486,10 +1487,36 @@ export class Editor extends EventEmitter { * Set editor options and update state. */ setOptions(options: Partial = {}): void { - this.options = { + const prevLayout = this.options.viewOptions?.layout; + const prevDisableContextMenu = this.options.disableContextMenu; + const disableContextMenuProvided = Object.prototype.hasOwnProperty.call(options, 'disableContextMenu'); + const nextOptions = { ...this.options, ...options, }; + const nextLayout = nextOptions.viewOptions?.layout; + const nextDisableContextMenu = disableContextMenuProvided ? options.disableContextMenu : prevDisableContextMenu; + + if (nextLayout === 'web') { + if (prevLayout !== 'web') { + this.#disableContextMenuBeforeWebLayout = nextDisableContextMenu; + } else if (disableContextMenuProvided) { + this.#disableContextMenuBeforeWebLayout = nextDisableContextMenu; + } + // Web layout mode should not surface the context menu (e.g., on mobile long-press). + nextOptions.disableContextMenu = true; + } else if (prevLayout === 'web') { + if (disableContextMenuProvided) { + nextOptions.disableContextMenu = nextDisableContextMenu; + } else { + nextOptions.disableContextMenu = this.#disableContextMenuBeforeWebLayout ?? false; + } + this.#disableContextMenuBeforeWebLayout = undefined; + } else if (disableContextMenuProvided) { + nextOptions.disableContextMenu = nextDisableContextMenu; + } + + this.options = nextOptions; if ((this.options.isNewFile || !this.options.ydoc) && this.options.isCommentsEnabled) { this.options.shouldLoadComments = true; diff --git a/packages/super-editor/src/core/Editor.webLayout.test.ts b/packages/super-editor/src/core/Editor.webLayout.test.ts index e9456d117..485d3a66c 100644 --- a/packages/super-editor/src/core/Editor.webLayout.test.ts +++ b/packages/super-editor/src/core/Editor.webLayout.test.ts @@ -76,6 +76,71 @@ describe('Editor Web Layout Mode', () => { }); }); + describe('context menu behavior', () => { + it('forces disableContextMenu when viewOptions.layout is "web"', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'web' }, + disableContextMenu: false, + }); + + expect(editor.options.disableContextMenu).toBe(true); + }); + + it('respects disableContextMenu when viewOptions.layout is "print"', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'print' }, + disableContextMenu: false, + }); + + expect(editor.options.disableContextMenu).toBe(false); + }); + + it('resets disableContextMenu when switching from "web" to "print" without explicit override', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'web' }, + }); + + expect(editor.options.disableContextMenu).toBe(true); + + editor.setOptions({ viewOptions: { layout: 'print' } }); + + expect(editor.options.disableContextMenu).toBe(false); + }); + + it('keeps explicit disableContextMenu when switching from "web" to "print"', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'web' }, + disableContextMenu: true, + }); + + editor.setOptions({ viewOptions: { layout: 'print' } }); + + expect(editor.options.disableContextMenu).toBe(true); + }); + + it('defaults to enabled context menu after leaving web when created in web mode', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'web' }, + }); + + editor.setOptions({ disableContextMenu: true }); + editor.setOptions({ viewOptions: { layout: 'print' } }); + + expect(editor.options.disableContextMenu).toBe(true); + }); + + it('does not override disableContextMenu when switching layouts if it was explicitly set before', () => { + const editor = createTestEditor({ + viewOptions: { layout: 'web' }, + }); + + editor.setOptions({ disableContextMenu: false }); + editor.setOptions({ viewOptions: { layout: 'print' } }); + + expect(editor.options.disableContextMenu).toBe(false); + }); + }); + describe('getMaxContentSize()', () => { describe('web layout mode', () => { it('returns empty object to skip image constraints', async () => { diff --git a/packages/superdoc/src/dev/components/SuperdocDev.vue b/packages/superdoc/src/dev/components/SuperdocDev.vue index 4f6746005..46026e16c 100644 --- a/packages/superdoc/src/dev/components/SuperdocDev.vue +++ b/packages/superdoc/src/dev/components/SuperdocDev.vue @@ -180,7 +180,7 @@ const init = async () => { documentMode: 'editing', licenseKey: 'community-and-eval-agplv3', telemetry: { - enabled: false, + enabled: true, }, comments: { visible: true,