From: 7PH Date: Wed, 3 May 2023 11:56:07 +0000 (+0200) Subject: SONAR-18670 Drop scrollToElement helper function in favor of native browser scroll... X-Git-Tag: 10.1.0.73491~353 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=28c90e506a83f31e09b5c9ea5abf098a6dc37df8;p=sonarqube.git SONAR-18670 Drop scrollToElement helper function in favor of native browser scroll utilities --- diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx index 131c63ce23c..beda11b6f7d 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx @@ -20,9 +20,9 @@ import { DropdownMenu, + INTERACTIVE_TOOLTIP_DELAY, InputSearch, InteractiveIcon, - INTERACTIVE_TOOLTIP_DELAY, MenuSearchIcon, Popup, PopupZLevel, @@ -40,7 +40,6 @@ import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; 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'; @@ -279,11 +278,9 @@ export class GlobalSearch extends React.PureComponent { 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', }); } } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx index feb557ff042..987dd463d0e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx @@ -25,10 +25,6 @@ import { mockRouter } from '../../../../helpers/testMocks'; 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 { diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index 22c70815a62..a909faf1575 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -19,14 +19,13 @@ */ 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 { @@ -160,12 +159,10 @@ export default class FilesView extends React.PureComponent { }; 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() { diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts index 9598f118904..50d36594ac6 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts @@ -25,10 +25,6 @@ import { shouldOpenStandardsFacet, } from '../utils'; -jest.mock('../../../helpers/scrolling', () => ({ - scrollToElement: jest.fn(), -})); - beforeEach(() => { jest.clearAllMocks(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx index 16fe5128a63..71915f77d7a 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx @@ -26,10 +26,6 @@ import { mockIssue } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import SnippetViewer from '../SnippetViewer'; -jest.mock('../../../../helpers/scrolling', () => ({ - scrollHorizontally: jest.fn(), -})); - beforeEach(() => { jest.clearAllMocks(); }); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx index 864000e8f1e..5ed0f7692b7 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx @@ -22,7 +22,6 @@ import { throttle } from 'lodash'; 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'; @@ -46,7 +45,6 @@ const STICKY_BADGE_SCROLL_OFFSET = 10; export default class BranchAnalysisList extends React.PureComponent { mounted = false; badges: Dict = {}; - scrollableNode?: HTMLDivElement; state: State = { analyses: [], loading: true, @@ -69,10 +67,10 @@ export default class BranchAnalysisList extends React.PureComponent { - this.scrollableNode = el; - }} selectedAnalysisKey={analysis} shouldStick={this.shouldStick} /> diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx index 79425097aef..8bb4bf17c76 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx @@ -40,7 +40,6 @@ export interface BranchAnalysisListRendererProps { onSelectAnalysis: (analysis: ParsedAnalysis) => void; range: number; registerBadgeNode: (version: string) => (el: HTMLDivElement) => void; - registerScrollableNode: (el: HTMLDivElement) => void; selectedAnalysisKey: string; shouldStick: (version: string) => boolean; } @@ -138,11 +137,7 @@ function BranchAnalysisListRenderer( />
-
+
{!loading && !hasFilteredData ? ( diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 1db2708e3ba..580f7592375 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -25,7 +25,6 @@ import Suggestions from '../../components/embed-docs-modal/Suggestions'; 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'; @@ -90,12 +89,16 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe 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 ( diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx index b0750860495..975fa64fb53 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx @@ -27,11 +27,14 @@ import { Hotspot } from '../../../types/security-hotspots'; 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; } @@ -41,11 +44,16 @@ export function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) const locationRef = React.useRef(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 (
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx index 6da6418b9b8..fbdc9e0062c 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx @@ -20,7 +20,6 @@ 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 { @@ -53,30 +52,8 @@ export interface HotspotSnippetContainerRendererProps { } 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) { - 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, @@ -154,7 +131,6 @@ export default function HotspotSnippetContainerRenderer( ), diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx index c1cfa10fca9..238ba20eed3 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx @@ -24,7 +24,6 @@ import { DropdownOverlay } from '../../../components/controls/Dropdown'; 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'; @@ -46,15 +45,10 @@ export interface SettingsSearchRendererProps { export default function SettingsSearchRenderer(props: SettingsSearchRendererProps) { const { className, component, results, searchQuery, selectedResult, showResults } = props; - const scrollableNodeRef = React.useRef(null); const selectedNodeRef = React.useRef(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 ( @@ -72,7 +66,6 @@ export default function SettingsSearchRenderer(props: SettingsSearchRendererProp
    {results && results.length > 0 ? ( results.map((r) => ( diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index 9ce6e41c7d2..0392524410d 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -29,15 +29,14 @@ import Suggestions from '../../../components/embed-docs-modal/Suggestions'; 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'; @@ -96,11 +95,9 @@ export class WebApiApp extends React.PureComponent { 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) => { diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx index b37ff46faec..f4552defa6a 100644 --- a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx +++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx @@ -22,13 +22,12 @@ import * as React from 'react'; 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 { component: ComponentDescriptor; @@ -78,12 +77,7 @@ export class WorkspaceComponentViewer extends React.PureComponent { `.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' }); } } }; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/scrolling-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/scrolling-test.ts deleted file mode 100644 index f9e938f2c13..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/scrolling-test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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) => () => - ({ - bottom: 0, - height: 0, - left: 0, - right: 0, - top: 0, - width: 0, - ...overrides, - } as DOMRect); diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts index afc79f6c77f..946b1f79a24 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts @@ -67,17 +67,17 @@ export function mockHotspot(overrides?: Partial): Hotspot { 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], diff --git a/server/sonar-web/src/main/js/helpers/scrolling.ts b/server/sonar-web/src/main/js/helpers/scrolling.ts deleted file mode 100644 index 5edda86d2e0..00000000000 --- a/server/sonar-web/src/main/js/helpers/scrolling.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 { - 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 { - 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; - -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; - } -}