mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
Merge pull request #34936 from overleaf/jel-scroll-to-notification
[web] Scroll to new notification added above domains table GitOrigin-RevId: f62851398de3a54ef07c0e125c65a29dd84af6f8
This commit is contained in:
@@ -2,26 +2,7 @@ import Notification, {
|
||||
NotificationProps,
|
||||
} from '@/shared/components/notification'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
function elementIsInView(el: HTMLElement) {
|
||||
const scroll = window.scrollY
|
||||
const boundsTop = el.getBoundingClientRect().top + scroll
|
||||
|
||||
const viewport = {
|
||||
top: scroll,
|
||||
bottom: scroll + window.innerHeight,
|
||||
}
|
||||
|
||||
const bounds = {
|
||||
top: boundsTop,
|
||||
bottom: boundsTop + el.clientHeight,
|
||||
}
|
||||
|
||||
return (
|
||||
(bounds.bottom >= viewport.top && bounds.bottom <= viewport.bottom) ||
|
||||
(bounds.top <= viewport.bottom && bounds.top >= viewport.top)
|
||||
)
|
||||
}
|
||||
import { elementIsInView } from '@/shared/utils/element-in-view'
|
||||
|
||||
function NotificationScrolledTo({ ...props }: NotificationProps) {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { elementIsInView } from '@/shared/utils/element-in-view'
|
||||
|
||||
type UseScrollIntoViewProps = {
|
||||
id: string | null
|
||||
}
|
||||
|
||||
function useScrollIntoView({ id }: UseScrollIntoViewProps) {
|
||||
const hasMountedRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMountedRef.current) {
|
||||
hasMountedRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetElement = document.getElementById(id)
|
||||
if (targetElement && !elementIsInView(targetElement)) {
|
||||
targetElement.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [id])
|
||||
}
|
||||
|
||||
export default useScrollIntoView
|
||||
@@ -0,0 +1,5 @@
|
||||
export function elementIsInView(el: HTMLElement): boolean {
|
||||
const rect = el.getBoundingClientRect()
|
||||
// Any vertical intersection with the viewport counts as "in view".
|
||||
return rect.bottom > 0 && rect.top < window.innerHeight
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import useScrollIntoView from '../../../../frontend/js/shared/hooks/use-scroll-into-view'
|
||||
|
||||
describe('useScrollIntoView', function () {
|
||||
function createTargetElement({ id, top }: { id: string; top: number }) {
|
||||
const element = document.createElement('div')
|
||||
element.id = id
|
||||
Object.defineProperty(element, 'clientHeight', {
|
||||
configurable: true,
|
||||
value: 20,
|
||||
})
|
||||
sinon.stub(element, 'getBoundingClientRect').returns({
|
||||
top,
|
||||
bottom: top + 20,
|
||||
} as DOMRect)
|
||||
document.body.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('does not scroll on the initial render', function () {
|
||||
createTargetElement({ id: 'target-1', top: 10000 })
|
||||
const scrollIntoViewStub = sinon.stub(
|
||||
globalThis.HTMLElement.prototype,
|
||||
'scrollIntoView'
|
||||
)
|
||||
|
||||
renderHook(() => useScrollIntoView({ id: 'target-1' }))
|
||||
|
||||
expect(scrollIntoViewStub.called).to.be.false
|
||||
})
|
||||
|
||||
it('scrolls when id changes and the target is out of view', function () {
|
||||
createTargetElement({ id: 'target-1', top: 10000 })
|
||||
createTargetElement({ id: 'target-2', top: 10000 })
|
||||
const scrollIntoViewStub = sinon.stub(
|
||||
globalThis.HTMLElement.prototype,
|
||||
'scrollIntoView'
|
||||
)
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ id }: { id: string }) => useScrollIntoView({ id }),
|
||||
{
|
||||
initialProps: { id: 'target-1' },
|
||||
}
|
||||
)
|
||||
|
||||
rerender({ id: 'target-2' })
|
||||
|
||||
expect(scrollIntoViewStub.calledOnceWithExactly({ behavior: 'smooth' })).to
|
||||
.be.true
|
||||
})
|
||||
|
||||
it('does not scroll when id changes and the target is already in view', function () {
|
||||
createTargetElement({ id: 'target-1', top: 10000 })
|
||||
createTargetElement({ id: 'target-2', top: 10 })
|
||||
const scrollIntoViewStub = sinon.stub(
|
||||
globalThis.HTMLElement.prototype,
|
||||
'scrollIntoView'
|
||||
)
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ id }: { id: string }) => useScrollIntoView({ id }),
|
||||
{
|
||||
initialProps: { id: 'target-1' },
|
||||
}
|
||||
)
|
||||
|
||||
rerender({ id: 'target-2' })
|
||||
|
||||
expect(scrollIntoViewStub.called).to.be.false
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user