diff --git a/src/editor/Editor.test.tsx b/src/editor/Editor.test.tsx new file mode 100644 index 00000000..a0b838e4 --- /dev/null +++ b/src/editor/Editor.test.tsx @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { I18nContext, I18nManager } from '@shopify/react-i18n'; +import { + fireEvent, + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import Editor from './Editor'; + +function getTextArea(): HTMLTextAreaElement { + // the textarea in ace editor doesn't actually have any contents, but + // it gets the focus for input. + return screen.getByDisplayValue('') as HTMLTextAreaElement; +} + +it('should focus the text area', () => { + const store = ({ + getState: jest.fn(() => ({ settings: { darkMode: false, showDocs: false } })), + dispatch: jest.fn(), + subscribe: jest.fn(), + } as unknown) as Store; + const i18n = new I18nManager({ locale: 'en' }); + render( + + + + + , + ); + + expect(getTextArea()).toHaveFocus(); +}); + +describe('context menu', () => { + it('should show the context menu', () => { + const store = ({ + getState: jest.fn(() => ({ + settings: { darkMode: false, showDocs: false }, + })), + dispatch: jest.fn(), + subscribe: jest.fn(), + } as unknown) as Store; + const i18n = new I18nManager({ locale: 'en' }); + + render( + + + + + , + ); + + fireEvent.contextMenu(screen.getByText('Write your program here...')); + + expect(screen.getByText('Copy')).toBeInTheDocument(); + }); + + it('should hide the context menu when Escape is pressed', async () => { + const store = ({ + getState: jest.fn(() => ({ + settings: { darkMode: false, showDocs: false }, + })), + dispatch: jest.fn(), + subscribe: jest.fn(), + } as unknown) as Store; + const i18n = new I18nManager({ locale: 'en' }); + + render( + + + + + , + ); + + fireEvent.contextMenu(screen.getByText('Write your program here...')); + + expect(screen.getByText('Copy')).toBeInTheDocument(); + + userEvent.type(document.activeElement || document.body, '{esc}'); + + await waitForElementToBeRemoved(() => screen.queryByText('Copy')); + + // editor should be focused after context menu closes + expect(document.activeElement).toBe(getTextArea()); + }); +}); diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 3033742e..361d66a5 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -2,7 +2,7 @@ // Copyright (c) 2020-2021 The Pybricks Authors import { - ContextMenuTarget, + ContextMenu, Menu, MenuDivider, MenuItem, @@ -47,7 +47,6 @@ type DispatchProps = { type EditorProps = StateProps & DispatchProps & WithI18nProps; -@ContextMenuTarget class Editor extends React.Component { private editorRef: React.RefObject; private keyBindings?: Array<{ key: string; command: string }>; @@ -83,7 +82,34 @@ class Editor extends React.Component { render(): JSX.Element { const { i18n, darkMode, onSessionChanged, onCheck, onToggleDocs } = this.props; return ( -
+
{ + // istanbul ignore if: not expected + if (e.defaultPrevented) { + return; + } + + e.preventDefault(); + const menu = this.renderContextMenu(); + const listener = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + // istanbul ignore if: not expected + if (e.defaultPrevented) { + return; + } + + e.preventDefault(); + ContextMenu.hide(); + } + }; + window.addEventListener('keydown', listener); + ContextMenu.show(menu, { left: e.clientX, top: e.clientY }, () => { + window.removeEventListener('keydown', listener); + this.onContextMenuClose(); + }); + }} + > this.editor?.resize()}> { ); } + + onContextMenuClose = () => { + this.editorRef.current?.editor.focus(); + }; } const mapStateToProps = (state: RootState): StateProps => ({