diff --git a/e2e-tests/tests/selection/right-click-selection.spec.js b/e2e-tests/tests/selection/right-click-selection.spec.js new file mode 100644 index 0000000000..c8f20731fc --- /dev/null +++ b/e2e-tests/tests/selection/right-click-selection.spec.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { goToPageAndWaitForEditor } from '../helpers.js'; + +// TODO: Add Firefox to playwright.config.js projects to catch Firefox-specific regressions. +// This test was added for SD-1623 where Firefox clears selection on right-click mousedown +// when preventDefault() is called, unlike Chrome/Safari. +test.describe('Right-click selection preservation', () => { + test('preserves text selection after right-click @SD-1623', async ({ page }) => { + await goToPageAndWaitForEditor(page); + + const editor = page.locator('div.super-editor').first(); + await editor.click(); + + // Type some text to select + const testText = 'Hello World'; + await page.keyboard.type(testText); + + // Select all the text we just typed + await page.keyboard.press('ControlOrMeta+a'); + + // Verify text is selected before right-click + const selectionBefore = await page.evaluate(() => window.getSelection()?.toString()); + expect(selectionBefore).toContain(testText); + + // Right-click on the selected text + const textElement = editor.getByText(testText); + await textElement.click({ button: 'right' }); + + // Wait a moment for the selection handlers to run + await page.waitForTimeout(100); + + // Verify selection is preserved - check for either: + // 1. Native selection still present, OR + // 2. Visual selection decoration applied (sd-custom-selection class) + const selectionAfter = await page.evaluate(() => window.getSelection()?.toString()); + const hasVisualSelection = await editor.locator('.sd-custom-selection').count(); + + // Either the native selection should be preserved OR the visual decoration should be shown + const isSelectionPreserved = selectionAfter.includes(testText) || hasVisualSelection > 0; + expect(isSelectionPreserved).toBe(true); + }); +}); diff --git a/packages/super-editor/src/extensions/custom-selection/custom-selection.js b/packages/super-editor/src/extensions/custom-selection/custom-selection.js index 1695b762a8..cd72bd1a60 100644 --- a/packages/super-editor/src/extensions/custom-selection/custom-selection.js +++ b/packages/super-editor/src/extensions/custom-selection/custom-selection.js @@ -187,7 +187,9 @@ export const CustomSelection = Extension.create({ return false; } - event.preventDefault(); // Prevent default right-click behavior + // Note: Do NOT call event.preventDefault() here. + // Firefox clears native selection when preventDefault is called on mousedown. + // The contextmenu handler already prevents the native menu. const { selection } = view.state; if (!selection.empty) { // Ensure selection stays visible for right-click/context menu diff --git a/packages/super-editor/src/extensions/custom-selection/custom-selection.test.js b/packages/super-editor/src/extensions/custom-selection/custom-selection.test.js index 0b085db526..3c8e3e678e 100644 --- a/packages/super-editor/src/extensions/custom-selection/custom-selection.test.js +++ b/packages/super-editor/src/extensions/custom-selection/custom-selection.test.js @@ -267,6 +267,32 @@ describe('CustomSelection plugin', () => { expect(firstDeco?.to).toBe(6); }); + it('does not call preventDefault on right-click mousedown to preserve Firefox selection', () => { + const { plugin, view, editor } = createEnvironment(); + + const mouseDownEvent = { + button: 2, + preventDefault: vi.fn(), + target: editor.options.element, + type: 'mousedown', + }; + + plugin.props.handleDOMEvents.mousedown(view, mouseDownEvent); + + // preventDefault should NOT be called for right-click mousedown + // because Firefox clears native selection when preventDefault is called + expect(mouseDownEvent.preventDefault).not.toHaveBeenCalled(); + + // But selection should still be preserved in plugin state + expect(view.dispatch).toHaveBeenCalled(); + const dispatchedTr = view.dispatch.mock.calls[0][0]; + expect(dispatchedTr.getMeta(CustomSelectionPluginKey)).toMatchObject({ + focused: true, + preservedSelection: expect.any(Object), + showVisualSelection: true, + }); + }); + it('keeps selection visible when toolbar elements retain focus', () => { const { plugin, view, editor } = createEnvironment();