aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2020-08-11 15:44:01 +0200
committersonartech <sonartech@sonarsource.com>2020-08-26 20:06:43 +0000
commitc1f7a7e59d34b1f5b961841ee1004739fcfa7a0f (patch)
treeec7332a18c7aa2d3f77127bc20b8fbc24f3b78d1 /server
parentc393a120cf35f1abeefc7302421cc67ac98842b3 (diff)
downloadsonarqube-c1f7a7e59d34b1f5b961841ee1004739fcfa7a0f.tar.gz
sonarqube-c1f7a7e59d34b1f5b961841ee1004739fcfa7a0f.zip
SONAR-12950 Add keyboard navigation to hotspots
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx26
2 files changed, 61 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index f4dc3e08c16..7f49f682cc8 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Location } from 'history';
+import * as key from 'keymaster';
import { flatMap, range } from 'lodash';
import * as React from 'react';
import { addSideBarClass, removeSideBarClass } from 'sonar-ui-common/helpers/pages';
@@ -40,6 +41,7 @@ import {
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
+const HOTSPOT_KEYMASTER_SCOPE = 'hotspots-list';
const PAGE_SIZE = 500;
interface Props {
@@ -91,6 +93,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
this.mounted = true;
addSideBarClass();
this.fetchInitialData();
+ this.registerKeyboardEvents();
}
componentDidUpdate(previous: Props) {
@@ -115,9 +118,41 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
componentWillUnmount() {
removeSideBarClass();
+ this.unregisterKeyboardEvents();
this.mounted = false;
}
+ registerKeyboardEvents() {
+ key.setScope(HOTSPOT_KEYMASTER_SCOPE);
+ key('up', HOTSPOT_KEYMASTER_SCOPE, () => {
+ this.selectNeighboringHotspot(-1);
+ return false;
+ });
+ key('down', HOTSPOT_KEYMASTER_SCOPE, () => {
+ this.selectNeighboringHotspot(+1);
+ return false;
+ });
+ }
+
+ selectNeighboringHotspot = (shift: number) => {
+ this.setState(({ hotspots, selectedHotspot }) => {
+ const index = selectedHotspot && hotspots.findIndex(h => h.key === selectedHotspot.key);
+
+ if (index !== undefined && index > -1) {
+ const newIndex = Math.max(0, Math.min(hotspots.length - 1, index + shift));
+ return {
+ selectedHotspot: hotspots[newIndex]
+ };
+ }
+
+ return { selectedHotspot };
+ });
+ };
+
+ unregisterKeyboardEvents() {
+ key.deleteScope(HOTSPOT_KEYMASTER_SCOPE);
+ }
+
constructFiltersFromProps(
props: Props
): Pick<HotspotFilters, 'assignedToMe' | 'sinceLeakPeriod'> {
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
index caf2e3ba6f1..ce99870b36e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
@@ -343,6 +343,32 @@ it('should handle leakPeriod filter change', async () => {
expect(getSecurityHotspots).toBeCalledWith(expect.objectContaining({ sinceLeakPeriod: true }));
});
+describe('keyboard navigation', () => {
+ const hotspots = [
+ mockRawHotspot({ key: 'k1' }),
+ mockRawHotspot({ key: 'k2' }),
+ mockRawHotspot({ key: 'k3' })
+ ];
+ (getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ hotspots, paging: { total: 3 } });
+
+ 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', 2, 1, 2],
+ ['jumping down', 0, 18, 2],
+ ['jumping up', 2, -18, 0],
+ ['none selected', 4, -2, 4]
+ ])('should work when %s', (_, start, shift, expected) => {
+ wrapper.setState({ selectedHotspot: hotspots[start] });
+ wrapper.instance().selectNeighboringHotspot(shift);
+
+ expect(wrapper.state().selectedHotspot).toBe(hotspots[expected]);
+ });
+});
+
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {
return shallow<SecurityHotspotsApp>(
<SecurityHotspotsApp