Skip to content

Commit 3c66d93

Browse files
committed
better maintenance
1 parent 495bb09 commit 3c66d93

File tree

8 files changed

+514
-437
lines changed

8 files changed

+514
-437
lines changed

packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/devtools-indicator.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { CSSProperties } from 'react'
22
import type { OverlayState, OverlayDispatch } from '../../shared'
33
import type { DevToolsScale } from '../errors/dev-tools-indicator/dev-tools-info/preferences'
44

5-
import { useState, useRef } from 'react'
5+
import { useState } from 'react'
66
import { NextLogo } from './next-logo'
77
import { Toast } from '../toast'
88
import {
@@ -31,8 +31,6 @@ export function DevToolsIndicator({
3131
const [open, setOpen] = useState(false)
3232
const [position, setPosition] = useState(getInitialPosition())
3333

34-
const triggerRef = useRef<HTMLButtonElement | null>(null)
35-
3634
const [vertical, horizontal] = position.split('-', 2)
3735

3836
const toggleErrorOverlay = () => {
@@ -66,7 +64,6 @@ export function DevToolsIndicator({
6664
>
6765
{/* Trigger */}
6866
<NextLogo
69-
ref={triggerRef}
7067
aria-haspopup="menu"
7168
aria-expanded={open}
7269
aria-controls="nextjs-dev-tools-menu"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect, useState } from 'react'
2+
3+
export function useMeasureWidth(
4+
ref: React.RefObject<HTMLDivElement | null>
5+
): number {
6+
const [width, setWidth] = useState<number>(0)
7+
8+
useEffect(() => {
9+
const el = ref.current
10+
11+
if (!el) {
12+
return
13+
}
14+
15+
const observer = new ResizeObserver(([{ contentRect }]) => {
16+
setWidth(contentRect.width)
17+
})
18+
19+
observer.observe(el)
20+
return () => observer.disconnect()
21+
}, [ref])
22+
23+
return width
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
3+
/**
4+
* A React hook that ensures a loading state persists
5+
* at least up to the next multiple of a given interval (default: 750ms).
6+
*
7+
* For example, if you're done loading at 1200ms, it forces you to wait
8+
* until 1500ms. If it’s 1800ms, it waits until 2250ms, etc.
9+
*
10+
* @param isLoadingTrigger - Boolean that triggers the loading state
11+
* @param interval - The time interval multiple in ms (default: 750ms)
12+
* @returns Current loading state that respects multiples of the interval
13+
*/
14+
export function useMinimumLoadingTimeMultiple(
15+
isLoadingTrigger: boolean,
16+
interval = 750
17+
) {
18+
const [isLoading, setIsLoading] = useState(false)
19+
const loadStartTimeRef = useRef<number | null>(null)
20+
const timeoutIdRef = useRef<NodeJS.Timeout | null>(null)
21+
22+
useEffect(() => {
23+
// Clear any pending timeout to avoid overlap
24+
if (timeoutIdRef.current) {
25+
clearTimeout(timeoutIdRef.current)
26+
timeoutIdRef.current = null
27+
}
28+
29+
if (isLoadingTrigger) {
30+
// If we enter "loading" state, record start time if not already
31+
if (loadStartTimeRef.current === null) {
32+
loadStartTimeRef.current = Date.now()
33+
}
34+
setIsLoading(true)
35+
} else {
36+
// If we're exiting the "loading" state:
37+
if (loadStartTimeRef.current === null) {
38+
// No start time was recorded, so just stop loading immediately
39+
setIsLoading(false)
40+
} else {
41+
// How long we've been "loading"
42+
const timeDiff = Date.now() - loadStartTimeRef.current
43+
44+
// Next multiple of `interval` after `timeDiff`
45+
const nextMultiple = interval * Math.ceil(timeDiff / interval)
46+
47+
// Remaining time needed to reach that multiple
48+
const remainingTime = nextMultiple - timeDiff
49+
50+
if (remainingTime > 0) {
51+
// If not yet at that multiple, schedule the final step
52+
timeoutIdRef.current = setTimeout(() => {
53+
setIsLoading(false)
54+
loadStartTimeRef.current = null
55+
}, remainingTime)
56+
} else {
57+
// We're already past the multiple boundary
58+
setIsLoading(false)
59+
loadStartTimeRef.current = null
60+
}
61+
}
62+
}
63+
64+
// Cleanup when effect is about to re-run or component unmounts
65+
return () => {
66+
if (timeoutIdRef.current) {
67+
clearTimeout(timeoutIdRef.current)
68+
}
69+
}
70+
}, [isLoadingTrigger, interval])
71+
72+
return isLoading
73+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
3+
export function useUpdateAnimation(
4+
issueCount: number,
5+
animationDurationMs = 0
6+
) {
7+
const lastUpdatedTimeStamp = useRef<number | null>(null)
8+
const [animate, setAnimate] = useState(false)
9+
10+
useEffect(() => {
11+
if (issueCount > 0) {
12+
const deltaMs = lastUpdatedTimeStamp.current
13+
? Date.now() - lastUpdatedTimeStamp.current
14+
: -1
15+
lastUpdatedTimeStamp.current = Date.now()
16+
17+
// We don't animate if `issueCount` changes too quickly
18+
if (deltaMs <= animationDurationMs) {
19+
return
20+
}
21+
22+
setAnimate(true)
23+
// It is important to use a CSS transitioned state, not a CSS keyframed animation
24+
// because if the issue count increases faster than the animation duration, it
25+
// will abruptly stop and not transition smoothly back to its original state.
26+
const timeoutId = window.setTimeout(() => {
27+
setAnimate(false)
28+
}, animationDurationMs)
29+
30+
return () => {
31+
clearTimeout(timeoutId)
32+
}
33+
}
34+
}, [issueCount, animationDurationMs])
35+
36+
return animate
37+
}

0 commit comments

Comments
 (0)