diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2020-08-11 15:44:01 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-08-26 20:06:43 +0000 |
commit | c1f7a7e59d34b1f5b961841ee1004739fcfa7a0f (patch) | |
tree | ec7332a18c7aa2d3f77127bc20b8fbc24f3b78d1 /server | |
parent | c393a120cf35f1abeefc7302421cc67ac98842b3 (diff) | |
download | sonarqube-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.tsx | 35 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx | 26 |
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 |