Browse Source

SONAR-12112 horizontal scrolling

tags/7.8
Jeremy Davis 5 years ago
parent
commit
5340427e78

+ 29
- 151
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx View File

@@ -19,27 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import {
createSnippets,
expandSnippet,
inSnippet,
EXPAND_BY_LINES,
LINES_BELOW_LAST,
MERGE_DISTANCE
} from './utils';
import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
import Line from '../../../components/SourceViewer/components/Line';
import { createSnippets, expandSnippet, EXPAND_BY_LINES, MERGE_DISTANCE } from './utils';
import SnippetViewer from './SnippetViewer';
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { getSources } from '../../../api/components';
import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
import {
optimizeLocationMessage,
optimizeHighlightedSymbols,
optimizeSelectedIssue
} from '../../../components/SourceViewer/helpers/lines';
import { translate } from '../../../helpers/l10n';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getBranchLikeQuery } from '../../../helpers/branches';

interface Props {
@@ -209,155 +194,49 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
};

renderLine({
displayDuplications,
renderSnippet({
index,
issuesForLine,
issueLocations,
line,
snippet,
symbols,
verticalBuffer
issuesByLine,
last,
locationsByLine,
snippet
}: {
displayDuplications: boolean;
index: number;
issuesForLine: T.Issue[];
issueLocations: T.LinearIssueLocation[];
line: T.SourceLine;
issuesByLine: T.IssuesByLine;
last: boolean;
locationsByLine: { [line: number]: T.LinearIssueLocation[] };
snippet: T.SourceLine[];
symbols: string[];
verticalBuffer: number;
}) {
const { openIssuesByLine } = this.state;
const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);

const { duplications, duplicationsByLine } = this.props;
const duplicationsCount = duplications ? duplications.length : 0;
const lineDuplications =
(duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];

const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);

return (
<Line
<SnippetViewer
branchLike={this.props.branchLike}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={displayDuplications}
displayIssues={!isSinkLine || issuesForLine.length > 1}
displayLocationMarkers={true}
duplications={lineDuplications}
duplicationsCount={duplicationsCount}
highlighted={false}
highlightedLocationMessage={optimizeLocationMessage(
this.props.highlightedLocationMessage,
secondaryIssueLocations
)}
highlightedSymbols={optimizeHighlightedSymbols(symbols, this.state.highlightedSymbols)}
issueLocations={issueLocations}
issuePopup={this.props.issuePopup}
issues={issuesForLine}
key={line.line}
last={false}
line={line}
linePopup={this.props.linePopup}
component={this.props.snippetGroup.component}
expandBlock={this.expandBlock}
handleCloseIssues={this.handleCloseIssues}
handleLinePopupToggle={this.handleLinePopupToggle}
handleOpenIssues={this.handleOpenIssues}
handleSymbolClick={this.handleSymbolClick}
highlightedLocationMessage={this.props.highlightedLocationMessage}
highlightedSymbols={this.state.highlightedSymbols}
index={index}
issue={this.props.issue}
issuesByLine={issuesByLine}
key={index}
last={last}
loadDuplications={this.loadDuplications}
locations={this.props.locations}
locationsByLine={locationsByLine}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
onIssueSelect={() => {}}
onIssueUnselect={() => {}}
onIssuesClose={this.handleCloseIssues}
onIssuesOpen={this.handleOpenIssues}
onLinePopupToggle={this.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.handleSymbolClick}
openIssues={openIssuesByLine[line.line]}
previousLine={index > 0 ? snippet[index - 1] : undefined}
openIssuesByLine={this.state.openIssuesByLine}
renderDuplicationPopup={this.renderDuplicationPopup}
scroll={this.props.scroll}
secondaryIssueLocations={secondaryIssueLocations}
selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
verticalBuffer={verticalBuffer}
snippet={snippet}
/>
);
}

renderSnippet({
snippet,
index,
issue,
issuesByLine = {},
locationsByLine,
last
}: {
snippet: T.SourceLine[];
index: number;
issue: T.Issue;
issuesByLine: T.IssuesByLine;
locationsByLine: { [line: number]: T.LinearIssueLocation[] };
last: boolean;
}) {
const { component } = this.props.snippetGroup;
const lastLine =
component.measures && component.measures.lines && parseInt(component.measures.lines, 10);

const symbols = symbolsByLine(snippet);

const expandBlock = (direction: T.ExpandDirection) => () => this.expandBlock(index, direction);

const bottomLine = snippet[snippet.length - 1].line;
const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
const lowestVisibleIssue = Math.max(
...Object.keys(issuesByLine)
.map(k => parseInt(k, 10))
.filter(l => inSnippet(l, snippet) && (l === issueLine || this.state.openIssuesByLine[l]))
);
const verticalBuffer = last
? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
: 0;

const displayDuplications = snippet.some(s => !!s.duplicated);

return (
<div className="source-viewer-code snippet" key={index}>
{snippet[0].line > 1 && (
<button
aria-label={translate('source_viewer.expand_above')}
className="expand-block expand-block-above"
onClick={expandBlock('up')}
type="button">
<ExpandSnippetIcon />
</button>
)}
<table className="source-table">
<tbody>
{snippet.map((line, index) =>
this.renderLine({
displayDuplications,
index,
issuesForLine: issuesByLine[line.line] || [],
issueLocations: locationsByLine[line.line] || [],
line,
snippet,
symbols: symbols[line.line],
verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
})
)}
</tbody>
</table>
{(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
<button
aria-label={translate('source_viewer.expand_below')}
className="expand-block expand-block-below"
onClick={expandBlock('down')}
type="button">
<ExpandSnippetIcon />
</button>
)}
</div>
);
}

render() {
const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
const { loading, snippets } = this.state;
@@ -384,7 +263,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
this.renderSnippet({
snippet,
index,
issue,
issuesByLine: last ? issuesByLine : {},
locationsByLine: last && index === snippets.length - 1 ? locations : {},
last: last && index === snippets.length - 1

+ 237
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx View File

@@ -0,0 +1,237 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.
*/
import * as React from 'react';
import { inSnippet, LINES_BELOW_LAST } from './utils';
import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
import Line from '../../../components/SourceViewer/components/Line';
import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
import {
optimizeLocationMessage,
optimizeHighlightedSymbols,
optimizeSelectedIssue
} from '../../../components/SourceViewer/helpers/lines';
import { translate } from '../../../helpers/l10n';
import { scrollHorizontally } from '../../../helpers/scrolling';

interface Props {
branchLike: T.BranchLike | undefined;
component: T.SourceViewerFile;
duplications?: T.Duplication[];
duplicationsByLine?: { [line: number]: number[] };
expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => void;
handleCloseIssues: (line: T.SourceLine) => void;
handleLinePopupToggle: (line: T.SourceLine) => void;
handleOpenIssues: (line: T.SourceLine) => void;
handleSymbolClick: (symbols: string[]) => void;
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
highlightedSymbols: string[];
index: number;
issue: T.Issue;
issuePopup?: { issue: string; name: string };
issuesByLine: T.IssuesByLine;
last: boolean;
linePopup?: T.LinePopup;
loadDuplications: (line: T.SourceLine) => void;
locations: T.FlowLocation[];
locationsByLine: { [line: number]: T.LinearIssueLocation[] };
onIssueChange: (issue: T.Issue) => void;
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
onLocationSelect: (index: number) => void;
openIssuesByLine: T.Dict<boolean>;
renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
scroll?: (element: HTMLElement) => void;
snippet: T.SourceLine[];
}

const SCROLL_LEFT_OFFSET = 32;

export default class SnippetViewer extends React.PureComponent<Props> {
node: React.RefObject<HTMLDivElement>;

constructor(props: Props) {
super(props);
this.node = React.createRef();
}

doScroll = (element: HTMLElement) => {
if (this.props.scroll) {
this.props.scroll(element);
}
const parent = this.node.current as Element;

if (parent) {
scrollHorizontally(element, {
leftOffset: SCROLL_LEFT_OFFSET,
rightOffset: parent.getBoundingClientRect().width - SCROLL_LEFT_OFFSET,
parent
});
}
};

expandBlock = (direction: T.ExpandDirection) => () =>
this.props.expandBlock(this.props.index, direction);

renderLine({
displayDuplications,
index,
issuesForLine,
issueLocations,
line,
snippet,
symbols,
verticalBuffer
}: {
displayDuplications: boolean;
index: number;
issuesForLine: T.Issue[];
issueLocations: T.LinearIssueLocation[];
line: T.SourceLine;
snippet: T.SourceLine[];
symbols: string[];
verticalBuffer: number;
}) {
const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);

const { duplications, duplicationsByLine } = this.props;
const duplicationsCount = duplications ? duplications.length : 0;
const lineDuplications =
(duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];

const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);

return (
<Line
branchLike={this.props.branchLike}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={displayDuplications}
displayIssues={!isSinkLine || issuesForLine.length > 1}
displayLocationMarkers={true}
duplications={lineDuplications}
duplicationsCount={duplicationsCount}
highlighted={false}
highlightedLocationMessage={optimizeLocationMessage(
this.props.highlightedLocationMessage,
secondaryIssueLocations
)}
highlightedSymbols={optimizeHighlightedSymbols(symbols, this.props.highlightedSymbols)}
issueLocations={issueLocations}
issuePopup={this.props.issuePopup}
issues={issuesForLine}
key={line.line}
last={false}
line={line}
linePopup={this.props.linePopup}
loadDuplications={this.props.loadDuplications}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
onIssueSelect={() => {}}
onIssueUnselect={() => {}}
onIssuesClose={this.props.handleCloseIssues}
onIssuesOpen={this.props.handleOpenIssues}
onLinePopupToggle={this.props.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.handleSymbolClick}
openIssues={this.props.openIssuesByLine[line.line]}
previousLine={index > 0 ? snippet[index - 1] : undefined}
renderDuplicationPopup={this.props.renderDuplicationPopup}
scroll={this.doScroll}
secondaryIssueLocations={secondaryIssueLocations}
selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
verticalBuffer={verticalBuffer}
/>
);
}

render() {
const {
component,
issue,
issuesByLine = {},
last,
locationsByLine,
openIssuesByLine,
snippet
} = this.props;
const lastLine =
component.measures && component.measures.lines && parseInt(component.measures.lines, 10);

const symbols = symbolsByLine(snippet);

const bottomLine = snippet[snippet.length - 1].line;
const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
const lowestVisibleIssue = Math.max(
...Object.keys(issuesByLine)
.map(k => parseInt(k, 10))
.filter(l => inSnippet(l, snippet) && (l === issueLine || openIssuesByLine[l]))
);
const verticalBuffer = last
? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
: 0;

const displayDuplications = snippet.some(s => !!s.duplicated);

return (
<div className="source-viewer-code snippet" ref={this.node}>
<table className="source-table">
<tbody>
{snippet[0].line > 1 && (
<tr className="expand-block expand-block-above">
<td colSpan={5}>
<button
aria-label={translate('source_viewer.expand_above')}
onClick={this.expandBlock('up')}
type="button">
<ExpandSnippetIcon />
</button>
</td>
</tr>
)}
{snippet.map((line, index) =>
this.renderLine({
displayDuplications,
index,
issuesForLine: issuesByLine[line.line] || [],
issueLocations: locationsByLine[line.line] || [],
line,
snippet,
symbols: symbols[line.line],
verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
})
)}
{(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
<tr className="expand-block expand-block-below">
<td colSpan={5}>
<button
aria-label={translate('source_viewer.expand_below')}
onClick={this.expandBlock('down')}
type="button">
<ExpandSnippetIcon />
</button>
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
}

+ 5
- 5
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx View File

@@ -102,7 +102,7 @@ it('should expand full component', async () => {
expect(wrapper.state('snippets')[0]).toHaveLength(14);
});

it.only('should get the right branch when expanding', async () => {
it('should get the right branch when expanding', async () => {
(getSources as jest.Mock).mockResolvedValueOnce(
Object.values(
mockSnippetsByComponent('a', [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]).sources
@@ -168,19 +168,19 @@ it('should correctly handle lines actions', () => {

const line = mockSourceLine();
wrapper
.find('Line')
.find('SnippetViewer')
.first()
.prop<Function>('loadDuplications')(line);
expect(loadDuplications).toHaveBeenCalledWith('a', line);

wrapper
.find('Line')
.find('SnippetViewer')
.first()
.prop<Function>('onLinePopupToggle')({ line: 13, name: 'foo' });
.prop<Function>('handleLinePopupToggle')({ line: 13, name: 'foo' });
expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' });

wrapper
.find('Line')
.find('SnippetViewer')
.first()
.prop<Function>('renderDuplicationPopup')(1, 13);
expect(renderDuplicationPopup).toHaveBeenCalledWith(

+ 115
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx View File

@@ -0,0 +1,115 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.
*/
import * as React from 'react';
import { range } from 'lodash';
import { shallow } from 'enzyme';
import SnippetViewer from '../SnippetViewer';
import {
mockSourceViewerFile,
mockMainBranch,
mockIssue,
mockSourceLine
} from '../../../../helpers/testMocks';

it('should render correctly', () => {
const snippet = range(5, 8).map(line => mockSourceLine({ line }));
const wrapper = shallowRender({
snippet
});

expect(wrapper).toMatchSnapshot();
});

it('should render correctly when at the top of the file', () => {
const snippet = range(1, 8).map(line => mockSourceLine({ line }));
const wrapper = shallowRender({
snippet
});

expect(wrapper).toMatchSnapshot();
});

it('should render correctly when at the bottom of the file', () => {
const component = mockSourceViewerFile({ measures: { lines: '14' } });
const snippet = range(10, 14).map(line => mockSourceLine({ line }));
const wrapper = shallowRender({
component,
snippet
});

expect(wrapper).toMatchSnapshot();
});

it('should correctly handle expansion', () => {
const snippet = range(5, 8).map(line => mockSourceLine({ line }));
const expandBlock = jest.fn();

const wrapper = shallowRender({
expandBlock,
index: 2,
snippet
});

wrapper
.find('.expand-block-above button')
.first()
.simulate('click');
expect(expandBlock).toHaveBeenCalledWith(2, 'up');

wrapper
.find('.expand-block-below button')
.first()
.simulate('click');
expect(expandBlock).toHaveBeenCalledWith(2, 'down');
});

function shallowRender(props: Partial<SnippetViewer['props']> = {}) {
return shallow<SnippetViewer>(
<SnippetViewer
branchLike={mockMainBranch()}
component={mockSourceViewerFile()}
duplications={undefined}
duplicationsByLine={undefined}
expandBlock={jest.fn()}
handleCloseIssues={jest.fn()}
handleLinePopupToggle={jest.fn()}
handleOpenIssues={jest.fn()}
handleSymbolClick={jest.fn()}
highlightedLocationMessage={{ index: 0, text: '' }}
highlightedSymbols={[]}
index={0}
issue={mockIssue()}
issuesByLine={{}}
last={false}
linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
locationsByLine={{}}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
openIssuesByLine={{}}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
snippet={[]}
{...props}
/>
);
}

+ 930
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap View File

@@ -0,0 +1,930 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="source-viewer-code snippet"
>
<table
className="source-table"
>
<tbody>
<tr
className="expand-block expand-block-above"
>
<td
colSpan={5}
>
<button
aria-label="source_viewer.expand_above"
onClick={[Function]}
type="button"
>
<ExpandSnippetIcon />
</button>
</td>
</tr>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="5"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 5,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="6"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 6,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 5,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="7"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 7,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 6,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<tr
className="expand-block expand-block-below"
>
<td
colSpan={5}
>
<button
aria-label="source_viewer.expand_below"
onClick={[Function]}
type="button"
>
<ExpandSnippetIcon />
</button>
</td>
</tr>
</tbody>
</table>
</div>
`;

exports[`should render correctly when at the bottom of the file 1`] = `
<div
className="source-viewer-code snippet"
>
<table
className="source-table"
>
<tbody>
<tr
className="expand-block expand-block-above"
>
<td
colSpan={5}
>
<button
aria-label="source_viewer.expand_above"
onClick={[Function]}
type="button"
>
<ExpandSnippetIcon />
</button>
</td>
</tr>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="10"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 10,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="11"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 11,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 10,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="12"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 12,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 11,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="13"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 13,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 12,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<tr
className="expand-block expand-block-below"
>
<td
colSpan={5}
>
<button
aria-label="source_viewer.expand_below"
onClick={[Function]}
type="button"
>
<ExpandSnippetIcon />
</button>
</td>
</tr>
</tbody>
</table>
</div>
`;

exports[`should render correctly when at the top of the file 1`] = `
<div
className="source-viewer-code snippet"
>
<table
className="source-table"
>
<tbody>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="1"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 1,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="2"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 2,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 1,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="3"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 3,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 2,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="4"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 4,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 3,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="5"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 5,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 4,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="6"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 6,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 5,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<Line
branchLike={
Object {
"analysisDate": "2018-01-01",
"isMain": true,
"name": "master",
}
}
displayAllIssues={false}
displayCoverage={true}
displayDuplications={false}
displayIssues={true}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
highlighted={false}
issueLocations={Array []}
issues={Array []}
key="7"
last={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 7,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
loadDuplications={[MockFunction]}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[MockFunction]}
onIssueSelect={[Function]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
"duplicated": false,
"isNew": true,
"line": 6,
"scmAuthor": "simon.brandhof@sonarsource.com",
"scmDate": "2018-12-11T10:48:39+0100",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
renderDuplicationPopup={[MockFunction]}
scroll={[Function]}
secondaryIssueLocations={Array []}
verticalBuffer={0}
/>
<tr
className="expand-block expand-block-below"
>
<td
colSpan={5}
>
<button
aria-label="source_viewer.expand_below"
onClick={[Function]}
type="button"
>
<ExpandSnippetIcon />
</button>
</td>
</tr>
</tbody>
</table>
</div>
`;

+ 11
- 6
server/sonar-web/src/main/js/apps/issues/styles.css View File

@@ -244,7 +244,12 @@
overflow-x: auto;
}

.snippet > .expand-block {
.snippet > table {
width: 100%;
}

.expand-block > td > button {
background: transparent;
box-sizing: border-box;
color: var(--secondFontColor);
height: 20px;
@@ -254,16 +259,16 @@
text-align: left;
cursor: pointer;
}
.snippet > .expand-block:hover,
.snippet > .expand-block:focus,
.snippet > .expand-block:active {
.expand-block > td > button:hover,
.expand-block > td > button:focus,
.expand-block > td > button:active {
color: var(--darkBlue);
outline: none;
}
.snippet > .expand-block-above {
.expand-block-above {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII=');
}
.snippet > .expand-block-below {
.expand-block-below {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC');
}


+ 194
- 0
server/sonar-web/src/main/js/helpers/__tests__/scrolling-test.ts View File

@@ -0,0 +1,194 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.
*/
import { scrollToElement, scrollHorizontally } from '../scrolling';

jest.useFakeTimers();

describe('scrollToElement', () => {
it('should scroll parent up to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ top: 5, bottom: 20 });

const parent = document.createElement('div');
parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
parent.scrollTop = 10;
parent.scrollLeft = 12;
parent.appendChild(element);

document.body.appendChild(parent);
scrollToElement(element, { parent, smooth: false });

expect(parent.scrollTop).toEqual(0);
expect(parent.scrollLeft).toEqual(12);
});

it('should scroll parent down to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ top: 25, bottom: 50 });

const parent = document.createElement('div');
parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
parent.scrollTop = 10;
parent.scrollLeft = 12;
parent.appendChild(element);

document.body.appendChild(parent);
scrollToElement(element, { parent, smooth: false });

expect(parent.scrollTop).toEqual(15);
expect(parent.scrollLeft).toEqual(12);
});

it('should scroll window down to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });

Object.defineProperty(window, 'innerHeight', { value: 400 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollToElement(element, { smooth: false });

expect(window.scrollTo).toBeCalledWith(0, 445);
});

it('should scroll window up to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ top: -10, bottom: 10 });

Object.defineProperty(window, 'innerHeight', { value: 50 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollToElement(element, { smooth: false });

expect(window.scrollTo).toBeCalledWith(0, -10);
});

it('should scroll window down to element smoothly', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });

Object.defineProperty(window, 'innerHeight', { value: 400 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollToElement(element, {});

jest.runAllTimers();

expect(window.scrollTo).toBeCalledTimes(10);
});
});

describe('scrollHorizontally', () => {
it('should scroll parent left to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 42 });

const parent = document.createElement('div');
parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 46 });
parent.scrollTop = 12;
parent.scrollLeft = 38;
parent.appendChild(element);

document.body.appendChild(parent);

scrollHorizontally(element, { parent, smooth: false });

expect(parent.scrollTop).toEqual(12);
expect(parent.scrollLeft).toEqual(17);
});

it('should scroll parent right to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 99 });

const parent = document.createElement('div');
parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 20 });
parent.scrollTop = 12;
parent.scrollLeft = 20;
parent.appendChild(element);

document.body.appendChild(parent);

scrollHorizontally(element, { parent, smooth: false });

expect(parent.scrollTop).toEqual(12);
expect(parent.scrollLeft).toEqual(32);
});

it('should scroll window right to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });

Object.defineProperty(window, 'innerWidth', { value: 400 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollHorizontally(element, { smooth: false });

expect(window.scrollTo).toBeCalledWith(445, 0);
});

it('should scroll window left to element', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ left: -10, right: 10 });

Object.defineProperty(window, 'innerWidth', { value: 50 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollHorizontally(element, { smooth: false });

expect(window.scrollTo).toBeCalledWith(-10, 0);
});

it('should scroll window right to element smoothly', () => {
const element = document.createElement('a');
element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });

Object.defineProperty(window, 'innerWidth', { value: 400 });
window.scrollTo = jest.fn();

document.body.appendChild(element);

scrollHorizontally(element, {});

jest.runAllTimers();

expect(window.scrollTo).toBeCalledTimes(10);
});
});

const mockGetBoundingClientRect = (overrides: Partial<ClientRect>) => () => ({
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
...overrides
});

+ 71
- 21
server/sonar-web/src/main/js/helpers/scrolling.ts View File

@@ -27,42 +27,53 @@ function isWindow(element: Element | Window): element is Window {
return element === window;
}

function getScrollPosition(element: Element | Window): number {
return isWindow(element) ? window.pageYOffset : element.scrollTop;
function getScroll(element: Element | Window) {
return isWindow(element)
? { x: window.pageXOffset, y: window.pageYOffset }
: { x: element.scrollLeft, y: element.scrollTop };
}

function scrollElement(element: Element | Window, position: number): void {
function scrollElement(element: Element | Window, x: number, y: number): void {
if (isWindow(element)) {
window.scrollTo(0, position);
window.scrollTo(x, y);
} else {
element.scrollTop = position;
element.scrollLeft = x;
element.scrollTop = y;
}
}

let smoothScrollTop = (y: number, parent: Element | Window) => {
let scrollTop = getScrollPosition(parent);
const scrollingDown = y > scrollTop;
const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS);
let smoothScroll = (target: number, current: number, scroll: (position: number) => void) => {
const positiveDirection = target > current;
const step = Math.ceil(Math.abs(target - current) / SCROLLING_STEPS);
let stepsDone = 0;

const interval = setInterval(() => {
if (scrollTop === y || SCROLLING_STEPS === stepsDone) {
if (current === target || SCROLLING_STEPS === stepsDone) {
clearInterval(interval);
} else {
let goal;
if (scrollingDown) {
goal = Math.min(y, scrollTop + step);
if (positiveDirection) {
goal = Math.min(target, current + step);
} else {
goal = Math.max(y, scrollTop - step);
goal = Math.max(target, current - step);
}
stepsDone++;
scrollTop = goal;
scrollElement(parent, goal);
current = goal;
scroll(goal);
}
}, SCROLLING_INTERVAL);
};
smoothScroll = debounce(smoothScroll, SCROLLING_DURATION, { leading: true });

smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true });
function smoothScrollTop(position: number, parent: Element | Window) {
const scroll = getScroll(parent);
smoothScroll(position, scroll.y, position => scrollElement(parent, scroll.x, position));
}

function smoothScrollLeft(position: number, parent: Element | Window) {
const scroll = getScroll(parent);
smoothScroll(position, scroll.x, position => scrollElement(parent, position, scroll.y));
}

export function scrollToElement(
element: Element,
@@ -78,7 +89,7 @@ export function scrollToElement(

const { top, bottom } = element.getBoundingClientRect();

const scrollTop = getScrollPosition(parent);
const scroll = getScroll(parent);

const height: number = isWindow(parent)
? window.innerHeight
@@ -87,20 +98,59 @@ export function scrollToElement(
const parentTop = isWindow(parent) ? 0 : parent.getBoundingClientRect().top;

if (top - parentTop < opts.topOffset) {
const goal = scrollTop - opts.topOffset + top - parentTop;
const goal = scroll.y - opts.topOffset + top - parentTop;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
scrollElement(parent, goal);
scrollElement(parent, scroll.x, goal);
}
}

if (bottom - parentTop > height - opts.bottomOffset) {
const goal = scrollTop + bottom - parentTop - height + opts.bottomOffset;
const goal = scroll.y + bottom - parentTop - height + opts.bottomOffset;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
scrollElement(parent, goal);
scrollElement(parent, scroll.x, goal);
}
}
}

export function scrollHorizontally(
element: Element,
options: {
leftOffset?: number;
rightOffset?: number;
parent?: Element;
smooth?: boolean;
}
): void {
const opts = { leftOffset: 0, rightOffset: 0, parent: window, smooth: true, ...options };
const { parent } = opts;

const { left, right } = element.getBoundingClientRect();

const scroll = getScroll(parent);

const { left: parentLeft, width } = isWindow(parent)
? { left: 0, width: window.innerWidth }
: parent.getBoundingClientRect();

if (left - parentLeft < opts.leftOffset) {
const goal = scroll.x - opts.leftOffset + left - parentLeft;
if (opts.smooth) {
smoothScrollLeft(goal, parent);
} else {
scrollElement(parent, goal, scroll.y);
}
}

if (right - parentLeft > width - opts.rightOffset) {
const goal = scroll.x + right - parentLeft - width + opts.rightOffset;
if (opts.smooth) {
smoothScrollLeft(goal, parent);
} else {
scrollElement(parent, goal, scroll.y);
}
}
}

Loading…
Cancel
Save