From ec01b0f2e37d0ac1aa9107c1abde9fde7ef7f9b0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 8 Aug 2017 16:29:03 +0200 Subject: [PATCH] SONAR-9614 Add keyboard shortcuts on project measures page --- .../apps/component-measures/components/App.js | 4 +- .../components/Breadcrumbs.js | 17 +++ .../components/MeasureContent.js | 20 ++- .../components/MeasureViewSelect.js | 1 + .../MeasureViewSelect-test.js.snap | 1 + .../drilldown/ComponentCell.js | 10 +- .../drilldown/ComponentsListRow.js | 8 +- .../component-measures/drilldown/FilesView.js | 117 +++++++++++++++--- .../main/js/apps/component-measures/style.css | 4 + 9 files changed, 151 insertions(+), 31 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index 2a4e2ad6b62..a71b9e69b73 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -20,6 +20,7 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; +import key from 'keymaster'; import MeasureContentContainer from './MeasureContentContainer'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; @@ -71,7 +72,7 @@ export default class App extends React.PureComponent { this.mounted = true; this.props.fetchMetrics(); this.fetchMeasures(this.props); - + key.setScope('measures-files'); const footer = document.getElementById('footer'); if (footer) { footer.classList.add('search-navigator-footer'); @@ -89,6 +90,7 @@ export default class App extends React.PureComponent { componentWillUnmount() { this.mounted = false; + key.deleteScope('measures-files'); const footer = document.getElementById('footer'); if (footer) { footer.classList.remove('search-navigator-footer'); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js index a9b9c3ecbcf..6edb170b06e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import key from 'keymaster'; import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; import type { Component } from '../types'; @@ -44,6 +45,7 @@ export default class Breadcrumbs extends React.PureComponent { componentDidMount() { this.mounted = true; this.fetchBreadcrumbs(this.props); + this.attachShortcuts(); } componentWillReceiveProps(nextProps: Props) { @@ -54,6 +56,21 @@ export default class Breadcrumbs extends React.PureComponent { componentWillUnmount() { this.mounted = false; + this.detachShortcuts(); + } + + attachShortcuts() { + key('left', 'measures-files', () => { + const { breadcrumbs } = this.state; + if (breadcrumbs.length > 1) { + this.props.handleSelect(breadcrumbs[breadcrumbs.length - 2].key); + } + return false; + }); + } + + detachShortcuts() { + key.unbind('left', 'measures-files'); } fetchBreadcrumbs = ({ component, rootComponent }: Props) => { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index dbfd60547a0..66e9fe47756 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -58,6 +58,7 @@ type State = { components: Array, metric: ?Metric, paging?: Paging, + selected: ?string, view: ?string }; @@ -68,6 +69,7 @@ export default class MeasureContent extends React.PureComponent { components: [], metric: null, paging: null, + selected: null, view: null }; @@ -86,6 +88,13 @@ export default class MeasureContent extends React.PureComponent { this.mounted = false; } + getSelectedIndex = (): ?number => { + const index = this.state.components.findIndex( + component => component.key === this.state.selected + ); + return index !== -1 ? index : null; + }; + getComponentRequestParams = (view: string, metric: Metric, options: Object = {}) => { const strategy = view === 'list' ? 'leaves' : 'children'; const metricKeys = [metric.key]; @@ -127,6 +136,7 @@ export default class MeasureContent extends React.PureComponent { ), metric, paging: r.paging, + selected: r.components.length > 0 ? r.components[0].key : null, view }); } @@ -168,6 +178,8 @@ export default class MeasureContent extends React.PureComponent { ); }; + onSelectComponent = (component: string) => this.setState({ selected: component }); + renderContent() { const { component, leakPeriod } = this.props; @@ -201,14 +213,18 @@ export default class MeasureContent extends React.PureComponent { } if (['list', 'tree'].includes(view)) { + const selectedIdx = this.getSelectedIndex(); return ( ); } @@ -253,7 +269,7 @@ export default class MeasureContent extends React.PureComponent { view={view} />} void }; @@ -69,24 +67,20 @@ export default class ComponentCell extends React.PureComponent { render() { const { component } = this.props; - const linkClassName = classNames('link-no-underline', { - selected: this.props.isSelected - }); - return (
{component.refId == null ? {this.renderInner()} : diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js index 29613eb6b1e..9b813b15ba3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import classNames from 'classnames'; import ComponentCell from './ComponentCell'; import MeasureCell from './MeasureCell'; import type { Component } from '../types'; @@ -38,9 +39,12 @@ export default function ComponentsListRow(props: Props) { const measure = component.measures.find(measure => measure.metric === metric.key); return { ...measure, metric }; }); + const rowClass = classNames('measure-details-component-row', { + selected: props.isSelected + }); return ( - - + + diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js index a37c37af1d6..c6d5c826fb5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js @@ -19,8 +19,11 @@ */ // @flow import React from 'react'; +import key from 'keymaster'; +import { throttle } from 'lodash'; import ComponentsList from './ComponentsList'; import ListFooter from '../../../components/controls/ListFooter'; +import { scrollToElement } from '../../../helpers/scrolling'; import type { ComponentEnhanced, Paging } from '../types'; import type { Metric } from '../../../store/metrics/actions'; @@ -28,26 +31,104 @@ type Props = {| components: Array, fetchMore: () => void, handleSelect: string => void, + handleOpen: string => void, metric: Metric, metrics: { [string]: Metric }, - paging: ?Paging + paging: ?Paging, + selectedKey: ?string, + selectedIdx: ?number |}; -export default function ListView(props: Props) { - return ( -
- - {props.paging && - } -
- ); +export default class ListView extends React.PureComponent { + listContainer: HTMLElement; + props: Props; + + constructor(props: Props) { + super(props); + this.selectNext = throttle(this.selectNext, 100); + this.selectPrevious = throttle(this.selectPrevious, 100); + } + + componentDidMount() { + this.attachShortcuts(); + } + + componentDidUpdate() { + if (this.listContainer && this.props.selectedIdx != null) { + const elem = this.listContainer.getElementsByClassName('selected')[0]; + if (elem) { + scrollToElement(elem, { topOffset: 215, bottomOffset: 100 }); + } + } + } + + componentWillUnmount() { + this.detachShortcuts(); + } + + attachShortcuts() { + key('up', 'measures-files', () => { + this.selectPrevious(); + return false; + }); + key('down', 'measures-files', () => { + this.selectNext(); + return false; + }); + key('right', 'measures-files', () => { + this.openSelected(); + return false; + }); + } + + detachShortcuts() { + ['up', 'down', 'right'].map(action => key.unbind(action, 'measures-files')); + } + + openSelected = () => { + if (this.props.selectedKey != null) { + this.props.handleOpen(this.props.selectedKey); + } + }; + + selectPrevious = () => { + const { selectedIdx } = this.props; + if (selectedIdx != null && selectedIdx > 0) { + this.props.handleSelect(this.props.components[selectedIdx - 1].key); + } else { + this.props.handleSelect(this.props.components[this.props.components.length - 1].key); + } + }; + + selectNext = () => { + const { selectedIdx } = this.props; + if (selectedIdx != null && selectedIdx < this.props.components.length - 1) { + this.props.handleSelect(this.props.components[selectedIdx + 1].key); + } else { + this.props.handleSelect(this.props.components[0].key); + } + }; + + render() { + return ( +
{ + this.listContainer = elem; + }}> + + {this.props.paging && + } +
+ ); + } } diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index 5f8e064860b..2dba101338e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -52,6 +52,10 @@ margin-top: 4px; } +.measure-details-component-row.selected { + background-color: #cae3f2 !important; +} + .measure-details-component-cell { max-width: 0; } -- 2.39.5