Merge pull request #32754 from overleaf/mg-native-context-shortcut
Allow Shift+right-click to use native browser context menu GitOrigin-RevId: 0455283e47511e6e770d389c5cc6994018766f74
This commit is contained in:
@@ -65,15 +65,26 @@ function FileTreeContextMenu() {
|
||||
keyboardInputRef.current = false
|
||||
}, [])
|
||||
|
||||
const handleShiftContextMenu = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (event.shiftKey) {
|
||||
setContextMenuCoords(null)
|
||||
}
|
||||
},
|
||||
[setContextMenuCoords]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
document.addEventListener('contextmenu', handleShiftContextMenu)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('mousedown', handleMouseDown)
|
||||
document.removeEventListener('contextmenu', handleShiftContextMenu)
|
||||
}
|
||||
}, [handleKeyDown, handleMouseDown])
|
||||
}, [handleKeyDown, handleMouseDown, handleShiftContextMenu])
|
||||
|
||||
if (!contextMenuCoords || fileTreeReadOnly) return null
|
||||
|
||||
|
||||
+5
@@ -61,6 +61,11 @@ function FileTreeItemInner({
|
||||
}, [isSelected, itemRef])
|
||||
|
||||
function handleContextMenu(ev: React.MouseEvent<HTMLDivElement>) {
|
||||
if (ev.shiftKey) {
|
||||
setContextMenuCoords(null)
|
||||
return
|
||||
}
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
setContextMenuCoords({
|
||||
|
||||
@@ -246,6 +246,11 @@ const gutterContextMenuPlugin = (): Extension =>
|
||||
gutters.addEventListener('contextmenu', (event: Event) => {
|
||||
const mouseEvent = event as MouseEvent
|
||||
|
||||
if (mouseEvent.shiftKey) {
|
||||
update.view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
|
||||
return
|
||||
}
|
||||
|
||||
const pos = update.view.posAtCoords({
|
||||
x: mouseEvent.clientX,
|
||||
y: mouseEvent.clientY,
|
||||
@@ -284,6 +289,11 @@ const emptyLineFillerContextMenuPlugin = (): Extension =>
|
||||
return
|
||||
}
|
||||
|
||||
if (mouseEvent.shiftKey) {
|
||||
view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -293,6 +303,7 @@ const emptyLineFillerContextMenuPlugin = (): Extension =>
|
||||
cancelable: true,
|
||||
clientX: mouseEvent.clientX,
|
||||
clientY: mouseEvent.clientY,
|
||||
shiftKey: mouseEvent.shiftKey,
|
||||
})
|
||||
contentDOM.dispatchEvent(customEvent)
|
||||
}
|
||||
@@ -310,6 +321,11 @@ const emptyLineFillerContextMenuPlugin = (): Extension =>
|
||||
const editorContextMenuHandlers = (): Extension =>
|
||||
EditorView.domEventHandlers({
|
||||
contextmenu(event: MouseEvent, view: EditorView) {
|
||||
if (event.shiftKey) {
|
||||
view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
|
||||
return false
|
||||
}
|
||||
|
||||
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY })
|
||||
if (pos === null) {
|
||||
return false
|
||||
@@ -347,8 +363,9 @@ const editorContextMenuHandlers = (): Extension =>
|
||||
closeContextMenu(view)
|
||||
}
|
||||
|
||||
// Prevent default on right-click to preserve selection
|
||||
if (isRightClick) {
|
||||
// Prevent default on right-click to preserve selection,
|
||||
// but not when Shift is held (native context menu shortcut)
|
||||
if (isRightClick && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ const handleClickEvent = (event: MouseEvent, view: EditorView) => {
|
||||
* and show a menu of suggestions
|
||||
*/
|
||||
const handleContextMenuEvent = (event: MouseEvent, view: EditorView) => {
|
||||
if (event.shiftKey) {
|
||||
view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
|
||||
return
|
||||
}
|
||||
|
||||
const position = view.posAtCoords(
|
||||
{
|
||||
x: event.pageX,
|
||||
|
||||
@@ -41,6 +41,86 @@ describe('FileTree Context Menu Flow', function () {
|
||||
cy.findByRole('menu')
|
||||
})
|
||||
|
||||
it('should not open on Shift+right-click', function () {
|
||||
const rootFolder = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [{ _id: '456def', name: 'main.tex' }],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
rootFolder={rootFolder as any}
|
||||
projectId="123abc"
|
||||
rootDocId="456def"
|
||||
>
|
||||
<FileTreeRoot
|
||||
refProviders={{}}
|
||||
setRefProviderEnabled={cy.stub()}
|
||||
setStartedFreeTrial={cy.stub()}
|
||||
onSelect={cy.stub()}
|
||||
onInit={cy.stub()}
|
||||
isConnected
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
cy.findByRole('treeitem', { name: 'main.tex' }).trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('should close an already-open menu on Shift+right-click', function () {
|
||||
const rootFolder = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [{ _id: '456def', name: 'main.tex' }],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
rootFolder={rootFolder as any}
|
||||
projectId="123abc"
|
||||
rootDocId="456def"
|
||||
>
|
||||
<FileTreeRoot
|
||||
refProviders={{}}
|
||||
setRefProviderEnabled={cy.stub()}
|
||||
setStartedFreeTrial={cy.stub()}
|
||||
onSelect={cy.stub()}
|
||||
onInit={cy.stub()}
|
||||
isConnected
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('treeitem', { name: 'main.tex' }).trigger('contextmenu')
|
||||
cy.findByRole('menu').should('exist')
|
||||
|
||||
cy.findByRole('treeitem', { name: 'main.tex' }).trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('closes when a new selection is started', function () {
|
||||
const rootFolder = [
|
||||
{
|
||||
|
||||
+49
@@ -119,6 +119,55 @@ describe('editor context menu', { scrollBehavior: false }, function () {
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('should not open on Shift+right-click', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
|
||||
cy.get('.cm-line').eq(10).trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('should close an already-open menu on Shift+right-click', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(10).rightclick()
|
||||
cy.findByRole('menu').should('be.visible')
|
||||
|
||||
cy.get('.cm-line').eq(5).trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('should open on Shift+F10', { retries: 1 }, function () {
|
||||
const scope = mockScope()
|
||||
|
||||
|
||||
+74
-4
@@ -95,10 +95,7 @@ const suggestions = {
|
||||
sv: ['medecin', 'medicin'],
|
||||
}
|
||||
|
||||
forEach(Object.keys(suggestions)).describe(
|
||||
'Spell check in client (%s)',
|
||||
(spellCheckLanguage: keyof typeof suggestions) => {
|
||||
const content = `
|
||||
const spellCheckerContent = `
|
||||
\\documentclass{}
|
||||
|
||||
\\title{}
|
||||
@@ -114,6 +111,79 @@ forEach(Object.keys(suggestions)).describe(
|
||||
|
||||
\\end{document}`
|
||||
|
||||
describe('Spell check context menu — Shift+right-click', function () {
|
||||
const spellCheckLanguage = 'en_GB'
|
||||
const [misspelled] = suggestions[spellCheckLanguage]
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
win.metaAttributesCache.set('ol-learnedWords', ['baz'])
|
||||
win.metaAttributesCache.set(
|
||||
'ol-dictionariesRoot',
|
||||
`js/dictionaries/${PackageVersions.version.dictionaries}/`
|
||||
)
|
||||
win.metaAttributesCache.set('ol-baseAssetPath', '/__cypress/src/')
|
||||
win.metaAttributesCache.set('ol-languages', languages)
|
||||
})
|
||||
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope(spellCheckerContent)
|
||||
const project = mockProject({ spellCheckLanguage })
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(13).as('line')
|
||||
cy.get('@line').click()
|
||||
cy.get('@line').type(misspelled)
|
||||
cy.get('@line')
|
||||
.find('.ol-cm-spelling-error', { timeout: 10000 })
|
||||
.should('have.length', 1)
|
||||
})
|
||||
|
||||
it('should not open spelling menu on Shift+right-click', function () {
|
||||
cy.get('@line').find('.ol-cm-spelling-error').trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.get('.ol-cm-spelling-context-menu-tooltip').should('not.exist')
|
||||
})
|
||||
|
||||
it('should close an already-open spelling menu on Shift+right-click', function () {
|
||||
cy.get('@line').find('.ol-cm-spelling-error').rightclick()
|
||||
cy.get('.ol-cm-spelling-context-menu-tooltip').should('be.visible')
|
||||
|
||||
cy.get('@line').find('.ol-cm-spelling-error').trigger('contextmenu', {
|
||||
button: 2,
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.get('.ol-cm-spelling-context-menu-tooltip').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
forEach(Object.keys(suggestions)).describe(
|
||||
'Spell check in client (%s)',
|
||||
(spellCheckLanguage: keyof typeof suggestions) => {
|
||||
const content = spellCheckerContent
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
|
||||
Reference in New Issue
Block a user