aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorGuillaume Peoc'h <guillaume.peoch@sonarsource.com>2022-02-22 11:41:15 +0100
committersonartech <sonartech@sonarsource.com>2022-02-25 20:02:54 +0000
commita44aac692f5cc63740eb64eab9519b998c860f9d (patch)
tree1d560f65cfb6cd05ae89a78d197daf7876e6c8cc /server/sonar-web
parent50fa615acd453be581adda89e7fec784d3adcc34 (diff)
downloadsonarqube-a44aac692f5cc63740eb64eab9519b998c860f9d.tar.gz
sonarqube-a44aac692f5cc63740eb64eab9519b998c860f9d.zip
SONAR-16008 Support moving between tabs with the keyboard
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx61
2 files changed, 102 insertions, 1 deletions
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 bb4faab83bf..5593345a7ed 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
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import key from 'keymaster';
import * as React from 'react';
import BoxedTabs from '../../../components/controls/BoxedTabs';
import { translate } from '../../../helpers/l10n';
@@ -47,6 +48,8 @@ export enum TabKeys {
FixRecommendation = 'fix'
}
+const HOTSPOT_KEYMASTER_SCOPE = 'hotspots-list';
+
export default class HotspotViewerTabs extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
@@ -57,6 +60,10 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
};
}
+ componentDidMount() {
+ this.registerKeyboardEvents();
+ }
+
componentDidUpdate(prevProps: Props) {
if (this.props.hotspot.key !== prevProps.hotspot.key) {
const tabs = this.computeTabs();
@@ -75,6 +82,26 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
}
}
+ componentWillUnmount() {
+ this.unregisterKeyboardEvents();
+ }
+
+ registerKeyboardEvents() {
+ key.setScope(HOTSPOT_KEYMASTER_SCOPE);
+ key('left', HOTSPOT_KEYMASTER_SCOPE, () => {
+ this.selectNeighboringTab(-1);
+ return false;
+ });
+ key('right', HOTSPOT_KEYMASTER_SCOPE, () => {
+ this.selectNeighboringTab(+1);
+ return false;
+ });
+ }
+
+ unregisterKeyboardEvents() {
+ key.deleteScope(HOTSPOT_KEYMASTER_SCOPE);
+ }
+
handleSelectTabs = (tabKey: TabKeys) => {
const { tabs } = this.state;
const currentTab = tabs.find(tab => tab.key === tabKey)!;
@@ -112,6 +139,21 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
];
}
+ selectNeighboringTab(shift: number) {
+ this.setState(({ tabs, currentTab }) => {
+ const index = currentTab && tabs.findIndex(tab => tab.key === currentTab.key);
+
+ if (index !== undefined && index > -1) {
+ const newIndex = Math.max(0, Math.min(tabs.length - 1, index + shift));
+ return {
+ currentTab: tabs[newIndex]
+ };
+ }
+
+ return { currentTab };
+ });
+ }
+
render() {
const { codeTabContent } = this.props;
const { tabs, currentTab } = this.state;
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 2ba96c577f8..ffbc475c3da 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
@@ -17,13 +17,36 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import * as React from 'react';
import BoxedTabs, { BoxedTabsProps } from '../../../../components/controls/BoxedTabs';
+import { KeyboardCodes } from '../../../../helpers/keycodes';
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
+import { KEYCODE_MAP, keydown } from '../../../../helpers/testUtils';
import HotspotViewerTabs, { TabKeys } from '../HotspotViewerTabs';
+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 = 'hotspots-list';
+
+ key.getScope = () => scope;
+ key.setScope = (newScope: string) => {
+ scope = newScope;
+ };
+ key.deleteScope = jest.fn();
+
+ return key;
+});
+
it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot('risk');
@@ -113,6 +136,42 @@ it('should select first tab when hotspot location is selected and is not undefin
expect(wrapper.state().currentTab.key).toBe(TabKeys.VulnerabilityDescription);
});
+describe('keyboard navigation', () => {
+ const tabList = [
+ TabKeys.Code,
+ TabKeys.RiskDescription,
+ TabKeys.VulnerabilityDescription,
+ TabKeys.FixRecommendation
+ ];
+ const wrapper = shallowRender();
+
+ it.each([
+ ['selecting next', 0, 1, 1],
+ ['selecting previous', 1, -1, 0],
+ ['selecting previous, non-existent', 0, -1, 0],
+ ['selecting next, non-existent', 3, 1, 3]
+ ])('should work when %s', (_, start, shift, expected) => {
+ wrapper.setState({ currentTab: wrapper.state().tabs[start] });
+ wrapper.instance().selectNeighboringTab(shift);
+
+ expect(wrapper.state().currentTab.key).toBe(tabList[expected]);
+ });
+});
+
+it('should navigate when up and down key are pressed', () => {
+ const wrapper = mount<HotspotViewerTabs>(
+ <HotspotViewerTabs codeTabContent={<div>CodeTabContent</div>} hotspot={mockHotspot()} />
+ );
+
+ const selectNeighboringTab = jest.spyOn(wrapper.instance(), 'selectNeighboringTab');
+
+ keydown({ code: KeyboardCodes.LeftArrow });
+ expect(selectNeighboringTab).toBeCalledWith(-1);
+
+ keydown({ code: KeyboardCodes.RightArrow });
+ expect(selectNeighboringTab).toBeCalledWith(1);
+});
+
function shallowRender(props?: Partial<HotspotViewerTabs['props']>) {
return shallow<HotspotViewerTabs>(
<HotspotViewerTabs