From: Jeremy Davis Date: Thu, 20 Jan 2022 17:20:23 +0000 (+0100) Subject: SONAR-15942 Remove deprecated keycode X-Git-Tag: 9.3.0.51899~29 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7aadbd0c9d08df36f21f31be759357cebf439a13;p=sonarqube.git SONAR-15942 Remove deprecated keycode --- diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx index eb2e08e8395..e9d079c80c6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx @@ -28,7 +28,7 @@ import { isPullRequest, isSameBranchLike } from '../../../../../helpers/branch-like'; -import { KeyCodes } from '../../../../../helpers/keycodes'; +import { KeyboardCodes } from '../../../../../helpers/keycodes'; import { translate } from '../../../../../helpers/l10n'; import { getBranchLikeUrl } from '../../../../../helpers/urls'; import { BranchLike, BranchLikeTree } from '../../../../../types/branch-like'; @@ -109,16 +109,16 @@ export class Menu extends React.PureComponent { }; handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case KeyCodes.Enter: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: event.preventDefault(); this.openHighlightedBranchLike(); break; - case KeyCodes.UpArrow: + case KeyboardCodes.UpArrow: event.preventDefault(); this.highlightSiblingBranchlike(-1); break; - case KeyCodes.DownArrow: + case KeyboardCodes.DownArrow: event.preventDefault(); this.highlightSiblingBranchlike(+1); break; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx index f0ca9926d3c..788febe0ecb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { Link } from 'react-router'; import SearchBox from '../../../../../../components/controls/SearchBox'; -import { KeyCodes } from '../../../../../../helpers/keycodes'; +import { KeyboardCodes } from '../../../../../../helpers/keycodes'; import { mockPullRequest, mockSetOfBranchAndPullRequest @@ -93,14 +93,14 @@ it('should handle keyboard shortcut correctly', () => { const { onKeyDown } = wrapper.find(SearchBox).props(); - onKeyDown!(mockEvent({ keyCode: KeyCodes.UpArrow })); + onKeyDown!(mockEvent({ nativeEvent: { code: KeyboardCodes.UpArrow } })); expect(wrapper.state().selectedBranchLike).toBe(branchLikes[5]); - onKeyDown!(mockEvent({ keyCode: KeyCodes.DownArrow })); - onKeyDown!(mockEvent({ keyCode: KeyCodes.DownArrow })); + onKeyDown!(mockEvent({ nativeEvent: { code: KeyboardCodes.DownArrow } })); + onKeyDown!(mockEvent({ nativeEvent: { code: KeyboardCodes.DownArrow } })); expect(wrapper.state().selectedBranchLike).toBe(branchLikes[0]); - onKeyDown!(mockEvent({ keyCode: KeyCodes.Enter })); + onKeyDown!(mockEvent({ nativeEvent: { code: KeyboardCodes.Enter } })); expect(push).toHaveBeenCalled(); }); diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx index 6818bdabe43..5bcfd07fec5 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx @@ -29,6 +29,7 @@ import SearchBox from '../../../components/controls/SearchBox'; import ClockIcon from '../../../components/icons/ClockIcon'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import { getComponentOverviewUrl } from '../../../helpers/urls'; @@ -286,16 +287,16 @@ export class Search extends React.PureComponent { }; handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case 13: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: event.preventDefault(); this.openSelected(); return; - case 38: + case KeyboardCodes.UpArrow: event.preventDefault(); this.selectPrevious(); return; - case 40: + case KeyboardCodes.DownArrow: event.preventDefault(); this.selectNext(); // keep this return to prevent fall-through in case more cases will be adder later diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx index d7ab82e39d1..67d98023a44 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx @@ -19,6 +19,7 @@ */ import { shallow, ShallowWrapper } from 'enzyme'; import * as React from 'react'; +import { KeyboardCodes } from '../../../../helpers/keycodes'; import { mockRouter } from '../../../../helpers/testMocks'; import { elementKeydown } from '../../../../helpers/testUtils'; import { ComponentQualifier } from '../../../../types/component'; @@ -56,7 +57,7 @@ it('opens selected project on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), 13); + elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); expect(router.push).toBeCalledWith({ pathname: '/dashboard', query: { id: selectedKey } }); }); @@ -72,7 +73,7 @@ it('opens selected portfolio on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), 13); + elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); }); @@ -88,7 +89,7 @@ it('opens selected subportfolio on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), 13); + elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); }); @@ -112,12 +113,12 @@ function component(key: string, qualifier = ComponentQualifier.Project) { } function next(form: ShallowWrapper, expected: string) { - elementKeydown(form.find('SearchBox'), 40); + elementKeydown(form.find('SearchBox'), KeyboardCodes.DownArrow); expect(form.state().selected).toBe(expected); } function prev(form: ShallowWrapper, expected: string) { - elementKeydown(form.find('SearchBox'), 38); + elementKeydown(form.find('SearchBox'), KeyboardCodes.UpArrow); expect(form.state().selected).toBe(expected); } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx b/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx index 7a0060f4569..b83d7555ff6 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx @@ -25,6 +25,7 @@ import { ResetButtonLink, SubmitButton } from '../../../components/controls/butt import { DropdownOverlay } from '../../../components/controls/Dropdown'; import SearchBox from '../../../components/controls/SearchBox'; import SimpleModal from '../../../components/controls/SimpleModal'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -61,16 +62,16 @@ export default class ProjectModal extends React.PureComponent { } handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case 13: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: event.preventDefault(); this.handleSelectHighlighted(); break; - case 38: + case KeyboardCodes.UpArrow: event.preventDefault(); this.handleHighlightPrevious(); break; - case 40: + case KeyboardCodes.DownArrow: event.preventDefault(); this.handleHighlightNext(); break; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectModal-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectModal-test.tsx index e8707186222..9d47ebc3a1d 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectModal-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { getSuggestions } from '../../../../api/components'; +import { KeyboardCodes } from '../../../../helpers/keycodes'; import { change, elementKeydown, submit, waitAndUpdate } from '../../../../helpers/testUtils'; import ProjectModal from '../ProjectModal'; @@ -102,27 +103,24 @@ it('should handle up and down keys', async () => { }); await waitAndUpdate(wrapper); - // Down. - elementKeydown(wrapper.dive().find('SearchBox'), 40); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.DownArrow); expect(wrapper.state('highlighted')).toEqual(foo); - elementKeydown(wrapper.dive().find('SearchBox'), 40); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.DownArrow); expect(wrapper.state('highlighted')).toEqual(bar); - elementKeydown(wrapper.dive().find('SearchBox'), 40); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.DownArrow); expect(wrapper.state('highlighted')).toEqual(foo); - // Up. - elementKeydown(wrapper.dive().find('SearchBox'), 38); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.UpArrow); expect(wrapper.state('highlighted')).toEqual(bar); - elementKeydown(wrapper.dive().find('SearchBox'), 38); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.UpArrow); expect(wrapper.state('highlighted')).toEqual(foo); - elementKeydown(wrapper.dive().find('SearchBox'), 38); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.UpArrow); expect(wrapper.state('highlighted')).toEqual(bar); - // Enter. - elementKeydown(wrapper.dive().find('SearchBox'), 13); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.Enter); expect(wrapper.state('selectedProject')).toEqual(bar); expect(onSubmit).not.toHaveBeenCalled(); - elementKeydown(wrapper.dive().find('SearchBox'), 13); + elementKeydown(wrapper.dive().find('SearchBox'), KeyboardCodes.Enter); expect(onSubmit).toHaveBeenCalledWith(bar); }); diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 90d9fbb8c83..188c4dff9c1 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -24,6 +24,7 @@ import SearchBox from '../../../components/controls/SearchBox'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; import PortfolioNewCodeToggle from './PortfolioNewCodeToggle'; @@ -74,10 +75,10 @@ export class Search extends React.PureComponent { } handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case 13: - case 38: - case 40: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: + case KeyboardCodes.UpArrow: + case KeyboardCodes.DownArrow: event.preventDefault(); event.currentTarget.blur(); break; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index c64244bb8f6..474aacb0a11 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -43,7 +43,7 @@ import { isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; -import { KeyCodes } from '../../../helpers/keycodes'; +import { KeyboardCodes, KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { addSideBarClass, @@ -268,19 +268,19 @@ export default class App extends React.PureComponent { if (key.getScope() !== 'issues') { return; } - if (event.keyCode === KeyCodes.Alt) { + if (event.key === KeyboardKeys.Alt) { event.preventDefault(); this.setState(actions.enableLocationsNavigator); - } else if (event.keyCode === KeyCodes.DownArrow && event.altKey) { + } else if (event.code === KeyboardCodes.DownArrow && event.altKey) { event.preventDefault(); this.selectNextLocation(); - } else if (event.keyCode === KeyCodes.UpArrow && event.altKey) { + } else if (event.code === KeyboardCodes.UpArrow && event.altKey) { event.preventDefault(); this.selectPreviousLocation(); - } else if (event.keyCode === KeyCodes.LeftArrow && event.altKey) { + } else if (event.code === KeyboardCodes.LeftArrow && event.altKey) { event.preventDefault(); this.selectPreviousFlow(); - } else if (event.keyCode === KeyCodes.RightArrow && event.altKey) { + } else if (event.code === KeyboardCodes.RightArrow && event.altKey) { event.preventDefault(); this.selectNextFlow(); } @@ -290,8 +290,7 @@ export default class App extends React.PureComponent { if (key.getScope() !== 'issues') { return; } - if (event.keyCode === KeyCodes.Alt) { - // alt + if (event.key === KeyboardKeys.Alt) { this.setState(actions.disableLocationsNavigator); } }; diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx index ec5f4e5e767..404e5859b82 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import key from 'keymaster'; import * as React from 'react'; import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication'; -import { KeyCodes } from '../../../../helpers/keycodes'; +import { KeyboardCodes, KeyboardKeys } from '../../../../helpers/keycodes'; import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; import { @@ -63,7 +63,8 @@ jest.mock('../../../../helpers/handleRequiredAuthentication', () => jest.fn()); jest.mock('keymaster', () => { const key: any = (bindKey: string, _: string, callback: Function) => { document.addEventListener('keydown', (event: KeyboardEvent) => { - if (bindKey.split(',').includes(KEYCODE_MAP[event.keyCode])) { + const keymasterCode = event.code && KEYCODE_MAP[event.code as KeyboardCodes]; + if (keymasterCode && bindKey.split(',').includes(keymasterCode)) { return callback(); } return true; @@ -217,25 +218,25 @@ it('should correctly bind key events for issue navigation', async () => { expect(wrapper.state('selected')).toBe(ISSUES[0].key); - keydown(KeyCodes.DownArrow); + keydown({ code: KeyboardCodes.DownArrow }); expect(wrapper.state('selected')).toBe(ISSUES[1].key); - keydown(KeyCodes.UpArrow); - keydown(KeyCodes.UpArrow); + keydown({ code: KeyboardCodes.UpArrow }); + keydown({ code: KeyboardCodes.UpArrow }); expect(wrapper.state('selected')).toBe(ISSUES[0].key); - keydown(KeyCodes.DownArrow); - keydown(KeyCodes.DownArrow); - keydown(KeyCodes.DownArrow); - keydown(KeyCodes.DownArrow); - keydown(KeyCodes.DownArrow); - keydown(KeyCodes.DownArrow); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); expect(wrapper.state('selected')).toBe(ISSUES[3].key); - keydown(KeyCodes.RightArrow); + keydown({ code: KeyboardCodes.RightArrow }); expect(push).toBeCalledTimes(1); - keydown(KeyCodes.LeftArrow); + keydown({ code: KeyboardCodes.LeftArrow }); expect(push).toBeCalledTimes(2); expect(window.addEventListener).toBeCalledTimes(2); }); @@ -432,28 +433,28 @@ describe('keydown event handler', () => { }); it('should handle alt', () => { - instance.handleKeyDown(mockEvent({ keyCode: 18 })); + instance.handleKeyDown(mockEvent({ key: KeyboardKeys.Alt })); expect(instance.setState).toHaveBeenCalledWith(enableLocationsNavigator); }); it('should handle alt+↓', () => { - instance.handleKeyDown(mockEvent({ altKey: true, keyCode: 40 })); + instance.handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.DownArrow })); expect(instance.setState).toHaveBeenCalledWith(selectNextLocation); }); it('should handle alt+↑', () => { - instance.handleKeyDown(mockEvent({ altKey: true, keyCode: 38 })); + instance.handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.UpArrow })); expect(instance.setState).toHaveBeenCalledWith(selectPreviousLocation); }); it('should handle alt+←', () => { - instance.handleKeyDown(mockEvent({ altKey: true, keyCode: 37 })); + instance.handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.LeftArrow })); expect(instance.setState).toHaveBeenCalledWith(selectPreviousFlow); }); it('should handle alt+→', () => { - instance.handleKeyDown(mockEvent({ altKey: true, keyCode: 39 })); + instance.handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.RightArrow })); expect(instance.setState).toHaveBeenCalledWith(selectNextFlow); }); it('should ignore different scopes', () => { key.setScope('notissues'); - instance.handleKeyDown(mockEvent({ keyCode: 18 })); + instance.handleKeyDown(mockEvent({ key: KeyboardKeys.Alt })); expect(instance.setState).not.toHaveBeenCalled(); }); }); @@ -472,13 +473,13 @@ describe('keyup event handler', () => { }); it('should handle alt', () => { - instance.handleKeyUp(mockEvent({ keyCode: 18 })); + instance.handleKeyUp(mockEvent({ key: KeyboardKeys.Alt })); expect(instance.setState).toHaveBeenCalledWith(disableLocationsNavigator); }); it('should ignore different scopes', () => { key.setScope('notissues'); - instance.handleKeyUp(mockEvent({ keyCode: 18 })); + instance.handleKeyUp(mockEvent({ key: KeyboardKeys.Alt })); expect(instance.setState).not.toHaveBeenCalled(); }); }); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx index 5a2d364fcb6..4469a4d3d6b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx @@ -20,7 +20,7 @@ import { debounce } from 'lodash'; import * as React from 'react'; import { searchUsers } from '../../../../api/users'; -import { KeyCodes } from '../../../../helpers/keycodes'; +import { KeyboardCodes } from '../../../../helpers/keycodes'; import { translate } from '../../../../helpers/l10n'; import { isUserActive } from '../../../../helpers/users'; import AssigneeSelectionRenderer from './AssigneeSelectionRenderer'; @@ -104,16 +104,16 @@ export default class AssigneeSelection extends React.PureComponent }; handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case KeyCodes.Enter: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: event.preventDefault(); this.selectHighlighted(); break; - case KeyCodes.UpArrow: + case KeyboardCodes.UpArrow: event.preventDefault(); this.highlightPrevious(); break; - case KeyCodes.DownArrow: + case KeyboardCodes.DownArrow: event.preventDefault(); this.highlightNext(); break; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx index 7820149048a..73fa3302350 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { searchUsers } from '../../../../../api/users'; -import { KeyCodes } from '../../../../../helpers/keycodes'; +import { KeyboardCodes } from '../../../../../helpers/keycodes'; import { mockLoggedInUser, mockUser } from '../../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../../helpers/testUtils'; import AssigneeSelection from '../AssigneeSelection'; @@ -34,7 +34,7 @@ it('should render correctly', () => { }); it('should handle keydown', () => { - const mockEvent = (keyCode: number) => ({ preventDefault: jest.fn(), keyCode }); + const mockEvent = (code: KeyboardCodes) => ({ preventDefault: jest.fn(), nativeEvent: { code } }); const suggestedUsers = [ mockUser({ login: '1' }) as T.UserActive, mockUser({ login: '2' }) as T.UserActive, @@ -44,29 +44,29 @@ it('should handle keydown', () => { const onSelect = jest.fn(); const wrapper = shallowRender({ onSelect }); - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.UpArrow) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.UpArrow) as any); expect(wrapper.state().highlighted).toEqual({ login: '', name: 'unassigned' }); wrapper.setState({ suggestedUsers }); // press down to highlight the first - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.DownArrow) as any); expect(wrapper.state().highlighted).toBe(suggestedUsers[0]); // press up to loop around to last - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.UpArrow) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.UpArrow) as any); expect(wrapper.state().highlighted).toBe(suggestedUsers[2]); // press down to loop around to first - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.DownArrow) as any); expect(wrapper.state().highlighted).toBe(suggestedUsers[0]); // press down highlight the next - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.DownArrow) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.DownArrow) as any); expect(wrapper.state().highlighted).toBe(suggestedUsers[1]); // press enter to select the highlighted user - wrapper.instance().handleKeyDown(mockEvent(KeyCodes.Enter) as any); + wrapper.instance().handleKeyDown(mockEvent(KeyboardCodes.Enter) as any); expect(onSelect).toBeCalledWith(suggestedUsers[1]); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx index f237d48a304..0bc426876f8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -62,6 +62,8 @@ interface State { const SAFE_SET_STATE_DELAY = 3000; +const formNoop = (e: React.FormEvent) => e.preventDefault(); + export class Definition extends React.PureComponent { timeout?: number; mounted = false; @@ -189,7 +191,7 @@ export class Definition extends React.PureComponent { )} -
+ { }; handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - case KeyCodes.Enter: + switch (event.nativeEvent.code) { + case KeyboardCodes.Enter: event.preventDefault(); this.openSelected(); return; - case KeyCodes.UpArrow: + case KeyboardCodes.UpArrow: event.preventDefault(); this.selectPrevious(); return; - case KeyCodes.DownArrow: + case KeyboardCodes.DownArrow: event.preventDefault(); this.selectNext(); // keep this return to prevent fall-through in case more cases will be adder later diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx index a29b3db703e..f6ea708a34d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsSearch-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { KeyboardCodes } from '../../../../helpers/keycodes'; import { mockDefinition } from '../../../../helpers/mocks/settings'; import { mockRouter } from '../../../../helpers/testMocks'; import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; @@ -96,11 +97,11 @@ describe('instance', () => { it('should handle "enter" keyboard event', () => { wrapper.setState({ selectedResult: undefined }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 13 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.Enter } })); expect(router.push).not.toBeCalled(); wrapper.setState({ selectedResult: 'foo' }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 13 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.Enter } })); expect(router.push).toBeCalledWith({ hash: '#foo', @@ -111,27 +112,27 @@ describe('instance', () => { it('should handle "down" keyboard event', () => { wrapper.setState({ selectedResult: undefined }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 40 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.DownArrow } })); expect(wrapper.state().selectedResult).toBeUndefined(); wrapper.setState({ selectedResult: 'foo' }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 40 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.DownArrow } })); expect(wrapper.state().selectedResult).toBe('sonar.new_code_period'); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 40 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.DownArrow } })); expect(wrapper.state().selectedResult).toBe('sonar.new_code_period'); }); it('should handle "up" keyboard event', () => { wrapper.setState({ selectedResult: undefined }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 38 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.UpArrow } })); expect(wrapper.state().selectedResult).toBeUndefined(); wrapper.setState({ selectedResult: 'sonar.new_code_period' }); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 38 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.UpArrow } })); expect(wrapper.state().selectedResult).toBe('foo'); - wrapper.instance().handleKeyDown(mockEvent({ keyCode: 38 })); + wrapper.instance().handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.UpArrow } })); expect(wrapper.state().selectedResult).toBe('foo'); }); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap index c7425dafd23..3bf97fc1334 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap @@ -34,7 +34,9 @@ exports[`should render correctly 1`] = `
- + { +export default class SimpleInput extends React.PureComponent { handleInputChange = (event: React.ChangeEvent) => { this.props.onChange(event.currentTarget.value); }; handleKeyDown = (event: React.KeyboardEvent) => { - if (event.keyCode === 13 && this.props.onSave) { + if (event.nativeEvent.code === KeyboardCodes.Enter && this.props.onSave) { this.props.onSave(); - } else if (event.keyCode === 27 && this.props.onCancel) { + } else if (event.nativeEvent.code === KeyboardCodes.Escape && this.props.onCancel) { this.props.onCancel(); } }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx index f61a4ee4ce5..3c326c7f17c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx @@ -19,23 +19,14 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { KeyboardCodes } from '../../../../../helpers/keycodes'; import { mockSetting } from '../../../../../helpers/mocks/settings'; +import { mockEvent } from '../../../../../helpers/testMocks'; import { change } from '../../../../../helpers/testUtils'; -import SimpleInput from '../SimpleInput'; +import SimpleInput, { SimpleInputProps } from '../SimpleInput'; it('should render input', () => { - const onChange = jest.fn(); - const input = shallow( - - ).find('input'); + const input = shallowRender().find('input'); expect(input.length).toBe(1); expect(input.prop('type')).toBe('text'); expect(input.prop('className')).toContain('input-large'); @@ -46,20 +37,51 @@ it('should render input', () => { it('should call onChange', () => { const onChange = jest.fn(); - const input = shallow( + const input = shallowRender({ onChange }).find('input'); + expect(input.length).toBe(1); + expect(input.prop('onChange')).toBeDefined(); + + change(input, 'qux'); + expect(onChange).toBeCalledWith('qux'); +}); + +it('should handle enter', () => { + const onSave = jest.fn(); + shallowRender({ onSave }) + .instance() + .handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.Enter } })); + expect(onSave).toBeCalled(); +}); + +it('should handle esc', () => { + const onCancel = jest.fn(); + shallowRender({ onCancel }) + .instance() + .handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.Escape } })); + expect(onCancel).toBeCalled(); +}); + +it('should ignore other keys', () => { + const onSave = jest.fn(); + const onCancel = jest.fn(); + shallowRender({ onCancel, onSave }) + .instance() + .handleKeyDown(mockEvent({ nativeEvent: { code: KeyboardCodes.LeftArrow } })); + expect(onSave).not.toBeCalled(); + expect(onCancel).not.toBeCalled(); +}); + +function shallowRender(overrides: Partial = {}) { + return shallow( - ).find('input'); - expect(input.length).toBe(1); - expect(input.prop('onChange')).toBeDefined(); - - change(input, 'qux'); - expect(onChange).toBeCalledWith('qux'); -}); + ); +} diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx index 6f0aab023c8..67075ca2cc5 100644 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx @@ -21,10 +21,11 @@ import classNames from 'classnames'; import { difference } from 'lodash'; import * as React from 'react'; import SearchBox from '../../components/controls/SearchBox'; +import { KeyboardCodes } from '../../helpers/keycodes'; import { translateWithParameters } from '../../helpers/l10n'; import MultiSelectOption from './MultiSelectOption'; -interface Props { +export interface MultiSelectProps { allowNewElements?: boolean; allowSelection?: boolean; elements: string[]; @@ -55,9 +56,9 @@ interface DefaultProps { validateSearchInput: (value: string) => string; } -type PropsWithDefault = Props & DefaultProps; +type PropsWithDefault = MultiSelectProps & DefaultProps; -export default class MultiSelect extends React.PureComponent { +export default class MultiSelect extends React.PureComponent { container?: HTMLDivElement | null; searchInput?: HTMLInputElement | null; mounted = false; @@ -70,7 +71,7 @@ export default class MultiSelect extends React.PureComponent { validateSearchInput: (value: string) => value }; - constructor(props: Props) { + constructor(props: MultiSelectProps) { super(props); this.state = { activeIdx: 0, @@ -137,23 +138,23 @@ export default class MultiSelect extends React.PureComponent { }); }; - handleKeyboard = (evt: KeyboardEvent) => { - switch (evt.keyCode) { - case 40: // down - evt.stopPropagation(); - evt.preventDefault(); + handleKeyboard = (event: KeyboardEvent) => { + switch (event.code) { + case KeyboardCodes.DownArrow: + event.stopPropagation(); + event.preventDefault(); this.setState(this.selectNextElement); break; - case 38: // up - evt.stopPropagation(); - evt.preventDefault(); + case KeyboardCodes.UpArrow: + event.stopPropagation(); + event.preventDefault(); this.setState(this.selectPreviousElement); break; - case 37: // left - case 39: // right - evt.stopPropagation(); + case KeyboardCodes.LeftArrow: + case KeyboardCodes.RightArrow: + event.stopPropagation(); break; - case 13: // enter + case KeyboardCodes.Enter: if (this.state.activeIdx >= 0) { this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]); } @@ -175,7 +176,7 @@ export default class MultiSelect extends React.PureComponent { onUnselectItem = (item: string) => this.props.onUnselect(item); - isNewElement = (elem: string, { selectedElements, elements }: Props) => + isNewElement = (elem: string, { selectedElements, elements }: MultiSelectProps) => elem.length > 0 && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; updateSelectedElements = (props: PropsWithDefault) => { @@ -207,7 +208,7 @@ export default class MultiSelect extends React.PureComponent { }); }; - getAllElements = (props: Props, state: State) => { + getAllElements = (props: MultiSelectProps, state: State) => { if (this.isNewElement(state.query, props)) { return [...state.selectedElements, ...state.unselectedElements, state.query]; } else { @@ -217,7 +218,7 @@ export default class MultiSelect extends React.PureComponent { setElementActive = (idx: number) => this.setState({ activeIdx: idx }); - selectNextElement = (state: State, props: Props) => { + selectNextElement = (state: State, props: MultiSelectProps) => { const { activeIdx } = state; const allElements = this.getAllElements(props, state); if (activeIdx < 0 || activeIdx >= allElements.length - 1) { @@ -227,7 +228,7 @@ export default class MultiSelect extends React.PureComponent { } }; - selectPreviousElement = (state: State, props: Props) => { + selectPreviousElement = (state: State, props: MultiSelectProps) => { const { activeIdx } = state; const allElements = this.getAllElements(props, state); if (activeIdx <= 0) { diff --git a/server/sonar-web/src/main/js/components/common/SelectList.tsx b/server/sonar-web/src/main/js/components/common/SelectList.tsx index e27ce103f2c..cf5cc752154 100644 --- a/server/sonar-web/src/main/js/components/common/SelectList.tsx +++ b/server/sonar-web/src/main/js/components/common/SelectList.tsx @@ -21,6 +21,7 @@ import classNames from 'classnames'; import key from 'keymaster'; import { uniqueId } from 'lodash'; import * as React from 'react'; +import { KeyboardCodes } from '../../helpers/keycodes'; import SelectListItem from './SelectListItem'; interface Props { @@ -76,7 +77,11 @@ export default class SelectList extends React.PureComponent { filter: (event: KeyboardEvent & { target: HTMLElement }) => { const { tagName } = event.target || event.srcElement; const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; - return [13, 38, 40].includes(event.keyCode) || !isInput; + return ( + [KeyboardCodes.Enter, KeyboardCodes.UpArrow, KeyboardCodes.DownArrow].includes( + event.code as KeyboardCodes + ) || !isInput + ); } }); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx index 2915951fc2d..29cd2d8a565 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx @@ -19,7 +19,9 @@ */ import { mount, shallow } from 'enzyme'; import * as React from 'react'; -import MultiSelect from '../MultiSelect'; +import { KeyboardCodes } from '../../../helpers/keycodes'; +import { mockEvent } from '../../../helpers/testUtils'; +import MultiSelect, { MultiSelectProps } from '../MultiSelect'; const props = { selectedElements: ['bar'], @@ -31,14 +33,10 @@ const props = { placeholder: '' }; -const elements = [ - { key: 'foo', label: 'foo' }, - { key: 'bar', label: 'bar' }, - { key: 'baz', label: 'baz' } -]; +const elements = ['foo', 'bar', 'baz']; it('should render multiselect with selected elements', () => { - const multiselect = shallow(); + const multiselect = shallowRender(); // Will not only the selected element expect(multiselect).toMatchSnapshot(); @@ -55,9 +53,53 @@ it('should render with the focus inside the search input', () => { * Need to attach to document body to have it set to `document.activeElement` * See: https://github.com/jsdom/jsdom/issues/2723#issuecomment-580163361 */ - const multiselect = mount(, { attachTo: document.body }); + const container = document.createElement('div'); + document.body.appendChild(container); + const multiselect = mount(, { attachTo: container }); expect(multiselect.find('input').getDOMNode()).toBe(document.activeElement); multiselect.unmount(); }); + +it.each([ + [KeyboardCodes.DownArrow, 1, 1], + [KeyboardCodes.UpArrow, 1, 1], + [KeyboardCodes.LeftArrow, 1, 0] +])('should handle keyboard event: %s', (code, stopPropagationCalls, preventDefaultCalls) => { + const wrapper = shallowRender(); + + const stopPropagation = jest.fn(); + const preventDefault = jest.fn(); + const event = mockEvent({ preventDefault, stopPropagation, code }); + + wrapper.instance().handleKeyboard(event); + + expect(stopPropagation).toBeCalledTimes(stopPropagationCalls); + expect(preventDefault).toBeCalledTimes(preventDefaultCalls); +}); + +it('should handle keyboard event: enter', () => { + const wrapper = shallowRender(); + + wrapper.instance().toggleSelect = jest.fn(); + + wrapper.instance().handleKeyboard(mockEvent({ code: KeyboardCodes.Enter })); + + expect(wrapper.instance().toggleSelect).toBeCalled(); +}); + +function shallowRender(overrides: Partial = {}) { + return shallow( + Promise.resolve()} + onSelect={jest.fn()} + onUnselect={jest.fn()} + renderLabel={(element: string) => element} + placeholder="" + {...overrides} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.tsx index 8c4449fcf9b..1c83728fef7 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.tsx @@ -19,10 +19,32 @@ */ import { mount, shallow } from 'enzyme'; import * as React from 'react'; -import { click, keydown } from '../../../helpers/testUtils'; +import { KeyboardCodes } from '../../../helpers/keycodes'; +import { click, KEYCODE_MAP, keydown } from '../../../helpers/testUtils'; import SelectList from '../SelectList'; import SelectListItem from '../SelectListItem'; +jest.mock('keymaster', () => { + const key: any = (bindKey: string, _: string, callback: Function) => { + document.addEventListener('keydown', (event: KeyboardEvent) => { + const keymasterCode = event.code && KEYCODE_MAP[event.code as KeyboardCodes]; + if (keymasterCode && bindKey.split(',').includes(keymasterCode)) { + return callback(); + } + return true; + }); + }; + let scope = 'key-scope'; + + key.getScope = () => scope; + key.setScope = (newScope: string) => { + scope = newScope; + }; + key.deleteScope = jest.fn(); + + return key; +}); + it('should render correctly without children', () => { const onSelect = jest.fn(); expect( @@ -56,7 +78,7 @@ it('should render correctly with children', () => { it('should correclty handle user actions', () => { const onSelect = jest.fn(); const items = ['item', 'seconditem', 'third']; - const list = mount( + const list = mount( {items.map(item => ( @@ -66,12 +88,15 @@ it('should correclty handle user actions', () => { ))} ); - keydown(40); - expect(list.state()).toMatchSnapshot(); - keydown(40); - expect(list.state()).toMatchSnapshot(); - keydown(38); - expect(list.state()).toMatchSnapshot(); + expect(list.state().active).toBe('seconditem'); + keydown({ code: KeyboardCodes.DownArrow }); + expect(list.state().active).toBe('third'); + keydown({ code: KeyboardCodes.DownArrow }); + expect(list.state().active).toBe('item'); + keydown({ code: KeyboardCodes.UpArrow }); + expect(list.state().active).toBe('third'); + keydown({ code: KeyboardCodes.UpArrow }); + expect(list.state().active).toBe('seconditem'); click(list.find('a').at(2)); - expect(onSelect.mock.calls).toMatchSnapshot(); // eslint-disable-linelist + expect(onSelect).toBeCalledWith('third'); }); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap index 87a1bfd052c..bd2ca89e1dc 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap @@ -63,13 +63,8 @@ exports[`should render multiselect with selected elements 2`] = ` - - - { } handleKeyDown = (event: KeyboardEvent) => { - if (event.keyCode === KeyCodes.Escape) { + if (event.code === KeyboardCodes.Escape) { this.props.onKeydown(); } }; diff --git a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx index 70c97cb3b12..d417471bae0 100644 --- a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx +++ b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx @@ -20,6 +20,7 @@ import classNames from 'classnames'; import { Cancelable, debounce } from 'lodash'; import * as React from 'react'; +import { KeyboardCodes } from '../../helpers/keycodes'; import { translate, translateWithParameters } from '../../helpers/l10n'; import SearchIcon from '../icons/SearchIcon'; import DeferredSpinner from '../ui/DeferredSpinner'; @@ -94,8 +95,7 @@ export default class SearchBox extends React.PureComponent { }; handleInputKeyDown = (event: React.KeyboardEvent) => { - if (event.keyCode === 27) { - // escape + if (event.nativeEvent.code === KeyboardCodes.Escape) { event.preventDefault(); this.handleResetClick(); } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/EscKeydownHandler-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/EscKeydownHandler-test.tsx index 6c852477b81..de7f00a27b8 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/EscKeydownHandler-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/EscKeydownHandler-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { KeyCodes } from '../../../helpers/keycodes'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { keydown } from '../../../helpers/testUtils'; import EscKeydownHandler from '../EscKeydownHandler'; @@ -33,7 +33,7 @@ it('should correctly trigger the keydown handler when hitting Esc', () => { const onKeydown = jest.fn(); shallowRender({ onKeydown }); jest.runAllTimers(); - keydown(KeyCodes.Escape); + keydown({ code: KeyboardCodes.Escape }); expect(onKeydown).toBeCalled(); }); diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withKeyboardNavigation-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withKeyboardNavigation-test.tsx index 22d907817b9..ac05b25a60f 100644 --- a/server/sonar-web/src/main/js/components/hoc/__tests__/withKeyboardNavigation-test.tsx +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withKeyboardNavigation-test.tsx @@ -19,6 +19,7 @@ */ import { mount, shallow } from 'enzyme'; import * as React from 'react'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { mockComponent } from '../../../helpers/mocks/component'; import { KEYCODE_MAP, keydown } from '../../../helpers/testUtils'; import withKeyboardNavigation, { WithKeyboardNavigationProps } from '../withKeyboardNavigation'; @@ -43,7 +44,8 @@ const COMPONENTS = [ jest.mock('keymaster', () => { const key: any = (bindKey: string, _: string, callback: Function) => { document.addEventListener('keydown', (event: KeyboardEvent) => { - if (bindKey.split(',').includes(KEYCODE_MAP[event.keyCode])) { + const keymasterCode = event.code && KEYCODE_MAP[event.code as KeyboardCodes]; + if (keymasterCode && bindKey.split(',').includes(keymasterCode)) { return callback(); } return true; @@ -78,28 +80,28 @@ it('should correctly bind key events for component navigation', () => { }) ); - keydown('down'); + keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[2]); expect(onSelect).not.toBeCalled(); - keydown('up'); - keydown('up'); + keydown({ code: KeyboardCodes.UpArrow }); + keydown({ code: KeyboardCodes.UpArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[0]); expect(onSelect).not.toBeCalled(); - keydown('up'); + keydown({ code: KeyboardCodes.UpArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[2]); - keydown('down'); + keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[0]); - keydown('right'); + keydown({ code: KeyboardCodes.RightArrow }); expect(onSelect).toBeCalledWith(COMPONENTS[0]); - keydown('enter'); + keydown({ code: KeyboardCodes.Enter }); expect(onSelect).toBeCalledWith(COMPONENTS[0]); - keydown('left'); + keydown({ code: KeyboardCodes.LeftArrow }); expect(onGoToParent).toBeCalled(); }); @@ -116,18 +118,18 @@ it('should support not cycling through elements, and triggering a callback on re }) ); - keydown('down'); + keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[0]); - keydown('down'); - keydown('down'); - keydown('down'); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); + keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[2]); expect(onEndOfList).toBeCalled(); - keydown('up'); - keydown('up'); - keydown('up'); - keydown('up'); + keydown({ code: KeyboardCodes.UpArrow }); + keydown({ code: KeyboardCodes.UpArrow }); + keydown({ code: KeyboardCodes.UpArrow }); + keydown({ code: KeyboardCodes.UpArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[0]); }); @@ -148,19 +150,19 @@ it('should correctly bind key events for codeview navigation', () => { expect(onHighlight).not.toBeCalled(); - keydown('down'); + keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).not.toBeCalled(); - keydown('up'); + keydown({ code: KeyboardCodes.UpArrow }); expect(onHighlight).not.toBeCalled(); - keydown('right'); + keydown({ code: KeyboardCodes.RightArrow }); expect(onSelect).not.toBeCalled(); - keydown('enter'); + keydown({ code: KeyboardCodes.Enter }); expect(onSelect).not.toBeCalled(); - keydown('left'); + keydown({ code: KeyboardCodes.LeftArrow }); expect(onGoToParent).toBeCalled(); }); diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx index e03c28cb175..4481dacee72 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx @@ -21,10 +21,11 @@ import * as React from 'react'; import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; import { PopupPlacement } from '../../../components/ui/popups'; +import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; import FormattingTips from '../../common/FormattingTips'; -interface Props { +export interface CommentPopupProps { comment?: Pick; onComment: (text: string) => void; toggleComment: (visible: boolean) => void; @@ -37,8 +38,8 @@ interface State { textComment: string; } -export default class CommentPopup extends React.PureComponent { - constructor(props: Props) { +export default class CommentPopup extends React.PureComponent { + constructor(props: CommentPopupProps) { super(props); this.state = { textComment: props.comment ? props.comment.markdown : '' @@ -60,10 +61,16 @@ export default class CommentPopup extends React.PureComponent { }; handleKeyboard = (event: React.KeyboardEvent) => { - if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) { - // Ctrl + Enter + if (event.nativeEvent.code === KeyboardCodes.Enter && (event.metaKey || event.ctrlKey)) { this.handleCommentClick(); - } else if ([37, 38, 39, 40].includes(event.keyCode)) { + } else if ( + [ + KeyboardCodes.UpArrow, + KeyboardCodes.DownArrow, + KeyboardCodes.LeftArrow, + KeyboardCodes.RightArrow + ].includes(event.nativeEvent.code as KeyboardCodes) + ) { // Arrow keys event.stopPropagation(); } diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx index 3893fee0716..5de1a41accd 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx @@ -19,53 +19,69 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { KeyboardCodes } from '../../../../helpers/keycodes'; +import { mockEvent } from '../../../../helpers/testMocks'; import { click } from '../../../../helpers/testUtils'; -import CommentPopup from '../CommentPopup'; +import CommentPopup, { CommentPopupProps } from '../CommentPopup'; it('should render the comment popup correctly without existing comment', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); }); it('should render the comment popup correctly when changing a comment', () => { - const element = shallow( - - ); - expect(element).toMatchSnapshot(); + expect(shallowRender({ comment: { markdown: '*test*' } })).toMatchSnapshot(); }); it('should render not allow to send comment with only spaces', () => { const onComment = jest.fn(); - const element = shallow( - - ); - click(element.find('.js-issue-comment-submit')); + const wrapper = shallowRender({ onComment }); + click(wrapper.find('.js-issue-comment-submit')); expect(onComment.mock.calls.length).toBe(0); - element.setState({ textComment: 'mycomment' }); - click(element.find('.js-issue-comment-submit')); + wrapper.setState({ textComment: 'mycomment' }); + click(wrapper.find('.js-issue-comment-submit')); expect(onComment.mock.calls.length).toBe(1); }); it('should render the alternative cancel button label', () => { - const element = shallow( - - ); + const wrapper = shallowRender({ autoTriggered: true }); expect( - element + wrapper .find('.js-issue-comment-cancel') .childAt(0) .text() ).toBe('skip'); }); + +it('should handle ctrl+enter', () => { + const onComment = jest.fn(); + const wrapper = shallowRender({ comment: { markdown: 'yes' }, onComment }); + + wrapper + .instance() + .handleKeyboard(mockEvent({ ctrlKey: true, nativeEvent: { code: KeyboardCodes.Enter } })); + + expect(onComment).toBeCalled(); +}); + +it('should stopPropagation for arrow keys events', () => { + const wrapper = shallowRender(); + + const event = mockEvent({ + nativeEvent: { code: KeyboardCodes.UpArrow }, + stopPropagation: jest.fn() + }); + wrapper.instance().handleKeyboard(event); + + expect(event.stopPropagation).toBeCalled(); +}); + +function shallowRender(overrides: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap index 9962424c1c3..9fc6c4cc228 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap @@ -12,7 +12,7 @@ exports[`should render the comment popup correctly when changing a comment 1`] = autoFocus={true} onChange={[Function]} onKeyDown={[Function]} - placeholder="" + placeholder="placeholder test" rows={2} value="*test*" /> diff --git a/server/sonar-web/src/main/js/helpers/keycodes.ts b/server/sonar-web/src/main/js/helpers/keycodes.ts index c57d397ae89..2976b56ae77 100644 --- a/server/sonar-web/src/main/js/helpers/keycodes.ts +++ b/server/sonar-web/src/main/js/helpers/keycodes.ts @@ -17,52 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export enum KeyCodes { - LeftArrow = 37, - UpArrow = 38, - RightArrow = 39, - DownArrow = 40, - - Alt = 18, - Backspace = 8, - CapsLock = 20, - Command = 93, - Ctrl = 17, - Delete = 46, - End = 35, - Enter = 13, - Escape = 27, - Home = 36, - PageDown = 34, - PageUp = 33, - Shift = 16, - Space = 32, - Tab = 9, +export enum KeyboardCodes { + LeftArrow = 'ArrowLeft', + UpArrow = 'ArrowUp', + RightArrow = 'ArrowRight', + DownArrow = 'ArrowDown', + Backspace = 'Backspace', + CapsLock = 'CapsLock', + Command = 'ContextMenu', + Delete = 'Delete', + End = 'End', + Enter = 'Enter', + Escape = 'Escape', + Home = 'Home', + PageDown = 'PageDown', + PageUp = 'PageUp', + Space = 'Space', + Tab = 'Tab' +} - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90 +export enum KeyboardKeys { + Alt = 'Alt' } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index 9a7b019eef1..32fa9d709bd 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { ReactWrapper, ShallowWrapper } from 'enzyme'; +import { KeyboardCodes, KeyboardKeys } from './keycodes'; export function mockEvent(overrides = {}) { return { @@ -68,37 +69,26 @@ export function change(element: ShallowWrapper | ReactWrapper, value: string, ev } } -export const KEYCODE_MAP: { [keycode: number]: string } = { - 13: 'enter', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down' +export const KEYCODE_MAP: { [code in KeyboardCodes]?: string } = { + [KeyboardCodes.Enter]: 'enter', + [KeyboardCodes.LeftArrow]: 'left', + [KeyboardCodes.UpArrow]: 'up', + [KeyboardCodes.RightArrow]: 'right', + [KeyboardCodes.DownArrow]: 'down' }; -export function keydown(key: number | string): void { - let keyCode; - if (typeof key === 'number') { - keyCode = key; - } else { - // eslint-disable-next-line no-console - console.warn('Using strings in keydown() is deprecated. Consider using the KeyCodes enum.'); - const mapped = Object.entries(KEYCODE_MAP).find(([_, value]) => value === key); - if (!mapped) { - throw new Error(`Cannot map key "${key}" to a keyCode!`); - } - keyCode = mapped[0]; - } - - const event = new KeyboardEvent('keydown', { keyCode, which: keyCode } as KeyboardEventInit); +export function keydown(args: { code?: KeyboardCodes; key?: KeyboardKeys }): void { + const event = new KeyboardEvent('keydown', args as KeyboardEventInit); document.dispatchEvent(event); } -export function elementKeydown(element: ShallowWrapper, keyCode: number): void { +export function elementKeydown(element: ShallowWrapper, code: KeyboardCodes): void { const event = { currentTarget: { element }, - keyCode, - preventDefault() {} + nativeEvent: { code }, + preventDefault() { + /*noop*/ + } }; if (typeof element.type() === 'string') {