diff options
author | Guillaume Peoc'h <guillaume.peoch@sonarsource.com> | 2022-05-30 11:17:09 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-05-31 20:02:50 +0000 |
commit | a5b423674c8da134b60f464857fed2ea3df74b01 (patch) | |
tree | fc394b2715d1d8358fcd247dc8c113a6787181d2 /server/sonar-web/src | |
parent | 5dc735487ccaa4fd9557a2bc99b1654756c65d8d (diff) | |
download | sonarqube-a5b423674c8da134b60f464857fed2ea3df74b01.tar.gz sonarqube-a5b423674c8da134b60f464857fed2ea3df74b01.zip |
SONAR-16337 Handle shortcuts and inputs when keydown
Diffstat (limited to 'server/sonar-web/src')
16 files changed, 96 insertions, 12 deletions
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 47549c7d890..03b0ea8d70c 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 @@ -28,6 +28,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 { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; @@ -272,9 +273,10 @@ export class Search extends React.PureComponent<WithRouterProps, State> { }; handleSKeyDown = (event: KeyboardEvent) => { - const { tagName } = event.target as HTMLElement; - const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; - if (event.key === KeyboardKeys.KeyS && !isInput) { + if (isInput(event) || isShortcut(event)) { + return true; + } + if (event.key === KeyboardKeys.KeyS) { event.preventDefault(); this.focusInput(); this.openSearch(); 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 39d4e1d3895..8b10c19f072 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 @@ -104,6 +104,8 @@ it('shows warning about short input', () => { it('should open the results when pressing key S and close it when pressing Escape', () => { const router = mockRouter(); const form = shallowRender({ router }); + keydown({ key: KeyboardKeys.KeyS, ctrlKey: true }); + expect(form.state().open).toBe(false); keydown({ key: KeyboardKeys.KeyS }); expect(form.state().open).toBe(true); elementKeydown(form.find('SearchBox'), KeyboardKeys.Escape); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 210d99ccf96..94a9b44e2ea 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -32,6 +32,7 @@ import SearchBox from '../../../components/controls/SearchBox'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import BackIcon from '../../../components/icons/BackIcon'; import '../../../components/search-navigator.css'; +import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; import { @@ -152,10 +153,8 @@ export class App extends React.PureComponent<Props, State> { }; handleKeyPress = (event: KeyboardEvent) => { - const { tagName } = event.target as HTMLElement; - const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; - if (isInput) { - return false; + if (isInput(event) || isShortcut(event)) { + return true; } switch (event.key) { case KeyboardKeys.LeftArrow: diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index 74c2a9195ab..a9a0eadb745 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { Button } from '../../../components/controls/buttons'; import ListFooter from '../../../components/controls/ListFooter'; import { Alert } from '../../../components/ui/Alert'; +import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures'; @@ -92,6 +93,9 @@ export default class FilesView extends React.PureComponent<Props, State> { } handleKeyDown = (event: KeyboardEvent) => { + if (isInput(event) || isShortcut(event)) { + return true; + } if (event.code === KeyboardCodes.UpArrow) { event.preventDefault(); this.selectPrevious(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx index 53eb6307322..5d4b6754548 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx @@ -92,6 +92,9 @@ it('should correctly bind key events for file navigation', () => { keydown({ code: KeyboardCodes.UpArrow }); expect(handleSelect).toBeCalledWith(FILES[2]); + keydown({ code: KeyboardCodes.RightArrow, ctrlKey: true }); + expect(handleOpen).not.toBeCalled(); + keydown({ code: KeyboardCodes.RightArrow }); expect(handleOpen).toBeCalled(); }); 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 8d148446839..911b422183f 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 @@ -45,6 +45,7 @@ import { } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; import { parseIssueFromResponse } from '../../../helpers/issues'; +import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardCodes, KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { @@ -263,6 +264,10 @@ export default class App extends React.PureComponent<Props, State> { return; } + if (isInput(event) || isShortcut(event)) { + return true; + } + if (event.key === KeyboardKeys.Alt) { event.preventDefault(); this.setState(actions.enableLocationsNavigator); 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 689eec93251..952aeb5d9b7 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 @@ -241,6 +241,8 @@ it('should correctly bind key events for issue navigation', async () => { keydown({ code: KeyboardCodes.DownArrow }); expect(wrapper.state('selected')).toBe(ISSUES[3].key); + keydown({ code: KeyboardCodes.RightArrow, ctrlKey: true }); + expect(push).not.toBeCalled(); keydown({ code: KeyboardCodes.RightArrow }); expect(push).toBeCalledTimes(1); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx index 4adab7cdcb5..996ea790cd4 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx @@ -20,6 +20,7 @@ import classNames from 'classnames'; import * as React from 'react'; import BoxedTabs from '../../../components/controls/BoxedTabs'; +import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardCodes } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; import { sanitizeString } from '../../../helpers/sanitize'; @@ -86,6 +87,9 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> } handleKeyboardNavigation = (event: KeyboardEvent) => { + if (isInput(event) || isShortcut(event)) { + return true; + } if (event.code === KeyboardCodes.LeftArrow) { event.preventDefault(); this.selectNeighboringTab(-1); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx index 50fbf5f4392..db529dd1645 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx @@ -157,6 +157,19 @@ describe('keyboard navigation', () => { }); }); +it("shouldn't navigate when ctrl or command are pressed with up and down", () => { + const wrapper = mount<HotspotViewerTabs>( + <HotspotViewerTabs codeTabContent={<div>CodeTabContent</div>} hotspot={mockHotspot()} /> + ); + + wrapper.setState({ currentTab: wrapper.state().tabs[0] }); + wrapper + .instance() + .handleKeyboardNavigation(mockEvent({ code: KeyboardCodes.LeftArrow, metaKey: true })); + + expect(wrapper.state().currentTab.key).toBe(TabKeys.Code); +}); + it('should navigate when up and down key are pressed', () => { const wrapper = mount<HotspotViewerTabs>( <HotspotViewerTabs codeTabContent={<div>CodeTabContent</div>} hotspot={mockHotspot()} /> 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 695c062d3db..b042c9b196d 100644 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx @@ -21,6 +21,7 @@ import classNames from 'classnames'; import { difference } from 'lodash'; import * as React from 'react'; import SearchBox from '../../components/controls/SearchBox'; +import { isShortcut } from '../../helpers/keyboardEventHelpers'; import { KeyboardCodes } from '../../helpers/keycodes'; import { translateWithParameters } from '../../helpers/l10n'; import MultiSelectOption from './MultiSelectOption'; @@ -139,6 +140,9 @@ export default class MultiSelect extends React.PureComponent<PropsWithDefault, S }; handleKeyboard = (event: KeyboardEvent) => { + if (isShortcut(event)) { + return true; + } switch (event.code) { case KeyboardCodes.DownArrow: event.stopPropagation(); 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 b4197c05a62..4ee53ee75b2 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 @@ -79,12 +79,16 @@ it('should correctly bind key events for component navigation', () => { keydown({ code: KeyboardCodes.DownArrow }); expect(onHighlight).toBeCalledWith(COMPONENTS[0]); + keydown({ code: KeyboardCodes.RightArrow, metaKey: true }); + expect(onSelect).not.toBeCalled(); keydown({ code: KeyboardCodes.RightArrow }); expect(onSelect).toBeCalledWith(COMPONENTS[0]); keydown({ code: KeyboardCodes.Enter }); expect(onSelect).toBeCalledWith(COMPONENTS[0]); + keydown({ code: KeyboardCodes.LeftArrow, metaKey: true }); + expect(onGoToParent).not.toBeCalled(); keydown({ code: KeyboardCodes.LeftArrow }); expect(onGoToParent).toBeCalled(); }); diff --git a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx index b92a470199e..4232c27bc57 100644 --- a/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import PageActions from '../../components/ui/PageActions'; import { getComponentMeasureUniqueKey } from '../../helpers/component'; +import { isInput, isShortcut } from '../../helpers/keyboardEventHelpers'; import { KeyboardCodes } from '../../helpers/keycodes'; import { ComponentMeasure } from '../../types/types'; import { getWrappedDisplayName } from './utils'; @@ -50,6 +51,9 @@ export default function withKeyboardNavigation<P>( } handleKeyDown = (event: KeyboardEvent) => { + if (isInput(event) || isShortcut(event)) { + return true; + } if (event.code === KeyboardCodes.UpArrow) { event.preventDefault(); return this.skipIfFile(this.handleHighlightPrevious); diff --git a/server/sonar-web/src/main/js/components/issue/Issue.tsx b/server/sonar-web/src/main/js/components/issue/Issue.tsx index 76de0d88fa3..b91d7065d73 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.tsx +++ b/server/sonar-web/src/main/js/components/issue/Issue.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { setIssueAssignee } from '../../api/issues'; +import { isInput, isShortcut } from '../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../helpers/keycodes'; import { BranchLike } from '../../types/branch-like'; import { Issue as TypeIssue } from '../../types/types'; @@ -68,10 +69,8 @@ export default class Issue extends React.PureComponent<Props> { } handleKeyDown = (event: KeyboardEvent) => { - const { tagName } = event.target as HTMLElement; - const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; - if (isInput) { - return false; + if (isInput(event) || isShortcut(event)) { + return true; } else if (event.key === KeyboardKeys.KeyF) { event.preventDefault(); return this.togglePopup('transition'); diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx index 85bfbe69d00..91a7b49da56 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx @@ -50,6 +50,9 @@ it('should call the proper function with the proper props when pressing shortcut }); shallowRender({ onPopupToggle, issue, onCheck }); + keydown({ key: KeyboardKeys.KeyF, metaKey: true }); + expect(onPopupToggle).not.toBeCalledWith(issue.key, 'transition', undefined); + keydown({ key: KeyboardKeys.KeyF }); expect(onPopupToggle).toBeCalledWith(issue.key, 'transition', undefined); @@ -63,6 +66,9 @@ it('should call the proper function with the proper props when pressing shortcut keydown({ key: KeyboardKeys.KeyI }); expect(onPopupToggle).toBeCalledWith(issue.key, 'set-severity', undefined); + keydown({ key: KeyboardKeys.KeyC, metaKey: true }); + expect(onPopupToggle).not.toBeCalledWith(issue.key, 'comment', undefined); + keydown({ key: KeyboardKeys.KeyC }); expect(onPopupToggle).toBeCalledWith(issue.key, 'comment', undefined); keydown({ key: KeyboardKeys.Escape }); diff --git a/server/sonar-web/src/main/js/helpers/keyboardEventHelpers.ts b/server/sonar-web/src/main/js/helpers/keyboardEventHelpers.ts new file mode 100644 index 00000000000..fc156ad6da7 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/keyboardEventHelpers.ts @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export function isShortcut(event: KeyboardEvent): boolean { + return event.ctrlKey || event.metaKey; +} + +export function isInput(event: KeyboardEvent): boolean { + const { tagName } = event.target as HTMLElement; + return ['INPUT', 'SELECT', 'TEXTAREA'].includes(tagName); +} diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index 1b1abba7dbe..72228cf16e0 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -82,7 +82,12 @@ export const KEYCODE_MAP: { [code in KeyboardCodes]?: string } = { [KeyboardCodes.DownArrow]: 'down' }; -export function keydown(args: { code?: KeyboardCodes; key?: KeyboardKeys }): void { +export function keydown(args: { + code?: KeyboardCodes; + key?: KeyboardKeys; + metaKey?: boolean; + ctrlKey?: boolean; +}): void { const event = new KeyboardEvent('keydown', args as KeyboardEventInit); document.dispatchEvent(event); } |