import {
DropdownMenu,
+ INTERACTIVE_TOOLTIP_DELAY,
InputSearch,
InteractiveIcon,
- INTERACTIVE_TOOLTIP_DELAY,
MenuSearchIcon,
Popup,
PopupZLevel,
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getKeyboardShortcutEnabled } from '../../../helpers/preferences';
-import { scrollToElement } from '../../../helpers/scrolling';
import { getComponentOverviewUrl } from '../../../helpers/urls';
import { ComponentQualifier } from '../../../types/component';
import { Dict } from '../../../types/types';
const node = this.nodes[this.state.selected];
if (node && this.node) {
- // using scrollIntoView here is creating some weird scroll behaviour when scrolling
- scrollToElement(node, {
- topOffset: 30,
- bottomOffset: 30,
- parent: this.node,
+ node.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
});
}
}
import { waitAndUpdate } from '../../../../helpers/testUtils';
import MeasureContent from '../MeasureContent';
-jest.mock('../../../../helpers/scrolling', () => ({
- scrollToElement: jest.fn(),
-}));
-
jest.mock('../../../../api/components', () => {
const { mockComponentMeasure } = jest.requireActual('../../../../helpers/mocks/component');
return {
*/
import { throttle } from 'lodash';
import * as React from 'react';
-import { Button } from '../../../components/controls/buttons';
import ListFooter from '../../../components/controls/ListFooter';
+import { Button } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
-import { scrollToElement } from '../../../helpers/scrolling';
import { BranchLike } from '../../../types/branch-like';
import { MeasurePageView } from '../../../types/measures';
import {
};
scrollToElement = () => {
- if (this.listContainer) {
- const elem = this.listContainer.getElementsByClassName('selected')[0];
- if (elem) {
- scrollToElement(elem, { topOffset: 215, bottomOffset: 100 });
- }
- }
+ this.listContainer?.getElementsByClassName('selected')[0]?.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
+ });
};
render() {
shouldOpenStandardsFacet,
} from '../utils';
-jest.mock('../../../helpers/scrolling', () => ({
- scrollToElement: jest.fn(),
-}));
-
beforeEach(() => {
jest.clearAllMocks();
});
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import SnippetViewer from '../SnippetViewer';
-jest.mock('../../../../helpers/scrolling', () => ({
- scrollHorizontally: jest.fn(),
-}));
-
beforeEach(() => {
jest.clearAllMocks();
});
import * as React from 'react';
import { getProjectActivity } from '../../../api/projectActivity';
import { parseDate, toShortNotSoISOString } from '../../../helpers/dates';
-import { scrollToElement } from '../../../helpers/scrolling';
import { Analysis, ParsedAnalysis } from '../../../types/project-activity';
import { Dict } from '../../../types/types';
import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
export default class BranchAnalysisList extends React.PureComponent<Props, State> {
mounted = false;
badges: Dict<HTMLDivElement> = {};
- scrollableNode?: HTMLDivElement;
state: State = {
analyses: [],
loading: true,
}
scrollToSelected() {
- const selectedNode = document.querySelector('.branch-analysis.selected');
- if (this.scrollableNode && selectedNode) {
- scrollToElement(selectedNode, { parent: this.scrollableNode, bottomOffset: 40 });
- }
+ document.querySelector('.branch-analysis.selected')?.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
+ });
}
fetchAnalyses(initial = false) {
onSelectAnalysis={onSelectAnalysis}
range={range}
registerBadgeNode={this.registerBadgeNode}
- registerScrollableNode={(el) => {
- this.scrollableNode = el;
- }}
selectedAnalysisKey={analysis}
shouldStick={this.shouldStick}
/>
onSelectAnalysis: (analysis: ParsedAnalysis) => void;
range: number;
registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
- registerScrollableNode: (el: HTMLDivElement) => void;
selectedAnalysisKey: string;
shouldStick: (version: string) => boolean;
}
/>
</div>
<div className="branch-analysis-list-wrapper">
- <div
- className="bordered branch-analysis-list"
- onScroll={props.handleScroll}
- ref={props.registerScrollableNode}
- >
+ <div className="bordered branch-analysis-list" onScroll={props.handleScroll}>
<DeferredSpinner className="big-spacer-top" loading={loading} />
{!loading && !hasFilteredData ? (
import DeferredSpinner from '../../components/ui/DeferredSpinner';
import { isBranch } from '../../helpers/branch-like';
import { translate } from '../../helpers/l10n';
-import { scrollToElement } from '../../helpers/scrolling';
import { BranchLike } from '../../types/branch-like';
import { SecurityStandard, Standards } from '../../types/security';
import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots';
const scrollableRef = React.useRef(null);
React.useEffect(() => {
- const parent = scrollableRef.current;
- const element =
- selectedHotspot && document.querySelector(`[data-hotspot-key="${selectedHotspot.key}"]`);
- if (parent && element) {
- scrollToElement(element, { parent, smooth: true, topOffset: 100, bottomOffset: 100 });
+ if (!selectedHotspot) {
+ return;
}
+ // Wait for next tick, in case newly selected hotspot is not yet expanded
+ setTimeout(() => {
+ document.querySelector(`[data-hotspot-key="${selectedHotspot.key}"]`)?.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
+ });
+ });
}, [selectedHotspot]);
return (
import { CurrentUser, isLoggedIn } from '../../../types/users';
import './HotspotPrimaryLocationBox.css';
+const SCROLL_DELAY = 100;
+const SCROLL_TOP_OFFSET = 100; // 5 lines above
+const SCROLL_BOTTOM_OFFSET = 28; // 1 line below + margin
+
export interface HotspotPrimaryLocationBoxProps {
hotspot: Hotspot;
onCommentClick: () => void;
currentUser: CurrentUser;
- scroll: (element: HTMLElement, offset?: number) => void;
secondaryLocationSelected: boolean;
}
const locationRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
- const { current } = locationRef;
- if (current && !secondaryLocationSelected) {
- props.scroll(current);
+ if (locationRef.current && !secondaryLocationSelected) {
+ // We need this delay to let the parent resize itself before scrolling
+ setTimeout(() => {
+ locationRef.current?.scrollIntoView({
+ block: 'nearest',
+ behavior: 'smooth',
+ });
+ }, SCROLL_DELAY);
}
- });
+ }, [locationRef, secondaryLocationSelected]);
return (
<div
'display-flex-space-between display-flex-center padded-top padded-bottom big-padded-left big-padded-right',
`hotspot-risk-exposure-${hotspot.rule.vulnerabilityProbability}`
)}
+ style={{
+ scrollMarginTop: `${SCROLL_TOP_OFFSET}px`,
+ scrollMarginBottom: `${SCROLL_BOTTOM_OFFSET}px`,
+ }}
ref={locationRef}
>
<div className="text-bold">
import * as React from 'react';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
-import { scrollToElement } from '../../../helpers/scrolling';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import {
}
const noop = () => undefined;
-const SCROLL_DELAY = 100;
const EXPAND_ANIMATION_SPEED = 200;
-const TOP_OFFSET = 100; // 5 lines above
-const BOTTOM_OFFSET = 28; // 1 line below + margin
-
-/* Exported for testing */
-export function getScrollHandler(scrollableRef: React.RefObject<HTMLDivElement>) {
- return (element: Element, offset?: number, smooth = true) => {
- /* We need this delay to let the parent resize itself before scrolling */
- setTimeout(() => {
- const parent = scrollableRef.current;
- if (parent) {
- scrollToElement(element, {
- parent,
- topOffset: offset ?? TOP_OFFSET,
- bottomOffset: offset ?? BOTTOM_OFFSET,
- smooth,
- });
- }
- }, SCROLL_DELAY);
- };
-}
-
/* Exported for testing */
export async function animateExpansion(
scrollableRef: React.RefObject<HTMLDivElement>,
<HotspotPrimaryLocationBox
hotspot={hotspot}
onCommentClick={props.onCommentButtonClick}
- scroll={getScrollHandler(scrollableRef)}
secondaryLocationSelected={secondaryLocationSelected}
/>
),
import OutsideClickHandler from '../../../components/controls/OutsideClickHandler';
import SearchBox from '../../../components/controls/SearchBox';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { scrollToElement } from '../../../helpers/scrolling';
import { ExtendedSettingDefinition } from '../../../types/settings';
import { Component } from '../../../types/types';
import { buildSettingLink, isRealSettingKey } from '../utils';
export default function SettingsSearchRenderer(props: SettingsSearchRendererProps) {
const { className, component, results, searchQuery, selectedResult, showResults } = props;
- const scrollableNodeRef = React.useRef(null);
const selectedNodeRef = React.useRef<HTMLLIElement>(null);
React.useEffect(() => {
- const parent = scrollableNodeRef.current;
- const selectedNode = selectedNodeRef.current;
- if (selectedNode && parent) {
- scrollToElement(selectedNode, { topOffset: 30, bottomOffset: 30, parent });
- }
+ selectedNodeRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
});
return (
<ul
className="settings-search-results menu"
title={translate('settings.search.results')}
- ref={scrollableNodeRef}
>
{results && results.length > 0 ? (
results.map((r) => (
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import { translate } from '../../../helpers/l10n';
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
-import { scrollToElement } from '../../../helpers/scrolling';
import { WebApi } from '../../../types/types';
import '../styles/web-api.css';
import {
+ Query,
getActionKey,
isDomainPathActive,
parseQuery,
parseVersion,
- Query,
serializeQuery,
} from '../utils';
import Domain from './Domain';
scrollToAction = () => {
const splat = this.props.params.splat || '';
const action = document.getElementById(splat);
- if (action) {
- scrollToElement(action, { topOffset: 20, bottomOffset: 20 });
- } else {
- window.scrollTo(0, 0);
- }
+ action?.scrollIntoView({
+ block: 'center',
+ });
};
updateQuery = (newQuery: Partial<Query>) => {
import { getParents } from '../../api/components';
import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions';
import { isPullRequest } from '../../helpers/branch-like';
-import { scrollToElement } from '../../helpers/scrolling';
import { BranchLike } from '../../types/branch-like';
import { Issue, SourceViewerFile } from '../../types/types';
import SourceViewer from '../SourceViewer/SourceViewer';
-import { ComponentDescriptor } from './context';
import WorkspaceComponentTitle from './WorkspaceComponentTitle';
import WorkspaceHeader, { Props as WorkspaceHeaderProps } from './WorkspaceHeader';
+import { ComponentDescriptor } from './context';
export interface Props extends Omit<WorkspaceHeaderProps, 'children' | 'onClose'> {
component: ComponentDescriptor;
`.source-line[data-line-number="${this.props.component.line}"]`
);
if (row) {
- scrollToElement(row, {
- smooth: false,
- parent: this.container,
- topOffset: 50,
- bottomOffset: 50,
- });
+ row.scrollIntoView({ block: 'center' });
}
}
};
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 } from '../scrolling';
-
-beforeAll(() => {
- jest.useFakeTimers();
-});
-
-afterAll(() => {
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
-});
-
-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).toHaveBeenCalledWith(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).toHaveBeenCalledWith(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).toHaveBeenCalledTimes(10);
- });
-});
-
-const mockGetBoundingClientRect = (overrides: Partial<ClientRect>) => () =>
- ({
- bottom: 0,
- height: 0,
- left: 0,
- right: 0,
- top: 0,
- width: 0,
- ...overrides,
- } as DOMRect);
creationDate: '2013-05-13T17:55:41+0200',
flows: [{ locations: [mockFlowLocation()] }],
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
- line: 142,
+ line: 6,
message: "'3' is a magic number.",
project: mockHotspotComponent({ qualifier: ComponentQualifier.Project }),
resolution: HotspotResolution.FIXED,
rule: mockHotspotRule(),
status: HotspotStatus.REVIEWED,
textRange: {
- startLine: 142,
- endLine: 142,
- startOffset: 26,
- endOffset: 83,
+ startLine: 6,
+ endLine: 6,
+ startOffset: 3,
+ endOffset: 9,
},
updateDate: '2013-05-13T17:55:42+0200',
users: [assigneeUser, authorUser],
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-const SCROLLING_DURATION = 100;
-const SCROLLING_INTERVAL = 10;
-const SCROLLING_STEPS = SCROLLING_DURATION / SCROLLING_INTERVAL;
-
-function isWindow(element: Element | Window): element is Window {
- return element === window;
-}
-
-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, x: number, y: number): Promise<void> {
- if (isWindow(element)) {
- window.scrollTo(x, y);
- } else {
- element.scrollLeft = x;
- element.scrollTop = y;
- }
- return Promise.resolve();
-}
-
-function smoothScroll(
- target: number,
- current: number,
- scroll: (position: number) => void
-): Promise<void> {
- const positiveDirection = target > current;
- const step = Math.ceil(Math.abs(target - current) / SCROLLING_STEPS);
- let stepsDone = 0;
-
- return new Promise((resolve) => {
- const interval = setInterval(() => {
- if (current === target || SCROLLING_STEPS === stepsDone) {
- clearInterval(interval);
- resolve();
- } else {
- let goal;
- if (positiveDirection) {
- goal = Math.min(target, current + step);
- } else {
- goal = Math.max(target, current - step);
- }
- stepsDone++;
- current = goal;
- scroll(goal);
- }
- }, SCROLLING_INTERVAL);
- });
-}
-
-function smoothScrollTop(parent: Element | Window, position: number) {
- const scroll = getScroll(parent);
- return smoothScroll(position, scroll.y, (position) => scrollElement(parent, scroll.x, position));
-}
-
-/**
- * @deprecated use scrollIntoView instead
- */
-export function scrollToElement(
- element: Element,
- options: {
- topOffset?: number;
- bottomOffset?: number;
- parent?: Element;
- smooth?: boolean;
- }
-): void {
- const opts = { topOffset: 0, bottomOffset: 0, parent: window, smooth: true, ...options };
- const { parent } = opts;
-
- const { top, bottom } = element.getBoundingClientRect();
-
- const scroll = getScroll(parent);
-
- const height: number = isWindow(parent)
- ? window.innerHeight
- : parent.getBoundingClientRect().height;
-
- const parentTop = isWindow(parent) ? 0 : parent.getBoundingClientRect().top;
-
- if (top - parentTop < opts.topOffset) {
- const goal = scroll.y - opts.topOffset + top - parentTop;
- if (opts.smooth) {
- addToScrollQueue(smoothScrollTop, parent, goal);
- } else {
- addToScrollQueue(scrollElement, parent, scroll.x, goal);
- }
- }
-
- if (bottom - parentTop > height - opts.bottomOffset) {
- const goal = scroll.y + bottom - parentTop - height + opts.bottomOffset;
- if (opts.smooth) {
- addToScrollQueue(smoothScrollTop, parent, goal);
- } else {
- addToScrollQueue(scrollElement, parent, scroll.x, goal);
- }
- }
-}
-
-type ScrollFunction = (element: Element | Window, x: number, y?: number) => Promise<void>;
-
-interface ScrollQueueItem {
- element: Element | Window;
- fn: ScrollFunction;
- x: number;
- y?: number;
-}
-
-const queue: ScrollQueueItem[] = [];
-let queueRunning: boolean;
-
-function addToScrollQueue(
- fn: ScrollFunction,
- element: Element | Window,
- x: number,
- y?: number
-): void {
- queue.push({ fn, element, x, y });
- if (!queueRunning) {
- processQueue();
- }
-}
-
-function processQueue() {
- if (queue.length > 0) {
- queueRunning = true;
- const { fn, element, x, y } = queue.shift()!;
- fn(element, x, y).then(processQueue).catch(processQueue);
- } else {
- queueRunning = false;
- }
-}