aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorGuillaume Peoc'h <guillaume.peoch@sonarsource.com>2022-05-30 11:17:09 +0200
committersonartech <sonartech@sonarsource.com>2022-05-31 20:02:50 +0000
commita5b423674c8da134b60f464857fed2ea3df74b01 (patch)
treefc394b2715d1d8358fcd247dc8c113a6787181d2 /server/sonar-web
parent5dc735487ccaa4fd9557a2bc99b1654756c65d8d (diff)
downloadsonarqube-a5b423674c8da134b60f464857fed2ea3df74b01.tar.gz
sonarqube-a5b423674c8da134b60f464857fed2ea3df74b01.zip
SONAR-16337 Handle shortcuts and inputs when keydown
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/common/MultiSelect.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/withKeyboardNavigation-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withKeyboardNavigation.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/issue-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/helpers/keyboardEventHelpers.ts28
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts7
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);
}